Repository: chartjs/Chart.js Branch: master Commit: a15355686107 Files: 1068 Total size: 2.5 MB Directory structure: gitextract_dt3kbibo/ ├── .browserslistrc ├── .codeclimate.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.yml ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.yml │ │ ├── config.yml │ │ ├── docs.yml │ │ └── feature.yml │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ ├── dependabot.yml │ ├── release-drafter.yml │ └── workflows/ │ ├── ci.yml │ ├── compressed-size.yml │ ├── deploy-docs.yml │ ├── release-drafter.yml │ └── release.yml ├── .gitignore ├── .htmllintrc ├── LICENSE.md ├── MAINTAINING.md ├── README.md ├── auto/ │ ├── auto.cjs │ ├── auto.d.ts │ ├── auto.js │ └── package.json ├── composer.json ├── docs/ │ ├── .vuepress/ │ │ ├── config.ts │ │ ├── redirects │ │ └── styles/ │ │ └── index.styl │ ├── axes/ │ │ ├── _common.md │ │ ├── _common_ticks.md │ │ ├── cartesian/ │ │ │ ├── _common.md │ │ │ ├── _common_ticks.md │ │ │ ├── category.md │ │ │ ├── index.md │ │ │ ├── linear.md │ │ │ ├── logarithmic.md │ │ │ ├── time.md │ │ │ └── timeseries.md │ │ ├── index.md │ │ ├── labelling.md │ │ ├── radial/ │ │ │ ├── index.md │ │ │ └── linear.md │ │ └── styling.md │ ├── charts/ │ │ ├── area.md │ │ ├── bar.md │ │ ├── bubble.md │ │ ├── doughnut.md │ │ ├── line.md │ │ ├── mixed.md │ │ ├── polar.md │ │ ├── radar.md │ │ └── scatter.md │ ├── configuration/ │ │ ├── animations.md │ │ ├── canvas-background.md │ │ ├── decimation.md │ │ ├── device-pixel-ratio.md │ │ ├── elements.md │ │ ├── index.md │ │ ├── interactions.md │ │ ├── layout.md │ │ ├── legend.md │ │ ├── locale.md │ │ ├── responsive.md │ │ ├── subtitle.md │ │ ├── title.md │ │ └── tooltip.md │ ├── developers/ │ │ ├── api.md │ │ ├── axes.md │ │ ├── charts.md │ │ ├── contributing.md │ │ ├── index.md │ │ ├── plugin_flowcharts.drawio │ │ ├── plugins.md │ │ ├── publishing.md │ │ └── updates.md │ ├── general/ │ │ ├── accessibility.md │ │ ├── colors.md │ │ ├── data-structures.md │ │ ├── fonts.md │ │ ├── options.md │ │ ├── padding.md │ │ └── performance.md │ ├── getting-started/ │ │ ├── index.md │ │ ├── installation.md │ │ ├── integration.md │ │ ├── usage.md │ │ └── using-from-node-js.md │ ├── index.md │ ├── migration/ │ │ ├── v3-migration.md │ │ └── v4-migration.md │ ├── package.json │ ├── samples/ │ │ ├── .eslintrc.yml │ │ ├── advanced/ │ │ │ ├── data-decimation.md │ │ │ ├── derived-axis-type.md │ │ │ ├── derived-chart-type.md │ │ │ ├── linear-gradient.md │ │ │ ├── programmatic-events.md │ │ │ ├── progress-bar.md │ │ │ └── radial-gradient.md │ │ ├── animations/ │ │ │ ├── delay.md │ │ │ ├── drop.md │ │ │ ├── loop.md │ │ │ ├── progressive-line-easing.md │ │ │ └── progressive-line.md │ │ ├── area/ │ │ │ ├── line-boundaries.md │ │ │ ├── line-datasets.md │ │ │ ├── line-drawtime.md │ │ │ ├── line-stacked.md │ │ │ └── radar.md │ │ ├── bar/ │ │ │ ├── border-radius.md │ │ │ ├── floating.md │ │ │ ├── horizontal.md │ │ │ ├── stacked-groups.md │ │ │ ├── stacked.md │ │ │ └── vertical.md │ │ ├── information.md │ │ ├── legend/ │ │ │ ├── events.md │ │ │ ├── html.md │ │ │ ├── point-style.md │ │ │ ├── position.md │ │ │ └── title.md │ │ ├── line/ │ │ │ ├── interpolation.md │ │ │ ├── line.md │ │ │ ├── multi-axis.md │ │ │ ├── point-styling.md │ │ │ ├── segments.md │ │ │ ├── stepped.md │ │ │ └── styling.md │ │ ├── other-charts/ │ │ │ ├── bubble.md │ │ │ ├── combo-bar-line.md │ │ │ ├── doughnut.md │ │ │ ├── multi-series-pie.md │ │ │ ├── pie.md │ │ │ ├── polar-area-center-labels.md │ │ │ ├── polar-area.md │ │ │ ├── radar-skip-points.md │ │ │ ├── radar.md │ │ │ ├── scatter-multi-axis.md │ │ │ ├── scatter.md │ │ │ └── stacked-bar-line.md │ │ ├── plugins/ │ │ │ ├── chart-area-border.md │ │ │ ├── doughnut-empty-state.md │ │ │ └── quadrants.md │ │ ├── scale-options/ │ │ │ ├── center.md │ │ │ ├── grid.md │ │ │ ├── ticks.md │ │ │ └── titles.md │ │ ├── scales/ │ │ │ ├── linear-min-max-suggested.md │ │ │ ├── linear-min-max.md │ │ │ ├── linear-step-size.md │ │ │ ├── log.md │ │ │ ├── stacked.md │ │ │ ├── time-combo.md │ │ │ ├── time-line.md │ │ │ └── time-max-span.md │ │ ├── scriptable/ │ │ │ ├── bar.md │ │ │ ├── bubble.md │ │ │ ├── line.md │ │ │ ├── pie.md │ │ │ ├── polar.md │ │ │ └── radar.md │ │ ├── subtitle/ │ │ │ └── basic.md │ │ ├── title/ │ │ │ └── alignment.md │ │ ├── tooltip/ │ │ │ ├── content.md │ │ │ ├── html.md │ │ │ ├── interactions.md │ │ │ ├── point-style.md │ │ │ └── position.md │ │ └── utils.md │ └── scripts/ │ ├── analyzer.js │ ├── components.js │ ├── derived-bubble.js │ ├── helpers.js │ ├── log2.js │ ├── register.js │ └── utils.js ├── helpers/ │ ├── helpers.cjs │ ├── helpers.d.ts │ ├── helpers.js │ └── package.json ├── karma.conf.cjs ├── package.json ├── pnpm-workspace.yaml ├── rollup.config.js ├── scripts/ │ ├── deploy-docs.sh │ ├── docs-config.sh │ ├── publish.sh │ ├── sample-redirect-template.html │ └── utils.sh ├── src/ │ ├── controllers/ │ │ ├── controller.bar.js │ │ ├── controller.bubble.js │ │ ├── controller.doughnut.js │ │ ├── controller.line.js │ │ ├── controller.pie.js │ │ ├── controller.polarArea.js │ │ ├── controller.radar.js │ │ ├── controller.scatter.js │ │ └── index.js │ ├── core/ │ │ ├── core.adapters.ts │ │ ├── core.animation.js │ │ ├── core.animations.defaults.js │ │ ├── core.animations.js │ │ ├── core.animator.js │ │ ├── core.config.js │ │ ├── core.controller.js │ │ ├── core.datasetController.js │ │ ├── core.defaults.js │ │ ├── core.element.ts │ │ ├── core.interaction.js │ │ ├── core.layouts.defaults.js │ │ ├── core.layouts.js │ │ ├── core.plugins.js │ │ ├── core.registry.js │ │ ├── core.scale.autoskip.js │ │ ├── core.scale.defaults.js │ │ ├── core.scale.js │ │ ├── core.ticks.js │ │ ├── core.typedRegistry.js │ │ └── index.ts │ ├── elements/ │ │ ├── element.arc.ts │ │ ├── element.bar.js │ │ ├── element.line.js │ │ ├── element.point.ts │ │ └── index.js │ ├── helpers/ │ │ ├── helpers.canvas.ts │ │ ├── helpers.collection.ts │ │ ├── helpers.color.ts │ │ ├── helpers.config.ts │ │ ├── helpers.config.types.ts │ │ ├── helpers.core.ts │ │ ├── helpers.curve.ts │ │ ├── helpers.dataset.ts │ │ ├── helpers.dom.ts │ │ ├── helpers.easing.ts │ │ ├── helpers.extras.ts │ │ ├── helpers.interpolation.ts │ │ ├── helpers.intl.ts │ │ ├── helpers.math.ts │ │ ├── helpers.options.ts │ │ ├── helpers.rtl.ts │ │ ├── helpers.segment.js │ │ └── index.ts │ ├── index.ts │ ├── index.umd.ts │ ├── platform/ │ │ ├── index.js │ │ ├── platform.base.js │ │ ├── platform.basic.js │ │ └── platform.dom.js │ ├── plugins/ │ │ ├── index.js │ │ ├── plugin.colors.ts │ │ ├── plugin.decimation.js │ │ ├── plugin.filler/ │ │ │ ├── filler.drawing.js │ │ │ ├── filler.helper.js │ │ │ ├── filler.options.js │ │ │ ├── filler.segment.js │ │ │ ├── filler.target.js │ │ │ ├── filler.target.stack.js │ │ │ ├── index.js │ │ │ └── simpleArc.js │ │ ├── plugin.legend.js │ │ ├── plugin.subtitle.js │ │ ├── plugin.title.js │ │ └── plugin.tooltip.js │ ├── scales/ │ │ ├── index.js │ │ ├── scale.category.js │ │ ├── scale.linear.js │ │ ├── scale.linearbase.js │ │ ├── scale.logarithmic.js │ │ ├── scale.radialLinear.js │ │ ├── scale.time.js │ │ └── scale.timeseries.js │ ├── types/ │ │ ├── animation.d.ts │ │ ├── basic.d.ts │ │ ├── color.d.ts │ │ ├── geometric.d.ts │ │ ├── index.d.ts │ │ ├── layout.d.ts │ │ └── utils.d.ts │ └── types.ts ├── test/ │ ├── .eslintrc.yml │ ├── BasicChartWebWorker.js │ ├── fixtures/ │ │ ├── controller.bar/ │ │ │ ├── aligned-pixels.js │ │ │ ├── backgroundColor/ │ │ │ │ ├── indexable.js │ │ │ │ ├── loopable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── bar-animation-hide-show.js │ │ │ ├── bar-base-value.js │ │ │ ├── bar-default-begin-at-zero.js │ │ │ ├── bar-thickness-absolute.json │ │ │ ├── bar-thickness-flex-offset.json │ │ │ ├── bar-thickness-flex-single-reverse.json │ │ │ ├── bar-thickness-flex-single.json │ │ │ ├── bar-thickness-flex.json │ │ │ ├── bar-thickness-max.json │ │ │ ├── bar-thickness-min-interval-multi.json │ │ │ ├── bar-thickness-min-interval.json │ │ │ ├── bar-thickness-multiple.json │ │ │ ├── bar-thickness-no-overlap.json │ │ │ ├── bar-thickness-offset.json │ │ │ ├── bar-thickness-per-dataset-stacked.json │ │ │ ├── bar-thickness-per-dataset.json │ │ │ ├── bar-thickness-reverse.json │ │ │ ├── bar-thickness-single-xy.json │ │ │ ├── bar-thickness-single.json │ │ │ ├── bar-thickness-stacked.json │ │ │ ├── baseLine/ │ │ │ │ ├── bottom.js │ │ │ │ ├── left.js │ │ │ │ ├── mid-x.js │ │ │ │ ├── mid-y.js │ │ │ │ ├── right.js │ │ │ │ ├── top.js │ │ │ │ ├── value-x.js │ │ │ │ └── value-y.js │ │ │ ├── borderColor/ │ │ │ │ ├── border+dpr.js │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderRadius/ │ │ │ │ ├── border-radius-stacked-number-mixed-chart.js │ │ │ │ ├── border-radius-stacked-number-with-order.js │ │ │ │ ├── border-radius-stacked-number.js │ │ │ │ ├── border-radius.js │ │ │ │ └── no-spacing.js │ │ │ ├── borderSkipped/ │ │ │ │ ├── indexable.js │ │ │ │ ├── middle.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderWidth/ │ │ │ │ ├── indexable-object.js │ │ │ │ ├── indexable.js │ │ │ │ ├── negative.js │ │ │ │ ├── object.js │ │ │ │ ├── scriptable-object.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── chart-area-clip.js │ │ │ ├── data/ │ │ │ │ ├── object-index-axis-y.js │ │ │ │ ├── object.js │ │ │ │ └── parsing.js │ │ │ ├── floatBar/ │ │ │ │ ├── data-as-objects-horizontal.js │ │ │ │ ├── data-as-objects.js │ │ │ │ ├── float-bar-horizontal.json │ │ │ │ ├── float-bar-stacked-horizontal.json │ │ │ │ ├── float-bar-stacked.json │ │ │ │ └── float-bar.json │ │ │ ├── horizontal-borders.js │ │ │ ├── minBarLength/ │ │ │ │ ├── horizontal-neg.js │ │ │ │ ├── horizontal-pos.js │ │ │ │ ├── horizontal-stacked-no-overlap.js │ │ │ │ ├── horizontal-stacked.js │ │ │ │ ├── horizontal.js │ │ │ │ ├── vertical-neg.js │ │ │ │ ├── vertical-pos.js │ │ │ │ ├── vertical-stacked-no-overlap.js │ │ │ │ ├── vertical-stacked.js │ │ │ │ └── vertical.js │ │ │ ├── not-grouped/ │ │ │ │ ├── mixed.js │ │ │ │ └── on-time.js │ │ │ ├── skipNull/ │ │ │ │ ├── bar-skip-null-object-data.js │ │ │ │ ├── bar-skip-null.js │ │ │ │ └── combinations.js │ │ │ └── stacking/ │ │ │ ├── issue-9105.js │ │ │ ├── logarithmic-strings.js │ │ │ ├── logarithmic.js │ │ │ ├── order-default.json │ │ │ ├── order-specified.json │ │ │ ├── remove-dataset.js │ │ │ ├── replace-data.js │ │ │ └── stacked-and-multiple-axis.js │ │ ├── controller.bubble/ │ │ │ ├── autoPadding-disabled.js │ │ │ ├── clip.js │ │ │ ├── hover-radius-zero.js │ │ │ ├── padding-update.js │ │ │ ├── padding.js │ │ │ ├── point-style.json │ │ │ ├── radius-data.js │ │ │ └── radius-scriptable.js │ │ ├── controller.doughnut/ │ │ │ ├── backgroundColor/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderAlign/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderColor/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderDash/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderJoinStyle/ │ │ │ │ ├── bevel-default.js │ │ │ │ ├── miter.js │ │ │ │ └── round.js │ │ │ ├── borderRadius/ │ │ │ │ ├── scriptable.js │ │ │ │ ├── value-corners.js │ │ │ │ ├── value-large-radius.js │ │ │ │ └── value-small-number.js │ │ │ ├── borderWidth/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── doughnut-NaN.js │ │ │ ├── doughnut-animation-hide-last.js │ │ │ ├── doughnut-animation.js │ │ │ ├── doughnut-border-align-center.json │ │ │ ├── doughnut-border-align-inner.json │ │ │ ├── doughnut-circumference-over-2pi.json │ │ │ ├── doughnut-circumference-per-dataset.js │ │ │ ├── doughnut-circumference.json │ │ │ ├── doughnut-full-to-semi.js │ │ │ ├── doughnut-hidden-single.js │ │ │ ├── doughnut-hidden.js │ │ │ ├── doughnut-offset.js │ │ │ ├── doughnut-outer-radius-percent.js │ │ │ ├── doughnut-outer-radius-pixels.js │ │ │ ├── doughnut-parsing.js │ │ │ ├── doughnut-rotation-300.js │ │ │ ├── doughnut-rotation-circumference-8x8.js │ │ │ ├── doughnut-rotation-per-dataset.js │ │ │ ├── doughnut-set-active-elements.js │ │ │ ├── doughnut-spacing-and-offset.js │ │ │ ├── doughnut-spacing.js │ │ │ ├── doughnut-weight.json │ │ │ ├── event-replay.js │ │ │ ├── pie-border-align-center.json │ │ │ ├── pie-border-align-inner.json │ │ │ ├── pie-circumference.json │ │ │ ├── pie-offset.js │ │ │ ├── pie-weight.json │ │ │ ├── selfJoin/ │ │ │ │ ├── doughnut.js │ │ │ │ └── pie.js │ │ │ ├── single-slice-circumference-405.js │ │ │ ├── single-slice-offset.js │ │ │ └── single-slice-opacity.js │ │ ├── controller.line/ │ │ │ ├── backgroundColor/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderCapStyle/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderColor/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderDash/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderDashOffset/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderJoinStyle/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderWidth/ │ │ │ │ ├── scriptable.js │ │ │ │ ├── value.js │ │ │ │ └── zero.js │ │ │ ├── clip/ │ │ │ │ ├── default-x-max.json │ │ │ │ ├── default-x-min.json │ │ │ │ ├── default-x.json │ │ │ │ ├── default-y-max.json │ │ │ │ ├── default-y-min.json │ │ │ │ ├── default-y.json │ │ │ │ ├── false.js │ │ │ │ └── specified.json │ │ │ ├── cubicInterpolationMode/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── fill/ │ │ │ │ ├── no-border.js │ │ │ │ ├── order-default.js │ │ │ │ ├── order.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── issue-8902.js │ │ │ ├── non-numeric-y.json │ │ │ ├── point-style-offscreen-canvas.json │ │ │ ├── point-style.json │ │ │ ├── pointBackgroundColor/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── pointBorderColor/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── pointBorderWidth/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── pointStyle/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── radius/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable-to-value.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── rotation/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── segments/ │ │ │ │ ├── gap.js │ │ │ │ ├── gradient.js │ │ │ │ ├── range.js │ │ │ │ ├── single.js │ │ │ │ ├── slope.js │ │ │ │ └── spanGaps.js │ │ │ ├── showLine/ │ │ │ │ ├── dataset.js │ │ │ │ └── false.js │ │ │ └── stacking/ │ │ │ ├── bounds-data.js │ │ │ ├── order-default.js │ │ │ ├── order-specified.js │ │ │ ├── single.js │ │ │ ├── stacked-scatter.js │ │ │ └── updates.js │ │ ├── controller.polarArea/ │ │ │ ├── angle-array.json │ │ │ ├── angle-lines.json │ │ │ ├── angle-undefined.json │ │ │ ├── backgroundColor/ │ │ │ │ ├── indexable-dataset.js │ │ │ │ ├── indexable-element-options.js │ │ │ │ ├── scriptable-dataset.js │ │ │ │ ├── scriptable-element-options.js │ │ │ │ ├── value-dataset.js │ │ │ │ └── value-element-options.js │ │ │ ├── border-align-center.json │ │ │ ├── border-align-inner.json │ │ │ ├── borderAlign/ │ │ │ │ ├── indexable-dataset.js │ │ │ │ ├── indexable-element-options.js │ │ │ │ ├── scriptable-dataset.js │ │ │ │ ├── scriptable-element-options.js │ │ │ │ ├── value-dataset.js │ │ │ │ └── value-element-options.js │ │ │ ├── borderColor/ │ │ │ │ ├── indexable-dataset.js │ │ │ │ ├── indexable-element-options.js │ │ │ │ ├── scriptable-dataset.js │ │ │ │ ├── scriptable-element-options.js │ │ │ │ ├── value-dataset.js │ │ │ │ └── value-element-options.js │ │ │ ├── borderDash/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderWidth/ │ │ │ │ ├── indexable-dataset.js │ │ │ │ ├── indexable-element-options.js │ │ │ │ ├── scriptable-dataset.js │ │ │ │ ├── scriptable-element-options.js │ │ │ │ ├── value-dataset.js │ │ │ │ └── value-element-options.js │ │ │ ├── last-slice-animate.js │ │ │ ├── parse-object-data.json │ │ │ ├── pointLabels/ │ │ │ │ ├── centered-180.js │ │ │ │ ├── centered-45.js │ │ │ │ ├── centered.js │ │ │ │ ├── default-180.js │ │ │ │ ├── default-45.js │ │ │ │ ├── default.js │ │ │ │ ├── displayAuto-180.js │ │ │ │ ├── displayAuto.js │ │ │ │ ├── overlapping.js │ │ │ │ └── withTitle/ │ │ │ │ ├── bottom-centered-45.js │ │ │ │ ├── bottom-centered.js │ │ │ │ ├── left-centered-45.js │ │ │ │ ├── left-centered.js │ │ │ │ ├── right-centered-45.js │ │ │ │ ├── right-centered.js │ │ │ │ ├── top-centered-45.js │ │ │ │ ├── top-centered.js │ │ │ │ ├── top-default-45.js │ │ │ │ └── top-default.js │ │ │ ├── polar-area-animation-rotate.js │ │ │ └── polar-area-animation-scale.js │ │ ├── controller.radar/ │ │ │ ├── backgroundColor/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderCapStyle/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderColor/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderDash/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderDashOffset/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderJoinStyle/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── borderWidth/ │ │ │ │ ├── scriptable.js │ │ │ │ ├── value.js │ │ │ │ └── zero.js │ │ │ ├── fill/ │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── point-style.json │ │ │ ├── pointBackgroundColor/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── pointBorderColor/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── pointBorderWidth/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── pointStyle/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── radius/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── rotation/ │ │ │ │ ├── indexable.js │ │ │ │ ├── scriptable.js │ │ │ │ └── value.js │ │ │ ├── showLine/ │ │ │ │ └── value.js │ │ │ └── startAngle/ │ │ │ ├── 135.js │ │ │ ├── 180.js │ │ │ ├── 225.js │ │ │ ├── 270.js │ │ │ ├── 315.js │ │ │ ├── 45.js │ │ │ ├── 90.js │ │ │ └── default.js │ │ ├── controller.scatter/ │ │ │ └── showLine/ │ │ │ ├── changed.js │ │ │ ├── true.js │ │ │ └── undefined.js │ │ ├── core.datasetController/ │ │ │ └── stacked-initial-render.js │ │ ├── core.interaction/ │ │ │ ├── drawActiveElementsOnTop-false.js │ │ │ ├── nearest-partial-bar.js │ │ │ └── nearest-point-behind-scale.js │ │ ├── core.layouts/ │ │ │ ├── hidden-vertical-boxes.js │ │ │ ├── long-labels.js │ │ │ ├── no-boxes-all-padding.js │ │ │ ├── refit-vertical-boxes.js │ │ │ ├── scriptable.js │ │ │ ├── stacked-boxes-max-index-without-clip.js │ │ │ ├── stacked-boxes-max-index.js │ │ │ ├── stacked-boxes-max-without-clip.js │ │ │ ├── stacked-boxes-max.js │ │ │ ├── stacked-boxes-with-weight.js │ │ │ └── stacked-boxes.js │ │ ├── core.scale/ │ │ │ ├── autoSkip/ │ │ │ │ ├── fit-after.js │ │ │ │ ├── no-offset.js │ │ │ │ └── offset.js │ │ │ ├── backgroundColor.js │ │ │ ├── border-behind-elements.js │ │ │ ├── cartesian-axis-border-settings.json │ │ │ ├── crossAlignment/ │ │ │ │ ├── cross-align-bottom-center.js │ │ │ │ ├── cross-align-bottom-far.js │ │ │ │ ├── cross-align-bottom-near.js │ │ │ │ ├── cross-align-left-center.js │ │ │ │ ├── cross-align-left-far-clipped.js │ │ │ │ ├── cross-align-left-far.js │ │ │ │ ├── cross-align-left-near.js │ │ │ │ ├── cross-align-right-center.js │ │ │ │ ├── cross-align-right-far-clipped.js │ │ │ │ ├── cross-align-right-far.js │ │ │ │ ├── cross-align-right-near.js │ │ │ │ ├── cross-align-top-center.js │ │ │ │ ├── cross-align-top-far.js │ │ │ │ ├── cross-align-top-near.js │ │ │ │ ├── mirror-cross-align-left-center.js │ │ │ │ ├── mirror-cross-align-left-far.js │ │ │ │ ├── mirror-cross-align-left-near.js │ │ │ │ ├── mirror-cross-align-right-center.js │ │ │ │ ├── mirror-cross-align-right-far.js │ │ │ │ └── mirror-cross-align-right-near.js │ │ │ ├── grid/ │ │ │ │ ├── border-over-grid.js │ │ │ │ ├── colors.js │ │ │ │ └── scriptable-borderDash.js │ │ │ ├── label-align-center.js │ │ │ ├── label-align-end.js │ │ │ ├── label-align-inner-onlyX.js │ │ │ ├── label-align-inner-reverse.js │ │ │ ├── label-align-inner-rotate.js │ │ │ ├── label-align-inner.js │ │ │ ├── label-align-start.js │ │ │ ├── label-offset-vertical-axes.json │ │ │ ├── tick-backdrop-alignment-inner.js │ │ │ ├── tick-backdrop-rotation.js │ │ │ ├── tick-backdrop.js │ │ │ ├── tick-drawing.json │ │ │ ├── tick-override-styles.json │ │ │ ├── ticks/ │ │ │ │ ├── rotated-long.js │ │ │ │ ├── rotated-multi-line.js │ │ │ │ └── skip-by-callback.js │ │ │ ├── ticks-mirror-x.js │ │ │ ├── ticks-mirror.js │ │ │ ├── title/ │ │ │ │ ├── align-end.js │ │ │ │ ├── align-start.js │ │ │ │ ├── default.js │ │ │ │ ├── horizontal-center.js │ │ │ │ ├── horizontal-value.js │ │ │ │ ├── multi-line/ │ │ │ │ │ ├── align-end.js │ │ │ │ │ ├── align-start.js │ │ │ │ │ └── default.js │ │ │ │ ├── vertical-center.js │ │ │ │ └── vertical-value.js │ │ │ ├── x-axis-position-center.json │ │ │ ├── x-axis-position-dynamic-margin.js │ │ │ ├── x-axis-position-dynamic.json │ │ │ ├── y-axis-position-center.json │ │ │ └── y-axis-position-dynamic.json │ │ ├── element.line/ │ │ │ ├── cubicInterpolationMode/ │ │ │ │ ├── monotone-horizontal.js │ │ │ │ └── monotone-vertical.js │ │ │ ├── default.js │ │ │ ├── skip/ │ │ │ │ ├── all.js │ │ │ │ ├── first-span.js │ │ │ │ ├── first.js │ │ │ │ ├── last-span.js │ │ │ │ ├── last.js │ │ │ │ ├── middle-span.js │ │ │ │ └── middle.js │ │ │ ├── stepped/ │ │ │ │ ├── after.js │ │ │ │ ├── before.js │ │ │ │ ├── default.js │ │ │ │ └── middle.js │ │ │ └── tension/ │ │ │ ├── default.js │ │ │ ├── one.js │ │ │ └── zero.js │ │ ├── element.point/ │ │ │ ├── point-style-circle.json │ │ │ ├── point-style-cross-rot.json │ │ │ ├── point-style-cross.json │ │ │ ├── point-style-dash.json │ │ │ ├── point-style-image.js │ │ │ ├── point-style-line.json │ │ │ ├── point-style-rect-rot.json │ │ │ ├── point-style-rect-rounded.json │ │ │ ├── point-style-rect.json │ │ │ ├── point-style-star.json │ │ │ ├── point-style-triangle.json │ │ │ └── rotation.js │ │ ├── mixed/ │ │ │ ├── bar+line-stacked.js │ │ │ └── bar+line.js │ │ ├── plugin.colors/ │ │ │ ├── bar.js │ │ │ ├── bubble.js │ │ │ ├── chart-options-colors.js │ │ │ ├── doughnut.js │ │ │ ├── dynamic-datasets-default.js │ │ │ ├── dynamic-datasets-force-override.js │ │ │ ├── line.js │ │ │ ├── mixed.js │ │ │ ├── pie.js │ │ │ ├── polarArea.js │ │ │ ├── radar.js │ │ │ └── scatter.js │ │ ├── plugin.filler/ │ │ │ ├── line/ │ │ │ │ ├── above-below-vertical-linechart.js │ │ │ │ ├── before-dataset-draw.js │ │ │ │ ├── before-datasets-draw.js │ │ │ │ ├── boundary/ │ │ │ │ │ ├── above-below-line-null-start.json │ │ │ │ │ ├── above-below-line-null.json │ │ │ │ │ ├── end-span.json │ │ │ │ │ ├── end.json │ │ │ │ │ ├── origin-span-dual.json │ │ │ │ │ ├── origin-span.json │ │ │ │ │ ├── origin-spline-above.json │ │ │ │ │ ├── origin-spline-span.json │ │ │ │ │ ├── origin-spline.json │ │ │ │ │ ├── origin-stepped-span.json │ │ │ │ │ ├── origin-stepped.json │ │ │ │ │ ├── origin.json │ │ │ │ │ ├── start-span.json │ │ │ │ │ └── start.json │ │ │ │ ├── dataset/ │ │ │ │ │ ├── border.json │ │ │ │ │ ├── clip-bounds-x-off.js │ │ │ │ │ ├── clip-bounds-x.js │ │ │ │ │ ├── clip-bounds-y-off.js │ │ │ │ │ ├── clip-bounds-y.js │ │ │ │ │ ├── dual.json │ │ │ │ │ ├── interpolated.js │ │ │ │ │ ├── no-border.json │ │ │ │ │ ├── span-dual.json │ │ │ │ │ ├── span.json │ │ │ │ │ ├── spline-span-above.json │ │ │ │ │ ├── spline-span-below.json │ │ │ │ │ ├── spline-span.json │ │ │ │ │ ├── spline.json │ │ │ │ │ └── stepped.json │ │ │ │ ├── drawTimeFillFalse/ │ │ │ │ │ ├── beforeDatasetDraw.js │ │ │ │ │ ├── beforeDatasetsDraw.js │ │ │ │ │ └── beforeDraw.js │ │ │ │ ├── points-outside-canvas-initial.js │ │ │ │ ├── points-outside-canvas-update.js │ │ │ │ ├── segments/ │ │ │ │ │ ├── alignToPixels.js │ │ │ │ │ ├── gap.js │ │ │ │ │ └── slope.js │ │ │ │ ├── shape.js │ │ │ │ ├── stack-multiple-scales.js │ │ │ │ ├── stack.json │ │ │ │ ├── value.json │ │ │ │ └── vertical.js │ │ │ └── radar/ │ │ │ ├── beforeDraw.js │ │ │ ├── boundary/ │ │ │ │ ├── end-circular.json │ │ │ │ ├── end-span.json │ │ │ │ ├── end.json │ │ │ │ ├── origin-circular.json │ │ │ │ ├── origin-span.json │ │ │ │ ├── origin-spline-span.json │ │ │ │ ├── origin-spline.json │ │ │ │ ├── origin.json │ │ │ │ ├── start-circular.json │ │ │ │ ├── start-span.json │ │ │ │ └── start.json │ │ │ ├── dataset/ │ │ │ │ ├── border.json │ │ │ │ ├── default.json │ │ │ │ ├── order.js │ │ │ │ ├── span.json │ │ │ │ └── spline.json │ │ │ └── value.json │ │ ├── plugin.legend/ │ │ │ ├── borderRadius/ │ │ │ │ └── legend-border-radius.js │ │ │ ├── horizontal-rtl-hitbox.js │ │ │ ├── label-textAlign/ │ │ │ │ ├── center.js │ │ │ │ ├── horizontal-left.js │ │ │ │ ├── horizontal-right.js │ │ │ │ ├── horizontal-rtl-left.js │ │ │ │ ├── horizontal-rtl-right.js │ │ │ │ ├── left.js │ │ │ │ ├── right.js │ │ │ │ ├── rtl-center.js │ │ │ │ ├── rtl-left.js │ │ │ │ └── rtl-right.js │ │ │ ├── legend-doughnut-bottom-center-mulitiline.json │ │ │ ├── legend-doughnut-bottom-center-single.json │ │ │ ├── legend-doughnut-bottom-end-mulitiline.json │ │ │ ├── legend-doughnut-bottom-start-mulitiline.json │ │ │ ├── legend-doughnut-left-center-mulitiline.json │ │ │ ├── legend-doughnut-left-center-single.json │ │ │ ├── legend-doughnut-left-default-center.json │ │ │ ├── legend-doughnut-left-end-mulitiline.json │ │ │ ├── legend-doughnut-left-start-mulitiline.json │ │ │ ├── legend-doughnut-point-style.json │ │ │ ├── legend-doughnut-right-center-mulitiline-labels.json │ │ │ ├── legend-doughnut-right-center-mulitiline.json │ │ │ ├── legend-doughnut-right-center-single.json │ │ │ ├── legend-doughnut-right-default-center.json │ │ │ ├── legend-doughnut-right-end-mulitiline.json │ │ │ ├── legend-doughnut-right-start-mulitiline.json │ │ │ ├── legend-doughnut-top-center-mulitiline.json │ │ │ ├── legend-doughnut-top-center-single.json │ │ │ ├── legend-doughnut-top-end-mulitiline.json │ │ │ ├── legend-doughnut-top-start-mulitiline.json │ │ │ ├── legend-line-chart-area.json │ │ │ ├── maxWidth/ │ │ │ │ ├── infinity.js │ │ │ │ ├── undefined.js │ │ │ │ └── value.js │ │ │ ├── padding/ │ │ │ │ ├── 2cols-with-padding.js │ │ │ │ └── add-column.js │ │ │ ├── pointStyle-width/ │ │ │ │ ├── legend-pointStyle-width-default.json │ │ │ │ └── legend-pointStyle-width.json │ │ │ └── title/ │ │ │ ├── bottom-center-center.js │ │ │ ├── bottom-end-end.js │ │ │ ├── bottom-start-start.js │ │ │ ├── left-center-center.js │ │ │ ├── left-end-end.js │ │ │ ├── left-start-start.js │ │ │ ├── right-center-center.js │ │ │ ├── right-end-end.js │ │ │ ├── right-start-start.js │ │ │ ├── top-center-center.js │ │ │ ├── top-end-end.js │ │ │ └── top-start-start.js │ │ ├── plugin.subtitle/ │ │ │ └── basic.js │ │ ├── plugin.title/ │ │ │ └── scriptable-options.js │ │ ├── plugin.tooltip/ │ │ │ ├── box-padding.js │ │ │ ├── caret-position.js │ │ │ ├── color-box-border-dash.js │ │ │ ├── color-box-border-radius.js │ │ │ ├── corner-radius.js │ │ │ ├── opacity.js │ │ │ ├── point-style.js │ │ │ └── positioning.js │ │ ├── scale.category/ │ │ │ ├── invalid-data.js │ │ │ ├── max-ticks-limit-a.js │ │ │ ├── max-ticks-limit-b.js │ │ │ ├── max-ticks-limit-norotation.js │ │ │ └── ticks-from-data.js │ │ ├── scale.linear/ │ │ │ ├── grace/ │ │ │ │ ├── grace-10%.js │ │ │ │ ├── grace-beginAtZero.js │ │ │ │ ├── grace-neg.js │ │ │ │ ├── grace-pos.js │ │ │ │ ├── grace.js │ │ │ │ └── issue-8912.js │ │ │ ├── issue-8806.js │ │ │ ├── min-max-skip/ │ │ │ │ ├── edge-case-1.js │ │ │ │ ├── edge-case-2.js │ │ │ │ ├── edge-case-3.js │ │ │ │ ├── edge-case-4.js │ │ │ │ ├── includeBounds.js │ │ │ │ ├── min-max-skip.js │ │ │ │ ├── no-collision.js │ │ │ │ ├── rotated-case-1.js │ │ │ │ ├── rotated-case-2.js │ │ │ │ ├── rotated-case-3.js │ │ │ │ └── rotated-case-4.js │ │ │ ├── rotated-45.js │ │ │ ├── rotated-5.js │ │ │ ├── rotated-85.js │ │ │ ├── tick-count-data-limits.js │ │ │ ├── tick-count-min-max-not-aligned.js │ │ │ ├── tick-count-min-max-not-int.js │ │ │ ├── tick-count-min-max.js │ │ │ ├── tick-step-min-max-step-fp.js │ │ │ ├── tick-step-min-max.js │ │ │ └── tiny-numbers.js │ │ ├── scale.logarithmic/ │ │ │ ├── large-range.js │ │ │ ├── large-values-small-range.js │ │ │ ├── med-range.js │ │ │ ├── min-max.js │ │ │ ├── null-values.js │ │ │ └── small-range.js │ │ ├── scale.radialLinear/ │ │ │ ├── anglelines-disable.json │ │ │ ├── anglelines-indexable.js │ │ │ ├── anglelines-reverse-scale.js │ │ │ ├── anglelines-scriptable.js │ │ │ ├── backgroundColor.js │ │ │ ├── border-dash.json │ │ │ ├── circular-backgroundColor.js │ │ │ ├── circular-border-dash.json │ │ │ ├── gridlines-disable.json │ │ │ ├── gridlines-no-z.json │ │ │ ├── gridlines-scriptable.js │ │ │ ├── gridlines-z.json │ │ │ ├── indexable-gridlines.json │ │ │ ├── pointLabels/ │ │ │ │ ├── background.js │ │ │ │ ├── border-radius.js │ │ │ │ ├── no-more-than-half-radius.js │ │ │ │ ├── padding.js │ │ │ │ └── scriptable-color-small.js │ │ │ └── ticks-below-zero.js │ │ ├── scale.time/ │ │ │ ├── autoskip-major.js │ │ │ ├── bar-large-gap-between-data.js │ │ │ ├── custom-parser.js │ │ │ ├── data-ty.js │ │ │ ├── data-xy.js │ │ │ ├── invalid-data.js │ │ │ ├── labels-date.js │ │ │ ├── labels-strings.js │ │ │ ├── labels.js │ │ │ ├── negative-times.js │ │ │ ├── offset-auto-skip-ticks.js │ │ │ ├── offset-with-1-tick.js │ │ │ ├── offset-with-2-ticks.js │ │ │ ├── offset-with-no-ticks.js │ │ │ ├── skip-null-gridlines.js │ │ │ ├── skip-undefined-gridlines.js │ │ │ ├── source-auto-linear.js │ │ │ ├── source-data-linear.js │ │ │ ├── source-labels-linear-offset-min-max.js │ │ │ ├── source-labels-linear.js │ │ │ ├── ticks-capacity.js │ │ │ ├── ticks-minunit.js │ │ │ ├── ticks-reverse-linear-min-max.js │ │ │ ├── ticks-reverse-linear.js │ │ │ ├── ticks-reverse-offset.js │ │ │ ├── ticks-reverse.js │ │ │ ├── ticks-round.js │ │ │ ├── ticks-stepsize.js │ │ │ └── ticks-unit.js │ │ └── scale.timeseries/ │ │ ├── data-timestamps.js │ │ ├── financial-daily.js │ │ ├── normalize.js │ │ ├── source-auto.js │ │ ├── source-data-offset-min-max.js │ │ ├── source-data.js │ │ ├── source-labels-offset-min-max.js │ │ ├── source-labels.js │ │ ├── ticks-reverse-max.js │ │ ├── ticks-reverse-min-max.js │ │ ├── ticks-reverse-min.js │ │ └── ticks-reverse.js │ ├── index.js │ ├── integration/ │ │ ├── node/ │ │ │ ├── package.json │ │ │ ├── test.cjs │ │ │ └── test.js │ │ ├── node-commonjs/ │ │ │ ├── package.json │ │ │ ├── test-auto.js │ │ │ └── test.js │ │ ├── react-browser/ │ │ │ ├── package.json │ │ │ ├── public/ │ │ │ │ └── index.html │ │ │ ├── src/ │ │ │ │ ├── App.tsx │ │ │ │ ├── AppAuto.tsx │ │ │ │ └── index.tsx │ │ │ └── tsconfig.json │ │ ├── typescript-node/ │ │ │ ├── package.json │ │ │ ├── src/ │ │ │ │ └── index.ts │ │ │ └── tsconfig.json │ │ └── typescript-node-next/ │ │ ├── package.json │ │ ├── src/ │ │ │ └── index.ts │ │ └── tsconfig.json │ ├── seed-reporter.cjs │ ├── specs/ │ │ ├── controller.bar.tests.js │ │ ├── controller.bubble.tests.js │ │ ├── controller.doughnut.tests.js │ │ ├── controller.line.tests.js │ │ ├── controller.polarArea.tests.js │ │ ├── controller.radar.tests.js │ │ ├── controller.scatter.tests.js │ │ ├── core.animation.tests.js │ │ ├── core.animations.tests.js │ │ ├── core.animator.tests.js │ │ ├── core.controller.tests.js │ │ ├── core.datasetController.tests.js │ │ ├── core.defaults.tests.js │ │ ├── core.element.tests.js │ │ ├── core.helpers.tests.js │ │ ├── core.interaction.tests.js │ │ ├── core.layouts.tests.js │ │ ├── core.plugin.tests.js │ │ ├── core.registry.tests.js │ │ ├── core.scale.tests.js │ │ ├── core.ticks.tests.js │ │ ├── element.arc.tests.js │ │ ├── element.bar.tests.js │ │ ├── element.line.tests.js │ │ ├── element.point.tests.js │ │ ├── global.defaults.tests.js │ │ ├── global.namespace.tests.js │ │ ├── helpers.canvas.tests.js │ │ ├── helpers.collection.tests.js │ │ ├── helpers.color.tests.js │ │ ├── helpers.config.tests.js │ │ ├── helpers.core.tests.js │ │ ├── helpers.curve.tests.js │ │ ├── helpers.dom.tests.js │ │ ├── helpers.easing.tests.js │ │ ├── helpers.interpolation.tests.js │ │ ├── helpers.math.tests.js │ │ ├── helpers.options.tests.js │ │ ├── helpers.segment.tests.js │ │ ├── mixed.tests.js │ │ ├── platform.basic.tests.js │ │ ├── platform.dom.tests.js │ │ ├── plugin.colors.tests.js │ │ ├── plugin.decimation.tests.js │ │ ├── plugin.filler.tests.js │ │ ├── plugin.legend.tests.js │ │ ├── plugin.subtitle.tests.js │ │ ├── plugin.title.tests.js │ │ ├── plugin.tooltip.tests.js │ │ ├── scale.category.tests.js │ │ ├── scale.linear.tests.js │ │ ├── scale.logarithmic.tests.js │ │ ├── scale.radialLinear.tests.js │ │ └── scale.time.tests.js │ └── types/ │ ├── .eslintrc.yml │ ├── animation.ts │ ├── autogen.js │ ├── chart_types.ts │ ├── controllers/ │ │ ├── bar_floating_data.ts │ │ ├── bubble_chart_options.ts │ │ ├── doughnut_meta_total.ts │ │ ├── doughnut_offset.ts │ │ ├── doughnut_outer_radius.ts │ │ ├── doughnut_spacing_offset.ts │ │ ├── line_scriptable_parsed_data.ts │ │ ├── line_segments.ts │ │ ├── line_span_gaps.ts │ │ ├── line_styling_array.ts │ │ └── radar_dataset_indexable_options.ts │ ├── data_types.ts │ ├── dataset_null_data.ts │ ├── date_adapter.ts │ ├── defaults.ts │ ├── elements/ │ │ └── scriptable_element_options.ts │ ├── extensions/ │ │ ├── plugin.ts │ │ └── scale.ts │ ├── helpers/ │ │ ├── dom.ts │ │ └── options.ts │ ├── interaction.ts │ ├── layout/ │ │ └── position.ts │ ├── options.ts │ ├── overrides.ts │ ├── parsed.data.type.ts │ ├── plugins/ │ │ ├── defaults.ts │ │ ├── plugin.colors/ │ │ │ └── colors.ts │ │ ├── plugin.decimation/ │ │ │ └── decimation_algorithm.ts │ │ ├── plugin.filler/ │ │ │ └── fill_target_true.ts │ │ └── plugin.tooltip/ │ │ ├── chart.tooltip.ts │ │ ├── tooltip_dataset_type.ts │ │ ├── tooltip_parsed_data.ts │ │ ├── tooltip_parsed_data_chart_defaults.ts │ │ └── tooltip_scriptable_background_color.ts │ ├── register.ts │ ├── scales/ │ │ ├── chart_options.ts │ │ ├── options.ts │ │ └── time_string_max.ts │ ├── scriptable.ts │ ├── scriptable_core_chart_options.ts │ ├── test_instance_assignment.ts │ ├── ticks/ │ │ └── ticks.ts │ └── tsconfig.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .browserslistrc ================================================ defaults not IE 11 not IE_Mob 11 maintained node versions ================================================ FILE: .codeclimate.yml ================================================ version: "2" plugins: duplication: enabled: true config: languages: - javascript fixme: enabled: true checks: argument-count: config: threshold: 5 method-complexity: config: threshold: 7 exclude_patterns: - "dist/" - "docs/" - "scripts/" - "test/" - "*.js" - "*.json" - "*.md" - ".*" ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org root = true [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.html] indent_style = tab indent_size = 4 ================================================ FILE: .eslintignore ================================================ dist/* test/integration/react-browser/* ================================================ FILE: .eslintrc.yml ================================================ extends: - chartjs - plugin:es/restrict-to-es2018 - plugin:markdown/recommended settings: es: aggressive: true env: es6: true browser: true node: true parserOptions: ecmaVersion: 2022 sourceType: module ecmaFeatures: impliedStrict: true modules: true plugins: ['html', 'es'] rules: class-methods-use-this: "off" complexity: ["warn", 10] max-statements: ["warn", 30] no-empty-function: "off" no-use-before-define: ["error", { "functions": false }] # disable everything, except Rest/Spread Properties in ES2018 es/no-import-meta: "off" es/no-async-iteration: "error" es/no-malformed-template-literals: "error" es/no-regexp-lookbehind-assertions: "error" es/no-regexp-named-capture-groups: "error" es/no-regexp-s-flag: "error" es/no-regexp-unicode-property-escapes: "error" es/no-dynamic-import: "off" overrides: - files: ['**/*.ts'] parser: '@typescript-eslint/parser' plugins: - '@typescript-eslint' extends: - chartjs - plugin:@typescript-eslint/recommended rules: complexity: ["warn", 10] max-statements: ["warn", 30] # Replace stock eslint rules with typescript-eslint equivalents for proper # TypeScript support. indent: "off" "@typescript-eslint/indent": ["error", 2] no-use-before-define: "off" '@typescript-eslint/no-use-before-define': "error" no-shadow: "off" '@typescript-eslint/no-shadow': "error" space-before-function-paren: "off" '@typescript-eslint/space-before-function-paren': [2, never] ================================================ FILE: .github/ISSUE_TEMPLATE/bug.yml ================================================ name: Bug Report description: Something went awry labels: ["type: bug"] body: - type: markdown attributes: value: | Need help or support? Please don't open an issue! Head to https://stackoverflow.com/questions/tagged/chart.js. - type: markdown attributes: value: "Bug reports MUST be submitted with an interactive example: https://codepen.io/leelenaleee/pen/WNyJXEe." - type: markdown attributes: value: Chart.js versions lower then 4.x are NOT supported anymore, new issues will be disregarded. - type: textarea attributes: label: Expected behavior description: Tell us what should happen. validations: required: true - type: textarea attributes: label: Current behavior description: Tell us what happens instead of the expected behavior. validations: required: true - type: input attributes: label: Reproducible sample description: | Please provide issue reproduction. You can use [this codepen](https://codepen.io/leelenaleee/pen/WNyJXEe) to make a reproducible sample. Major framework wrappers for chart.js templates: [vue-chart-3 sandbox (Vue)](https://codesandbox.io/s/vue-chart-3-chart-js-issue-template-bpg7k?file=/src/App.vue) [ng2-charts sandbox (Angular)](https://codesandbox.io/s/ng2charts-chart-js-issue-template-fhezt?file=/src/app/app.component.ts) [react-chartjs-2 sandbox (React)](https://codesandbox.io/p/sandbox/react-chartjs-2-chart-js-issue-template-v4-forked-lqz5tn?file=%2Fsrc%2FApp.tsx) For typescript issues you can make use of [this TS Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgYQBYENZwL5wGZQQhwDkAxhrAHQBWAziQNwCwAUGwG6ZxkwAecALxwAJhDIBXEAFMAdjCoBzaTACiAG2kz5AIQCeASREAKAEQg9aTDFMBKOOjpwAEgBUAsgBlk6WVzoaWnIwLKxcUHAWVljCstIA7iiUMMa8fAA0iGxwOXAwemDSAFyk6sBxJOnZuSLoMOglCNW5ueroAEbS6nQlANqmAErSIqaZpjrqEtKjcKYAml3qEPEzpgDiUNJyqwAKElBgmqsA8lC+yqYAulWsLS219XQqPXC9Tbd3n22d6iUkAMRwCB4OAANQgMGkDBun0+DwarwAjAAmTKIgCcmQAzJkAKyZVFwLHXZp3bCXUnYGG5CBgGDACCyF7vT50MjoTTM0ktPiNbl3fk5KmCuB6PkfWFwEXYfkyiU4NjYWyMIA) to make a reproducible sample. If filing a bug against `master`, you may reference the latest code via https://www.chartjs.org/dist/master/chart.umd.min.js (changing the filename to point at the file you need as appropriate). Do not rely on these files for production purposes as they may be removed at any time. validations: required: true - type: textarea attributes: label: Optional extra steps/info to reproduce - type: textarea attributes: label: Possible solution description: If you have suggestions on a fix for the bug. - type: textarea attributes: label: Context description: | How has this issue affected you? What are you trying to accomplish? Providing context helps us come up with a solution that is most useful in the real world. - type: input attributes: label: chart.js version description: Which version of `chart.js` are you using? placeholder: "v0.0.0" validations: required: true - type: input attributes: label: Browser name and version - type: input attributes: label: Link to your project ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Support, Help, and Advice url: https://stackoverflow.com/questions/tagged/chart.js about: Need help or support? Head to https://stackoverflow.com/questions/tagged/chart.js ================================================ FILE: .github/ISSUE_TEMPLATE/docs.yml ================================================ name: Documentation description: Are the docs lacking or missing something? labels: ["type: documentation"] body: - type: checkboxes attributes: label: "Documentation Is:" options: - label: Missing or needed? - label: Confusing - label: Not sure? - type: textarea attributes: label: Please Explain in Detail... validations: required: true - type: textarea attributes: label: Your Proposal for Changes validations: required: true - type: input attributes: label: Example description: | Provide a link to a live example demonstrating the issue or feature to be documented: Normal: https://codepen.io/pen?template=BapRepQ TS: [TS Playground](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgYQBYENZwL5wGZQQhwDkAxhrAHQBWAziQNwCwAUGwG6ZxkwAecALxwAJhDIBXEAFMAdjCoBzaTACiAG2kz5AIQCeASREAKAEQg9aTDFMBKOOjpwAEgBUAsgBlk6WVzoaWnIwLKxcUHAWVljCstIA7iiUMMa8fAA0iGxwOXAwemDSAFyk6sBxJOnZuSLoMOglCNW5ueroAEbS6nQlANqmAErSIqaZpjrqEtKjcKYAml3qEPEzpgDiUNJyqwAKElBgmqsA8lC+yqYAulWsLS219XQqPXC9Tbd3n22d6iUkAMRwCB4OAANQgMGkDBun0+DwarwAjAAmTKIgCcmQAzJkAKyZVFwLHXZp3bCXUnYGG5CBgGDACCyF7vT50MjoTTM0ktPiNbl3fk5KmCuB6PkfWFwEXYfkyiU4NjYWyMIA) ================================================ FILE: .github/ISSUE_TEMPLATE/feature.yml ================================================ name: Feature Request description: Suggest an idea labels: ["type: enhancement"] body: - type: markdown attributes: value: | Most features should start as plugins outside of Chart.js (https://www.chartjs.org/docs/latest/developers/plugins.html). Please consider whether your changes are useful for all users, or if this is specific to your usecase and a Chart.js plugin would be more appropriate. Need help or tech support? Please don't open an issue! Head to https://stackoverflow.com/questions/tagged/chart.js - type: textarea attributes: label: Feature Proposal description: | What are you trying to accomplish? Providing context helps us come up with a solution that is most useful in the real world validations: required: true - type: textarea attributes: label: Possible Implementation description: Not obligatory, but suggest ideas for how to implement the addition or change ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/release-drafter.yml ================================================ name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' categories: - title: 'Breaking Changes' labels: - 'breaking change' - title: 'Enhancements' labels: - 'type: enhancement' - title: 'Performance' labels: - 'type: performance' - title: 'Bugs Fixed' labels: - 'type: bug' - title: 'Types' labels: - 'type: types' - title: 'Documentation' labels: - 'type: documentation' - title: 'Development' labels: - 'type: chore' - 'dependencies' exclude-labels: - 'type: infrastructure' change-template: '- #$NUMBER $TITLE' change-title-escapes: '\<*_&`#@' version-resolver: major: labels: - 'breaking change' minor: labels: - 'type: enhancement' patch: labels: - 'type: bug' - 'type: chore' - 'type: types' default: patch template: | # Essential Links * [npm](https://www.npmjs.com/package/chart.js) * [Migration guide](https://www.chartjs.org/docs/$RESOLVED_VERSION/migration/v4-migration.html) * [Docs](https://www.chartjs.org/docs/$RESOLVED_VERSION/) * [API](https://www.chartjs.org/docs/$RESOLVED_VERSION/api/) * [Samples](https://www.chartjs.org/docs/$RESOLVED_VERSION/samples/information.html) $CHANGES Thanks to $CONTRIBUTORS ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: branches: - master - "2.9" pull_request: branches: - master - "2.9" workflow_dispatch: permissions: contents: read jobs: build: permissions: checks: write # for coverallsapp/github-action to create new checks contents: read # for dorny/paths-filter to fetch a list of changed files pull-requests: read # for dorny/paths-filter to read pull requests runs-on: ${{ matrix.os }} outputs: coveralls: ${{ steps.changes.outputs.src }} strategy: matrix: os: [ubuntu-latest, windows-latest] fail-fast: false steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4.2.0 - name: Use Node.js uses: actions/setup-node@v6 with: node-version: 16 cache: pnpm - uses: dorny/paths-filter@v3 id: changes with: filters: | docs: - 'docs/**' - 'package.json' - 'tsconfig.json' src: - 'src/**' - 'package.json' test: - 'test/**' - 'karma.conf.js' - 'package.json' types: - 'package.json' - 'tsconfig.json' - name: Install run: pnpm install - name: Lint run: pnpm run lint - name: Build run: pnpm run build - name: Test if: | (steps.changes.outputs.src == 'true' || steps.changes.outputs.test == 'true') && runner.os != 'Windows' run: | pnpm run build if [ "${{ runner.os }}" == "macOS" ]; then pnpm run test-ci --browsers chrome,safari else xvfb-run --auto-servernum pnpm run test-ci fi shell: bash - name: Package if: steps.changes.outputs.docs == 'true' run: | pnpm run docs pnpm pack - name: Coveralls Parallel - Chrome if: | steps.changes.outputs.src == 'true' && runner.os != 'Windows' uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} path-to-lcov: './coverage/chrome/lcov.info' flag-name: ${{ matrix.os }}-chrome parallel: true - name: Coveralls Parallel - Firefox if: | steps.changes.outputs.src == 'true' && runner.os != 'Windows' uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} path-to-lcov: './coverage/firefox/lcov.info' flag-name: ${{ matrix.os }}-firefox parallel: true finish: permissions: checks: write # for coverallsapp/github-action to create new checks needs: build runs-on: ubuntu-latest steps: - name: Coveralls Finished if: needs.build.outputs.coveralls == 'true' uses: coverallsapp/github-action@master with: github-token: ${{ secrets.github_token }} parallel-finished: true ================================================ FILE: .github/workflows/compressed-size.yml ================================================ name: Compressed Size on: [pull_request] permissions: contents: read jobs: build: permissions: checks: write # for preactjs/compressed-size-action to create and update the checks contents: read # for actions/checkout to fetch code issues: write # for preactjs/compressed-size-action to create comments pull-requests: write # for preactjs/compressed-size-action to write a PR review runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4.2.0 - uses: preactjs/compressed-size-action@v2 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" ================================================ FILE: .github/workflows/deploy-docs.yml ================================================ # This workflow publishes new documentation to https://chartjs.org/docs/master after every commit name: Deploy docs on: push: branches: - master permissions: contents: read jobs: correct_repository: permissions: contents: none runs-on: ubuntu-latest steps: - name: fail on fork if: github.repository_owner != 'chartjs' run: exit 1 build: needs: correct_repository runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4.2.0 - name: Use Node.js uses: actions/setup-node@v6 with: node-version: 16 cache: pnpm - name: Package & Deploy Docs run: | pnpm install pnpm run build ./scripts/docs-config.sh "master" pnpm run docs pnpm pack ./scripts/deploy-docs.sh "master" env: GITHUB_TOKEN: ${{ secrets.GH_AUTH_TOKEN }} GH_AUTH_EMAIL: ${{ secrets.GH_AUTH_EMAIL }} ================================================ FILE: .github/workflows/release-drafter.yml ================================================ name: Release Drafter on: push: branches: - master workflow_dispatch: permissions: contents: read jobs: correct_repository: permissions: contents: none runs-on: ubuntu-latest steps: - name: fail on fork if: github.repository_owner != 'chartjs' run: exit 1 update_release_draft: permissions: contents: write # for release-drafter/release-drafter to create a github release pull-requests: write # for release-drafter/release-drafter to add label to PR needs: correct_repository runs-on: ubuntu-latest steps: - uses: release-drafter/release-drafter@v6 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: release: types: [published] permissions: contents: read jobs: setup: permissions: contents: none runs-on: ubuntu-latest outputs: version: ${{ steps.trim.outputs.version }} steps: - id: trim run: echo "version=${TAG:1}" >> $GITHUB_OUTPUT env: TAG: ${{ github.event.release.tag_name }} release: permissions: contents: write # for actions/upload-release-asset to upload release asset needs: setup runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4.2.0 - uses: actions/setup-node@v6 with: registry-url: https://registry.npmjs.org/ node-version: 16 cache: pnpm - name: Setup and build run: | pnpm install pnpm install -g json json -I -f package.json -e "this.version=\"$VERSION\"" pnpm run build ./scripts/docs-config.sh "$VERSION" release pnpm run docs pnpm pack env: VERSION: ${{ needs.setup.outputs.version }} - name: Publish to NPM run: ./scripts/publish.sh env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} VERSION: ${{ needs.setup.outputs.version }} - name: Deploy Docs run: ./scripts/deploy-docs.sh "$VERSION" release env: GITHUB_TOKEN: ${{ secrets.GH_AUTH_TOKEN }} GH_AUTH_EMAIL: ${{ secrets.GH_AUTH_EMAIL }} VERSION: ${{ needs.setup.outputs.version }} - name: Upload NPM package file id: upload-npm-package-file uses: actions/upload-release-asset@v1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VERSION: ${{ needs.setup.outputs.version }} with: upload_url: ${{ github.event.release.upload_url }} asset_path: ${{ format('chart.js-{0}.tgz', needs.setup.outputs.version) }} asset_name: ${{ format('chart.js-{0}.tgz', needs.setup.outputs.version) }} asset_content_type: application/gzip release-tag: needs: [setup, release] runs-on: ubuntu-latest if: "!github.event.release.prerelease" steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v4.2.0 - uses: actions/setup-node@v6 with: registry-url: https://registry.npmjs.org/ node-version: 16 cache: pnpm - name: Setup and build run: | pnpm install pnpm install -g json json -I -f package.json -e "this.version=\"$VERSION\"" pnpm run build ./scripts/docs-config.sh "$VERSION" pnpm run docs env: VERSION: ${{ needs.setup.outputs.version }} - name: Deploy Docs run: ./scripts/deploy-docs.sh "$VERSION" env: GITHUB_TOKEN: ${{ secrets.GH_AUTH_TOKEN }} GH_AUTH_EMAIL: ${{ secrets.GH_AUTH_EMAIL }} VERSION: ${{ needs.setup.outputs.version }} ================================================ FILE: .gitignore ================================================ # Deployment /coverage /custom /dist /gh-pages # Node.js node_modules/ npm-debug.log* # Docs .cache-loader build/ # Generated type docs docs/api docs/.vuepress/dist # Development .DS_Store .env.local .env.development.local .env.test.local .env.production.local .idea .project .settings .vscode .zed *.log *.swp *.stackdump # Generated /test/types/autogen*.ts # Eslint .eslintcache ================================================ FILE: .htmllintrc ================================================ { "indent-style": "tabs", "line-end-style": false, "attr-quote-style": "double", "spec-char-escape": false, "attr-bans": [ "align", "background", "bgcolor", "border", "frameborder", "longdesc", "marginwidth", "marginheight", "scrolling" ], "tag-bans": [ "b", "i" ], "id-class-style": false } ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2014-2024 Chart.js Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MAINTAINING.md ================================================ # Maintaining ## Release Process Chart.js relies on [Travis CI](https://travis-ci.org/) to automate the library [releases](https://github.com/chartjs/Chart.js/releases). ### Releasing a New Version 1. Update the release version on [GitHub](https://github.com/chartjs/Chart.js/releases/new) for the release drafted by the `release-drafter` tool 2. Publish the release 3. follow the build process on [GitHub Actions](https://github.com/chartjs/Chart.js/actions?query=workflow%3A%22Node.js+Package%22) Creation of this tag triggers a new build: * `Chart.js.zip` package is generated, containing dist files and examples * `dist/*.js`, `types/*.ts`, and `Chart.js.zip` are attached to the GitHub release (downloads) * A new npm package is published on [npmjs](https://www.npmjs.com/package/chart.js) Finally, [cdnjs](https://cdnjs.com/libraries/Chart.js) is automatically updated from the npm release. ### Releasing a patch version If there is a need to create a patch version for an older release: 1. Create a branch for the patch version (without the `v` prefix) 2. Cherry pick the needed commit(s) to that new branch from master 3. Trigger the release-drafter workflow on that branch from the actions. 4. Follow the procedure for [Releasing a New Version](#releasing-a-new-version) ### Further Reading * [GitHub Action releases](https://github.com/chartjs/Chart.js/pull/7891) * [dist/* files](https://github.com/chartjs/Chart.js/issues/3033) * [cdnjs npm auto update](https://github.com/cdnjs/cdnjs/pull/8401) ================================================ FILE: README.md ================================================

https://www.chartjs.org/
Simple yet flexible JavaScript charting for designers & developers

Downloads GitHub Workflow Status Coverage Awesome Discord

## Documentation All the links point to the new version 4 of the lib. * [Introduction](https://www.chartjs.org/docs/latest/) * [Getting Started](https://www.chartjs.org/docs/latest/getting-started/index) * [General](https://www.chartjs.org/docs/latest/general/data-structures) * [Configuration](https://www.chartjs.org/docs/latest/configuration/index) * [Charts](https://www.chartjs.org/docs/latest/charts/line) * [Axes](https://www.chartjs.org/docs/latest/axes/index) * [Developers](https://www.chartjs.org/docs/latest/developers/index) * [Popular Extensions](https://github.com/chartjs/awesome) * [Samples](https://www.chartjs.org/samples/) In case you are looking for an older version of the docs, you will have to specify the specific version in the url like this: [https://www.chartjs.org/docs/2.9.4/](https://www.chartjs.org/docs/2.9.4/) ## Contributing Instructions on building and testing Chart.js can be found in [the documentation](https://www.chartjs.org/docs/master/developers/contributing.html#building-and-testing). Before submitting an issue or a pull request, please take a moment to look over the [contributing guidelines](https://www.chartjs.org/docs/master/developers/contributing) first. For support, please post questions on [Stack Overflow](https://stackoverflow.com/questions/tagged/chart.js) with the `chart.js` tag. ## License Chart.js is available under the [MIT license](LICENSE.md). ================================================ FILE: auto/auto.cjs ================================================ const chartjs = require('../dist/chart.cjs'); const {Chart, registerables} = chartjs; Chart.register(...registerables); module.exports = Object.assign(Chart, chartjs); ================================================ FILE: auto/auto.d.ts ================================================ import {Chart} from '../dist/types.js'; export * from '../dist/types.js'; export default Chart; ================================================ FILE: auto/auto.js ================================================ import {Chart, registerables} from '../dist/chart.js'; Chart.register(...registerables); export * from '../dist/chart.js'; export default Chart; ================================================ FILE: auto/package.json ================================================ { "name": "chart.js-auto", "private": true, "description": "Auto registering package. Exists to support bundlers without exports support such as webpack 4.", "type": "module", "main": "./auto.cjs", "module": "./auto.js", "exports": { "types": "./auto.d.ts", "import": "./auto.js", "require": "./auto.cjs" }, "types": "./auto.d.ts" } ================================================ FILE: composer.json ================================================ { "name": "nnnick/chartjs", "type": "library", "description": "Simple HTML5 charts using the canvas element.", "keywords": [ "chart", "js" ], "homepage": "https://www.chartjs.org/", "license": "MIT", "authors": [ { "name": "NICK DOWNIE", "email": "hello@nickdownie.com" } ], "require": { "php": ">=5.3.3" }, "minimum-stability": "stable", "extra": { "branch-alias": { "release/2.0": "v2.0-dev" } } } ================================================ FILE: docs/.vuepress/config.ts ================================================ import * as path from 'path'; import markdownItInclude from 'markdown-it-include'; import { DefaultThemeConfig, defineConfig, PluginTuple } from 'vuepress/config'; const docsVersion = "VERSION"; const base: `/${string}/` = process.env.NODE_ENV === "development" ? '/docs/master/' : `/docs/${docsVersion}/`; export default defineConfig({ title: 'Chart.js', description: 'Open source HTML5 Charts for your website', theme: 'chartjs', base, dest: path.resolve(__dirname, '../../dist/docs'), head: [ ['link', {rel: 'icon', href: '/favicon.ico'}], ], plugins: [ 'tabs', ['flexsearch'], ['@vuepress/html-redirect', { countdown: 0, }], [ '@vuepress/google-analytics', { 'ga': 'UA-28909194-3' } ], ['redirect', { redirectors: [ // Default sample page when accessing /samples. {base: '/samples', alternative: ['information']}, ], }], ['vuepress-plugin-code-copy', true], ['vuepress-plugin-typedoc', { entryPoints: ['../../src/types/index.d.ts'], hideInPageTOC: true, tsconfig: path.resolve(__dirname, '../../tsconfig.json'), }, ], ['@simonbrunel/vuepress-plugin-versions', { filters: { suffix: (tag) => tag ? ` (${tag})` : '', title: (v, vars) => { return window.location.href.includes('master') ? 'Development (master)' : vars.tag === 'latest' ? 'Latest (' + v + ')' : v + (vars.tag ? ` (${vars.tag})` : '') + ' (outdated)'; }, }, menu: { text: '{{version|title}}', items: [ { text: 'Documentation', items: [ { text: 'Development (master)', link: '/docs/master/', }, { text: 'Latest version', link: '/docs/latest/', }, { type: 'versions', text: '{{version}}{{tag|suffix}}', link: '/docs/{{version}}/', exclude: /^[01]\.|2\.[0-5]\./, group: 'minor', } ] }, { text: 'Release notes (5 latest)', items: [ { type: 'versions', limit: 5, target: '_blank', group: 'patch', link: 'https://github.com/chartjs/Chart.js/releases/tag/v{{version}}' } ] } ] }, }], ] as PluginTuple[], chainWebpack(config) { config.merge({ resolve: { alias: { 'chart.js': path.resolve(__dirname, '../../dist/chart.js'), } } }) config.module.rule('images').use('url-loader').tap(options => ({ ...options, esModule: false })) }, markdown: { extendMarkdown: md => { md.use(markdownItInclude, path.resolve(__dirname, '../')); } }, themeConfig: { repo: 'chartjs/Chart.js', logo: '/favicon.ico', lastUpdated: 'Last Updated', searchPlaceholder: 'Search...', editLinks: false, docsDir: 'docs', chart: { imports: [ ['scripts/register.js'], ['scripts/utils.js', 'Utils'], ['scripts/helpers.js', 'helpers'], ['scripts/components.js', 'components'] ] }, nav: [ {text: 'Home', link: '/'}, {text: 'API', link: '/api/'}, {text: 'Samples', link: `/samples/`}, { text: 'Ecosystem', ariaLabel: 'Community Menu', items: [ { text: 'Awesome', link: 'https://github.com/chartjs/awesome' }, { text: 'Discord', link: 'https://discord.gg/HxEguTK6av' }, { text: 'Stack Overflow', link: 'https://stackoverflow.com/questions/tagged/chart.js' } ] } ], sidebar: { '/api/': 'API', '/samples/': [ 'information', { title: 'Bar Charts', children: [ 'bar/border-radius', 'bar/floating', 'bar/horizontal', 'bar/stacked', 'bar/stacked-groups', 'bar/vertical', ] }, { title: 'Line Charts', children: [ 'line/interpolation', 'line/line', 'line/multi-axis', 'line/point-styling', 'line/segments', 'line/stepped', 'line/styling', ] }, { title: 'Other charts', children: [ 'other-charts/bubble', 'other-charts/combo-bar-line', 'other-charts/doughnut', 'other-charts/multi-series-pie', 'other-charts/pie', 'other-charts/polar-area', 'other-charts/polar-area-center-labels', 'other-charts/radar', 'other-charts/radar-skip-points', 'other-charts/scatter', 'other-charts/scatter-multi-axis', 'other-charts/stacked-bar-line', ] }, { title: 'Area charts', children: [ 'area/line-boundaries', 'area/line-datasets', 'area/line-drawtime', 'area/line-stacked', 'area/radar' ] }, { title: 'Scales', children: [ 'scales/linear-min-max', 'scales/linear-min-max-suggested', 'scales/linear-step-size', 'scales/log', 'scales/stacked', 'scales/time-line', 'scales/time-max-span', 'scales/time-combo', ] }, { title: 'Scale Options', children: [ 'scale-options/center', 'scale-options/grid', 'scale-options/ticks', 'scale-options/titles', ] }, { title: 'Legend', children: [ 'legend/events', 'legend/html', 'legend/point-style', 'legend/position', 'legend/title', ] }, { title: 'Title', children: [ 'title/alignment', ] }, { title: 'Subtitle', children: [ 'subtitle/basic', ] }, { title: 'Tooltip', children: [ 'tooltip/content', 'tooltip/html', 'tooltip/interactions', 'tooltip/point-style', 'tooltip/position', ] }, { title: 'Scriptable Options', children: [ 'scriptable/bar', 'scriptable/bubble', 'scriptable/line', 'scriptable/pie', 'scriptable/polar', 'scriptable/radar', ] }, { title: 'Animations', children: [ 'animations/delay', 'animations/drop', 'animations/loop', 'animations/progressive-line', 'animations/progressive-line-easing', ] }, { title: 'Advanced', children: [ 'advanced/data-decimation', 'advanced/derived-axis-type', 'advanced/derived-chart-type', 'advanced/linear-gradient', 'advanced/programmatic-events', 'advanced/progress-bar', 'advanced/radial-gradient', ] }, { title: 'Plugins', children: [ 'plugins/chart-area-border', 'plugins/doughnut-empty-state', 'plugins/quadrants', ] }, 'utils' ], '/': [ '', { title: 'Getting Started', children: [ 'getting-started/', 'getting-started/installation', 'getting-started/integration', 'getting-started/usage', 'getting-started/using-from-node-js', ] }, { title: 'General', children: [ 'general/accessibility', 'general/colors', 'general/data-structures', 'general/fonts', 'general/options', 'general/padding', 'general/performance' ] }, { title: 'Configuration', children: [ 'configuration/', 'configuration/animations', 'configuration/canvas-background', 'configuration/decimation', 'configuration/device-pixel-ratio', 'configuration/elements', 'configuration/interactions', 'configuration/layout', 'configuration/legend', 'configuration/locale', 'configuration/responsive', 'configuration/subtitle', 'configuration/title', 'configuration/tooltip', ] }, { title: 'Chart Types', children: [ 'charts/area', 'charts/bar', 'charts/bubble', 'charts/doughnut', 'charts/line', 'charts/mixed', 'charts/polar', 'charts/radar', 'charts/scatter', ] }, { title: 'Axes', children: [ 'axes/', { title: 'Cartesian', children: [ 'axes/cartesian/', 'axes/cartesian/category', 'axes/cartesian/linear', 'axes/cartesian/logarithmic', 'axes/cartesian/time', 'axes/cartesian/timeseries' ], }, { title: 'Radial', children: [ 'axes/radial/', 'axes/radial/linear' ], }, 'axes/labelling', 'axes/styling' ] }, { title: 'Developers', children: [ 'developers/', 'developers/api', 'developers/axes', 'developers/charts', 'developers/contributing', 'developers/plugins', 'developers/publishing', ['api/', 'TypeDoc'], 'developers/updates', ] }, { title: 'Migration', children: [ 'migration/v4-migration', 'migration/v3-migration', ] }, ], } as any } as DefaultThemeConfig }); ================================================ FILE: docs/.vuepress/redirects ================================================ /charts/ /charts/line.html /general/ /general/data-structures.html /samples/ /samples/information.html /getting-started/v3-migration/ /migration/v3-migration.html ================================================ FILE: docs/.vuepress/styles/index.styl ================================================ @require '~vuepress-plugin-tabs/dist/themes/default.styl' .theme-default-content &:not(.custom) max-width: unset .chart-view max-width 800px .sidebar-group.is-sub-group.depth-1 > .sidebar-group-items border-left 1px solid rgba($accentColor, 0.25) > .sidebar-heading:not(.open) border-left 1px solid rgba($accentColor, 0.25) margin-left: 0 > .sidebar-heading padding-left calc(1.475rem - 1px) transition border-color .25s padding 0.35rem 1.475rem border-left-width 3px margin-left -1px font-size 1em line-height 1.4 opacity 1 !important &.active, &.open border-left-color $accentColor color $accentColor font-weight bold >.arrow display none >.sidebar-group-items padding-left: 0 .sidebar-group.is-sub-group.depth-1:hover .sidebar-heading:not(.open) color $accentColor margin-left -1px border-left 3px solid rgba($accentColor, 0.25) padding-left calc(1.475rem - 1px) ================================================ FILE: docs/axes/_common.md ================================================ ### Common options to all axes Namespace: `options.scales[scaleId]` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `type` | `string` | | Type of scale being employed. Custom scales can be created and registered with a string key. This allows changing the type of an axis for a chart. | `alignToPixels` | `boolean` | `false` | Align pixel values to device pixels. | `backgroundColor` | [`Color`](/general/colors.md) | | Background color of the scale area. | `border` | `object` | | Border configuration. [more...](/axes/styling.md#border-configuration) | `display` | `boolean`\|`string` | `true` | Controls the axis global visibility (visible when `true`, hidden when `false`). When `display: 'auto'`, the axis is visible only if at least one associated dataset is visible. | `grid` | `object` | | Grid line configuration. [more...](/axes/styling.md#grid-line-configuration) | `min` | `number` | | User defined minimum number for the scale, overrides minimum value from data. [more...](/axes/index.md#axis-range-settings) | `max` | `number` | | User defined maximum number for the scale, overrides maximum value from data. [more...](/axes/index.md#axis-range-settings) | `reverse` | `boolean` | `false` | Reverse the scale. | `stacked` | `boolean`\|`string` | `false` | Should the data be stacked. [more...](/axes/index.md#stacking) | `suggestedMax` | `number` | | Adjustment used when calculating the maximum data value. [more...](/axes/index.md#axis-range-settings) | `suggestedMin` | `number` | | Adjustment used when calculating the minimum data value. [more...](/axes/index.md#axis-range-settings) | `ticks` | `object` | | Tick configuration. [more...](/axes/index.md#tick-configuration) | `weight` | `number` | `0` | The weight used to sort the axis. Higher weights are further away from the chart area. ================================================ FILE: docs/axes/_common_ticks.md ================================================ ### Common tick options to all axes Namespace: `options.scales[scaleId].ticks` | Name | Type | Scriptable | Default | Description | ---- | ---- | :-------------------------------: | ------- | ----------- | `backdropColor` | [`Color`](../../general/colors.md) | Yes | `'rgba(255, 255, 255, 0.75)'` | Color of label backdrops. | `backdropPadding` | [`Padding`](../../general/padding.md) | | `2` | Padding of label backdrop. | `callback` | `function` | | | Returns the string representation of the tick value as it should be displayed on the chart. See [callback](/axes/labelling.md#creating-custom-tick-formats). | `display` | `boolean` | | `true` | If true, show tick labels. | `color` | [`Color`](/general/colors.md) | Yes | `Chart.defaults.color` | Color of ticks. | `font` | `Font` | Yes | `Chart.defaults.font` | See [Fonts](/general/fonts.md) | `major` | `object` | | `{}` | [Major ticks configuration](/axes/styling.md#major-tick-configuration). | `padding` | `number` | | `3` | Sets the offset of the tick labels from the axis | `showLabelBackdrop` | `boolean` | Yes | `true` for radial scale, `false` otherwise | If true, draw a background behind the tick labels. | `textStrokeColor` | [`Color`](/general/colors.md) | Yes | `` | The color of the stroke around the text. | `textStrokeWidth` | `number` | Yes | `0` | Stroke width around the text. | `z` | `number` | | `0` | z-index of tick layer. Useful when ticks are drawn on chart area. Values <= 0 are drawn under datasets, > 0 on top. ================================================ FILE: docs/axes/cartesian/_common.md ================================================ ### Common options to all cartesian axes Namespace: `options.scales[scaleId]` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `bounds` | `string` | `'ticks'` | Determines the scale bounds. [more...](./index.md#scale-bounds) | `clip` | `boolean` | `true` | If true, clip the dataset drawing against the size of the scale instead of chart area | `position` | `string` \| `object` | | Position of the axis. [more...](./index.md#axis-position) | `stack` | `string` | | Stack group. Axes at the same `position` with same `stack` are stacked. | `stackWeight` | `number` | 1 | Weight of the scale in stack group. Used to determine the amount of allocated space for the scale within the group. | `axis` | `string` | | Which type of axis this is. Possible values are: `'x'`, `'y'`. If not set, this is inferred from the first character of the ID which should be `'x'` or `'y'`. | `offset` | `boolean` | `false` | If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to `true` for a bar chart by default. | `title` | `object` | | Scale title configuration. [more...](../labelling.md#scale-title-configuration) ================================================ FILE: docs/axes/cartesian/_common_ticks.md ================================================ ### Common tick options to all cartesian axes Namespace: `options.scales[scaleId].ticks` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `align` | `string` | `'center'` | The tick alignment along the axis. Can be `'start'`, `'center'`, `'end'`, or `'inner'`. `inner` alignment means align `start` for first tick and `end` for the last tick of horizontal axis | `crossAlign` | `string` | `'near'` | The tick alignment perpendicular to the axis. Can be `'near'`, `'center'`, or `'far'`. See [Tick Alignment](/axes/cartesian/#tick-alignment) | `sampleSize` | `number` | `ticks.length` | The number of ticks to examine when deciding how many labels will fit. Setting a smaller value will be faster, but may be less accurate when there is large variability in label length. | `autoSkip` | `boolean` | `true` | If true, automatically calculates how many labels can be shown and hides labels accordingly. Labels will be rotated up to `maxRotation` before skipping any. Turn `autoSkip` off to show all labels no matter what. | `autoSkipPadding` | `number` | `3` | Padding between the ticks on the horizontal axis when `autoSkip` is enabled. | `includeBounds` | `boolean` | `true` | Should the defined `min` and `max` values be presented as ticks even if they are not "nice". | `labelOffset` | `number` | `0` | Distance in pixels to offset the label from the centre point of the tick (in the x-direction for the x-axis, and the y-direction for the y-axis). *Note: this can cause labels at the edges to be cropped by the edge of the canvas* | `maxRotation` | `number` | `50` | Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. *Note: Only applicable to horizontal scales.* | `minRotation` | `number` | `0` | Minimum rotation for tick labels. *Note: Only applicable to horizontal scales.* | `mirror` | `boolean` | `false` | Flips tick labels around axis, displaying the labels inside the chart instead of outside. *Note: Only applicable to vertical scales.* | `padding` | `number` | `0` | Padding between the tick label and the axis. When set on a vertical axis, this applies in the horizontal (X) direction. When set on a horizontal axis, this applies in the vertical (Y) direction. | `maxTicksLimit` | `number` | `11` | Maximum number of ticks and gridlines to show. ================================================ FILE: docs/axes/cartesian/category.md ================================================ # Category Axis If the global configuration is used, labels are drawn from one of the label arrays included in the chart data. If only `data.labels` is defined, this will be used. If `data.xLabels` is defined and the axis is horizontal, this will be used. Similarly, if `data.yLabels` is defined and the axis is vertical, this property will be used. Using both `xLabels` and `yLabels` together can create a chart that uses strings for both the X and Y axes. Specifying any of the settings above defines the x-axis as `type: 'category'` if not defined otherwise. For more fine-grained control of category labels, it is also possible to add `labels` as part of the category axis definition. Doing so does not apply the global defaults. ## Category Axis Definition Globally: ```javascript let chart = new Chart(ctx, { type: ... data: { labels: ['January', 'February', 'March', 'April', 'May', 'June'], datasets: ... } }); ``` As part of axis definition: ```javascript let chart = new Chart(ctx, { type: ... data: ... options: { scales: { x: { type: 'category', labels: ['January', 'February', 'March', 'April', 'May', 'June'] } } } }); ``` ## Configuration Options ### Category Axis specific options Namespace: `options.scales[scaleId]` | Name | Type | Description | ---- | ---- | ----------- | `min` | `string`\|`number` | The minimum item to display. [more...](#min-max-configuration) | `max` | `string`\|`number` | The maximum item to display. [more...](#min-max-configuration) | `labels` | `string[]`\|`string[][]` | An array of labels to display. When an individual label is an array of strings, each item is rendered on a new line. !!!include(axes/cartesian/_common.md)!!! !!!include(axes/_common.md)!!! ## Tick Configuration !!!include(axes/cartesian/_common_ticks.md)!!! !!!include(axes/_common_ticks.md)!!! ## Min Max Configuration For both the `min` and `max` properties, the value must be `string` in the `labels` array or `numeric` value as an index of a label in that array. In the example below, the x axis would only display "March" through "June". ```javascript let chart = new Chart(ctx, { type: 'line', data: { datasets: [{ data: [10, 20, 30, 40, 50, 60] }], labels: ['January', 'February', 'March', 'April', 'May', 'June'] }, options: { scales: { x: { min: 'March' } } } }); ``` ## Internal data format Internally category scale uses label indices ================================================ FILE: docs/axes/cartesian/index.md ================================================ # Cartesian Axes Axes that follow a cartesian grid are known as 'Cartesian Axes'. Cartesian axes are used for line, bar, and bubble charts. Five cartesian axes are included in Chart.js by default. * [linear](./linear.md) * [logarithmic](./logarithmic.md) * [category](./category.md) * [time](./time.md) * [timeseries](./timeseries.md) ## Visual Components A cartesian axis is composed of visual components that can be individually configured. These components are: * [border](#border) * [grid lines](#grid-lines) * [tick](#ticks-and-tick-marks) * [tick mark](#ticks-and-tick-marks) * [title](#title) ### Border The axis border is drawn at the edge of the axis, beside the chart area. In the image below, it is drawn in red. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ label: 'My First dataset', backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgb(54, 162, 235)', borderWidth: 1, data: [10, 20, 30, 40, 50, 0, 5], }] }; // // const config = { type: 'line', data, options: { scales: { x: { border: { color: 'red' } } } } }; // module.exports = { actions: [], config: config, }; ``` ### Grid lines The grid lines for an axis are drawn on the chart area. In the image below, they are red. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ label: 'My First dataset', backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgb(54, 162, 235)', borderWidth: 1, data: [10, 20, 30, 40, 50, 0, 5], }] }; // // const config = { type: 'line', data, options: { scales: { x: { grid: { color: 'red', borderColor: 'grey', tickColor: 'grey' } } } } }; // module.exports = { actions: [], config: config, }; ``` ### Ticks and Tick Marks Ticks represent data values on the axis that appear as labels. The tick mark is the extension of the grid line from the axis border to the label. In this example, the tick mark is drawn in red while the tick label is drawn in blue. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ label: 'My First dataset', backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgb(54, 162, 235)', borderWidth: 1, data: [10, 20, 30, 40, 50, 0, 5], }] }; // // const config = { type: 'line', data, options: { scales: { x: { grid: { tickColor: 'red' }, ticks: { color: 'blue', } } } } }; // module.exports = { actions: [], config: config, }; ``` ### Title The title component of the axis is used to label the data. In the example below, it is shown in red. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ label: 'My First dataset', backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgb(54, 162, 235)', borderWidth: 1, data: [10, 20, 30, 40, 50, 0, 5], }] }; // // const config = { type: 'line', data, options: { scales: { x: { title: { color: 'red', display: true, text: 'Month' } } } } }; // module.exports = { actions: [], config: config, }; ``` ## Common Configuration :::tip Note These are only the common options supported by all cartesian axes. Please see the specific axis documentation for all the available options for that axis. ::: !!!include(axes/cartesian/_common.md)!!! !!!include(axes/_common.md)!!! ### Axis Position An axis can either be positioned at the edge of the chart, at the center of the chart area, or dynamically with respect to a data value. To position the axis at the edge of the chart, set the `position` option to one of: `'top'`, `'left'`, `'bottom'`, `'right'`. To position the axis at the center of the chart area, set the `position` option to `'center'`. In this mode, either the `axis` option must be specified or the axis ID has to start with the letter 'x' or 'y'. This is so chart.js knows what kind of axis (horizontal or vertical) it is. To position the axis with respect to a data value, set the `position` option to an object such as: ```javascript { x: -20 } ``` This will position the axis at a value of -20 on the axis with ID "x". For cartesian axes, only 1 axis may be specified. ### Scale Bounds The `bounds` property controls the scale boundary strategy (bypassed by `min`/`max` options). * `'data'`: makes sure data are fully visible, labels outside are removed * `'ticks'`: makes sure ticks are fully visible, data outside are truncated ### Tick Configuration :::tip Note These are only the common tick options supported by all cartesian axes. Please see specific axis documentation for all of the available options for that axis. ::: !!!include(axes/cartesian/_common_ticks.md)!!! !!!include(axes/_common_ticks.md)!!! ### Tick Alignment The alignment of ticks is primarily controlled using two settings on the tick configuration object: `align` and `crossAlign`. The `align` setting configures how labels align with the tick mark along the axis direction (i.e. horizontal for a horizontal axis and vertical for a vertical axis). The `crossAlign` setting configures how labels align with the tick mark in the perpendicular direction (i.e. vertical for a horizontal axis and horizontal for a vertical axis). In the example below, the `crossAlign` setting is used to left align the labels on the Y axis. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ label: 'My First dataset', backgroundColor: [ 'rgba(255, 99, 132, 0.2)', 'rgba(255, 159, 64, 0.2)', 'rgba(255, 205, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(201, 203, 207, 0.2)' ], borderColor: [ 'rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)', 'rgb(153, 102, 255)', 'rgb(201, 203, 207)' ], borderWidth: 1, data: [65, 59, 80, 81, 56, 55, 40], }] }; // // const config = { type: 'bar', data, options: { indexAxis: 'y', scales: { y: { ticks: { crossAlign: 'far', } } } } }; // module.exports = { actions: [], config: config, }; ``` :::tip Note The `crossAlign` setting is only effective when these preconditions are met: * tick rotation is `0` * axis position is `'top'`, '`left'`, `'bottom'` or `'right'` ::: ### Axis ID The properties `dataset.xAxisID` or `dataset.yAxisID` have to match to `scales` property. This is especially needed if multi-axes charts are used. ```javascript const myChart = new Chart(ctx, { type: 'line', data: { datasets: [{ // This dataset appears on the first axis yAxisID: 'first-y-axis' }, { // This dataset appears on the second axis yAxisID: 'second-y-axis' }] }, options: { scales: { 'first-y-axis': { type: 'linear' }, 'second-y-axis': { type: 'linear' } } } }); ``` ## Creating Multiple Axes With cartesian axes, it is possible to create multiple X and Y axes. To do so, you can add multiple configuration objects to the `xAxes` and `yAxes` properties. When adding new axes, it is important to ensure that you specify the type of the new axes as default types are **not** used in this case. In the example below, we are creating two Y axes. We then use the `yAxisID` property to map the datasets to their correct axes. ```javascript const myChart = new Chart(ctx, { type: 'line', data: { datasets: [{ data: [20, 50, 100, 75, 25, 0], label: 'Left dataset', // This binds the dataset to the left y axis yAxisID: 'left-y-axis' }, { data: [0.1, 0.5, 1.0, 2.0, 1.5, 0], label: 'Right dataset', // This binds the dataset to the right y axis yAxisID: 'right-y-axis' }], labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'] }, options: { scales: { 'left-y-axis': { type: 'linear', position: 'left' }, 'right-y-axis': { type: 'linear', position: 'right' } } } }); ``` ================================================ FILE: docs/axes/cartesian/linear.md ================================================ # Linear Axis The linear scale is used to chart numerical data. It can be placed on either the x or y-axis. The scatter chart type automatically configures a line chart to use one of these scales for the x-axis. As the name suggests, linear interpolation is used to determine where a value lies on the axis. ## Configuration Options ### Linear Axis specific options Namespace: `options.scales[scaleId]` | Name | Type | Description | ---- | ---- | ----------- | `beginAtZero` | `boolean` | if true, scale will include 0 if it is not already included. | `grace` | `number`\|`string` | Percentage (string ending with `%`) or amount (number) for added room in the scale range above and below data. [more...](#grace) !!!include(axes/cartesian/_common.md)!!! !!!include(axes/_common.md)!!! ## Tick Configuration ### Linear Axis specific tick options Namespace: `options.scales[scaleId].ticks` | Name | Type | Scriptable | Default | Description | ---- | ---- | ------- | ------- | ----------- | `count` | `number` | Yes | `undefined` | The number of ticks to generate. If specified, this overrides the automatic generation. | `format` | `object` | Yes | | The [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) options used by the default label formatter | `precision` | `number` | Yes | | if defined and `stepSize` is not specified, the step size will be rounded to this many decimal places. | `stepSize` | `number` | Yes | | User-defined fixed step size for the scale. [more...](#step-size) !!!include(axes/cartesian/_common_ticks.md)!!! !!!include(axes/_common_ticks.md)!!! ## Step Size If set, the scale ticks will be enumerated by multiple of `stepSize`, having one tick per increment. If not set, the ticks are labeled automatically using the nice numbers algorithm. This example sets up a chart with a y-axis that creates ticks at `0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5`. ```javascript let options = { scales: { y: { max: 5, min: 0, ticks: { stepSize: 0.5 } } } }; ``` ## Grace If the value is a string ending with `%`, it's treated as a percentage. If a number, it's treated as a value. The value is added to the maximum data value and subtracted from the minimum data. This extends the scale range as if the data values were that much greater. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: ['Positive', 'Negative'], datasets: [{ data: [100, -50], backgroundColor: 'rgb(255, 99, 132)' }], }; // // const config = { type: 'bar', data, options: { scales: { y: { type: 'linear', grace: '5%' } }, plugins: { legend: false } } }; // module.exports = { actions: [], config: config, }; ``` ## Internal data format Internally, the linear scale uses numeric data. ================================================ FILE: docs/axes/cartesian/logarithmic.md ================================================ # Logarithmic Axis The logarithmic scale is used to chart numerical data. It can be placed on either the x or y-axis. As the name suggests, logarithmic interpolation is used to determine where a value lies on the axis. ## Configuration Options !!!include(axes/cartesian/_common.md)!!! !!!include(axes/_common.md)!!! ## Tick Configuration ### Logarithmic Axis specific options Namespace: `options.scales[scaleId].ticks` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `format` | `object` | | The [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) options used by the default label formatter !!!include(axes/cartesian/_common_ticks.md)!!! !!!include(axes/_common_ticks.md)!!! ## Internal data format Internally, the logarithmic scale uses numeric data. ================================================ FILE: docs/axes/cartesian/time.md ================================================ # Time Cartesian Axis The time scale is used to display times and dates. Data are spread according to the amount of time between data points. When building its ticks, it will automatically calculate the most comfortable unit based on the size of the scale. ## Date Adapters The time scale **requires** both a date library and a corresponding adapter to be present. Please choose from the [available adapters](https://github.com/chartjs/awesome#adapters). ## Data Sets ### Input Data See [data structures](../../general/data-structures.md). ### Date Formats When providing data for the time scale, Chart.js uses timestamps defined as milliseconds since the epoch (midnight January 1, 1970, UTC) internally. However, Chart.js also supports all of the formats that your chosen date adapter accepts. You should use timestamps if you'd like to set `parsing: false` for better performance. ## Configuration Options ### Time Axis specific options Namespace: `options.scales[scaleId]` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `min` | `number`\|`string` | | The minimum item to display. [more...](#min-max-configuration) | `max` | `number`\|`string` | | The maximum item to display. [more...](#min-max-configuration) | `suggestedMin` | `number`\|`string` | | The minimum item to display if there is no datapoint before it. [more...](../index.md#axis-range-settings) | `suggestedMax` | `number`\|`string` | | The maximum item to display if there is no datapoint behind it. [more...](../index.md#axis-range-settings) | `adapters.date` | `object` | `{}` | Options for adapter for external date library if that adapter needs or supports options | `bounds` | `string` | `'data'` | Determines the scale bounds. [more...](./index.md#scale-bounds) | `offsetAfterAutoskip` | `boolean` | `false` | If true, bar chart offsets are computed with auto skipped ticks. | `ticks.source` | `string` | `'auto'` | How ticks are generated. [more...](#ticks-source) | `time.displayFormats` | `object` | | Sets how different time units are displayed. [more...](#display-formats) | `time.isoWeekday` | `boolean`\|`number` | `false` | If `boolean` and true and the unit is set to 'week', then the first day of the week will be Monday. Otherwise, it will be Sunday. If `number`, the index of the first day of the week (0 - Sunday, 6 - Saturday) | `time.parser` | `string`\|`function` | | Custom parser for dates. [more...](#parser) | `time.round` | `string` | `false` | If defined, dates will be rounded to the start of this unit. See [Time Units](#time-units) below for the allowed units. | `time.tooltipFormat` | `string` | | The format string to use for the tooltip. | `time.unit` | `string` | `false` | If defined, will force the unit to be a certain type. See [Time Units](#time-units) section below for details. | `time.minUnit` | `string` | `'millisecond'` | The minimum display format to be used for a time unit. !!!include(axes/cartesian/_common.md)!!! !!!include(axes/_common.md)!!! #### Time Units The following time measurements are supported. The names can be passed as strings to the `time.unit` config option to force a certain unit. * `'millisecond'` * `'second'` * `'minute'` * `'hour'` * `'day'` * `'week'` * `'month'` * `'quarter'` * `'year'` For example, to create a chart with a time scale that always displayed units per month, the following config could be used. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { scales: { x: { type: 'time', time: { unit: 'month' } } } } }); ``` #### Display Formats You may specify a map of display formats with a key for each unit: * `millisecond` * `second` * `minute` * `hour` * `day` * `week` * `month` * `quarter` * `year` The format string used as a value depends on the date adapter you chose to use. For example, to set the display format for the `quarter` unit to show the month and year, the following config might be passed to the chart constructor. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { scales: { x: { type: 'time', time: { displayFormats: { quarter: 'MMM YYYY' } } } } } }); ``` #### Ticks Source The `ticks.source` property controls the ticks generation. * `'auto'`: generates "optimal" ticks based on scale size and time options * `'data'`: generates ticks from data (including labels from data `{x|y}` objects) * `'labels'`: generates ticks from user given `labels` ONLY #### Parser If this property is defined as a string, it is interpreted as a custom format to be used by the date adapter to parse the date. If this is a function, it must return a type that can be handled by your date adapter's `parse` method. ## Min Max Configuration For both the `min` and `max` properties, the value must be `string` that is parsable by your date adapter or a number with the amount of milliseconds that have elapsed since UNIX epoch. In the example below the x axis will start at 7 November 2021. ```javascript let chart = new Chart(ctx, { type: 'line', data: { datasets: [{ data: [{ x: '2021-11-06 23:39:30', y: 50 }, { x: '2021-11-07 01:00:28', y: 60 }, { x: '2021-11-07 09:00:28', y: 20 }] }], }, options: { scales: { x: { min: '2021-11-07 00:00:00', } } } }); ``` ## Changing the scale type from Time scale to Logarithmic/Linear scale. When changing the scale type from Time scale to Logarithmic/Linear scale, you need to add `bounds: 'ticks'` to the scale options. Changing the `bounds` parameter is necessary because its default value is the `'data'` for the Time scale. Initial config: ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { scales: { x: { type: 'time', } } } }); ``` Scale update: ```javascript chart.options.scales.x = { type: 'logarithmic', bounds: 'ticks' }; ``` ## Internal data format Internally time scale uses milliseconds since epoch ================================================ FILE: docs/axes/cartesian/timeseries.md ================================================ # Time Series Axis The time series scale extends from the time scale and supports all the same options. However, for the time series scale, each data point is spread equidistant. ## Example ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { scales: { x: { type: 'timeseries', } } } }); ``` ## More details Please see [the time scale documentation](./time.md) for all other details. ================================================ FILE: docs/axes/index.md ================================================ # Axes Axes are an integral part of a chart. They are used to determine how data maps to a pixel value on the chart. In a cartesian chart, there is 1 or more X-axis and 1 or more Y-axis to map points onto the 2-dimensional canvas. These axes are known as ['cartesian axes'](./cartesian/). In a radial chart, such as a radar chart or a polar area chart, there is a single axis that maps points in the angular and radial directions. These are known as ['radial axes'](./radial/). Scales in Chart.js >v2.0 are significantly more powerful, but also different from those of v1.0. * Multiple X & Y axes are supported. * A built-in label auto-skip feature detects would-be overlapping ticks and labels and removes every nth label to keep things displayed normally. * Scale titles are supported. * New scale types can be extended without writing an entirely new chart type. ## Default scales The default `scaleId`'s for cartesian charts are `'x'` and `'y'`. For radial charts: `'r'`. Each dataset is mapped to a scale for each axis (x, y or r) it requires. The scaleId's that a dataset is mapped to is determined by the `xAxisID`, `yAxisID` or `rAxisID`. If the ID for an axis is not specified, the first scale for that axis is used. If no scale for an axis is found, a new scale is created. Some examples: The following chart will have `'x'` and `'y'` scales: ```js let chart = new Chart(ctx, { type: 'line' }); ``` The following chart will have scales `'x'` and `'myScale'`: ```js let chart = new Chart(ctx, { type: 'bar', data: { datasets: [{ data: [1, 2, 3] }] }, options: { scales: { myScale: { type: 'logarithmic', position: 'right', // `axis` is determined by the position as `'y'` } } } }); ``` The following chart will have scales `'xAxis'` and `'yAxis'`: ```js let chart = new Chart(ctx, { type: 'bar', data: { datasets: [{ yAxisID: 'yAxis' }] }, options: { scales: { xAxis: { // The axis for this scale is determined from the first letter of the id as `'x'` // It is recommended to specify `position` and / or `axis` explicitly. type: 'time', } } } }); ``` The following chart will have `'r'` scale: ```js let chart = new Chart(ctx, { type: 'radar' }); ``` The following chart will have `'myScale'` scale: ```js let chart = new Chart(ctx, { type: 'radar', scales: { myScale: { axis: 'r' } } }); ``` ## Common Configuration :::tip Note These are only the common options supported by all axes. Please see specific axis documentation for all the available options for that axis. ::: !!!include(axes/_common.md)!!! ## Tick Configuration :::tip Note These are only the common tick options supported by all axes. Please see specific axis documentation for all the available tick options for that axis. ::: !!!include(axes/_common_ticks.md)!!! ## Axis Range Settings Given the number of axis range settings, it is important to understand how they all interact with each other. The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto-fit behaviour. ```javascript let minDataValue = Math.min(mostNegativeValue, options.suggestedMin); let maxDataValue = Math.max(mostPositiveValue, options.suggestedMax); ``` In this example, the largest positive value is 50, but the data maximum is expanded out to 100. However, because the lowest data value is below the `suggestedMin` setting, it is ignored. ```javascript let chart = new Chart(ctx, { type: 'line', data: { datasets: [{ label: 'First dataset', data: [0, 20, 40, 50] }], labels: ['January', 'February', 'March', 'April'] }, options: { scales: { y: { suggestedMin: 50, suggestedMax: 100 } } } }); ``` In contrast to the `suggested*` settings, the `min` and `max` settings set explicit ends to the axes. When these are set, some data points may not be visible. ## Stacking By default, data is not stacked. If the `stacked` option of the value scale (y-axis on horizontal chart) is `true`, positive and negative values are stacked separately. Additionally, a `stack` option can be defined per dataset to further divide into stack groups [more...](../general/data-structures/#dataset-configuration). For some charts, you might want to stack positive and negative values together. That can be achieved by specifying `stacked: 'single'`. ## Callbacks There are a number of config callbacks that can be used to change parameters in the scale at different points in the update process. The options are supplied at the top level of the axis options. Namespace: `options.scales[scaleId]` | Name | Arguments | Description | ---- | --------- | ----------- | `beforeUpdate` | `axis` | Callback called before the update process starts. | `beforeSetDimensions` | `axis` | Callback that runs before dimensions are set. | `afterSetDimensions` | `axis` | Callback that runs after dimensions are set. | `beforeDataLimits` | `axis` | Callback that runs before data limits are determined. | `afterDataLimits` | `axis` | Callback that runs after data limits are determined. | `beforeBuildTicks` | `axis` | Callback that runs before ticks are created. | `afterBuildTicks` | `axis` | Callback that runs after ticks are created. Useful for filtering ticks. | `beforeTickToLabelConversion` | `axis` | Callback that runs before ticks are converted into strings. | `afterTickToLabelConversion` | `axis` | Callback that runs after ticks are converted into strings. | `beforeCalculateLabelRotation` | `axis` | Callback that runs before tick rotation is determined. | `afterCalculateLabelRotation` | `axis` | Callback that runs after tick rotation is determined. | `beforeFit` | `axis` | Callback that runs before the scale fits to the canvas. | `afterFit` | `axis` | Callback that runs after the scale fits to the canvas. | `afterUpdate` | `axis` | Callback that runs at the end of the update process. ### Updating Axis Defaults The default configuration for a scale can be easily changed. All you need to do is set the new options to `Chart.defaults.scales[type]`. For example, to set the minimum value of 0 for all linear scales, you would do the following. Any linear scales created after this time would now have a minimum of 0. ```javascript Chart.defaults.scales.linear.min = 0; ``` ## Creating New Axes To create a new axis, see the [developer docs](../developers/axes.md). ================================================ FILE: docs/axes/labelling.md ================================================ # Labeling Axes When creating a chart, you want to tell the viewer what data they are viewing. To do this, you need to label the axis. ## Scale Title Configuration Namespace: `options.scales[scaleId].title`, it defines options for the scale title. Note that this only applies to cartesian axes. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `display` | `boolean` | `false` | If true, display the axis title. | `align` | `string` | `'center'` | Alignment of the axis title. Possible options are `'start'`, `'center'` and `'end'` | `text` | `string`\|`string[]` | `''` | The text for the title. (i.e. "# of People" or "Response Choices"). | `color` | [`Color`](../general/colors.md) | `Chart.defaults.color` | Color of label. | `strokeColor` | [`Color`](../general/colors.md) | | Color of text stroke. | `strokeWidth` | `number` | | Size of stroke width, in pixels. | `font` | `Font` | `Chart.defaults.font` | See [Fonts](../general/fonts.md) | `padding` | [`Padding`](../general/padding.md) | `4` | Padding to apply around scale labels. Only `top`, `bottom` and `y` are implemented. ## Creating Custom Tick Formats It is also common to want to change the tick marks to include information about the data type. For example, adding a dollar sign ('$'). To do this, you need to override the `ticks.callback` method in the axis configuration. The method receives 3 arguments: * `value` - the tick value in the **internal data format** of the associated scale. For time scale, it is a timestamp. * `index` - the tick index in the ticks array. * `ticks` - the array containing all of the [tick objects](../api/interfaces/Tick). The call to the method is scoped to the scale. `this` inside the method is the scale object. If the callback returns `null` or `undefined` the associated grid line will be hidden. :::tip The [category axis](../axes/cartesian/category), which is the default x-axis for line and bar charts, uses the `index` as internal data format. For accessing the label, use `this.getLabelForValue(value)`. [API: getLabelForValue](../api/classes/Scale.md#getlabelforvalue) ::: In the following example, every label of the Y-axis would be displayed with a dollar sign at the front. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { scales: { y: { ticks: { // Include a dollar sign in the ticks callback: function(value, index, ticks) { return '$' + value; } } } } } }); ``` Keep in mind that overriding `ticks.callback` means that you are responsible for all formatting of the label. Depending on your use case, you may want to call the default formatter and then modify its output. In the example above, that would look like: ```javascript // call the default formatter, forwarding `this` return '$' + Chart.Ticks.formatters.numeric.apply(this, [value, index, ticks]); ``` Related samples: * [Tick configuration sample](../samples/scale-options/ticks) ================================================ FILE: docs/axes/radial/index.md ================================================ # Radial Axes Radial axes are used specifically for the radar and polar area chart types. These axes overlay the chart area, rather than being positioned on one of the edges. One radial axis is included by default in Chart.js. * [radialLinear](./linear.md) ## Visual Components A radial axis is composed of visual components that can be individually configured. These components are: * [angle lines](#angle-lines) * [grid lines](#grid-lines) * [point labels](#point-labels) * [ticks](#ticks) ### Angle Lines The grid lines for an axis are drawn on the chart area. They stretch out from the center towards the edge of the canvas. In the example below, they are red. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ label: 'My First dataset', backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgb(54, 162, 235)', borderWidth: 1, data: [10, 20, 30, 40, 50, 0, 5], }] }; // // const config = { type: 'radar', data, options: { scales: { r: { angleLines: { color: 'red' } } } } }; // module.exports = { actions: [], config: config, }; ``` ### Grid Lines The grid lines for an axis are drawn on the chart area. In the example below, they are red. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ label: 'My First dataset', backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgb(54, 162, 235)', borderWidth: 1, data: [10, 20, 30, 40, 50, 0, 5], }] }; // // const config = { type: 'radar', data, options: { scales: { r: { grid: { color: 'red' } } } } }; // module.exports = { actions: [], config: config, }; ``` ### Point Labels The point labels indicate the value for each angle line. In the example below, they are red. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ label: 'My First dataset', backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgb(54, 162, 235)', borderWidth: 1, data: [10, 20, 30, 40, 50, 0, 5], }] }; // // const config = { type: 'radar', data, options: { scales: { r: { pointLabels: { color: 'red' } } } } }; // module.exports = { actions: [], config: config, }; ``` ### Ticks The ticks are used to label values based on how far they are from the center of the axis. In the example below, they are red. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ label: 'My First dataset', backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgb(54, 162, 235)', borderWidth: 1, data: [10, 20, 30, 40, 50, 0, 5], }] }; // // const config = { type: 'radar', data, options: { scales: { r: { ticks: { color: 'red' } } } } }; // module.exports = { actions: [], config: config, }; ``` ================================================ FILE: docs/axes/radial/linear.md ================================================ # Linear Radial Axis The linear radial scale is used to chart numerical data. As the name suggests, linear interpolation is used to determine where a value lies in relation to the center of the axis. The following additional configuration options are provided by the radial linear scale. ## Configuration Options ### Linear Radial Axis specific options Namespace: `options.scales[scaleId]` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `animate` | `boolean` | `true` | Whether to animate scaling the chart from the centre | `angleLines` | `object` | | Angle line configuration. [more...](#angle-line-options) | `beginAtZero` | `boolean` | `false` | If true, scale will include 0 if it is not already included. | `pointLabels` | `object` | | Point label configuration. [more...](#point-label-options) | `startAngle` | `number` | `0` | Starting angle of the scale. In degrees, 0 is at top. ### Common options for all axes Namespace: `options.scales[scaleId]` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `type` | `string` | | Type of scale being employed. Custom scales can be created and registered with a string key. This allows changing the type of an axis for a chart. | `alignToPixels` | `boolean` | `false` | Align pixel values to device pixels. | `backgroundColor` | [`Color`](/general/colors.md) | | Background color of the scale area. | `display` | `boolean`\|`string` | `true` | Controls the axis global visibility (visible when `true`, hidden when `false`). When `display: 'auto'`, the axis is visible only if at least one associated dataset is visible. | `grid` | `object` | | Grid line configuration. [more...](#grid-line-configuration) | `min` | `number` | | User defined minimum number for the scale, overrides minimum value from data. [more...](/axes/index.md#axis-range-settings) | `max` | `number` | | User defined maximum number for the scale, overrides maximum value from data. [more...](/axes/index.md#axis-range-settings) | `reverse` | `boolean` | `false` | Reverse the scale. | `stacked` | `boolean`\|`string` | `false` | Should the data be stacked. [more...](/axes/index.md#stacking) | `suggestedMax` | `number` | | Adjustment used when calculating the maximum data value. [more...](/axes/index.md#axis-range-settings) | `suggestedMin` | `number` | | Adjustment used when calculating the minimum data value. [more...](/axes/index.md#axis-range-settings) | `ticks` | `object` | | Tick configuration. [more...](/axes/index.md#tick-configuration) | `weight` | `number` | `0` | The weight used to sort the axis. Higher weights are further away from the chart area. ## Tick Configuration ### Linear Radial Axis specific tick options Namespace: `options.scales[scaleId].ticks` | Name | Type | Scriptable | Default | Description | ---- | ---- | ------- | ------- | ----------- | `count` | `number` | Yes | `undefined` | The number of ticks to generate. If specified, this overrides the automatic generation. | `format` | `object` | Yes | | The [`Intl.NumberFormat`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) options used by the default label formatter | `maxTicksLimit` | `number` | Yes | `11` | Maximum number of ticks and gridlines to show. | `precision` | `number` | Yes | | If defined and `stepSize` is not specified, the step size will be rounded to this many decimal places. | `stepSize` | `number` | Yes | | User defined fixed step size for the scale. [more...](#step-size) !!!include(axes/_common_ticks.md)!!! The scriptable context is described in [Options](../../general/options.md#tick) section. ## Grid Line Configuration Namespace: `options.scales[scaleId].grid`, it defines options for the grid lines of the axis. | Name | Type | Scriptable | Indexable | Default | Description | ---- | ---- | :-------------------------------: | :-----------------------------: | ------- | ----------- | `borderDash` | `number[]` | | | `[]` | Length and spacing of dashes on grid lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | `borderDashOffset` | `number` | Yes | | `0.0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `circular` | `boolean` | | | `false` | If true, gridlines are circular (on radar and polar area charts only). | `color` | [`Color`](../general/colors.md) | Yes | Yes | `Chart.defaults.borderColor` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line, and so on. | `display` | `boolean` | | | `true` | If false, do not display grid lines for this axis. | `lineWidth` | `number` | Yes | Yes | `1` | Stroke width of grid lines. The scriptable context is described in [Options](../general/options.md#tick) section. ## Axis Range Settings Given the number of axis range settings, it is important to understand how they all interact with each other. The `suggestedMax` and `suggestedMin` settings only change the data values that are used to scale the axis. These are useful for extending the range of the axis while maintaining the auto-fit behaviour. ```javascript let minDataValue = Math.min(mostNegativeValue, options.ticks.suggestedMin); let maxDataValue = Math.max(mostPositiveValue, options.ticks.suggestedMax); ``` In this example, the largest positive value is 50, but the data maximum is expanded out to 100. However, because the lowest data value is below the `suggestedMin` setting, it is ignored. ```javascript let chart = new Chart(ctx, { type: 'radar', data: { datasets: [{ label: 'First dataset', data: [0, 20, 40, 50] }], labels: ['January', 'February', 'March', 'April'] }, options: { scales: { r: { suggestedMin: 50, suggestedMax: 100 } } } }); ``` In contrast to the `suggested*` settings, the `min` and `max` settings set explicit ends to the axes. When these are set, some data points may not be visible. ## Step Size If set, the scale ticks will be enumerated by multiple of `stepSize`, having one tick per increment. If not set, the ticks are labeled automatically using the nice numbers algorithm. This example sets up a chart with a y axis that creates ticks at `0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 4.5, 5`. ```javascript let options = { scales: { r: { max: 5, min: 0, ticks: { stepSize: 0.5 } } } }; ``` ## Angle Line Options The following options are used to configure angled lines that radiate from the center of the chart to the point labels. Namespace: `options.scales[scaleId].angleLines` | Name | Type | Scriptable | Default | Description | ---- | ---- | ------- | ------- | ----------- | `display` | `boolean` | | `true` | If true, angle lines are shown. | `color` | [`Color`](../../general/colors.md) | Yes | `Chart.defaults.borderColor` | Color of angled lines. | `lineWidth` | `number` | Yes | `1` | Width of angled lines. | `borderDash` | `number[]` | Yes1 | `[]` | Length and spacing of dashes on angled lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | `borderDashOffset` | `number` | Yes | `0.0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). 1. the `borderDash` setting only accepts a static value or a function. Passing an array of arrays is not supported. The scriptable context is described in [Options](../../general/options.md#pointLabel) section. ## Point Label Options The following options are used to configure the point labels that are shown on the perimeter of the scale. Namespace: `options.scales[scaleId].pointLabels` | Name | Type | Scriptable | Default | Description | ---- | ---- | ------- | ------- | ----------- | `backdropColor` | [`Color`](../../general/colors.md) | `true` | `undefined` | Background color of the point label. | `backdropPadding` | [`Padding`](../../general/padding.md) | | `2` | Padding of label backdrop. | `borderRadius` | `number`\|`object` | `true` | `0` | Border radius of the point label | `display` | `boolean`\|`string` | | `true` | If true, point labels are shown. When `display: 'auto'`, the label is hidden if it overlaps with another label. | `callback` | `function` | | | Callback function to transform data labels to point labels. The default implementation simply returns the current string. | `color` | [`Color`](../../general/colors.md) | Yes | `Chart.defaults.color` | Color of label. | `font` | `Font` | Yes | `Chart.defaults.font` | See [Fonts](../../general/fonts.md) | `padding` | `number` | Yes | 5 | Padding between chart and point labels. | [`centerPointLabels`](../../samples/other-charts/polar-area-center-labels.md) | `boolean` | | `false` | If true, point labels are centered. The scriptable context is described in [Options](../../general/options.md#pointLabel) section. ## Internal data format Internally, the linear radial scale uses numeric data ================================================ FILE: docs/axes/styling.md ================================================ # Styling There are a number of options to allow styling an axis. There are settings to control [grid lines](#grid-line-configuration) and [ticks](#tick-configuration). ## Grid Line Configuration Namespace: `options.scales[scaleId].grid`, it defines options for the grid lines that run perpendicular to the axis. | Name | Type | Scriptable | Indexable | Default | Description | ---- | ---- | :-------------------------------: | :-----------------------------: | ------- | ----------- | `circular` | `boolean` | | | `false` | If true, gridlines are circular (on radar and polar area charts only). | `color` | [`Color`](../general/colors.md) | Yes | Yes | `Chart.defaults.borderColor` | The color of the grid lines. If specified as an array, the first color applies to the first grid line, the second to the second grid line, and so on. | `display` | `boolean` | | | `true` | If false, do not display grid lines for this axis. | `drawOnChartArea` | `boolean` | | | `true` | If true, draw lines on the chart area inside the axis lines. This is useful when there are multiple axes and you need to control which grid lines are drawn. | `drawTicks` | `boolean` | | | `true` | If true, draw lines beside the ticks in the axis area beside the chart. | `lineWidth` | `number` | Yes | Yes | `1` | Stroke width of grid lines. | `offset` | `boolean` | | | `false` | If true, grid lines will be shifted to be between labels. This is set to `true` for a bar chart by default. | `tickBorderDash` | `number[]` | Yes | Yes | `[]` | Length and spacing of the tick mark line. If not set, defaults to the grid line `borderDash` value. | `tickBorderDashOffset` | `number` | Yes | Yes | | Offset for the line dash of the tick mark. If unset, defaults to the grid line `borderDashOffset` value | `tickColor` | [`Color`](../general/colors.md) | Yes | Yes | | Color of the tick line. If unset, defaults to the grid line color. | `tickLength` | `number` | | | `8` | Length in pixels that the grid lines will draw into the axis area. | `tickWidth` | `number` | Yes | Yes | | Width of the tick mark in pixels. If unset, defaults to the grid line width. | `z` | `number` | | | `-1` | z-index of the gridline layer. Values <= 0 are drawn under datasets, > 0 on top. The scriptable context is described in [Options](../general/options.md#tick) section. ## Tick Configuration !!!include(axes/_common_ticks.md)!!! The scriptable context is described in [Options](../general/options.md#tick) section. ## Major Tick Configuration Namespace: `options.scales[scaleId].ticks.major`, it defines options for the major tick marks that are generated by the axis. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `enabled` | `boolean` | `false` | If true, major ticks are generated. A major tick will affect autoskipping and `major` will be defined on ticks in the scriptable options context. ## Border Configuration Namespace: `options.scales[scaleId].border`, it defines options for the border that run perpendicular to the axis. | Name | Type | Scriptable | Indexable | Default | Description | ---- | ---- | :-------------------------------: | :-----------------------------: | ------- | ----------- | `display` | `boolean` | | | `true` | If true, draw a border at the edge between the axis and the chart area. | `color` | [`Color`](../general/colors.md) | | | `Chart.defaults.borderColor` | The color of the border line. | `width` | `number` | | | `1` | The width of the border line. | `dash` | `number[]` | Yes | | `[]` | Length and spacing of dashes on grid lines. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | `dashOffset` | `number` | Yes | | `0.0` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `z` | `number` | | | `0` | z-index of the border layer. Values <= 0 are drawn under datasets, > 0 on top. ================================================ FILE: docs/charts/area.md ================================================ # Area Chart Both [line](./line.md) and [radar](./radar.md) charts support a `fill` option on the dataset object which can be used to create space between two datasets or a dataset and a boundary, i.e. the scale `origin`, `start,` or `end` (see [filling modes](#filling-modes)). :::tip Note This feature is implemented by the [`filler` plugin](https://github.com/chartjs/Chart.js/blob/master/src/plugins/plugin.filler/index.js). ::: ## Filling modes | Mode | Type | Values | | :--- | :--- | :--- | | Absolute dataset index | `number` | `1`, `2`, `3`, ... | | Relative dataset index | `string` | `'-1'`, `'-2'`, `'+1'`, ... | | Boundary | `string` | `'start'`, `'end'`, `'origin'` | | Disabled 1 | `boolean` | `false` | | Stacked value below | `string` | `'stack'` | | Axis value | `object` | `{ value: number; }` | | Shape (fill inside line) | `string` | `'shape'` | > 1 for backward compatibility, `fill: true` is equivalent to `fill: 'origin'`
### Example ```javascript new Chart(ctx, { data: { datasets: [ {fill: 'origin'}, // 0: fill to 'origin' {fill: '+2'}, // 1: fill to dataset 3 {fill: 1}, // 2: fill to dataset 1 {fill: false}, // 3: no fill {fill: '-2'}, // 4: fill to dataset 2 {fill: {value: 25}} // 5: fill to axis value 25 ] } }); ``` If you need to support multiple colors when filling from one dataset to another, you may specify an object with the following option : | Param | Type | Description | | :--- | :--- | :--- | | `target` | `number`, `string`, `boolean`, `object` | The accepted values are the same as the filling mode values, so you may use absolute and relative dataset indexes and/or boundaries. | | `above` | `Color` | If no color is set, the default color will be the background color of the chart. | | `below` | `Color` | Same as the above. | ### Example with multiple colors ```javascript new Chart(ctx, { data: { datasets: [ { fill: { target: 'origin', above: 'rgb(255, 0, 0)', // Area will be red above the origin below: 'rgb(0, 0, 255)' // And blue below the origin } } ] } }); ``` ## Configuration Namespace: `options.plugins.filler` | Option | Type | Default | Description | | :--- | :--- | :--- | :--- | | `drawTime` | `string` | `beforeDatasetDraw` | Filler draw time. Supported values: `'beforeDraw'`, `'beforeDatasetDraw'`, `'beforeDatasetsDraw'` | [`propagate`](#propagate) | `boolean` | `true` | Fill propagation when target is hidden. ### propagate `propagate` takes a `boolean` value (default: `true`). If `true`, the fill area will be recursively extended to the visible target defined by the `fill` value of hidden dataset targets: #### Example using propagate ```javascript new Chart(ctx, { data: { datasets: [ {fill: 'origin'}, // 0: fill to 'origin' {fill: '-1'}, // 1: fill to dataset 0 {fill: 1}, // 2: fill to dataset 1 {fill: false}, // 3: no fill {fill: '-2'} // 4: fill to dataset 2 ] }, options: { plugins: { filler: { propagate: true } } } }); ``` `propagate: true`: -if dataset 2 is hidden, dataset 4 will fill to dataset 1 -if dataset 2 and 1 are hidden, dataset 4 will fill to `'origin'` `propagate: false`: -if dataset 2 and/or 4 are hidden, dataset 4 will not be filled ================================================ FILE: docs/charts/bar.md ================================================ # Bar Chart A bar chart provides a way of showing data values represented as vertical bars. It is sometimes used to show trend data, and the comparison of multiple data sets side by side. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ label: 'My First Dataset', data: [65, 59, 80, 81, 56, 55, 40], backgroundColor: [ 'rgba(255, 99, 132, 0.2)', 'rgba(255, 159, 64, 0.2)', 'rgba(255, 205, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(201, 203, 207, 0.2)' ], borderColor: [ 'rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)', 'rgb(153, 102, 255)', 'rgb(201, 203, 207)' ], borderWidth: 1 }] }; // // const config = { type: 'bar', data: data, options: { scales: { y: { beginAtZero: true } } }, }; // module.exports = { actions: [], config: config, }; ``` ## Dataset Properties Namespaces: * `data.datasets[index]` - options for this dataset only * `options.datasets.bar` - options for all bar datasets * `options.elements.bar` - options for all [bar elements](../configuration/elements.md#bar-configuration) * `options` - options for the whole chart The bar chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the color of the bars is generally set this way. Only the `data` option needs to be specified in the dataset namespace. | Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default | ---- | ---- | :----: | :----: | ---- | [`backgroundColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`base`](#general) | `number` | Yes | Yes | | [`barPercentage`](#barpercentage) | `number` | - | - | `0.9` | | [`barThickness`](#barthickness) | `number`\|`string` | - | - | | | [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`borderSkipped`](#borderskipped) | `string`\|`boolean` | Yes | Yes | `'start'` | [`borderWidth`](#borderwidth) | `number`\|`object` | Yes | Yes | `0` | [`borderRadius`](#borderradius) | `number`\|`object` | Yes | Yes | `0` | [`categoryPercentage`](#categorypercentage) | `number` | - | - | `0.8` | | [`clip`](#general) | `number`\|`object`\|`false` | - | - | | [`data`](#data-structure) | `object`\|`object[]`\| `number[]`\|`string[]` | - | - | **required** | [`grouped`](#general) | `boolean` | - | - | `true` | | [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | | [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | | [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1` | [`hoverBorderRadius`](#interactions) | `number` | Yes | Yes | `0` | [`indexAxis`](#general) | `string` | - | - | `'x'` | [`inflateAmount`](#inflateamount) | `number`\|`'auto'` | Yes | Yes | `'auto'` | [`maxBarThickness`](#maxbarthickness) | `number` | - | - | | | [`minBarLength`](#styling) | `number` | - | - | | | [`label`](#general) | `string` | - | - | `''` | [`order`](#general) | `number` | - | - | `0` | [`pointStyle`](../configuration/elements.md#point-styles) | [`pointStyle`](../configuration/elements.md#types) | Yes | - | `'circle'` | [`skipNull`](#general) | `boolean` | - | - | | | [`stack`](#general) | `string` | - | - | `'bar'` | | [`xAxisID`](#general) | `string` | - | - | first x axis | [`yAxisID`](#general) | `string` | - | - | first y axis All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) ### Example dataset configuration ```javascript data: { datasets: [{ barPercentage: 0.5, barThickness: 6, maxBarThickness: 8, minBarLength: 2, data: [10, 20, 30, 40, 50, 60, 70] }] }; ``` ### General | Name | Description | ---- | ---- | `base` | Base value for the bar in data units along the value axis. If not set, defaults to the value axis base value. | `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` | `grouped` | Should the bars be grouped on index axis. When `true`, all the datasets at same index value will be placed next to each other centering on that index value. When `false`, each bar is placed on its actual index-axis value. | `indexAxis` | The base axis of the dataset. `'x'` for vertical bars and `'y'` for horizontal bars. | `label` | The label for the dataset which appears in the legend and tooltips. | `order` | The drawing order of dataset. Also affects order for stacking, tooltip and legend. [more](mixed.md#drawing-order) | `skipNull` | If `true`, null or undefined values will not be used for spacing calculations when determining bar size. | `stack` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). [more](#stacked-bar-chart) | `xAxisID` | The ID of the x-axis to plot this dataset on. | `yAxisID` | The ID of the y-axis to plot this dataset on. ### Styling The style of each bar can be controlled with the following properties: | Name | Description | ---- | ---- | `backgroundColor` | The bar background color. | `borderColor` | The bar border color. | [`borderSkipped`](#borderskipped) | The edge to skip when drawing bar. | [`borderWidth`](#borderwidth) | The bar border width (in pixels). | [`borderRadius`](#borderradius) | The bar border radius (in pixels). | `minBarLength` | Set this to ensure that bars have a minimum length in pixels. | `pointStyle` | Style of the point for legend. [more...](../configuration/elements.md#point-styles) All these values, if `undefined`, fallback to the associated [`elements.bar.*`](../configuration/elements.md#bar-configuration) options. #### borderSkipped This setting is used to avoid drawing the bar stroke at the base of the fill, or disable the border radius. In general, this does not need to be changed except when creating chart types that derive from a bar chart. :::tip Note For negative bars in a vertical chart, `top` and `bottom` are flipped. Same goes for `left` and `right` in a horizontal chart. ::: Options are: * `'start'` * `'end'` * `'middle'` (only valid on stacked bars: the borders between bars are skipped) * `'bottom'` * `'left'` * `'top'` * `'right'` * `false` (don't skip any borders) * `true` (skip all borders) #### borderWidth If this value is a number, it is applied to all sides of the rectangle (left, top, right, bottom), except [`borderSkipped`](#borderskipped). If this value is an object, the `left` property defines the left border width. Similarly, the `right`, `top`, and `bottom` properties can also be specified. Omitted borders and [`borderSkipped`](#borderskipped) are skipped. #### borderRadius If this value is a number, it is applied to all corners of the rectangle (topLeft, topRight, bottomLeft, bottomRight), except corners touching the [`borderSkipped`](#borderskipped). If this value is an object, the `topLeft` property defines the top-left corners border radius. Similarly, the `topRight`, `bottomLeft`, and `bottomRight` properties can also be specified. Omitted corners and those touching the [`borderSkipped`](#borderskipped) are skipped. For example if the `top` border is skipped, the border radius for the corners `topLeft` and `topRight` will be skipped as well. :::tip Stacked Charts When the border radius is supplied as a number and the chart is stacked, the radius will only be applied to the bars that are at the edges of the stack or where the bar is floating. The object syntax can be used to override this behavior. ::: #### inflateAmount This option can be used to inflate the rects that are used to draw the bars. This can be used to hide artifacts between bars when [`barPercentage`](#barpercentage) * [`categoryPercentage`](#categorypercentage) is 1. The default value `'auto'` should work in most cases. ### Interactions The interaction with each bar can be controlled with the following properties: | Name | Description | ---- | ----------- | `hoverBackgroundColor` | The bar background color when hovered. | `hoverBorderColor` | The bar border color when hovered. | `hoverBorderWidth` | The bar border width when hovered (in pixels). | `hoverBorderRadius` | The bar border radius when hovered (in pixels). All these values, if `undefined`, fallback to the associated [`elements.bar.*`](../configuration/elements.md#bar-configuration) options. ### barPercentage Percent (0-1) of the available width each bar should be within the category width. 1.0 will take the whole category width and put the bars right next to each other. [more...](#barpercentage-vs-categorypercentage) ### categoryPercentage Percent (0-1) of the available width each category should be within the sample width. [more...](#barpercentage-vs-categorypercentage) ### barThickness If this value is a number, it is applied to the width of each bar, in pixels. When this is enforced, `barPercentage` and `categoryPercentage` are ignored. If set to `'flex'`, the base sample widths are calculated automatically based on the previous and following samples so that they take the full available widths without overlap. Then, bars are sized using `barPercentage` and `categoryPercentage`. There is no gap when the percentage options are 1. This mode generates bars with different widths when data are not evenly spaced. If not set (default), the base sample widths are calculated using the smallest interval that prevents bar overlapping, and bars are sized using `barPercentage` and `categoryPercentage`. This mode always generates bars equally sized. ### maxBarThickness Set this to ensure that bars are not sized thicker than this. ## Scale Configuration The bar chart sets unique default values for the following configuration from the associated `scale` options: | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `offset` | `boolean` | `true` | If true, extra space is added to both edges and the axis is scaled to fit into the chart area. | `grid.offset` | `boolean` | `true` | If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval. If false, the grid line will go right down the middle of the bars. [more...](#offsetgridlines) ### Example scale configuration ```javascript options = { scales: { x: { grid: { offset: true } } } }; ``` ### Offset Grid Lines If true, the bars for a particular data point fall between the grid lines. The grid line will move to the left by one half of the tick interval, which is the space between the grid lines. If false, the grid line will go right down the middle of the bars. This is set to true for a category scale in a bar chart while false for other scales or chart types by default. ## Default Options It is common to want to apply a configuration setting to all created bar charts. The global bar chart settings are stored in `Chart.overrides.bar`. Changing the global options only affects charts created after the change. Existing charts are not changed. ## barPercentage vs categoryPercentage The following shows the relationship between the bar percentage option and the category percentage option. ``` // categoryPercentage: 1.0 // barPercentage: 1.0 Bar: | 1.0 | 1.0 | Category: | 1.0 | Sample: |===========| // categoryPercentage: 1.0 // barPercentage: 0.5 Bar: |.5| |.5| Category: | 1.0 | Sample: |==============| // categoryPercentage: 0.5 // barPercentage: 1.0 Bar: |1.0||1.0| Category: | .5 | Sample: |==================| ``` ## Data Structure All the supported [data structures](../general/data-structures.md) can be used with bar charts. ## Stacked Bar Chart Bar charts can be configured into stacked bar charts by changing the settings on the X and Y axes to enable stacking. Stacked bar charts can be used to show how one data series is made up of a number of smaller pieces. ```javascript const stackedBar = new Chart(ctx, { type: 'bar', data: data, options: { scales: { x: { stacked: true }, y: { stacked: true } } } }); ``` ## Horizontal Bar Chart A horizontal bar chart is a variation on a vertical bar chart. It is sometimes used to show trend data, and the comparison of multiple data sets side by side. To achieve this, you will have to set the `indexAxis` property in the options object to `'y'`. The default for this property is `'x'` and thus will show vertical bars. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ axis: 'y', label: 'My First Dataset', data: [65, 59, 80, 81, 56, 55, 40], fill: false, backgroundColor: [ 'rgba(255, 99, 132, 0.2)', 'rgba(255, 159, 64, 0.2)', 'rgba(255, 205, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(201, 203, 207, 0.2)' ], borderColor: [ 'rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)', 'rgb(153, 102, 255)', 'rgb(201, 203, 207)' ], borderWidth: 1 }] }; // // const config = { type: 'bar', data, options: { indexAxis: 'y', } }; // module.exports = { actions: [], config: config, }; ``` ### Horizontal Bar Chart config Options The configuration options for the horizontal bar chart are the same as for the [bar chart](#scale-configuration). However, any options specified on the x-axis in a bar chart, are applied to the y-axis in a horizontal bar chart. ## Internal data format `{x, y, _custom}` where `_custom` is an optional object defining stacked bar properties: `{start, end, barStart, barEnd, min, max}`. `start` and `end` are the input values. Those two are repeated in `barStart` (closer to origin), `barEnd` (further from origin), `min` and `max`. ================================================ FILE: docs/charts/bubble.md ================================================ # Bubble Chart A bubble chart is used to display three dimensions of data at the same time. The location of the bubble is determined by the first two dimensions and the corresponding horizontal and vertical axes. The third dimension is represented by the size of the individual bubbles. ```js chart-editor // const data = { datasets: [{ label: 'First Dataset', data: [{ x: 20, y: 30, r: 15 }, { x: 40, y: 10, r: 10 }], backgroundColor: 'rgb(255, 99, 132)' }] }; // // const config = { type: 'bubble', data: data, options: {} }; // module.exports = { actions: [], config: config, }; ``` ## Dataset Properties Namespaces: * `data.datasets[index]` - options for this dataset only * `options.datasets.bubble` - options for all bubble datasets * `options.elements.point` - options for all [point elements](../configuration/elements.md#point-configuration) * `options` - options for the whole chart The bubble chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of the bubbles is generally set this way. | Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default | ---- | ---- | :----: | :----: | ---- | [`backgroundColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`borderWidth`](#styling) | `number` | Yes | Yes | `3` | [`clip`](#general) | `number`\|`object`\|`false` | - | - | `undefined` | [`data`](#data-structure) | `object[]` | - | - | **required** | [`drawActiveElementsOnTop`](#general) | `boolean` | Yes | Yes | `true` | [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` | [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` | [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1` | [`hoverRadius`](#interactions) | `number` | Yes | Yes | `4` | [`hitRadius`](#interactions) | `number` | Yes | Yes | `1` | [`label`](#general) | `string` | - | - | `undefined` | [`order`](#general) | `number` | - | - | `0` | [`pointStyle`](#styling) | [`pointStyle`](../configuration/elements.md#types) | Yes | Yes | `'circle'` | [`rotation`](#styling) | `number` | Yes | Yes | `0` | [`radius`](#styling) | `number` | Yes | Yes | `3` All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) ### General | Name | Description | ---- | ---- | `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` | `drawActiveElementsOnTop` | Draw the active bubbles of a dataset over the other bubbles of the dataset | `label` | The label for the dataset which appears in the legend and tooltips. | `order` | The drawing order of dataset. Also affects order for tooltip and legend. [more](mixed.md#drawing-order) ### Styling The style of each bubble can be controlled with the following properties: | Name | Description | ---- | ---- | `backgroundColor` | bubble background color. | `borderColor` | bubble border color. | `borderWidth` | bubble border width (in pixels). | `pointStyle` | bubble [shape style](../configuration/elements.md#point-styles). | `rotation` | bubble rotation (in degrees). | `radius` | bubble radius (in pixels). All these values, if `undefined`, fallback to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. ### Interactions The interaction with each bubble can be controlled with the following properties: | Name | Description | ---- | ----------- | `hitRadius` | bubble **additional** radius for hit detection (in pixels). | `hoverBackgroundColor` | bubble background color when hovered. | `hoverBorderColor` | bubble border color when hovered. | `hoverBorderWidth` | bubble border width when hovered (in pixels). | `hoverRadius` | bubble **additional** radius when hovered (in pixels). All these values, if `undefined`, fallback to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. ## Default Options We can also change the default values for the Bubble chart type. Doing so will give all bubble charts created after this point the new defaults. The default configuration for the bubble chart can be accessed at `Chart.overrides.bubble`. ## Data Structure Bubble chart datasets need to contain a `data` array of points, each point represented by an object containing the following properties: ```javascript { // X Value x: number, // Y Value y: number, // Bubble radius in pixels (not scaled). r: number } ``` **Important:** the radius property, `r` is **not** scaled by the chart, it is the raw radius in pixels of the bubble that is drawn on the canvas. ## Internal data format `{x, y, _custom}` where `_custom` is the radius. ================================================ FILE: docs/charts/doughnut.md ================================================ # Doughnut and Pie Charts Pie and doughnut charts are probably the most commonly used charts. They are divided into segments, the arc of each segment shows the proportional value of each piece of data. They are excellent at showing the relational proportions between data. Pie and doughnut charts are effectively the same class in Chart.js, but have one different default value - their `cutout`. This equates to what portion of the inner should be cut out. This defaults to `0` for pie charts, and `'50%'` for doughnuts. They are also registered under two aliases in the `Chart` core. Other than their different default value, and different alias, they are exactly the same. :::: tabs ::: tab Doughnut ```js chart-editor // const data = { labels: [ 'Red', 'Blue', 'Yellow' ], datasets: [{ label: 'My First Dataset', data: [300, 50, 100], backgroundColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)' ], hoverOffset: 4 }] }; // // const config = { type: 'doughnut', data: data, }; // module.exports = { actions: [], config: config, }; ``` ::: :::tab Pie ```js chart-editor // const data = { labels: [ 'Red', 'Blue', 'Yellow' ], datasets: [{ label: 'My First Dataset', data: [300, 50, 100], backgroundColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)' ], hoverOffset: 4 }] }; // // const config = { type: 'pie', data: data, }; // module.exports = { actions: [], config: config, }; ``` ::: :::: ## Dataset Properties Namespaces: * `data.datasets[index]` - options for this dataset only * `options.datasets.doughnut` - options for all doughnut datasets * `options.datasets.pie` - options for all pie datasets * `options.elements.arc` - options for all [arc elements](../configuration/elements.md#arc-configuration) * `options` - options for the whole chart The doughnut/pie chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colours of the dataset's arcs are generally set this way. | Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default | ---- | ---- | :----: | :----: | ---- | [`backgroundColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`borderAlign`](#border-alignment) | `'center'`\|`'inner'` | Yes | Yes | `'center'` | [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'#fff'` | [`borderDash`](#styling) | `number[]` | Yes | - | `[]` | [`borderDashOffset`](#styling) | `number` | Yes | - | `0.0` | [`borderJoinStyle`](#styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | Yes | `undefined` | [`borderRadius`](#border-radius) | `number`\|`object` | Yes | Yes | `0` | [`borderWidth`](#styling) | `number` | Yes | Yes | `2` | [`circumference`](#general) | `number` | - | - | `undefined` | [`clip`](#general) | `number`\|`object`\|`false` | - | - | `undefined` | [`data`](#data-structure) | `number[]` | - | - | **required** | [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` | [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` | [`hoverBorderDash`](#interactions) | `number[]` | Yes | - | `undefined` | [`hoverBorderDashOffset`](#interactions) | `number` | Yes | - | `undefined` | [`hoverBorderJoinStyle`](#interactions) | `'round'`\|`'bevel'`\|`'miter'` | Yes | Yes | `undefined` | [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `undefined` | [`hoverOffset`](#interactions) | `number` | Yes | Yes | `0` | [`offset`](#styling) | `number`\|`number[]` | Yes | Yes | `0` | [`rotation`](#general) | `number` | - | - | `undefined` | [`spacing`](#styling) | `number` | - | - | `0` | [`weight`](#styling) | `number` | - | - | `1` All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) ### General | Name | Description | ---- | ---- | `circumference` | Per-dataset override for the sweep that the arcs cover | `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` | `rotation` | Per-dataset override for the starting angle to draw arcs from ### Styling The style of each arc can be controlled with the following properties: | Name | Description | ---- | ---- | `backgroundColor` | arc background color. | `borderColor` | arc border color. | `borderDash` | arc border length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | `borderDashOffset` | arc border offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `borderJoinStyle` | arc border join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). | `borderWidth` | arc border width (in pixels). | `offset` | arc offset (in pixels). | `spacing` | Fixed arc offset (in pixels). Similar to `offset` but applies to all arcs. | `weight` | The relative thickness of the dataset. Providing a value for weight will cause the pie or doughnut dataset to be drawn with a thickness relative to the sum of all the dataset weight values. All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. ### Border Alignment The following values are supported for `borderAlign`. * `'center'` (default) * `'inner'` When `'center'` is set, the borders of arcs next to each other will overlap. When `'inner'` is set, it is guaranteed that all borders will not overlap. ### Border Radius If this value is a number, it is applied to all corners of the arc (outerStart, outerEnd, innerStart, innerRight). If this value is an object, the `outerStart` property defines the outer-start corner's border radius. Similarly, the `outerEnd`, `innerStart`, and `innerEnd` properties can also be specified. ### Interactions The interaction with each arc can be controlled with the following properties: | Name | Description | ---- | ----------- | `hoverBackgroundColor` | arc background color when hovered. | `hoverBorderColor` | arc border color when hovered. | `hoverBorderDash` | arc border length and spacing of dashes when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | `hoverBorderDashOffset` | arc border offset for line dashes when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `hoverBorderJoinStyle` | arc border join style when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). | `hoverBorderWidth` | arc border width when hovered (in pixels). | `hoverOffset` | arc offset when hovered (in pixels). All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. ## Config Options These are the customisation options specific to Pie & Doughnut charts. These options are looked up on access, and form together with the global chart configuration the options of the chart. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `cutout` | `number`\|`string` | `50%` - for doughnut, `0` - for pie | The portion of the chart that is cut out of the middle. If `string` and ending with '%', percentage of the chart radius. `number` is considered to be pixels. | `radius` | `number`\|`string` | `100%` | The outer radius of the chart. If `string` and ending with '%', percentage of the maximum radius. `number` is considered to be pixels. | `rotation` | `number` | 0 | Starting angle to draw arcs from. | `circumference` | `number` | 360 | Sweep to allow arcs to cover. | `animation.animateRotate` | `boolean` | `true` | If true, the chart will animate in with a rotation animation. This property is in the `options.animation` object. | `animation.animateScale` | `boolean` | `false` | If true, will animate scaling the chart from the center outwards. ## Default Options We can also change these default values for each Doughnut type that is created, this object is available at `Chart.overrides.doughnut`. Pie charts also have a clone of these defaults available to change at `Chart.overrides.pie`, with the only difference being `cutout` being set to 0. ## Data Structure For a pie chart, datasets need to contain an array of data points. The data points should be a number, Chart.js will total all the numbers and calculate the relative proportion of each. You also need to specify an array of labels so that tooltips appear correctly. ```javascript data = { datasets: [{ data: [10, 20, 30] }], // These labels appear in the legend and in the tooltips when hovering different arcs labels: [ 'Red', 'Yellow', 'Blue' ] }; ``` ================================================ FILE: docs/charts/line.md ================================================ # Line Chart A line chart is a way of plotting data points on a line. Often, it is used to show trend data, or the comparison of two data sets. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ label: 'My First Dataset', data: [65, 59, 80, 81, 56, 55, 40], fill: false, borderColor: 'rgb(75, 192, 192)', tension: 0.1 }] }; // // const config = { type: 'line', data: data, }; // module.exports = { actions: [], config: config, }; ``` ## Dataset Properties Namespaces: * `data.datasets[index]` - options for this dataset only * `options.datasets.line` - options for all line datasets * `options.elements.line` - options for all [line elements](../configuration/elements.md#line-configuration) * `options.elements.point` - options for all [point elements](../configuration/elements.md#point-configuration) * `options` - options for the whole chart The line chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of a line is generally set this way. | Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default | ---- | ---- | :----: | :----: | ---- | [`backgroundColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` | [`borderCapStyle`](#line-styling) | `string` | Yes | - | `'butt'` | [`borderColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` | [`borderDash`](#line-styling) | `number[]` | Yes | - | `[]` | [`borderDashOffset`](#line-styling) | `number` | Yes | - | `0.0` | [`borderJoinStyle`](#line-styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | - | `'miter'` | [`borderWidth`](#line-styling) | `number` | Yes | - | `3` | [`clip`](#general) | `number`\|`object`\|`false` | - | - | `undefined` | [`cubicInterpolationMode`](#cubicinterpolationmode) | `string` | Yes | - | `'default'` | [`data`](#data-structure) | `object`\|`object[]`\| `number[]`\|`string[]` | - | - | **required** | [`drawActiveElementsOnTop`](#general) | `boolean` | Yes | Yes | `true` | [`fill`](#line-styling) | `boolean`\|`string` | Yes | - | `false` | [`hoverBackgroundColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `undefined` | [`hoverBorderCapStyle`](#line-styling) | `string` | Yes | - | `undefined` | [`hoverBorderColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `undefined` | [`hoverBorderDash`](#line-styling) | `number[]` | Yes | - | `undefined` | [`hoverBorderDashOffset`](#line-styling) | `number` | Yes | - | `undefined` | [`hoverBorderJoinStyle`](#line-styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | - | `undefined` | [`hoverBorderWidth`](#line-styling) | `number` | Yes | - | `undefined` | [`indexAxis`](#general) | `string` | - | - | `'x'` | [`label`](#general) | `string` | - | - | `''` | [`order`](#general) | `number` | - | - | `0` | [`pointBackgroundColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`pointBorderColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`pointBorderWidth`](#point-styling) | `number` | Yes | Yes | `1` | [`pointHitRadius`](#point-styling) | `number` | Yes | Yes | `1` | [`pointHoverBackgroundColor`](#interactions) | `Color` | Yes | Yes | `undefined` | [`pointHoverBorderColor`](#interactions) | `Color` | Yes | Yes | `undefined` | [`pointHoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1` | [`pointHoverRadius`](#interactions) | `number` | Yes | Yes | `4` | [`pointRadius`](#point-styling) | `number` | Yes | Yes | `3` | [`pointRotation`](#point-styling) | `number` | Yes | Yes | `0` | [`pointStyle`](#point-styling) | [`pointStyle`](../configuration/elements.md#types) | Yes | Yes | `'circle'` | [`segment`](#segment) | `object` | - | - | `undefined` | [`showLine`](#line-styling) | `boolean` | - | - | `true` | [`spanGaps`](#line-styling) | `boolean`\|`number` | - | - | `undefined` | [`stack`](#general) | `string` | - | - | `'line'` | | [`stepped`](#stepped) | `boolean`\|`string` | - | - | `false` | [`tension`](#line-styling) | `number` | - | - | `0` | [`xAxisID`](#general) | `string` | - | - | first x axis | [`yAxisID`](#general) | `string` | - | - | first y axis All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) ### General | Name | Description | ---- | ---- | `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` | `drawActiveElementsOnTop` | Draw the active points of a dataset over the other points of the dataset | `indexAxis` | The base axis of the dataset. `'x'` for horizontal lines and `'y'` for vertical lines. | `label` | The label for the dataset which appears in the legend and tooltips. | `order` | The drawing order of dataset. Also affects order for stacking, tooltip and legend. [more](mixed.md#drawing-order) | `stack` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). [more](#stacked-area-chart) | `xAxisID` | The ID of the x-axis to plot this dataset on. | `yAxisID` | The ID of the y-axis to plot this dataset on. ### Point Styling The style of each point can be controlled with the following properties: | Name | Description | ---- | ---- | `pointBackgroundColor` | The fill color for points. | `pointBorderColor` | The border color for points. | `pointBorderWidth` | The width of the point border in pixels. | `pointHitRadius` | The pixel size of the non-displayed point that reacts to mouse events. | `pointRadius` | The radius of the point shape. If set to 0, the point is not rendered. | `pointRotation` | The rotation of the point in degrees. | `pointStyle` | Style of the point. [more...](../configuration/elements.md#point-styles) All these values, if `undefined`, fallback first to the dataset options then to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. ### Line Styling The style of the line can be controlled with the following properties: | Name | Description | ---- | ---- | `backgroundColor` | The line fill color. | `borderCapStyle` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap). | `borderColor` | The line color. | `borderDash` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | `borderDashOffset` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `borderJoinStyle` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). | `borderWidth` | The line width (in pixels). | `fill` | How to fill the area under the line. See [area charts](area.md). | `tension` | Bezier curve tension of the line. Set to 0 to draw straightlines. This option is ignored if monotone cubic interpolation is used. | `showLine` | If false, the line is not drawn for this dataset. | `spanGaps` | If true, lines will be drawn between points with no or null data. If false, points with `null` data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. If the value is `undefined`, the values fallback to the associated [`elements.line.*`](../configuration/elements.md#line-configuration) options. ### Interactions The interaction with each point can be controlled with the following properties: | Name | Description | ---- | ----------- | `pointHoverBackgroundColor` | Point background color when hovered. | `pointHoverBorderColor` | Point border color when hovered. | `pointHoverBorderWidth` | Border width of point when hovered. | `pointHoverRadius` | The radius of the point when hovered. ### cubicInterpolationMode The following interpolation modes are supported. * `'default'` * `'monotone'` The `'default'` algorithm uses a custom weighted cubic interpolation, which produces pleasant curves for all types of datasets. The `'monotone'` algorithm is more suited to `y = f(x)` datasets: it preserves monotonicity (or piecewise monotonicity) of the dataset being interpolated, and ensures local extremums (if any) stay at input data points. If left untouched (`undefined`), the global `options.elements.line.cubicInterpolationMode` property is used. ### Segment Line segment styles can be overridden by scriptable options in the `segment` object. Currently, all of the `border*` and `backgroundColor` options are supported. The segment styles are resolved for each section of the line between each point. `undefined` fallbacks to main line styles. :::tip To be able to style gaps, you need the [`spanGaps`](#line-styling) option enabled. ::: Context for the scriptable segment contains the following properties: * `type`: `'segment'` * `p0`: first point element * `p1`: second point element * `p0DataIndex`: index of first point in the data array * `p1DataIndex`: index of second point in the data array * `datasetIndex`: dataset index [Example usage](../samples/line/segments.md) ### Stepped The following values are supported for `stepped`. * `false`: No Step Interpolation (default) * `true`: Step-before Interpolation (eq. `'before'`) * `'before'`: Step-before Interpolation * `'after'`: Step-after Interpolation * `'middle'`: Step-middle Interpolation If the `stepped` value is set to anything other than false, `tension` will be ignored. ## Default Options It is common to want to apply a configuration setting to all created line charts. The global line chart settings are stored in `Chart.overrides.line`. Changing the global options only affects charts created after the change. Existing charts are not changed. For example, to configure all line charts with `spanGaps = true` you would do: ```javascript Chart.overrides.line.spanGaps = true; ``` ## Data Structure All the supported [data structures](../general/data-structures.md) can be used with line charts. ## Stacked Area Chart Line charts can be configured into stacked area charts by changing the settings on the y-axis to enable stacking. Stacked area charts can be used to show how one data trend is made up of a number of smaller pieces. ```javascript const stackedLine = new Chart(ctx, { type: 'line', data: data, options: { scales: { y: { stacked: true } } } }); ``` ## Vertical Line Chart A vertical line chart is a variation on the horizontal line chart. To achieve this, you will have to set the `indexAxis` property in the options object to `'y'`. The default for this property is `'x'` and thus will show horizontal lines. ```js chart-editor // const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [{ axis: 'y', label: 'My First Dataset', data: [65, 59, 80, 81, 56, 55, 40], fill: false, backgroundColor: [ 'rgba(255, 99, 132, 0.2)', 'rgba(255, 159, 64, 0.2)', 'rgba(255, 205, 86, 0.2)', 'rgba(75, 192, 192, 0.2)', 'rgba(54, 162, 235, 0.2)', 'rgba(153, 102, 255, 0.2)', 'rgba(201, 203, 207, 0.2)' ], borderColor: [ 'rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)', 'rgb(153, 102, 255)', 'rgb(201, 203, 207)' ], borderWidth: 1 }] }; // // const config = { type: 'line', data: data, options: { indexAxis: 'y', scales: { x: { beginAtZero: true } } } }; // module.exports = { actions: [], config: config, }; ``` ### Config Options The configuration options for the vertical line chart are the same as for the [line chart](#configuration-options). However, any options specified on the x-axis in a line chart, are applied to the y-axis in a vertical line chart. ## Internal data format `{x, y}` ================================================ FILE: docs/charts/mixed.md ================================================ # Mixed Chart Types With Chart.js, it is possible to create mixed charts that are a combination of two or more different chart types. A common example is a bar chart that also includes a line dataset. When creating a mixed chart, we specify the chart type on each dataset. ```javascript const mixedChart = new Chart(ctx, { data: { datasets: [{ type: 'bar', label: 'Bar Dataset', data: [10, 20, 30, 40] }, { type: 'line', label: 'Line Dataset', data: [50, 50, 50, 50], }], labels: ['January', 'February', 'March', 'April'] }, options: options }); ``` At this point, we have a chart rendering how we'd like. It's important to note that the default options for the charts are only considered at the dataset level and are not merged at the chart level in this case. ```js chart-editor // const data = { labels: [ 'January', 'February', 'March', 'April' ], datasets: [{ type: 'bar', label: 'Bar Dataset', data: [10, 20, 30, 40], borderColor: 'rgb(255, 99, 132)', backgroundColor: 'rgba(255, 99, 132, 0.2)' }, { type: 'line', label: 'Line Dataset', data: [50, 50, 50, 50], fill: false, borderColor: 'rgb(54, 162, 235)' }] }; // // const config = { type: 'scatter', data: data, options: { scales: { y: { beginAtZero: true } } } }; // module.exports = { actions: [], config: config, }; ``` ## Drawing order By default, datasets are drawn such that the first one is top-most. This can be altered by specifying `order` option to datasets. `order` defaults to `0`. Note that this also affects stacking, legend, and tooltip. So it's essentially the same as reordering the datasets. The `order` property behaves like a weight instead of a specific order, so the higher the number, the sooner that dataset is drawn on the canvas and thus other datasets with a lower order number will get drawn over it. ```javascript const mixedChart = new Chart(ctx, { type: 'bar', data: { datasets: [{ label: 'Bar Dataset', data: [10, 20, 30, 40], // this dataset is drawn below order: 2 }, { label: 'Line Dataset', data: [10, 10, 10, 10], type: 'line', // this dataset is drawn on top order: 1 }], labels: ['January', 'February', 'March', 'April'] }, options: options }); ``` ================================================ FILE: docs/charts/polar.md ================================================ # Polar Area Chart Polar area charts are similar to pie charts, but each segment has the same angle - the radius of the segment differs depending on the value. This type of chart is often useful when we want to show a comparison data similar to a pie chart, but also show a scale of values for context. ```js chart-editor // const data = { labels: [ 'Red', 'Green', 'Yellow', 'Grey', 'Blue' ], datasets: [{ label: 'My First Dataset', data: [11, 16, 7, 3, 14], backgroundColor: [ 'rgb(255, 99, 132)', 'rgb(75, 192, 192)', 'rgb(255, 205, 86)', 'rgb(201, 203, 207)', 'rgb(54, 162, 235)' ] }] }; // // const config = { type: 'polarArea', data: data, options: {} }; // module.exports = { actions: [], config: config, }; ``` ## Dataset Properties Namespaces: * `data.datasets[index]` - options for this dataset only * `options.datasets.polarArea` - options for all polarArea datasets * `options.elements.arc` - options for all [arc elements](../configuration/elements.md#arc-configuration) * `options` - options for the whole chart The following options can be included in a polar area chart dataset to configure options for that specific dataset. | Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default | ---- | ---- | :----: | :----: | ---- | [`backgroundColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`borderAlign`](#border-alignment) | `'center'`\|`'inner'` | Yes | Yes | `'center'` | [`borderColor`](#styling) | [`Color`](../general/colors.md) | Yes | Yes | `'#fff'` | [`borderDash`](#styling) | `number[]` | Yes | - | `[]` | [`borderDashOffset`](#styling) | `number` | Yes | - | `0.0` | [`borderJoinStyle`](#styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | Yes | `undefined` | [`borderWidth`](#styling) | `number` | Yes | Yes | `2` | [`clip`](#general) | `number`\|`object`\|`false` | - | - | `undefined` | [`data`](#data-structure) | `number[]` | - | - | **required** | [`hoverBackgroundColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` | [`hoverBorderColor`](#interactions) | [`Color`](../general/colors.md) | Yes | Yes | `undefined` | [`hoverBorderDash`](#interactions) | `number[]` | Yes | - | `undefined` | [`hoverBorderDashOffset`](#interactions) | `number` | Yes | - | `undefined` | [`hoverBorderJoinStyle`](#interactions) | `'round'`\|`'bevel'`\|`'miter'` | Yes | Yes | `undefined` | [`hoverBorderWidth`](#interactions) | `number` | Yes | Yes | `undefined` | [`circular`](#styling) | `boolean` | Yes | Yes | `true` All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) ### General | Name | Description | ---- | ---- | `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` ### Styling The style of each arc can be controlled with the following properties: | Name | Description | ---- | ---- | `backgroundColor` | arc background color. | `borderColor` | arc border color. | `borderDash` | arc border length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | `borderDashOffset` | arc border offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `borderJoinStyle` | arc border join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). | `borderWidth` | arc border width (in pixels). | `circular` | By default the Arc is curved. If `circular: false` the Arc will be flat. All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. ### Border Alignment The following values are supported for `borderAlign`. * `'center'` (default) * `'inner'` When `'center'` is set, the borders of arcs next to each other will overlap. When `'inner'` is set, it is guaranteed that all the borders do not overlap. ### Interactions The interaction with each arc can be controlled with the following properties: | Name | Description | ---- | ----------- | `hoverBackgroundColor` | arc background color when hovered. | `hoverBorderColor` | arc border color when hovered. | `hoverBorderDash` | arc border length and spacing of dashes when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | `hoverBorderDashOffset` | arc border offset for line dashes when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `hoverBorderJoinStyle` | arc border join style when hovered. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). | `hoverBorderWidth` | arc border width when hovered (in pixels). All these values, if `undefined`, fallback to the associated [`elements.arc.*`](../configuration/elements.md#arc-configuration) options. ## Config Options These are the customisation options specific to Polar Area charts. These options are looked up on access, and form together with the [global chart default options](#default-options) the options of the chart. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `animation.animateRotate` | `boolean` | `true` | If true, the chart will animate in with a rotation animation. This property is in the `options.animation` object. | `animation.animateScale` | `boolean` | `true` | If true, will animate scaling the chart from the center outwards. The polar area chart uses the [radialLinear](../axes/radial/linear.md) scale. Additional configuration is provided via the scale. ## Default Options We can also change these default values for each PolarArea type that is created, this object is available at `Chart.overrides.polarArea`. Changing the global options only affects charts created after the change. Existing charts are not changed. For example, to configure all new polar area charts with `animateScale = false` you would do: ```javascript Chart.overrides.polarArea.animation.animateScale = false; ``` ## Data Structure For a polar area chart, datasets need to contain an array of data points. The data points should be a number, Chart.js will total all of the numbers and calculate the relative proportion of each. You also need to specify an array of labels so that tooltips appear correctly for each slice. ```javascript data = { datasets: [{ data: [10, 20, 30] }], // These labels appear in the legend and in the tooltips when hovering different arcs labels: [ 'Red', 'Yellow', 'Blue' ] }; ``` ================================================ FILE: docs/charts/radar.md ================================================ # Radar Chart A radar chart is a way of showing multiple data points and the variation between them. They are often useful for comparing the points of two or more different data sets. ```js chart-editor // const data = { labels: [ 'Eating', 'Drinking', 'Sleeping', 'Designing', 'Coding', 'Cycling', 'Running' ], datasets: [{ label: 'My First Dataset', data: [65, 59, 90, 81, 56, 55, 40], fill: true, backgroundColor: 'rgba(255, 99, 132, 0.2)', borderColor: 'rgb(255, 99, 132)', pointBackgroundColor: 'rgb(255, 99, 132)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgb(255, 99, 132)' }, { label: 'My Second Dataset', data: [28, 48, 40, 19, 96, 27, 100], fill: true, backgroundColor: 'rgba(54, 162, 235, 0.2)', borderColor: 'rgb(54, 162, 235)', pointBackgroundColor: 'rgb(54, 162, 235)', pointBorderColor: '#fff', pointHoverBackgroundColor: '#fff', pointHoverBorderColor: 'rgb(54, 162, 235)' }] }; // // const config = { type: 'radar', data: data, options: { elements: { line: { borderWidth: 3 } } }, }; // module.exports = { actions: [], config: config, }; ``` ## Dataset Properties Namespaces: * `data.datasets[index]` - options for this dataset only * `options.datasets.line` - options for all line datasets * `options.elements.line` - options for all [line elements](../configuration/elements.md#line-configuration) * `options.elements.point` - options for all [point elements](../configuration/elements.md#point-configuration) * `options` - options for the whole chart The radar chart allows a number of properties to be specified for each dataset. These are used to set display properties for a specific dataset. For example, the colour of a line is generally set this way. | Name | Type | [Scriptable](../general/options.md#scriptable-options) | [Indexable](../general/options.md#indexable-options) | Default | ---- | ---- | :----: | :----: | ---- | [`backgroundColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` | [`borderCapStyle`](#line-styling) | `string` | Yes | - | `'butt'` | [`borderColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `'rgba(0, 0, 0, 0.1)'` | [`borderDash`](#line-styling) | `number[]` | Yes | - | `[]` | [`borderDashOffset`](#line-styling) | `number` | Yes | - | `0.0` | [`borderJoinStyle`](#line-styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | - | `'miter'` | [`borderWidth`](#line-styling) | `number` | Yes | - | `3` | [`hoverBackgroundColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `undefined` | [`hoverBorderCapStyle`](#line-styling) | `string` | Yes | - | `undefined` | [`hoverBorderColor`](#line-styling) | [`Color`](../general/colors.md) | Yes | - | `undefined` | [`hoverBorderDash`](#line-styling) | `number[]` | Yes | - | `undefined` | [`hoverBorderDashOffset`](#line-styling) | `number` | Yes | - | `undefined` | [`hoverBorderJoinStyle`](#line-styling) | `'round'`\|`'bevel'`\|`'miter'` | Yes | - | `undefined` | [`hoverBorderWidth`](#line-styling) | `number` | Yes | - | `undefined` | [`clip`](#general) | `number`\|`object`\|`false` | - | - | `undefined` | [`data`](#data-structure) | `number[]` | - | - | **required** | [`fill`](#line-styling) | `boolean`\|`string` | Yes | - | `false` | [`label`](#general) | `string` | - | - | `''` | [`order`](#general) | `number` | - | - | `0` | [`tension`](#line-styling) | `number` | - | - | `0` | [`pointBackgroundColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`pointBorderColor`](#point-styling) | `Color` | Yes | Yes | `'rgba(0, 0, 0, 0.1)'` | [`pointBorderWidth`](#point-styling) | `number` | Yes | Yes | `1` | [`pointHitRadius`](#point-styling) | `number` | Yes | Yes | `1` | [`pointHoverBackgroundColor`](#interactions) | `Color` | Yes | Yes | `undefined` | [`pointHoverBorderColor`](#interactions) | `Color` | Yes | Yes | `undefined` | [`pointHoverBorderWidth`](#interactions) | `number` | Yes | Yes | `1` | [`pointHoverRadius`](#interactions) | `number` | Yes | Yes | `4` | [`pointRadius`](#point-styling) | `number` | Yes | Yes | `3` | [`pointRotation`](#point-styling) | `number` | Yes | Yes | `0` | [`pointStyle`](#point-styling) | [`pointStyle`](../configuration/elements.md#types) | Yes | Yes | `'circle'` | [`spanGaps`](#line-styling) | `boolean` | - | - | `undefined` All these values, if `undefined`, fallback to the scopes described in [option resolution](../general/options) ### General | Name | Description | ---- | ---- | `clip` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. `0` = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` | `label` | The label for the dataset which appears in the legend and tooltips. | `order` | The drawing order of dataset. Also affects order for tooltip and legend. [more](mixed.md#drawing-order) ### Point Styling The style of each point can be controlled with the following properties: | Name | Description | ---- | ---- | `pointBackgroundColor` | The fill color for points. | `pointBorderColor` | The border color for points. | `pointBorderWidth` | The width of the point border in pixels. | `pointHitRadius` | The pixel size of the non-displayed point that reacts to mouse events. | `pointRadius` | The radius of the point shape. If set to 0, the point is not rendered. | `pointRotation` | The rotation of the point in degrees. | `pointStyle` | Style of the point. [more...](../configuration/elements#point-styles) All these values, if `undefined`, fallback first to the dataset options then to the associated [`elements.point.*`](../configuration/elements.md#point-configuration) options. ### Line Styling The style of the line can be controlled with the following properties: | Name | Description | ---- | ---- | `backgroundColor` | The line fill color. | `borderCapStyle` | Cap style of the line. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap). | `borderColor` | The line color. | `borderDash` | Length and spacing of dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | `borderDashOffset` | Offset for line dashes. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `borderJoinStyle` | Line joint style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). | `borderWidth` | The line width (in pixels). | `fill` | How to fill the area under the line. See [area charts](area.md). | `tension` | Bezier curve tension of the line. Set to 0 to draw straight lines. | `spanGaps` | If true, lines will be drawn between points with no or null data. If false, points with `null` data will create a break in the line. If the value is `undefined`, the values fallback to the associated [`elements.line.*`](../configuration/elements.md#line-configuration) options. ### Interactions The interaction with each point can be controlled with the following properties: | Name | Description | ---- | ----------- | `pointHoverBackgroundColor` | Point background color when hovered. | `pointHoverBorderColor` | Point border color when hovered. | `pointHoverBorderWidth` | Border width of point when hovered. | `pointHoverRadius` | The radius of the point when hovered. ## Scale Options The radar chart supports only a single scale. The options for this scale are defined in the `scales.r` property, which can be referenced from the [Linear Radial Axis page](../axes/radial/linear). ```javascript options = { scales: { r: { angleLines: { display: false }, suggestedMin: 50, suggestedMax: 100 } } }; ``` ## Default Options It is common to want to apply a configuration setting to all created radar charts. The global radar chart settings are stored in `Chart.overrides.radar`. Changing the global options only affects charts created after the change. Existing charts are not changed. ## Data Structure The `data` property of a dataset for a radar chart is specified as an array of numbers. Each point in the data array corresponds to the label at the same index. ```javascript data: [20, 10] ``` For a radar chart, to provide context of what each point means, we include an array of strings that show around each point in the chart. ```javascript data: { labels: ['Running', 'Swimming', 'Eating', 'Cycling'], datasets: [{ data: [20, 10, 4, 2] }] } ``` ## Internal data format `{x, y}` ================================================ FILE: docs/charts/scatter.md ================================================ # Scatter Chart Scatter charts are based on basic line charts with the x-axis changed to a linear axis. To use a scatter chart, data must be passed as objects containing X and Y properties. The example below creates a scatter chart with 4 points. ```js chart-editor // const data = { datasets: [{ label: 'Scatter Dataset', data: [{ x: -10, y: 0 }, { x: 0, y: 10 }, { x: 10, y: 5 }, { x: 0.5, y: 5.5 }], backgroundColor: 'rgb(255, 99, 132)' }], }; // // const config = { type: 'scatter', data: data, options: { scales: { x: { type: 'linear', position: 'bottom' } } } }; // module.exports = { actions: [], config: config, }; ``` ## Dataset Properties Namespaces: * `data.datasets[index]` - options for this dataset only * `options.datasets.scatter` - options for all scatter datasets * `options.elements.line` - options for all [line elements](../configuration/elements.md#line-configuration) * `options.elements.point` - options for all [point elements](../configuration/elements.md#point-configuration) * `options` - options for the whole chart The scatter chart supports all the same properties as the [line chart](./line.md#dataset-properties). By default, the scatter chart will override the showLine property of the line chart to `false`. The index scale is of the type `linear`. This means, if you are using the labels array, the values have to be numbers or parsable to numbers, the same applies to the object format for the keys. ## Data Structure Unlike the line chart where data can be supplied in two different formats, the scatter chart only accepts data in a point format. ```javascript data: [{ x: 10, y: 20 }, { x: 15, y: 10 }] ``` ## Internal data format `{x, y}` ================================================ FILE: docs/configuration/animations.md ================================================ # Animations Chart.js animates charts out of the box. A number of options are provided to configure how the animation looks and how long it takes. :::: tabs ::: tab "Looping tension [property]" ```js chart-editor // const data = { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ label: 'Looping tension', data: [65, 59, 80, 81, 26, 55, 40], fill: false, borderColor: 'rgb(75, 192, 192)', }] }; // // const config = { type: 'line', data: data, options: { animations: { tension: { duration: 1000, easing: 'linear', from: 1, to: 0, loop: true } }, scales: { y: { // defining min and max so hiding the dataset does not change scale range min: 0, max: 100 } } } }; // module.exports = { actions: [], config: config, }; ``` ::: ::: tab "Hide and show [mode]" ```js chart-editor // const data = { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ label: 'Try hiding me', data: [65, 59, 80, 81, 26, 55, 40], fill: false, borderColor: 'rgb(75, 192, 192)', }] }; // // const config = { type: 'line', data: data, options: { transitions: { show: { animations: { x: { from: 0 }, y: { from: 0 } } }, hide: { animations: { x: { to: 0 }, y: { to: 0 } } } } } }; // module.exports = { actions: [], config: config, }; ``` ::: :::: ## Animation configuration Animation configuration consists of 3 keys. | Name | Type | Details | ---- | ---- | ------- | animation | `object` | [animation](#animation) | animations | `object` | [animations](#animations) | transitions | `object` | [transitions](#transitions) These keys can be configured in following paths: * `` - chart options * `datasets[type]` - dataset type options * `overrides[type]` - chart type options These paths are valid under `defaults` for global configuration and `options` for instance configuration. ## animation The default configuration is defined here: core.animations.defaults.js Namespace: `options.animation` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `duration` | `number` | `1000` | The number of milliseconds an animation takes. | `easing` | `string` | `'easeOutQuart'` | Easing function to use. [more...](#easing) | `delay` | `number` | `undefined` | Delay before starting the animations. | `loop` | `boolean` | `undefined` | If set to `true`, the animations loop endlessly. These defaults can be overridden in `options.animation` or `dataset.animation` and `tooltip.animation`. These keys are also [Scriptable](../general/options.md#scriptable-options). ## animations Animations options configures which element properties are animated and how. In addition to the main [animation configuration](#animation-configuration), the following options are available: Namespace: `options.animations[animation]` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `properties` | `string[]` | `key` | The property names this configuration applies to. Defaults to the key name of this object. | `type` | `string` | `typeof property` | Type of property, determines the interpolator used. Possible values: `'number'`, `'color'` and `'boolean'`. Only really needed for `'color'`, because `typeof` does not get that right. | `from` | `number`\|`Color`\|`boolean` | `undefined` | Start value for the animation. Current value is used when `undefined` | `to` | `number`\|`Color`\|`boolean` | `undefined` | End value for the animation. Updated value is used when `undefined` | `fn` | <T>(from: T, to: T, factor: number) => T; | `undefined` | Optional custom interpolator, instead of using a predefined interpolator from `type` | ### Default animations | Name | Option | Value | ---- | ------ | ----- | `numbers` | `properties` | `['x', 'y', 'borderWidth', 'radius', 'tension']` | `numbers` | `type` | `'number'` | `colors` | `properties` | `['color', 'borderColor', 'backgroundColor']` | `colors` | `type` | `'color'` :::tip Note These default animations are overridden by most of the dataset controllers. ::: ## transitions The core transitions are `'active'`, `'hide'`, `'reset'`, `'resize'`, `'show'`. A custom transition can be used by passing a custom `mode` to [update](../developers/api.md#updatemode). Transition extends the main [animation configuration](#animation-configuration) and [animations configuration](#animations-configuration). ### Default transitions Namespace: `options.transitions[mode]` | Mode | Option | Value | Description | -----| ------ | ----- | ----- | `'active'` | animation.duration | 400 | Override default duration to 400ms for hover animations | `'resize'` | animation.duration | 0 | Override default duration to 0ms (= no animation) for resize | `'show'` | animations.colors | `{ type: 'color', properties: ['borderColor', 'backgroundColor'], from: 'transparent' }` | Colors are faded in from transparent when dataset is shown using legend / [api](../developers/api.md#showdatasetIndex). | `'show'` | animations.visible | `{ type: 'boolean', duration: 0 }` | Dataset visibility is immediately changed to true so the color transition from transparent is visible. | `'hide'` | animations.colors | `{ type: 'color', properties: ['borderColor', 'backgroundColor'], to: 'transparent' }` | Colors are faded to transparent when dataset id hidden using legend / [api](../developers/api.md#hidedatasetIndex). | `'hide'` | animations.visible | `{ type: 'boolean', easing: 'easeInExpo' }` | Visibility is changed to false at a very late phase of animation ## Disabling animation To disable an animation configuration, the animation node must be set to `false`, with the exception for animation modes which can be disabled by setting the `duration` to `0`. ```javascript chart.options.animation = false; // disables all animations chart.options.animations.colors = false; // disables animation defined by the collection of 'colors' properties chart.options.animations.x = false; // disables animation defined by the 'x' property chart.options.transitions.active.animation.duration = 0; // disables the animation for 'active' mode ``` ## Easing Available options are: * `'linear'` * `'easeInQuad'` * `'easeOutQuad'` * `'easeInOutQuad'` * `'easeInCubic'` * `'easeOutCubic'` * `'easeInOutCubic'` * `'easeInQuart'` * `'easeOutQuart'` * `'easeInOutQuart'` * `'easeInQuint'` * `'easeOutQuint'` * `'easeInOutQuint'` * `'easeInSine'` * `'easeOutSine'` * `'easeInOutSine'` * `'easeInExpo'` * `'easeOutExpo'` * `'easeInOutExpo'` * `'easeInCirc'` * `'easeOutCirc'` * `'easeInOutCirc'` * `'easeInElastic'` * `'easeOutElastic'` * `'easeInOutElastic'` * `'easeInBack'` * `'easeOutBack'` * `'easeInOutBack'` * `'easeInBounce'` * `'easeOutBounce'` * `'easeInOutBounce'` See [Robert Penner's easing equations](http://robertpenner.com/easing/). ## Animation Callbacks The animation configuration provides callbacks which are useful for synchronizing an external draw to the chart animation. The callbacks can be set only at main [animation configuration](#animation-configuration). Namespace: `options.animation` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `onProgress` | `function` | `null` | Callback called on each step of an animation. | `onComplete` | `function` | `null` | Callback called when all animations are completed. The callback is passed the following object: ```javascript { // Chart object chart: Chart, // Number of animations still in progress currentStep: number, // `true` for the initial animation of the chart initial: boolean, // Total number of animations at the start of current animation numSteps: number, } ``` The following example fills a progress bar during the chart animation. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { animation: { onProgress: function(animation) { progress.value = animation.currentStep / animation.numSteps; } } } }); ``` Another example usage of these callbacks can be found [in this progress bar sample,](../samples/advanced/progress-bar.md) which displays a progress bar showing how far along the animation is. ================================================ FILE: docs/configuration/canvas-background.md ================================================ # Canvas background In some use cases you would want a background image or color over the whole canvas. There is no built-in support for this, the way you can achieve this is by writing a custom plugin. In the two example plugins underneath here you can see how you can draw a color or image to the canvas as background. This way of giving the chart a background is only necessary if you want to export the chart with that specific background. For normal use you can set the background more easily with [CSS](https://www.w3schools.com/cssref/css3_pr_background.asp). :::: tabs ::: tab Color ```js chart-editor // const data = { labels: [ 'Red', 'Blue', 'Yellow' ], datasets: [{ label: 'My First Dataset', data: [300, 50, 100], backgroundColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)' ], hoverOffset: 4 }] }; // // // Note: changes to the plugin code is not reflected to the chart, because the plugin is loaded at chart construction time and editor changes only trigger an chart.update(). const plugin = { id: 'customCanvasBackgroundColor', beforeDraw: (chart, args, options) => { const {ctx} = chart; ctx.save(); ctx.globalCompositeOperation = 'destination-over'; ctx.fillStyle = options.color || '#99ffff'; ctx.fillRect(0, 0, chart.width, chart.height); ctx.restore(); } }; // // const config = { type: 'doughnut', data: data, options: { plugins: { customCanvasBackgroundColor: { color: 'lightGreen', } } }, plugins: [plugin], }; // module.exports = { actions: [], config: config, }; ``` ::: ::: tab Image ```js chart-editor // const data = { labels: [ 'Red', 'Blue', 'Yellow' ], datasets: [{ label: 'My First Dataset', data: [300, 50, 100], backgroundColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)' ], hoverOffset: 4 }] }; // // // Note: changes to the plugin code is not reflected to the chart, because the plugin is loaded at chart construction time and editor changes only trigger an chart.update(). const image = new Image(); image.src = 'https://www.chartjs.org/img/chartjs-logo.svg'; const plugin = { id: 'customCanvasBackgroundImage', beforeDraw: (chart) => { if (image.complete) { const ctx = chart.ctx; const {top, left, width, height} = chart.chartArea; const x = left + width / 2 - image.width / 2; const y = top + height / 2 - image.height / 2; ctx.drawImage(image, x, y); } else { image.onload = () => chart.draw(); } } }; // // const config = { type: 'doughnut', data: data, plugins: [plugin], }; // module.exports = { actions: [], config: config, }; ``` ::: :::: ================================================ FILE: docs/configuration/decimation.md ================================================ # Data Decimation The decimation plugin can be used with line charts to automatically decimate data at the start of the chart lifecycle. Before enabling this plugin, review the [requirements](#requirements) to ensure that it will work with the chart you want to create. ## Configuration Options Namespace: `options.plugins.decimation`, the global options for the plugin are defined in `Chart.defaults.plugins.decimation`. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `enabled` | `boolean` | `false` | Is decimation enabled? | `algorithm` | `string` | `'min-max'` | Decimation algorithm to use. See the [more...](#decimation-algorithms) | `samples` | `number` | | If the `'lttb'` algorithm is used, this is the number of samples in the output dataset. Defaults to the canvas width to pick 1 sample per pixel. | `threshold` | `number` | | If the number of samples in the current axis range is above this value, the decimation will be triggered. Defaults to 4 times the canvas width.
The number of point after decimation can be higher than the `threshold` value. ## Decimation Algorithms Decimation algorithm to use for data. Options are: * `'lttb'` * `'min-max'` ### Largest Triangle Three Bucket (LTTB) Decimation [LTTB](https://github.com/sveinn-steinarsson/flot-downsample) decimation reduces the number of data points significantly. This is most useful for showing trends in data using only a few data points. ### Min/Max Decimation [Min/max](https://digital.ni.com/public.nsf/allkb/F694FFEEA0ACF282862576020075F784) decimation will preserve peaks in your data but could require up to 4 points for each pixel. This type of decimation would work well for a very noisy signal where you need to see data peaks. ## Requirements To use the decimation plugin, the following requirements must be met: 1. The dataset must have an [`indexAxis`](../charts/line.md#general) of `'x'` 2. The dataset must be a line 3. The X axis for the dataset must be either a [`'linear'`](../axes/cartesian/linear.md) or [`'time'`](../axes/cartesian/time.md) type axis 4. Data must not need parsing, i.e. [`parsing`](../general/data-structures.md#dataset-configuration) must be `false` 5. The dataset object must be mutable. The plugin stores the original data as `dataset._data` and then defines a new `data` property on the dataset. 6. There must be more points on the chart than the threshold value. Take a look at the Configuration Options for more information. ## Related Samples * [Data Decimation Sample](../samples/advanced/data-decimation) ================================================ FILE: docs/configuration/device-pixel-ratio.md ================================================ # Device Pixel Ratio By default, the chart's canvas will use a 1:1 pixel ratio, unless the physical display has a higher pixel ratio (e.g. Retina displays). For applications where a chart will be converted to a bitmap, or printed to a higher DPI medium, it can be desirable to render the chart at a higher resolution than the default. Setting `devicePixelRatio` to a value other than 1 will force the canvas size to be scaled by that amount, relative to the container size. There should be no visible difference on screen; the difference will only be visible when the image is zoomed or printed. ## Configuration Options Namespace: `options` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `devicePixelRatio` | `number` | `window.devicePixelRatio` | Override the window's default devicePixelRatio. ================================================ FILE: docs/configuration/elements.md ================================================ # Elements While chart types provide settings to configure the styling of each dataset, you sometimes want to style **all datasets the same way**. A common example would be to stroke all the bars in a bar chart with the same colour but change the fill per dataset. Options can be configured for four different types of elements: **[arc](#arc-configuration)**, **[lines](#line-configuration)**, **[points](#point-configuration)**, and **[bars](#bar-configuration)**. When set, these options apply to all objects of that type unless specifically overridden by the configuration attached to a dataset. ## Global Configuration The element options can be specified per chart or globally. The global options for elements are defined in `Chart.defaults.elements`. For example, to set the border width of all bar charts globally, you would do: ```javascript Chart.defaults.elements.bar.borderWidth = 2; ``` ## Point Configuration Point elements are used to represent the points in a line, radar or bubble chart. Namespace: `options.elements.point`, global point options: `Chart.defaults.elements.point`. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `radius` | `number` | `3` | Point radius. | [`pointStyle`](#point-styles) | [`pointStyle`](#types) | `'circle'` | Point style. | `rotation` | `number` | `0` | Point rotation (in degrees). | `backgroundColor` | [`Color`](../general/colors.md) | `Chart.defaults.backgroundColor` | Point fill color. | `borderWidth` | `number` | `1` | Point stroke width. | `borderColor` | [`Color`](../general/colors.md) | `'Chart.defaults.borderColor` | Point stroke color. | `hitRadius` | `number` | `1` | Extra radius added to point radius for hit detection. | `hoverRadius` | `number` | `4` | Point radius when hovered. | `hoverBorderWidth` | `number` | `1` | Stroke width when hovered. ### Point Styles #### Types The `pointStyle` argument accepts the following type of inputs: `string`, `Image` and `HTMLCanvasElement` #### Info When a string is provided, the following values are supported: - `'circle'` - `'cross'` - `'crossRot'` - `'dash'` - `'line'` - `'rect'` - `'rectRounded'` - `'rectRot'` - `'star'` - `'triangle'` - `false` If the value is an image or a canvas element, that image or canvas element is drawn on the canvas using [drawImage](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/drawImage). ## Line Configuration Line elements are used to represent the line in a line chart. Namespace: `options.elements.line`, global line options: `Chart.defaults.elements.line`. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `tension` | `number` | `0` | Bézier curve tension (`0` for no Bézier curves). | `backgroundColor` | [`Color`](/general/colors.md) | `Chart.defaults.backgroundColor` | Line fill color. | `borderWidth` | `number` | `3` | Line stroke width. | `borderColor` | [`Color`](/general/colors.md) | `Chart.defaults.borderColor` | Line stroke color. | `borderCapStyle` | `string` | `'butt'` | Line cap style. See [MDN](https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap). | `borderDash` | `number[]` | `[]` | Line dash. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | `borderDashOffset` | `number` | `0.0` | Line dash offset. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `borderJoinStyle` | `'round'`\|`'bevel'`\|`'miter'` | `'miter'` | Line join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). | `capBezierPoints` | `boolean` | `true` | `true` to keep Bézier control inside the chart, `false` for no restriction. | `cubicInterpolationMode` | `string` | `'default'` | Interpolation mode to apply. [See more...](/charts/line.md#cubicinterpolationmode) | `fill` | `boolean`\|`string` | `false` | How to fill the area under the line. See [area charts](/charts/area.md#filling-modes). | `stepped` | `boolean` | `false` | `true` to show the line as a stepped line (`tension` will be ignored). ## Bar Configuration Bar elements are used to represent the bars in a bar chart. Namespace: `options.elements.bar`, global bar options: `Chart.defaults.elements.bar`. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `backgroundColor` | [`Color`](/general/colors.md) | `Chart.defaults.backgroundColor` | Bar fill color. | `borderWidth` | `number` | `0` | Bar stroke width. | `borderColor` | [`Color`](/general/colors.md) | `Chart.defaults.borderColor` | Bar stroke color. | `borderSkipped` | `string` | `'start'` | Skipped (excluded) border: `'start'`, `'end'`, `'middle'`, `'bottom'`, `'left'`, `'top'`, `'right'` or `false`. | `borderRadius` | `number`\|`object` | `0` | The bar border radius (in pixels). | `inflateAmount` | `number`\|`'auto'` | `'auto'` | The amount of pixels to inflate the bar rectangle(s) when drawing. | [`pointStyle`](#point-styles) | `string`\|`Image`\|`HTMLCanvasElement` | `'circle'` | Style of the point for legend. ## Arc Configuration Arcs are used in the polar area, doughnut and pie charts. Namespace: `options.elements.arc`, global arc options: `Chart.defaults.elements.arc`. | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `angle` - for polar only | `number` | `circumference / (arc count)` | Arc angle to cover. | `backgroundColor` | [`Color`](/general/colors.md) | `Chart.defaults.backgroundColor` | Arc fill color. | `borderAlign` | `'center'`\|`'inner'` | `'center'` | Arc stroke alignment. | `borderColor` | [`Color`](/general/colors.md) | `'#fff'` | Arc stroke color. | `borderDash` | `number[]` | `[]` | Arc line dash. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash). | `borderDashOffset` | `number` | `0.0` | Arc line dash offset. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset). | `borderJoinStyle` | `'round'`\|`'bevel'`\|`'miter'` | `'bevel'`\|`'round'` | Line join style. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin). The default is `'round'` when `borderAlign` is `'inner'` | `borderWidth`| `number` | `2` | Arc stroke width. | `circular` | `boolean` | `true` | By default the Arc is curved. If `circular: false` the Arc will be flat ================================================ FILE: docs/configuration/index.md ================================================ # Configuration The configuration is used to change how the chart behaves. There are properties to control styling, fonts, the legend, etc. ## Configuration object structure The top level structure of Chart.js configuration: ```javascript const config = { type: 'line', data: {}, options: {}, plugins: [] } ``` ### type Chart type determines the main type of the chart. **note** A dataset can override the `type`, this is how mixed charts are constructed. ### data See [Data Structures](../general/data-structures.md) for details. ### options Majority of the documentation talks about these options. ### plugins Inline plugins can be included in this array. It is an alternative way of adding plugins for single chart (vs registering the plugin globally). More about plugins in the [developers section](../developers/plugins.md). ## Global Configuration This concept was introduced in Chart.js 1.0 to keep configuration [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), and allow for changing options globally across chart types, avoiding the need to specify options for each instance, or the default for a particular chart type. Chart.js merges the `options` object passed to the chart with the global configuration using chart type defaults and scales defaults appropriately. This way you can be as specific as you would like in your individual chart configuration, while still changing the defaults for all chart types where applicable. The global general options are defined in `Chart.defaults`. The defaults for each chart type are discussed in the documentation for that chart type. The following example would set the interaction mode to 'nearest' for all charts where this was not overridden by the chart type defaults or the options passed to the constructor on creation. ```javascript Chart.defaults.interaction.mode = 'nearest'; // Interaction mode is set to nearest because it was not overridden here const chartInteractionModeNearest = new Chart(ctx, { type: 'line', data: data }); // This chart would have the interaction mode that was passed in const chartDifferentInteractionMode = new Chart(ctx, { type: 'line', data: data, options: { interaction: { // Overrides the global setting mode: 'index' } } }); ``` ## Dataset Configuration Options may be configured directly on the dataset. The dataset options can be changed at multiple different levels. See [options](../general/options.md#dataset-level-options) for details on how the options are resolved. The following example would set the `showLine` option to 'false' for all line datasets except for those overridden by options passed to the dataset on creation. ```javascript // Do not show lines for all datasets by default Chart.defaults.datasets.line.showLine = false; // This chart would show a line only for the third dataset const chart = new Chart(ctx, { type: 'line', data: { datasets: [{ data: [0, 0], }, { data: [0, 1] }, { data: [1, 0], showLine: true // overrides the `line` dataset default }, { type: 'scatter', // 'line' dataset default does not affect this dataset since it's a 'scatter' data: [1, 1] }] } }); ``` ================================================ FILE: docs/configuration/interactions.md ================================================ # Interactions Namespace: `options.interaction`, the global interaction configuration is at `Chart.defaults.interaction`. To configure which events trigger chart interactions, see [events](#events). | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `mode` | `string` | `'nearest'` | Sets which elements appear in the interaction. See [Interaction Modes](#modes) for details. | `intersect` | `boolean` | `true` | if true, the interaction mode only applies when the mouse position intersects an item on the chart. | `axis` | `string` | `'x'` | Can be set to `'x'`, `'y'`, `'xy'` or `'r'` to define which directions are used in calculating distances. Defaults to `'x'` for `'index'` mode and `'xy'` in `dataset` and `'nearest'` modes. | `includeInvisible` | `boolean` | `false` | if true, the invisible points that are outside of the chart area will also be included when evaluating interactions. By default, these options apply to both the hover and tooltip interactions. The same options can be set in the `options.hover` namespace, in which case they will only affect the hover interaction. Similarly, the options can be set in the `options.plugins.tooltip` namespace to independently configure the tooltip interactions. ## Events The following properties define how the chart interacts with events. Namespace: `options` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `events` | `string[]` | `['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove']` | The `events` option defines the browser events that the chart should listen to for. Each of these events trigger hover and are passed to plugins. [more...](#event-option) | `onHover` | `function` | `null` | Called when any of the events fire over chartArea. Passed the event, an array of active elements (bars, points, etc), and the chart. | `onClick` | `function` | `null` | Called if the event is of type `'mouseup'`, `'click'` or '`'contextmenu'` over chartArea. Passed the event, an array of active elements, and the chart. ### Event Option For example, to have the chart only respond to click events, you could do: ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { // This chart will not respond to mousemove, etc events: ['click'] } }); ``` Events for each plugin can be further limited by defining (allowed) events array in plugin options: ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { // All of these (default) events trigger a hover and are passed to all plugins, // unless limited at plugin options events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'], plugins: { tooltip: { // Tooltip will only receive click events events: ['click'] } } } }); ``` Events that do not fire over chartArea, like `mouseout`, can be captured using a simple plugin: ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { // these are the default events: // events: ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'], }, plugins: [{ id: 'myEventCatcher', beforeEvent(chart, args, pluginOptions) { const event = args.event; if (event.type === 'mouseout') { // process the event } } }] }); ``` For more information about plugins, see [Plugins](../developers/plugins.md) ### Converting Events to Data Values A common occurrence is taking an event, such as a click, and finding the data coordinates on the chart where the event occurred. Chart.js provides helpers that make this a straightforward process. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { onClick: (e) => { const canvasPosition = Chart.helpers.getRelativePosition(e, chart); // Substitute the appropriate scale IDs const dataX = chart.scales.x.getValueForPixel(canvasPosition.x); const dataY = chart.scales.y.getValueForPixel(canvasPosition.y); } } }); ``` When using a bundler, the helper functions have to be imported separately, for a full explanation of this please head over to the [integration](../getting-started/integration.md#helper-functions) page ## Modes When configuring the interaction with the graph via `interaction`, `hover` or `tooltips`, a number of different modes are available. `options.hover` and `options.plugins.tooltip` extend from `options.interaction`. So if `mode`, `intersect` or any other common settings are configured only in `options.interaction`, both hover and tooltips obey that. The modes are detailed below and how they behave in conjunction with the `intersect` setting. See how different modes work with the tooltip in [tooltip interactions sample](../samples/tooltip/interactions.md ) ### point Finds all of the items that intersect the point. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { interaction: { mode: 'point' } } }); ``` ### nearest Gets the items that are at the nearest distance to the point. The nearest item is determined based on the distance to the center of the chart item (point, bar). You can use the `axis` setting to define which coordinates are considered in distance calculation. If `intersect` is true, this is only triggered when the mouse position intersects an item in the graph. This is very useful for combo charts where points are hidden behind bars. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { interaction: { mode: 'nearest' } } }); ``` ### index Finds item at the same index. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item, in the x direction, is used to determine the index. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { interaction: { mode: 'index' } } }); ``` To use index mode in a chart like the horizontal bar chart, where we search along the y direction, you can use the `axis` setting introduced in v2.7.0. By setting this value to `'y'` on the y direction is used. ```javascript const chart = new Chart(ctx, { type: 'bar', data: data, options: { interaction: { mode: 'index', axis: 'y' } } }); ``` ### dataset Finds items in the same dataset. If the `intersect` setting is true, the first intersecting item is used to determine the index in the data. If `intersect` false the nearest item is used to determine the index. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { interaction: { mode: 'dataset' } } }); ``` ### x Returns all items that would intersect based on the `X` coordinate of the position only. Would be useful for a vertical cursor implementation. Note that this only applies to cartesian charts. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { interaction: { mode: 'x' } } }); ``` ### y Returns all items that would intersect based on the `Y` coordinate of the position. This would be useful for a horizontal cursor implementation. Note that this only applies to cartesian charts. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { interaction: { mode: 'y' } } }); ``` ## Custom Interaction Modes New modes can be defined by adding functions to the `Chart.Interaction.modes` map. You can use the `Chart.Interaction.evaluateInteractionItems` function to help implement these. Example: ```javascript import { Interaction } from 'chart.js'; import { getRelativePosition } from 'chart.js/helpers'; /** * Custom interaction mode * @function Interaction.modes.myCustomMode * @param {Chart} chart - the chart we are returning items from * @param {Event} e - the event we are find things at * @param {InteractionOptions} options - options to use * @param {boolean} [useFinalPosition] - use final element position (animation target) * @return {InteractionItem[]} - items that are found */ Interaction.modes.myCustomMode = function(chart, e, options, useFinalPosition) { const position = getRelativePosition(e, chart); const items = []; Interaction.evaluateInteractionItems(chart, 'x', position, (element, datasetIndex, index) => { if (element.inXRange(position.x, useFinalPosition) && myCustomLogic(element)) { items.push({element, datasetIndex, index}); } }); return items; }; // Then, to use it... new Chart.js(ctx, { type: 'line', data: data, options: { interaction: { mode: 'myCustomMode' } } }) ``` If you're using TypeScript, you'll also need to register the new mode: ```typescript declare module 'chart.js' { interface InteractionModeMap { myCustomMode: InteractionModeFunction; } } ``` ================================================ FILE: docs/configuration/layout.md ================================================ # Layout Namespace: `options.layout`, the global options for the chart layout is defined in `Chart.defaults.layout`. | Name | Type | Default | [Scriptable](../general/options.md#scriptable-options) | Description | ---- | ---- | ------- | :----: | ----------- | `autoPadding` | `boolean` | `true` | No | Apply automatic padding so visible elements are completely drawn. | `padding` | [`Padding`](../general/padding.md) | `0` | Yes | The padding to add inside the chart. ================================================ FILE: docs/configuration/legend.md ================================================ # Legend The chart legend displays data about the datasets that are appearing on the chart. ## Configuration options Namespace: `options.plugins.legend`, the global options for the chart legend is defined in `Chart.defaults.plugins.legend`. :::warning The doughnut, pie, and polar area charts override the legend defaults. To change the overrides for those chart types, the options are defined in `Chart.overrides[type].plugins.legend`. ::: | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `display` | `boolean` | `true` | Is the legend shown? | `position` | `string` | `'top'` | Position of the legend. [more...](#position) | `align` | `string` | `'center'` | Alignment of the legend. [more...](#align) | `maxHeight` | `number` | | Maximum height of the legend, in pixels | `maxWidth` | `number` | | Maximum width of the legend, in pixels | `fullSize` | `boolean` | `true` | Marks that this box should take the full width/height of the canvas (moving other boxes). This is unlikely to need to be changed in day-to-day use. | `onClick` | `function` | | A callback that is called when a click event is registered on a label item. Arguments: `[event, legendItem, legend]`. | `onHover` | `function` | | A callback that is called when a 'mousemove' event is registered on top of a label item. Arguments: `[event, legendItem, legend]`. | `onLeave` | `function` | | A callback that is called when a 'mousemove' event is registered outside of a previously hovered label item. Arguments: `[event, legendItem, legend]`. | `reverse` | `boolean` | `false` | Legend will show datasets in reverse order. | `labels` | `object` | | See the [Legend Label Configuration](#legend-label-configuration) section below. | `rtl` | `boolean` | | `true` for rendering the legends from right to left. | `textDirection` | `string` | canvas' default | This will force the text direction `'rtl'` or `'ltr'` on the canvas for rendering the legend, regardless of the css specified on the canvas | `title` | `object` | | See the [Legend Title Configuration](#legend-title-configuration) section below. :::tip Note If you need more visual customizations, please use an [HTML legend](../samples/legend/html.md). ::: ## Position Position of the legend. Options are: * `'top'` * `'left'` * `'bottom'` * `'right'` * `'chartArea'` When using the `'chartArea'` option the legend position is at the moment not configurable, it will always be on the left side of the chart in the middle. ## Align Alignment of the legend. Options are: * `'start'` * `'center'` * `'end'` Defaults to `'center'` for unrecognized values. ## Legend Label Configuration Namespace: `options.plugins.legend.labels` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `boxWidth` | `number` | `40` | Width of coloured box. | `boxHeight` | `number` | `font.size` | Height of the coloured box. | `color` | [`Color`](../general/colors.md) | `Chart.defaults.color` | Color of label and the strikethrough. | `font` | `Font` | `Chart.defaults.font` | See [Fonts](../general/fonts.md) | `padding` | `number` | `10` | Padding between rows of colored boxes. | `generateLabels` | `function` | | Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See [Legend Item](#legend-item-interface) for details. | `filter` | `function` | `null` | Filters legend items out of the legend. Receives 2 parameters, a [Legend Item](#legend-item-interface) and the chart data. | `sort` | `function` | `null` | Sorts legend items. Type is : `sort(a: LegendItem, b: LegendItem, data: ChartData): number;`. Receives 3 parameters, two [Legend Items](#legend-item-interface) and the chart data. The return value of the function is a number that indicates the order of the two legend item parameters. The ordering matches the [return value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#description) of `Array.prototype.sort()` | [`pointStyle`](elements.md#point-styles) | [`pointStyle`](elements.md#types) | `'circle'` | If specified, this style of point is used for the legend. Only used if `usePointStyle` is true. | `textAlign` | `string` | `'center'` | Horizontal alignment of the label text. Options are: `'left'`, `'right'` or `'center'`. | `usePointStyle` | `boolean` | `false` | Label style will match corresponding point style (size is based on pointStyleWidth or the minimum value between boxWidth and font.size). | `pointStyleWidth` | `number` | `null` | If `usePointStyle` is true, the width of the point style used for the legend. | `useBorderRadius` | `boolean` | `false` | Label borderRadius will match corresponding borderRadius. | `borderRadius` | `number` | `undefined` | Override the borderRadius to use. ## Legend Title Configuration Namespace: `options.plugins.legend.title` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `color` | [`Color`](../general/colors.md) | `Chart.defaults.color` | Color of text. | `display` | `boolean` | `false` | Is the legend title displayed. | `font` | `Font` | `Chart.defaults.font` | See [Fonts](../general/fonts.md) | `padding` | [`Padding`](../general/padding.md) | `0` | Padding around the title. | `text` | `string` | | The string title. ## Legend Item Interface Items passed to the legend `onClick` function are the ones returned from `labels.generateLabels`. These items must implement the following interface. ```javascript { // Label that will be displayed text: string, // Border radius of the legend item. // Introduced in 3.1.0 borderRadius?: number | BorderRadius, // Index of the associated dataset datasetIndex: number, // Fill style of the legend box fillStyle: Color, // Text color fontColor: Color, // If true, this item represents a hidden dataset. Label will be rendered with a strike-through effect hidden: boolean, // For box border. See https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap lineCap: string, // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash lineDash: number[], // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset lineDashOffset: number, // For box border. See https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin lineJoin: string, // Width of box border lineWidth: number, // Stroke style of the legend box strokeStyle: Color, // Point style of the legend box (only used if usePointStyle is true) pointStyle: string | Image | HTMLCanvasElement, // Rotation of the point in degrees (only used if usePointStyle is true) rotation: number } ``` ## Example The following example will create a chart with the legend enabled and turn all the text red in color. ```javascript const chart = new Chart(ctx, { type: 'bar', data: data, options: { plugins: { legend: { display: true, labels: { color: 'rgb(255, 99, 132)' } } } } }); ``` ## Custom On Click Actions It can be common to want to trigger different behaviour when clicking an item in the legend. This can be easily achieved using a callback in the config object. The default legend click handler is: ```javascript function(e, legendItem, legend) { const index = legendItem.datasetIndex; const ci = legend.chart; if (ci.isDatasetVisible(index)) { ci.hide(index); legendItem.hidden = true; } else { ci.show(index); legendItem.hidden = false; } } ``` Let's say we wanted instead to link the display of the first two datasets. We could change the click handler accordingly. ```javascript const defaultLegendClickHandler = Chart.defaults.plugins.legend.onClick; const pieDoughnutLegendClickHandler = Chart.controllers.doughnut.overrides.plugins.legend.onClick; const newLegendClickHandler = function (e, legendItem, legend) { const index = legendItem.datasetIndex; const type = legend.chart.config.type; if (index > 1) { // Do the original logic if (type === 'pie' || type === 'doughnut') { pieDoughnutLegendClickHandler(e, legendItem, legend) } else { defaultLegendClickHandler(e, legendItem, legend); } } else { let ci = legend.chart; [ ci.getDatasetMeta(0), ci.getDatasetMeta(1) ].forEach(function(meta) { meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null; }); ci.update(); } }; const chart = new Chart(ctx, { type: 'line', data: data, options: { plugins: { legend: { onClick: newLegendClickHandler } } } }); ``` Now when you click the legend in this chart, the visibility of the first two datasets will be linked together. ================================================ FILE: docs/configuration/locale.md ================================================ # Locale For applications where the numbers of ticks on scales must be formatted accordingly with a language sensitive number formatting, you can enable this kind of formatting by setting the `locale` option. The locale is a string that is a [Unicode BCP 47 locale identifier](https://www.unicode.org/reports/tr35/tr35.html#BCP_47_Conformance). A Unicode BCP 47 locale identifier consists of 1. a language code, 2. (optionally) a script code, 3. (optionally) a region (or country) code, 4. (optionally) one or more variant codes, and 5. (optionally) one or more extension sequences, with all present components separated by hyphens. By default, the chart is using the default locale of the platform which is running on. ## Configuration Options Namespace: `options` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `locale` | `string` | `undefined` | a string with a BCP 47 language tag, leveraging on [INTL NumberFormat](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat). ================================================ FILE: docs/configuration/responsive.md ================================================ # Responsive Charts When it comes to changing the chart size based on the window size, a major limitation is that the canvas *render* size (`canvas.width` and `.height`) can **not** be expressed with relative values, contrary to the *display* size (`canvas.style.width` and `.height`). Furthermore, these sizes are independent of each other and thus the canvas *render* size does not adjust automatically based on the *display* size, making the rendering inaccurate. The following examples **do not work**: - ``: **invalid** values, the canvas doesn't resize ([example](https://codepen.io/chartjs/pen/oWLZaR)) - ``: **invalid** behavior, the canvas is resized but becomes blurry ([example](https://codepen.io/chartjs/pen/WjxpmO)) - ``: **invalid** behavior, the canvas continually shrinks. Chart.js needs a dedicated container for each canvas and this styling should be applied there. Chart.js provides a [few options](#configuration-options) to enable responsiveness and control the resize behavior of charts by detecting when the canvas *display* size changes and update the *render* size accordingly. ## Configuration Options Namespace: `options` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `responsive` | `boolean` | `true` | Resizes the chart canvas when its container does ([important note...](#important-note)). | `maintainAspectRatio` | `boolean` | `true` | Maintain the original canvas aspect ratio `(width / height)` when resizing. | `aspectRatio` | `number` | `1`\|`2` | Canvas aspect ratio (i.e. `width / height`, a value of 1 representing a square canvas). Note that this option is ignored if the height is explicitly defined either as attribute or via the style. The default value varies by chart type; Radial charts (doughnut, pie, polarArea, radar) default to `1` and others default to `2`. | `onResize` | `function` | `null` | Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. | `resizeDelay` | `number` | `0` | Delay the resize update by the given amount of milliseconds. This can ease the resize process by debouncing the update of the elements. ## Important Note Detecting when the canvas size changes can not be done directly from the `canvas` element. Chart.js uses its parent container to update the canvas *render* and *display* sizes. However, this method requires the container to be **relatively positioned** and **dedicated to the chart canvas only**. Responsiveness can then be achieved by setting relative values for the container size ([example](https://codepen.io/chartjs/pen/YVWZbz)): ```html
``` The chart can also be programmatically resized by modifying the container size: ```javascript chart.canvas.parentNode.style.height = '128px'; chart.canvas.parentNode.style.width = '128px'; ``` Note that in order for the above code to correctly resize the chart height, the [`maintainAspectRatio`](#configuration-options) option must also be set to `false`. ## Flexbox / Grid Layout To prevent overflow issues when using flexbox / grid layout, you must set the flex / grid child element to have a `min-width` of `0`. See [issue 4156](https://github.com/chartjs/Chart.js/issues/4156#issuecomment-295180128) for more details. ```html
``` ## Printing Resizable Charts CSS media queries allow changing styles when printing a page. The CSS applied from these media queries may cause charts to need to resize. However, the resize won't happen automatically. To support resizing charts when printing, you need to hook the [onbeforeprint](https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeprint) event and manually trigger resizing of each chart. ```javascript function beforePrintHandler () { for (let id in Chart.instances) { Chart.instances[id].resize(); } } ``` You may also find that, due to complexities in when the browser lays out the document for printing and when resize events are fired, Chart.js is unable to properly resize for the print layout. To work around this, you can pass an explicit size to `.resize()` then use an [onafterprint](https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onafterprint) event to restore the automatic size when done. ```javascript window.addEventListener('beforeprint', () => { myChart.resize(600, 600); }); window.addEventListener('afterprint', () => { myChart.resize(); }); ``` ================================================ FILE: docs/configuration/subtitle.md ================================================ # Subtitle Subtitle is a second title placed under the main title, by default. It has exactly the same configuration options with the main [title](./title.md). ## Subtitle Configuration Namespace: `options.plugins.subtitle`. The global defaults for subtitle are configured in `Chart.defaults.plugins.subtitle`. Exactly the same configuration options with [title](./title.md) are available for subtitle, the namespaces only differ. ## Example Usage The example below would enable a title of 'Custom Chart Subtitle' on the chart that is created. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { plugins: { subtitle: { display: true, text: 'Custom Chart Subtitle' } } } }); ``` ================================================ FILE: docs/configuration/title.md ================================================ # Title The chart title defines text to draw at the top of the chart. ## Title Configuration Namespace: `options.plugins.title`, the global options for the chart title is defined in `Chart.defaults.plugins.title`. | Name | Type | Default | [Scriptable](../general/options.md#scriptable-options) | Description | ---- | ---- | ------- | :----: | ----------- | `align` | `string` | `'center'` | Yes | Alignment of the title. [more...](#align) | `color` | [`Color`](../general/colors.md) | `Chart.defaults.color` | Yes | Color of text. | `display` | `boolean` | `false` | Yes | Is the title shown? | `fullSize` | `boolean` | `true` | Yes | Marks that this box should take the full width/height of the canvas. If `false`, the box is sized and placed above/beside the chart area. | `position` | `string` | `'top'` | Yes | Position of title. [more...](#position) | `font` | `Font` | `{weight: 'bold'}` | Yes | See [Fonts](../general/fonts.md) | `padding` | [`Padding`](../general/padding.md) | `10` | Yes | Padding to apply around the title. Only `top` and `bottom` are implemented. | `text` | `string`\|`string[]` | `''` | Yes | Title text to display. If specified as an array, text is rendered on multiple lines. :::tip Note If you need more visual customizations, you can implement the title with HTML and CSS. ::: ### Position Possible title position values are: * `'top'` * `'left'` * `'bottom'` * `'right'` ## Align Alignment of the title. Options are: * `'start'` * `'center'` * `'end'` ## Example Usage The example below would enable a title of 'Custom Chart Title' on the chart that is created. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { plugins: { title: { display: true, text: 'Custom Chart Title' } } } }); ``` This example shows how to specify separate top and bottom title text padding: ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { plugins: { title: { display: true, text: 'Custom Chart Title', padding: { top: 10, bottom: 30 } } } } }); ``` ================================================ FILE: docs/configuration/tooltip.md ================================================ # Tooltip ## Tooltip Configuration Namespace: `options.plugins.tooltip`, the global options for the chart tooltips is defined in `Chart.defaults.plugins.tooltip`. :::warning The `titleFont`, `bodyFont` and `footerFont` options default to the `Chart.defaults.font` options. To change the overrides for those options, you will need to pass a function that returns a font object. See section about [overriding default fonts](#default-font-overrides) for extra information below. ::: | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `enabled` | `boolean` | `true` | Are on-canvas tooltips enabled? | `external` | `function` | `null` | See [external tooltip](#external-custom-tooltips) section. | `mode` | `string` | `interaction.mode` | Sets which elements appear in the tooltip. [more...](interactions.md#modes). | `intersect` | `boolean` | `interaction.intersect` | If true, the tooltip mode applies only when the mouse position intersects with an element. If false, the mode will be applied at all times. | `position` | `string` | `'average'` | The mode for positioning the tooltip. [more...](#position-modes) | `callbacks` | `object` | | See the [callbacks section](#tooltip-callbacks). | `itemSort` | `function` | | Sort tooltip items. [more...](#sort-callback) | `filter` | `function` | | Filter tooltip items. [more...](#filter-callback) | `backgroundColor` | [`Color`](../general/colors.md) | `'rgba(0, 0, 0, 0.8)'` | Background color of the tooltip. | `titleColor` | [`Color`](../general/colors.md) | `'#fff'` | Color of title text. | `titleFont` | `Font` | `{weight: 'bold'}` | See [Fonts](../general/fonts.md). | `titleAlign` | `string` | `'left'` | Horizontal alignment of the title text lines. [more...](#text-alignment) | `titleSpacing` | `number` | `2` | Spacing to add to top and bottom of each title line. | `titleMarginBottom` | `number` | `6` | Margin to add on bottom of title section. | `bodyColor` | [`Color`](../general/colors.md) | `'#fff'` | Color of body text. | `bodyFont` | `Font` | `{}` | See [Fonts](../general/fonts.md). | `bodyAlign` | `string` | `'left'` | Horizontal alignment of the body text lines. [more...](#text-alignment) | `bodySpacing` | `number` | `2` | Spacing to add to top and bottom of each tooltip item. | `footerColor` | [`Color`](../general/colors.md) | `'#fff'` | Color of footer text. | `footerFont` | `Font` | `{weight: 'bold'}` | See [Fonts](../general/fonts.md). | `footerAlign` | `string` | `'left'` | Horizontal alignment of the footer text lines. [more...](#text-alignment) | `footerSpacing` | `number` | `2` | Spacing to add to top and bottom of each footer line. | `footerMarginTop` | `number` | `6` | Margin to add before drawing the footer. | `padding` | [`Padding`](../general/padding.md) | `6` | Padding inside the tooltip. | `caretPadding` | `number` | `2` | Extra distance to move the end of the tooltip arrow away from the tooltip point. | `caretSize` | `number` | `5` | Size, in px, of the tooltip arrow. | `cornerRadius` | `number`\|`object` | `6` | Radius of tooltip corner curves. | `multiKeyBackground` | [`Color`](../general/colors.md) | `'#fff'` | Color to draw behind the colored boxes when multiple items are in the tooltip. | `displayColors` | `boolean` | `true` | If true, color boxes are shown in the tooltip. | `boxWidth` | `number` | `bodyFont.size` | Width of the color box if displayColors is true. | `boxHeight` | `number` | `bodyFont.size` | Height of the color box if displayColors is true. | `boxPadding` | `number` | `1` | Padding between the color box and the text. | `usePointStyle` | `boolean` | `false` | Use the corresponding point style (from dataset options) instead of color boxes, ex: star, triangle etc. (size is based on the minimum value between boxWidth and boxHeight). | `borderColor` | [`Color`](../general/colors.md) | `'rgba(0, 0, 0, 0)'` | Color of the border. | `borderWidth` | `number` | `0` | Size of the border. | `rtl` | `boolean` | | `true` for rendering the tooltip from right to left. | `textDirection` | `string` | canvas' default | This will force the text direction `'rtl'` or `'ltr'` on the canvas for rendering the tooltips, regardless of the css specified on the canvas | `xAlign` | `string` | `undefined` | Position of the tooltip caret in the X direction. [more](#tooltip-alignment) | `yAlign` | `string` | `undefined` | Position of the tooltip caret in the Y direction. [more](#tooltip-alignment) :::tip Note If you need more visual customizations, please use an [HTML tooltip](../samples/tooltip/html.md). ::: ### Position Modes Possible modes are: * `'average'` * `'nearest'` `'average'` mode will place the tooltip at the average position of the items displayed in the tooltip. `'nearest'` will place the tooltip at the position of the element closest to the event position. You can also define [custom position modes](#custom-position-modes). ### Tooltip Alignment The `xAlign` and `yAlign` options define the position of the tooltip caret. If these parameters are unset, the optimal caret position is determined. The following values for the `xAlign` setting are supported. * `'left'` * `'center'` * `'right'` The following values for the `yAlign` setting are supported. * `'top'` * `'center'` * `'bottom'` ### Text Alignment The `titleAlign`, `bodyAlign` and `footerAlign` options define the horizontal position of the text lines with respect to the tooltip box. The following values are supported. * `'left'` (default) * `'right'` * `'center'` These options are only applied to text lines. Color boxes are always aligned to the left edge. ### Sort Callback Allows sorting of [tooltip items](#tooltip-item-context). Must implement at minimum a function that can be passed to [Array.prototype.sort](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort). This function can also accept a third parameter that is the data object passed to the chart. ### Filter Callback Allows filtering of [tooltip items](#tooltip-item-context). Must implement at minimum a function that can be passed to [Array.prototype.filter](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Array/filter). This function can also accept a fourth parameter that is the data object passed to the chart. ## Tooltip Callbacks Namespace: `options.plugins.tooltip.callbacks`, the tooltip has the following callbacks for providing text. For all functions, `this` will be the tooltip object created from the `Tooltip` constructor. If the callback returns `undefined`, then the default callback will be used. To remove things from the tooltip callback should return an empty string. Namespace: `data.datasets[].tooltip.callbacks`, items marked with `Yes` in the column `Dataset override` can be overridden per dataset. A [tooltip item context](#tooltip-item-context) is generated for each item that appears in the tooltip. This is the primary model that the callback methods interact with. For functions that return text, arrays of strings are treated as multiple lines of text. | Name | Arguments | Return Type | Dataset override | Description | ---- | --------- | ----------- | ---------------- | ----------- | `beforeTitle` | `TooltipItem[]` | `string | string[] | undefined` | | Returns the text to render before the title. | `title` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render as the title of the tooltip. | `afterTitle` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render after the title. | `beforeBody` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render before the body section. | `beforeLabel` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render before an individual label. This will be called for each item in the tooltip. | `label` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render for an individual item in the tooltip. [more...](#label-callback) | `labelColor` | `TooltipItem` | `object | undefined` | Yes | Returns the colors to render for the tooltip item. [more...](#label-color-callback) | `labelTextColor` | `TooltipItem` | `Color | undefined` | Yes | Returns the colors for the text of the label for the tooltip item. | `labelPointStyle` | `TooltipItem` | `object | undefined` | Yes | Returns the point style to use instead of color boxes if usePointStyle is true (object with values `pointStyle` and `rotation`). Default implementation uses the point style from the dataset points. [more...](#label-point-style-callback) | `afterLabel` | `TooltipItem` | `string | string[] | undefined` | Yes | Returns text to render after an individual label. | `afterBody` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render after the body section. | `beforeFooter` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render before the footer section. | `footer` | `TooltipItem[]` | `string | string[] | undefined` | | Returns text to render as the footer of the tooltip. | `afterFooter` | `TooltipItem[]` | `string | string[] | undefined` | | Text to render after the footer section. ### Label Callback The `label` callback can change the text that displays for a given data point. A common example to show a unit. The example below puts a `'$'` before every row. ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { plugins: { tooltip: { callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.parsed.y !== null) { label += new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(context.parsed.y); } return label; } } } } } }); ``` ### Label Color Callback For example, to return a red box with a blue dashed border that has a border radius for each item in the tooltip you could do: ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { plugins: { tooltip: { callbacks: { labelColor: function(context) { return { borderColor: 'rgb(0, 0, 255)', backgroundColor: 'rgb(255, 0, 0)', borderWidth: 2, borderDash: [2, 2], borderRadius: 2, }; }, labelTextColor: function(context) { return '#543453'; } } } } } }); ``` ### Label Point Style Callback For example, to draw triangles instead of the regular color box for each item in the tooltip, you could do: ```javascript const chart = new Chart(ctx, { type: 'line', data: data, options: { plugins: { tooltip: { usePointStyle: true, callbacks: { labelPointStyle: function(context) { return { pointStyle: 'triangle', rotation: 0 }; } } } } } }); ``` ### Tooltip Item Context The tooltip items passed to the tooltip callbacks implement the following interface. ```javascript { // The chart the tooltip is being shown on chart: Chart // Label for the tooltip label: string, // Parsed data values for the given `dataIndex` and `datasetIndex` parsed: object, // Raw data values for the given `dataIndex` and `datasetIndex` raw: object, // Formatted value for the tooltip formattedValue: string, // The dataset the item comes from dataset: object // Index of the dataset the item comes from datasetIndex: number, // Index of this data item in the dataset dataIndex: number, // The chart element (point, arc, bar, etc.) for this tooltip item element: Element, } ``` ## External (Custom) Tooltips External tooltips allow you to hook into the tooltip rendering process so that you can render the tooltip in your own custom way. Generally this is used to create an HTML tooltip instead of an on-canvas tooltip. The `external` option takes a function which is passed a context parameter containing the `chart` and `tooltip`. You can enable external tooltips in the global or chart configuration like so: ```javascript const myPieChart = new Chart(ctx, { type: 'pie', data: data, options: { plugins: { tooltip: { // Disable the on-canvas tooltip enabled: false, external: function(context) { // Tooltip Element let tooltipEl = document.getElementById('chartjs-tooltip'); // Create element on first render if (!tooltipEl) { tooltipEl = document.createElement('div'); tooltipEl.id = 'chartjs-tooltip'; tooltipEl.innerHTML = '
'; document.body.appendChild(tooltipEl); } // Hide if no tooltip const tooltipModel = context.tooltip; if (tooltipModel.opacity === 0) { tooltipEl.style.opacity = 0; return; } // Set caret Position tooltipEl.classList.remove('above', 'below', 'no-transform'); if (tooltipModel.yAlign) { tooltipEl.classList.add(tooltipModel.yAlign); } else { tooltipEl.classList.add('no-transform'); } function getBody(bodyItem) { return bodyItem.lines; } // Set Text if (tooltipModel.body) { const titleLines = tooltipModel.title || []; const bodyLines = tooltipModel.body.map(getBody); let innerHtml = ''; titleLines.forEach(function(title) { innerHtml += '' + title + ''; }); innerHtml += ''; bodyLines.forEach(function(body, i) { const colors = tooltipModel.labelColors[i]; let style = 'background:' + colors.backgroundColor; style += '; border-color:' + colors.borderColor; style += '; border-width: 2px'; const span = '' + body + ''; innerHtml += '' + span + ''; }); innerHtml += ''; let tableRoot = tooltipEl.querySelector('table'); tableRoot.innerHTML = innerHtml; } const position = context.chart.canvas.getBoundingClientRect(); const bodyFont = Chart.helpers.toFont(tooltipModel.options.bodyFont); // Display, position, and set styles for font tooltipEl.style.opacity = 1; tooltipEl.style.position = 'absolute'; tooltipEl.style.left = position.left + window.pageXOffset + tooltipModel.caretX + 'px'; tooltipEl.style.top = position.top + window.pageYOffset + tooltipModel.caretY + 'px'; tooltipEl.style.font = bodyFont.string; tooltipEl.style.padding = tooltipModel.padding + 'px ' + tooltipModel.padding + 'px'; tooltipEl.style.pointerEvents = 'none'; } } } } }); ``` See [samples](/samples/tooltip/html.md) for examples on how to get started with external tooltips. ## Tooltip Model The tooltip model contains parameters that can be used to render the tooltip. ```javascript { chart: Chart, // The items that we are rendering in the tooltip. See Tooltip Item Interface section dataPoints: TooltipItem[], // Positioning xAlign: string, yAlign: string, // X and Y properties are the top left of the tooltip x: number, y: number, width: number, height: number, // Where the tooltip points to caretX: number, caretY: number, // Body // The body lines that need to be rendered // Each object contains 3 parameters // before: string[] // lines of text before the line with the color square // lines: string[], // lines of text to render as the main item with color square // after: string[], // lines of text to render after the main lines body: object[], // lines of text that appear after the title but before the body beforeBody: string[], // line of text that appear after the body and before the footer afterBody: string[], // Title // lines of text that form the title title: string[], // Footer // lines of text that form the footer footer: string[], // style to render for each item in body[]. This is the style of the squares in the tooltip labelColors: TooltipLabelStyle[], labelTextColors: Color[], labelPointStyles: { pointStyle: PointStyle; rotation: number }[], // 0 opacity is a hidden tooltip opacity: number, // tooltip options options: Object } ``` ## Custom Position Modes New modes can be defined by adding functions to the `Chart.Tooltip.positioners` map. Example: ```javascript import { Tooltip } from 'chart.js'; /** * Custom positioner * @function Tooltip.positioners.myCustomPositioner * @param elements {Chart.Element[]} the tooltip elements * @param eventPosition {Point} the position of the event in canvas coordinates * @returns {TooltipPosition} the tooltip position */ Tooltip.positioners.myCustomPositioner = function(elements, eventPosition) { // A reference to the tooltip model const tooltip = this; /* ... */ return { x: 0, y: 0 // You may also include xAlign and yAlign to override those tooltip options. }; }; // Then, to use it... new Chart(ctx, { data, options: { plugins: { tooltip: { position: 'myCustomPositioner' } } } }) ``` See [samples](/samples/tooltip/position.md) for a more detailed example. If you're using TypeScript, you'll also need to register the new mode: ```typescript declare module 'chart.js' { interface TooltipPositionerMap { myCustomPositioner: TooltipPositionerFunction; } } ``` ## Default font overrides By default, the `titleFont`, `bodyFont` and `footerFont` listen to the `Chart.defaults.font` options for setting its values. Overriding these normally by accessing the object won't work because it is backed by a get function that looks to the default `font` namespace. So you will need to override this get function with your own function that returns the desired config. Example: ```javascript Chart.defaults.plugins.tooltip.titleFont = () => ({ size: 20, lineHeight: 1.2, weight: 800 }); ``` ================================================ FILE: docs/developers/api.md ================================================ # API For each chart, there are a set of global prototype methods on the shared chart type which you may find useful. These are available on all charts created with Chart.js, but for the examples, let's use a line chart we've made. ```javascript // For example: const myLineChart = new Chart(ctx, config); ``` ## .destroy() Use this to destroy any chart instances that are created. This will clean up any references stored to the chart object within Chart.js, along with any associated event listeners attached by Chart.js. This must be called before the canvas is reused for a new chart. ```javascript // Destroys a specific chart instance myLineChart.destroy(); ``` ## .update(mode?) Triggers an update of the chart. This can be safely called after updating the data object. This will update all scales, legends, and then re-render the chart. ```javascript myLineChart.data.datasets[0].data[2] = 50; // Would update the first dataset's value of 'March' to be 50 myLineChart.update(); // Calling update now animates the position of March from 90 to 50. ``` A `mode` can be provided to indicate transition configuration should be used. This can be either: - **string value**: Core calls this method using any of `'active'`, `'hide'`, `'reset'`, `'resize'`, `'show'` or `undefined`. `'none'` is also supported for skipping animations for single update. Please see [animations](../configuration/animations.md) docs for more details. - **function**: that receives a context object `{ datasetIndex: number }` and returns a mode string, allowing different modes per dataset. Examples: ```javascript // Using string mode myChart.update('active'); // Using function mode for dataset-specific animations myChart.update(ctx => ctx.datasetIndex === 0 ? 'active' : 'none'); ``` See [Updating Charts](updates.md) for more details. ## .reset() Reset the chart to its state before the initial animation. A new animation can then be triggered using `update`. ```javascript myLineChart.reset(); ``` ## .render() Triggers a redraw of all chart elements. Note, this does not update elements for new data. Use `.update()` in that case. ## .stop() Use this to stop any current animation. This will pause the chart during any current animation frame. Call `.render()` to re-animate. ```javascript // Stops the charts animation loop at its current frame myLineChart.stop(); // => returns 'this' for chainability ``` ## .resize(width?, height?) Use this to manually resize the canvas element. This is run each time the canvas container is resized, but you can call this method manually if you change the size of the canvas nodes container element. You can call `.resize()` with no parameters to have the chart take the size of its container element, or you can pass explicit dimensions (e.g., for [printing](../configuration/responsive.md#printing-resizable-charts)). ```javascript // Resizes & redraws to fill its container element myLineChart.resize(); // => returns 'this' for chainability // With an explicit size: myLineChart.resize(width, height); ``` ## .clear() Will clear the chart canvas. Used extensively internally between animation frames, but you might find it useful. ```javascript // Will clear the canvas that myLineChart is drawn on myLineChart.clear(); // => returns 'this' for chainability ``` ## .toBase64Image(type?, quality?) This returns a base 64 encoded string of the chart in its current state. ```javascript myLineChart.toBase64Image(); // => returns png data url of the image on the canvas myLineChart.toBase64Image('image/jpeg', 1) // => returns a jpeg data url in the highest quality of the canvas ``` ## .getElementsAtEventForMode(e, mode, options, useFinalPosition) Calling `getElementsAtEventForMode(e, mode, options, useFinalPosition)` on your Chart instance passing an event and a mode will return the elements that are found. The `options` and `useFinalPosition` arguments are passed through to the handlers. To get an item that was clicked on, `getElementsAtEventForMode` can be used. ```javascript function clickHandler(evt) { const points = myChart.getElementsAtEventForMode(evt, 'nearest', { intersect: true }, true); if (points.length) { const firstPoint = points[0]; const label = myChart.data.labels[firstPoint.index]; const value = myChart.data.datasets[firstPoint.datasetIndex].data[firstPoint.index]; } } ``` ## .getSortedVisibleDatasetMetas() Returns an array of all the dataset meta's in the order that they are drawn on the canvas that are not hidden. ```javascript const visibleMetas = chart.getSortedVisibleDatasetMetas(); ``` ## .getDatasetMeta(index) Looks for the dataset that matches the current index and returns that metadata. This returned data has all of the metadata that is used to construct the chart. The `data` property of the metadata will contain information about each point, bar, etc. depending on the chart type. Extensive examples of usage are available in the [Chart.js tests](https://github.com/chartjs/Chart.js/tree/master/test). ```javascript const meta = myChart.getDatasetMeta(0); const x = meta.data[0].x; ``` ## getVisibleDatasetCount Returns the number of datasets that are currently not hidden. ```javascript const numberOfVisibleDatasets = chart.getVisibleDatasetCount(); ``` ## isDatasetVisible(datasetIndex) Returns a boolean if a dataset at the given index is currently visible. The visibility is determined by first checking the hidden property in the dataset metadata (set via [`setDatasetVisibility()`](#setdatasetvisibility-datasetindex-visibility) and accessible through [`getDatasetMeta()`](#getdatasetmeta-index)). If this is not set, the hidden property of the dataset object itself (`chart.data.datasets[n].hidden`) is returned. ```javascript chart.isDatasetVisible(1); ``` ## setDatasetVisibility(datasetIndex, visibility) Sets the visibility for a given dataset. This can be used to build a chart legend in HTML. During click on one of the HTML items, you can call `setDatasetVisibility` to change the appropriate dataset. ```javascript chart.setDatasetVisibility(1, false); // hides dataset at index 1 chart.update(); // chart now renders with dataset hidden ``` ## toggleDataVisibility(index) Toggles the visibility of an item in all datasets. A dataset needs to explicitly support this feature for it to have an effect. From internal chart types, doughnut / pie, polar area, and bar use this. ```javascript chart.toggleDataVisibility(2); // toggles the item in all datasets, at index 2 chart.update(); // chart now renders with item hidden ``` ## getDataVisibility(index) Returns the stored visibility state of a data index for all datasets. Set by [toggleDataVisibility](#toggledatavisibility-index). A dataset controller should use this method to determine if an item should not be visible. ```javascript const visible = chart.getDataVisibility(2); ``` ## hide(datasetIndex, dataIndex?) If dataIndex is not specified, sets the visibility for the given dataset to false. Updates the chart and animates the dataset with `'hide'` mode. This animation can be configured under the `hide` key in animation options. Please see [animations](../configuration/animations.md) docs for more details. If dataIndex is specified, sets the hidden flag of that element to true and updates the chart. ```javascript chart.hide(1); // hides dataset at index 1 and does 'hide' animation. chart.hide(0, 2); // hides the data element at index 2 of the first dataset. ``` ## show(datasetIndex, dataIndex?) If dataIndex is not specified, sets the visibility for the given dataset to true. Updates the chart and animates the dataset with `'show'` mode. This animation can be configured under the `show` key in animation options. Please see [animations](../configuration/animations.md) docs for more details. If dataIndex is specified, sets the hidden flag of that element to false and updates the chart. ```javascript chart.show(1); // shows dataset at index 1 and does 'show' animation. chart.show(0, 2); // shows the data element at index 2 of the first dataset. ``` ## setActiveElements(activeElements) Sets the active (hovered) elements for the chart. See the "Programmatic Events" sample file to see this in action. ```javascript chart.setActiveElements([ {datasetIndex: 0, index: 1}, ]); ``` ## isPluginEnabled(pluginId) Returns a boolean if a plugin with the given ID has been registered to the chart instance. ```javascript chart.isPluginEnabled('filler'); ``` ## Static: getChart(key) Finds the chart instance from the given key. If the key is a `string`, it is interpreted as the ID of the Canvas node for the Chart. The key can also be a `CanvasRenderingContext2D` or an `HTMLDOMElement`. This will return `undefined` if no Chart is found. To be found, the chart must have previously been created. ```javascript const chart = Chart.getChart("canvas-id"); ``` ## Static: register(chartComponentLike) Used to register plugins, axis types or chart types globally to all your charts. ```javascript import { Chart, Tooltip, LinearScale, PointElement, BubbleController } from 'chart.js'; Chart.register(Tooltip, LinearScale, PointElement, BubbleController); ``` ## Static: unregister(chartComponentLike) Used to unregister plugins, axis types or chart types globally from all your charts. ```javascript import { Chart, Tooltip, LinearScale, PointElement, BubbleController } from 'chart.js'; Chart.unregister(Tooltip, LinearScale, PointElement, BubbleController); ``` ================================================ FILE: docs/developers/axes.md ================================================ # New Axes Axes in Chart.js can be individually extended. Axes should always derive from `Chart.Scale` but this is not a mandatory requirement. ```javascript class MyScale extends Chart.Scale { /* extensions ... */ } MyScale.id = 'myScale'; MyScale.defaults = defaultConfigObject; // MyScale is now derived from Chart.Scale ``` Once you have created your scale class, you need to register it with the global chart object so that it can be used. ```javascript Chart.register(MyScale); // If the new scale is not extending Chart.Scale, the prototype can not be used to detect what // you are trying to register - so you need to be explicit: // Chart.registry.addScales(MyScale); ``` To use the new scale, simply pass in the string key to the config when creating a chart. ```javascript const lineChart = new Chart(ctx, { data: data, type: 'line', options: { scales: { y: { type: 'myScale' // this is the same id that was set on the scale } } } }); ``` ## Scale Properties Scale instances are given the following properties during the fitting process. ```javascript { left: number, // left edge of the scale bounding box right: number, // right edge of the bounding box top: number, bottom: number, width: number, // the same as right - left height: number, // the same as bottom - top // Margin on each side. Like css, this is outside the bounding box. margins: { left: number, right: number, top: number, bottom: number }, // Amount of padding on the inside of the bounding box (like CSS) paddingLeft: number, paddingRight: number, paddingTop: number, paddingBottom: number } ``` ## Scale Interface To work with Chart.js, custom scale types must implement the following interface. ```javascript { // Determines the data limits. Should set this.min and this.max to be the data max/min determineDataLimits: function() {}, // Generate tick marks. this.chart is the chart instance. The data object can be accessed as this.chart.data // buildTicks() should create a ticks array on the axis instance, if you intend to use any of the implementations from the base class buildTicks: function() {}, // Get the label to show for the given value getLabelForValue: function(value) {}, // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value // @param index: index into the ticks array getPixelForTick: function(index) {}, // Get the pixel (x coordinate for horizontal axis, y coordinate for vertical axis) for a given value // @param value : the value to get the pixel for // @param [index] : index into the data array of the value getPixelForValue: function(value, index) {}, // Get the value for a given pixel (x coordinate for horizontal axis, y coordinate for vertical axis) // @param pixel : pixel value getValueForPixel: function(pixel) {} } ``` Optionally, the following methods may also be overwritten, but an implementation is already provided by the `Chart.Scale` base class. ```javascript { // Adds labels to objects in the ticks array. The default implementation simply calls this.options.ticks.callback(numericalTick, index, ticks); generateTickLabels: function() {}, // Determine how much the labels will rotate by. The default implementation will only rotate labels if the scale is horizontal. calculateLabelRotation: function() {}, // Fits the scale into the canvas. // this.maxWidth and this.maxHeight will tell you the maximum dimensions the scale instance can be. Scales should endeavour to be as efficient as possible with canvas space. // this.margins is the amount of space you have on either side of your scale that you may expand in to. This is used already for calculating the best label rotation // You must set this.minSize to be the size of your scale. It must be an object containing 2 properties: width and height. // You must set this.width to be the width and this.height to be the height of the scale fit: function() {}, // Draws the scale onto the canvas. this.(left|right|top|bottom) will have been populated to tell you the area on the canvas to draw in // @param chartArea : an object containing four properties: left, right, top, bottom. This is the rectangle that lines, bars, etc will be drawn in. It may be used, for example, to draw grid lines. draw: function(chartArea) {} } ``` The Core.Scale base class also has some utility functions that you may find useful. ```javascript { // Returns true if the scale instance is horizontal isHorizontal: function() {}, // Returns the scale tick objects ({label, major}) getTicks: function() {} } ``` ================================================ FILE: docs/developers/charts.md ================================================ # New Charts Chart.js 2.0 introduced the concept of controllers for each dataset. Like scales, new controllers can be written as needed. ```javascript class MyType extends Chart.DatasetController { } Chart.register(MyType); // Now we can create a new instance of our chart, using the Chart.js API new Chart(ctx, { // this is the string the constructor was registered at, ie Chart.controllers.MyType type: 'MyType', data: data, options: options }); ``` ## Dataset Controller Interface Dataset controllers must implement the following interface. ```javascript { // Defaults for charts of this type defaults: { // If set to `false` or `null`, no dataset level element is created. // If set to a string, this is the type of element to create for the dataset. // For example, a line create needs to create a line element so this is the string 'line' datasetElementType: string | null | false, // If set to `false` or `null`, no elements are created for each data value. // If set to a string, this is the type of element to create for each data value. // For example, a line create needs to create a point element so this is the string 'point' dataElementType: string | null | false, } // ID of the controller id: string; // Update the elements in response to new data // @param mode : update mode, core calls this method using any of `'active'`, `'hide'`, `'reset'`, `'resize'`, `'show'` or `undefined` update: function(mode) {} } ``` The following methods may optionally be overridden by derived dataset controllers. ```javascript { // Draw the representation of the dataset. The base implementation works in most cases, and an example of a derived version // can be found in the line controller draw: function() {}, // Initializes the controller initialize: function() {}, // Ensures that the dataset represented by this controller is linked to a scale. Overridden to helpers.noop in the polar area and doughnut controllers as these // chart types using a single scale linkScales: function() {}, // Parse the data into the controller meta data. The default implementation will work for cartesian parsing, but an example of an overridden // version can be found in the doughnut controller parse: function(start, count) {}, } ``` ## Extending Existing Chart Types Extending or replacing an existing controller type is easy. Simply replace the constructor for one of the built-in types with your own. The built-in controller types are: * `BarController` * `BubbleController` * `DoughnutController` * `LineController` * `PieController` * `PolarAreaController` * `RadarController` * `ScatterController` These controllers are also available in the UMD package, directly under `Chart`. Eg: `Chart.BarController`. For example, to derive a new chart type that extends from a bubble chart, you would do the following. ```javascript import {BubbleController} from 'chart.js'; class Custom extends BubbleController { draw() { // Call bubble controller method to draw all the points super.draw(arguments); // Now we can do some custom drawing for this dataset. Here we'll draw a red box around the first point in each dataset const meta = this.getMeta(); const pt0 = meta.data[0]; const {x, y} = pt0.getProps(['x', 'y']); const {radius} = pt0.options; const ctx = this.chart.ctx; ctx.save(); ctx.strokeStyle = 'red'; ctx.lineWidth = 1; ctx.strokeRect(x - radius, y - radius, 2 * radius, 2 * radius); ctx.restore(); } }; Custom.id = 'derivedBubble'; Custom.defaults = BubbleController.defaults; // Stores the controller so that the chart initialization routine can look it up Chart.register(Custom); // Now we can create and use our new chart type new Chart(ctx, { type: 'derivedBubble', data: data, options: options }); ``` ## TypeScript Typings If you want your new chart type to be statically typed, you must provide a `.d.ts` TypeScript declaration file. Chart.js provides a way to augment built-in types with user-defined ones, by using the concept of "declaration merging". When adding a new chart type, `ChartTypeRegistry` must contain the declarations for the new type, either by extending an existing entry in `ChartTypeRegistry` or by creating a new one. For example, to provide typings for a new chart type that extends from a bubble chart, you would add a `.d.ts` containing: ```typescript import { ChartTypeRegistry } from 'chart.js'; declare module 'chart.js' { interface ChartTypeRegistry { derivedBubble: ChartTypeRegistry['bubble'] } } ``` ================================================ FILE: docs/developers/contributing.md ================================================ # Contributing New contributions to the library are welcome, but we ask that you please follow these guidelines: - Before opening a PR for major additions or changes, please discuss the expected API and/or implementation by [filing an issue](https://github.com/chartjs/Chart.js/issues) or asking about it in the [Chart.js Discord](https://discord.gg/HxEguTK6av) #dev channel. This will save you development time by getting feedback upfront and make reviews faster by giving the maintainers more context and details. - Consider whether your changes are useful for all users, or if creating a Chart.js [plugin](plugins.md) would be more appropriate. - Check that your code will pass tests and `eslint` code standards. `pnpm test` will run both the linter and tests for you. - Add unit tests and document new functionality (in the `test/` and `docs/` directories respectively). - Avoid breaking changes unless there is an upcoming major release, which is infrequent. We encourage people to write plugins for the most new advanced features, and care a lot about backward compatibility. - We strongly prefer new methods to be added as private whenever possible. A method can be made private either by making a top-level `function` outside of a class or by prefixing it with `_` and adding `@private` JSDoc if inside a class. Public APIs take considerable time to review and become locked once implemented as we have limited ability to change them without breaking backward compatibility. Private APIs allow the flexibility to address unforeseen cases. ## Joining the project Active committers and contributors are invited to introduce themselves and request commit access to this project. We have a very active Discord community that you can join [here](https://discord.gg/HxEguTK6av). If you think you can help, we'd love to have you! ## Building and Testing Firstly, we need to ensure development dependencies are installed. With node and pnpm installed, after cloning the Chart.js repo to a local directory, and navigating to that directory in the command line, we can run the following: ```bash > pnpm install ``` This will install the local development dependencies for Chart.js. The following commands are now available from the repository root: ```bash > pnpm run build // build dist files in ./dist > pnpm run autobuild // build and watch for source changes > pnpm run dev // run tests and watch for source and test changes > pnpm run lint // perform code linting (ESLint, tsc) > pnpm test // perform code linting and run unit tests with coverage ``` `pnpm run dev` and `pnpm test` can be appended with a string that is used to match the spec filenames. For example: `pnpm run dev plugins` will start karma in watch mode for `test/specs/**/*plugin*.js`. ### Documentation We use [Vuepress](https://vuepress.vuejs.org/) to manage the docs which are contained as Markdown files in the docs directory. You can run the doc server locally using these commands: ```bash > pnpm run docs:dev ``` ### Image-Based Tests Some display-related functionality is difficult to test via typical Jasmine units. For this reason, we introduced image-based tests ([#3988](https://github.com/chartjs/Chart.js/pull/3988) and [#5777](https://github.com/chartjs/Chart.js/pull/5777)) to assert that a chart is drawn pixel-for-pixel matching an expected image. Generated charts in image-based tests should be **as minimal as possible** and focus only on the tested feature to prevent failure if another feature breaks (e.g. disable the title and legend when testing scales). You can create a new image-based test by following the steps below: - Create a JS file ([example](https://github.com/chartjs/Chart.js/blob/f7b671006a86201808402c3b6fe2054fe834fd4a/test/fixtures/controller.bubble/radius-scriptable.js)) or JSON file ([example](https://github.com/chartjs/Chart.js/blob/4b421a50bfa17f73ac7aa8db7d077e674dbc148d/test/fixtures/plugin.filler/fill-line-dataset.json)) that defines chart config and generation options. - Add this file in `test/fixtures/{spec.name}/{feature-name}.json`. - Add a [describe line](https://github.com/chartjs/Chart.js/blob/4b421a50bfa17f73ac7aa8db7d077e674dbc148d/test/specs/plugin.filler.tests.js#L10) to the beginning of `test/specs/{spec.name}.tests.js` if it doesn't exist yet. - Run `pnpm run dev`. - Click the *"Debug"* button (top/right): a test should fail with the associated canvas visible. - Right-click on the chart and *"Save image as..."* `test/fixtures/{spec.name}/{feature-name}.png` making sure not to activate the tooltip or any hover functionality - Refresh the browser page (`CTRL+R`): test should now pass - Verify test relevancy by changing the feature values *slightly* in the JSON file. Tests should pass in both browsers. In general, we've hidden all text in image tests since it's quite difficult to get them to pass between different browsers. As a result, it is recommended to hide all scales in image-based tests. It is also recommended to disable animations. If tests still do not pass, adjust [`tolerance` and/or `threshold`](https://github.com/chartjs/Chart.js/blob/1ca0ffb5d5b6c2072176fd36fa85a58c483aa434/test/jasmine.matchers.js) at the beginning of the JSON file keeping them **as low as possible**. When a test fails, the expected and actual images are shown. If you'd like to see the images even when the tests pass, set `"debug": true` in the JSON file. ## Bugs and Issues Please report these on the GitHub page - at github.com/chartjs/Chart.js. Please do not use issues for support requests. For help using Chart.js, please take a look at the [`chart.js`](https://stackoverflow.com/questions/tagged/chart.js) tag on Stack Overflow. Well-structured, detailed bug reports are hugely valuable for the project. Guidelines for reporting bugs: - Check the issue search to see if it has already been reported - Isolate the problem to a simple test case - Please include a demonstration of the bug on a website such as [JS Bin](https://jsbin.com/), [JS Fiddle](https://jsfiddle.net/), or [Codepen](https://codepen.io/pen/). ([Template](https://codepen.io/pen?template=wvezeOq)). If filing a bug against `master`, you may reference the latest code via (changing the filename to point at the file you need as appropriate). Do not rely on these files for production purposes as they may be removed at any time. Please provide any additional details associated with the bug, if it's browser or screen density specific, or only happens with a certain configuration or data. ================================================ FILE: docs/developers/index.md ================================================ # Developers Developer features allow extending and enhancing Chart.js in many different ways. ## Latest resources The latest documentation and samples, including unreleased features, are available at: - - ## Development releases Latest builds are available for testing at: - - :::warning Warning Development builds **must not** be used for production purposes or as replacement for a CDN. See [available CDNs](../getting-started/installation.md#cdn). ::: ## Browser support All modern and up-to-date browsers are supported, including, but not limited to: * Chrome * Edge * Firefox * Safari As of version 3, we have dropped Internet Explorer 11 support. Browser support for the canvas element is available in all modern & major mobile browsers. [CanIUse](https://caniuse.com/#feat=canvas) Run `npx browserslist` at the root of the [codebase](https://github.com/chartjs/Chart.js) to get a list of supported browsers. Thanks to [BrowserStack](https://browserstack.com) for allowing our team to test on thousands of browsers. ## Previous versions To migrate from version 2 to version 3, please see [the v3 migration guide](../getting-started/v3-migration). Version 3 has a largely different API than earlier versions. Most earlier version options have current equivalents or are the same. Please note - documentation for previous versions is available online or in the GitHub repo. - [2.9.4 Documentation](https://www.chartjs.org/docs/2.9.4/) - [1.x Documentation](https://github.com/chartjs/Chart.js/tree/v1.1.1/docs) ================================================ FILE: docs/developers/plugin_flowcharts.drawio ================================================ 7V3dl5o4FP9rfGyPSQjiY50Z23M6O/sx7enOY5So7CBxEUftX79BkhEICiqSYLcvhUsC8d77y/2E6aC7+eZzSBaz35hL/Q7supsOuu9ACLrI4f/FlG1CwVYvIUxDzxWD9oRn7yeVMwV15bl0mRkYMeZH3iJLHLMgoOMoQyNhyNbZYRPmZ5+6IFOqEJ7HxFepPzw3miVUB3f39C/Um87kk0FXXJkTOVgQljPisnWKhB466C5kLEqO5ps76sfMk3xJ5g0PXH1fWEiDqMqEydz/ffvkTt3hcPBvb7x+GX65/4DE2qKt/MHU5b9fnLIwmrEpC4j/sKcOQrYKXBrftcvP9mMeGVtwIuDEf2gUbYUwySpinDSL5r64Sjde9Hc8/SMWZy+pK/cbcefdyVaeBFG4TU2KT1/S1/bTdmdynsolwbglW4VjeoQ1UttIOKXRkXEwGRfzLfUAIYPPlM0pXw8fEFKfRN5bVq+IUM/p+7i9BPmBEOIJAhX3fSP+SjxpQKdewEl/US61UJW373MsxXJdz7yIPi/IjitrDues1MhykQBs4m1i6R9m7RsNI7o5ygxxFTkCHGJ3kFhZp6AmSLMUyiStdu7h/+FQoualcLCMggNU4PDEIm8SL2BEJyykB1CxFyooR8bE8/075rNwNxe5mDquxenLKGSvNHXFgSNk2zVBx85CBxRg552WBo91LfA4Cq9fuOXWCCiQgtMeXGWAysBpj67aAWVVBFTPKEBJv8z8LfL6kukbJRlLgd8dCcZ8Ysz4obrDzdh8tFqW725XMfOa7XxP4dXzq7c47CSdZA5qYBiGOYZp39wBvH3c9yviHgCjgN8v83HuQ7K+AQ8H6QeBusdqdnG6nXNcnNNihpzwJ86YjsdFwh852MIXBt1Vo26AKmJQKNOH7keBk8qg3N3rUxiSbWrAgnlBtEw96o+YkFLablZpLSuXlSkZD5ySCRY8PoEfJGvOTZc/gE0mS87ZPBTeOXZBrKXmHp7Y7VuNyiorBWeI2QCquIx1GC3dDiOwW6zbGnKnqOo2js3CRO/WjXkDMq09LD7LEMO8Ye3n6yMlE6RlrmqIc+OzdrguGwuqJ9keyYj6uRS+700DfjzmwudRLhrEO7I3Jv4ncWHuuW6i03Tp/SSj3f1itRHc5jfHgw6+L1Sko4hS9v73kp14SiddFTvkweEMx2X16jRFatIlkphJiWsYknksMb7qOfECL5iaZmyB42i2tpbebbgZtxFX3E4t2ywTiQs8IZFuIBO+rdxMRYUbBN0JB9ia+vz5QJAMLY+foFFAkOtOASFJtHUfyZaGy53ecLWJx9k+/2GDEUeFPY2PfiZUMo/1fndtdwOpbxozz3kQYO1JN9iaovwFGIBVMWBYeR0eNgYi90wiwp2r5Y3koG3tJgG1pgDbhnwzrFrrhWZ5YUhN3pkdB0q1rSUOdDC2M7AUdzI3EERaHbnzGmEa6yyrikFklgOI1GrggZywIRCUyYg6IGjbMJuMMR6CsEUNMrbuDAx02uJlXLDv2FVtv1kteFCtRZ0XeO4oRkadoKhD7Gp+9urP4Os2+k6fwqkz+fp6D75j+J66NR8Bil/c3f27DBs9FRvFbDLLJkO1sy+bnbyROBTAAgvRcCBqtwUg58NAepzlrqlZdWy57tK8TEvhkO+P7dm60WBBnWgwO8BDlUtdZjXWIrXUZUzEgK0sAmQXoLaIARn3+o/Z7TSowMO6LOuR6pMAjgh2r9tjg/M9NgAd75nJT+iVNLti5+h4fb2uVmviY4M1u/6c+nlanO9/Ac6JLdi5Cddp/bKK3vMo1EIz0o0SIrWkGy0Eshl/83u/kBqKymJox6Dur7wjAbp93d1fVlv21vP3SOnplrvEVd9zaeglU7XuKHKPbqLbqi43nEq0c/os2341hoataWBp3lewqjaVI7Ny8Jba2JvLM7Y6v5IHEdDfCmmpVQ/Zacr9ifnCpxFVcWbWx210V/VQvy07UZM9QJXTU6btQWoDquE9QDKCqSMi6GGZcZIRgUkdCIW1sl+lolhcKFRBVjiuarardowdW/XhOsq35GOELbXzSlnR6umuuxcVcH9l3MCKuNHWn1qMG71dyg1k/hVZD07yRKrK+rKPXzT7rQuA+1o/dlGXnS74MoIxjtVRtNXTXN2X6mVgZ+cx2JhYplXercW4uRiwWFda825tbTtzDVa4aohY/858kbDL3tW9Me+1VwCuZr3X1pSFrwGSgn7qY1ppCEYOdlOL78xfBoxrqLmtPUgDekt0LXXvZa90a/17dCP+fZFZLFRoQ9x7+eZSLd/QgdIDNNehB61pLb9KprRqykdbt+zRZadQ9U1aUG6pdjdwY8tqWkiGrheS8dP9H3hJ0LH/Mzno4T8=7Zlbd6IwEMc/jY/u4Rarj/XWdtf2dLVb6754ogRJRUJD8PbpN0AQEEqt22r19EkymYTwn/klA5bUxmx5RaFj3hIdWSVF0pcltVlSFFlSq/zHt6xCC9AuQsOEYl04xYYeXqNopLB6WEduypERYjHspI1jYttozFI2SClZpN0MYqXv6sAJyhh6Y2hlrX2sMzO0VoEU268RnpjRnWVJ9Mxg5CwMrgl1skiY1FZJbVBCWHg1WzaQ5YsX6fJIbyvPjy910NJbDz+f5tPhqFYOJ2u/Z8jmESiy2d5T15V+D9eHvzpTZIyacDKd33ejqV22ivRCOpdPNAllJpkQG1qt2FqnxLN15M8q8Vbs0yHE4UaZG58RYyuRC9BjhJtMNrNEL1pi9uQP/wFEa5DoaS7FzEFjFTVsRleJQX5zkOyLhwWtaNyO0kU6EI+OhRALYtZ6tG4icN+/braf3VpnLSSWGKQTxAp0FX6+mIk0FIG5QmSG+CK5A0UWZHiezlUoUn6y8YvDyi9EZPOjXLTqObQ8caeGCSnzqaMIMh7LTBZYFgfUj/bCxAz1HBjIsuB7RDqW0HVCag289HPinYLPEWVoWSiR6FWr4inEPhRRuUhALUxmgufI9j+aTpWhWbu7af/BHdIe12WFyWpZORVy9iegKLOTBOTqczQCiladIOCOMGz4C8C2y6A/w1Y442DJb2NgYMtqEIvQYKyqA1TVNW53GSVTlOipKiO1UvlMTippTuQcUDa2JCnaB5Dirgee8XteGbVbtNcd9EC565TVY5IiJziJqfkqZ0xuripZwnJ1VY5FWO5qtAxhA1725UW+A0e8xkyfIhae2Px6zBVEnJS6n++YF3GXomOGdT1MDOTiNRwF8/naOwTbLHgWUC+BZm40irIyA9amEhU3SRV7ecCVpR+VFHFCiZ1lFzPf+0+ScCGG4fL4b8dls4D9Q1XJ2QxP5TQ7MUbBjozGuQT2yZ5LSuEq4SCoyCaXuI+mpA8JDWy9O7zPn1+EK/jMVC6KQyKVu8h1iO0G0rWzh7pJZiPPfftAP1QZq4LD1bG5GXrxTf5r5Beduh9G/mFO5ywnm/o3OFHRuZS/2hZfmnrs8rd2KoB9MChgR1CqXwoU8Doo0OC16Y2N2bmwsv2qCORjs1LNqO/rjaE/0HN0yLL71Nf/UgVqhzviKw9Dp+24y9Z1o1+TXjztL+18f+R9fefK1WvXj7xHewEvWnXezjVCBqHonLeuDTyH2Lpyv3eeDGT7w1L0nTcJS1F6Hh6WolXnweKy4K+RM+VE+jxOeDP+MzR8wY//UlZb/wA=7V1de6I4FP41XrqPIYD2sq2dzu52OrPTbWc6d1GiMgPExVi1v34DJvKRCDgVidaryoGEcM55k/OVtAWv/eVtiKaTT8TBXsvoOMsW7LcMA3Rgj/2JKCtOsc3umjIOXYfTEsKD+4pFU06duw6eZR6khHjUnWaJQxIEeEgzNBSGZJF9bES87FunaIwlwsMQeTL1m+vQyZraszoJ/SN2xxPxZtDhd3wkHuaE2QQ5ZJEiwZsWvA4Joetf/vIaexH3BF+e/hn5n6f/9nHnqW+5l9R+as/b684+7NJk8wkhDuhvdz1//bX8e/X9H9SfI2vY7X9ZPF61Tf5pdCX4hR3GPn5JQjohYxIg7yahXoVkHjg46rXDrpJn7giZMiJgxJ+Y0hXXBTSnhJEm1Pf4Xbx06feo+R8Wv3pO3ekvec/xxUpcBDRcpRpFl8/pe0mz+Eq0q8g6zuIZmYdDzoj//po/0sHHr5O7Ob15fqIfe9jlLO5QFI4x74+O/Vn38da9/wFfbs0uvvpx3xbPRcxMqSEXzC0mPmaDZA+E2EPUfcnqKuIqP948l4iV/eCSVUu5aNQvyJvzN13hsRsw0uPUQRTLSuB5DJ+RsBcTl+KHKYq5smBzRFaUaDZdg3bkLiOV2JHfLzikeFnIIX4X9vhH8HmoDQVOFylUc9IkBWhBewtTldCxztDZBh0lvxTQUT5nNAWdolGnoHNPqDuKBjDAIxLiLQhKJA3KUTRyPe+aeCSM20LHwj3HZPQZDckvnLrTMwbQtuuEmZ2FGTBklG1oaZiZdcEMSAJ4ZuZEg9ADKeAlMCyDXgZ4CQ63QC+nEKPeEA+HKoUY9CzTqgGsRkWwQq3A2j1PybVI2dZKyoY0I1yjYMgaRlL8IM/EE+IP5rPyWfhQpovRtOUCJQY+/HKnjDLfw1pWFxctI8fFxlemi2OZbfY8a9gVZ42eVrOGLc8aIY61vRN1bHT62MPxJcMAmrEvi2MSbMFlE0s4i0MPYTQwJ5KfaBRin7zE17yVbJpogZ68XQeroseuza4DEqc0hY8mi3WvIuxAY7GHomFvd6BuPOwzDs1OzJGysoAzgQJw5kEdqXOsbzfAgaoRC6CXFwTkmEV/y5ImDL4OXoOQ/WJjQNFadhzrmNmruI5ZtcHqvZqBQuvL4WHpBQ/Z/cmtSHdoReb0ZFaiHGQsu2nHCTQap/m9+F2zK5FVFWp6RWqAyvhTyv4ODbCXSy157jhgv4eMh5iB5SpSeXeIvEt+w3cdZ60aeOa+okHcX8T9KXGZPRl1bl21rL5SHoWKKYFrk57mb2mlM8Aq0LWZwsCelQEe76ky63nnX6KvST1CRqN4Kc/JZjOGN4jLksR1XJE1GzYcWTPOTu2OM1vVYJKhl1cL5HBSLB72qiCKBk29CAvMoCDLKFvViZPtdBKRhih4QZqGicwcoLqNB1kNeEbUbjmdynl2QytEGbLXujHL0YhZAKdlleeR1lMkhQ6MNLtJpB2hVS4AVI40vRxgQ1i55SUVepjlQjP3Y5ZbupvhxnuNKFUHlF6pRTHuFKAuA9dnbyZBZPzhINLP43CheoqA6mFdqJNPUzRe3FU5nKSZiSi8+9NVjX1PqJUFrdmEKgeivuJ1/irAi5a6MGPmE8JmrMgD53Pv23yFQ1n+F43H46FxLLjaNz6q1lRAoBc+SosqeML3xIsqQMdUYOegVRWwUXPlCL1mAaVyzOlVVQHlNUnrXJZQzL04zTawzCzyWpo70ULNjjaXBVRbbw5bJn402yn2PUVVrWyBeqXbYWllCzcLTssqyBvUQLXB4sBWwXsN4cGq+Vyol8cJ5XyuAEmyH0BLb/Iir/xVKyJrU37zvbqTsKo7KVivi/IXuJNx6vU0l408cmDjgRjzRDY7N+pgCunsz8FM/KALUTy8m+NzGYZolXqAO3WyX8TfZJs51RQmzoeqDUBJAysfSck1YD/Wg67TcbPdm+nXTz9fP49+DL7Z9v3ysTvciFn7xUNjdVcyVq81Rwxb3qQyi8fgIzdwg7GWrroF8vBUhCHrctWVsj0XyW0FTREWSjFjNIWZolGXmWknFvSX3HvlBpa63HulIBqtlDuMg1MEh1LYNFbvVjTqrbA5LbhIJzjZqhxZXW6NUgDnip66K3pUSbUjWNDklNqfgUtd5MUjYPIPZc3R/mRCoNo0UZc1qDx8stH42y4Rg99HQdGhm6WHczZWzKEcjQwCbaqxi7RrL3nlLjBhBjyidy0Sy8rPNyVxbSkDONT6pnWE7k04bawApGjUacHHtYhTb74+S5dJKNroN6NRnYGWgQvpEF2gcKUOulQdzylQe8bA3tcgdZzXzJ2atzH8RRdrEPJWiTB3jVhbuQB0Sfg5P6yi4PO+Jm45t7JxAxlkw5PdWdg+ZFpLyXk5vdhwVmuXNXPPuFeUFimf62q19r2Dcpaq0io6JXzPs/muk7Bxof0kLFeofRHmU3KU5igkvu6mlJFLoLYPuG9OvbydN0fthu3uUWEb6I9teVt5ysBi+nQi9hWAWdbWeEQKu0z+vdBaTMl/aYI3/wM=3Vpbc6IwFP41PtrhLvvYqrU72227a2d2uy87ESKkjYQJ8frrN4EgUBC1VdHtgyUnF5LvfN/JIdDSu5PFgILQ/05ciFua4i5aeq+laaqi2/yfsCwTi2l0EoNHkSsbZYYhWsG0p7ROkQujQkNGCGYoLBodEgTQYQUboJTMi83GBBfvGgIPlgxDB+Cy9RdymZ9YbVPJ7HcQeX56Z1WRNROQNpaGyAcumedMer+ldykhLLmaLLoQC/BSXB6JYdz9fMPfQwSfn17bg6+PT+1ksNt9uqyXQGHAPjz0b3IbrOw/9w+vU2fV9WZ/ekRtp0tjyxQv6HL4ZJFQ5hOPBAD3M+sNJdPAhWJUhZeyNveEhNyocuMrZGwpuQCmjHCTzyZY1sIFYr9F9ytTll5yNb2FHDkuLNNCwOgy10kUX/J1Wbe4lPbbEToJcUSm1IE1eKUMBtSDrK6dkTQUaOZ4KD0zgGQC+Sx5AwoxYGhWJCuQnPfW7dZdnwjiC9EUqU8jJadUp5qW0yGSmcpeGTv4RW4amSnmzB78kROeATyVS+jPYDzBn9CBfF1umWAYc+0LIs19xOAwBDHicx5+ijQBUZgEhDFaCLrt6csZpAwuasGXtbpdBDHFcJ6LF9Lk50KFrWz2VgHnGlCH5jPpPz7YULn5+mOo+9po4LXNSxHlocVllNVVCZB2aHHt6q7aaedE8EAYGosZjOCYUCg18c6pmcvU7WIYI4y7BBMa99VdE9quwe0Ro+QN5mpsbaRb1jHVYm0IOTm5rG15vRjH0ks5CL3wjKNBDak5BWV62raxFba1bJc7+MZWJ6mt0tObkt639orCO+S+0M7fv4NQvR4MrPXqzj5Snj592dXLlbgqTXm5btY5fXdB4PCOwou35bjqk8loGm2PqafLJxpOKPQSgMM3FMZJGpcC/dzOdCwUzXepbVVadrR9plIVnUuJNgeOGtaOUcM6q6hhbc7KwJhB+l8nZUZFyDmtWLT/Xyx1W+fWRKoxsdTNOieWOxC43Cl8hp/XyalYrze+RagXk5EemPbmjnvEl7OivVmi/TNFnidSIj6D882NjE6R+Fbj4d5ukvcfe94+1ZNYXa60VS/mWelFLT+KbTpquQcjiN8d6GLkBfza4RByZek3gvDIAfhaVkyQ6ybMgBFagVE8ngA/FOfX8WLMm5bZq3RHHS1Lylq/b5I3aeVf6VQprq1cWXbHLKhOjvTRI/y0CRmPI/jZ0/lqd13M652G1HVeu5Fafk5/IGcsrpReB1FXR9PVy1JX+QGz64PAu5BDqXUKcYJDqTrt5eDrkQCW6X72rwstu2Ekq19A8atrbnzCUw8FQpJA/KLA5UGBiQc7IIwxY/nFSCyQC4WhwBN11IuuHEln8cWG+KHTsns4ZmzzoUggPLpPbKrycXFDKp2tKPEfr3FB5K8T82M5n9+v4Hw9zQnymbdS4f0v+3ufF7OvS5KolX2jo/f/AQ==7VxbU9s4FP41eWTH8t2P5dZOh7I7S7stTx0RK4mKbWUcBRJ+/UqO7NhSCAr4CmEGiI9v8tH3nYvOcUbWWbz6nML57BsJUTQyjXA1ss5HpgkMy2f/uGQtJK7tbSTTFIdCthXc4CeUnyqkSxyiReVASkhE8bwqHJMkQWNakcE0JY/VwyYkqt51DqdIEdyMYaRKf+KQzjZS3zG28i8IT2f5nYEh9sQwP1gIFjMYkseSyLoYWWcpIXTzKV6doYhrL9dLSN2/Z+6P35Of/vLh6ov1359fwcnmYpeHnFI8QooS+upLf326vP7qBEsv+W1cGPG3ZOo/nbimeDa6zhWGQqY/sUlSOiNTksDoYis9TckyCRG/rMG2tsdcETJnQsCEfxClawEGuKSEiWY0jsRetML0Fz/9L0ds3Zb2nK/ElbONtdjQ1IHQ1YIs0zHa8+A5FGE6RXSfgsSBXCslQAkVf0YkRjRdswNSFEGKH6qogwK80+K47QSxD2KODpgvcd0HGC3FnU7RFCdM9GMeQorU2YwixjQ+a48zTNHNHGZqeWRsr84JXMw39JvgFZ/bAxX+gFKKVns1JPZavuCTsCgnpisEjyV+CtGsRM1cVrtSXXsoJGBTka5LJ/HN2/K+7WnZVkPkcbXZ49TNHnHqPwSzJykwZZsSpoApgWUzVHGahJdiHG+AkErMf9EC8SEuMl/E0bWLn1sYgZc52hglXVl9OyhZqLTMSbsxTjqKQm8ydYY4RskCk2QxFF0aXevSMxRVHe3bXvvm6Nq3oBv7JrvCzRM1Zt28TqNEUILPFkz9BlCgCSCvX+GlZylW95rsnPsreMcyxWoEGeFpwj6PmQ5RygTcaGLm/T6JHTEOww000AI/wbvselz7cw7c7Fmc05FzvnM+9gJTMc9FPinuMiqnbLvM9gkDDHC9CstEWPha7uaHkMkkiwTq56U/mOxtaLzMDWxPeJmPu8TLc0ghk0Q4xow6DO9wPOOzeqmGRTMS3y0XHYZEcsZnd5zwec5QeFMz/j3dxM2ze4V/T02vrgnFEz6COzQhKeJ0uBJceEtaMMFRdEYikmbnWqGD/NBm8gVNyT0q7fHNO8t1G2SNLbFmB2laTiO8j8oaW5c1br9YY6teA7HQLMYJX5AIqx6kj8m0zIEe5NLBRyWBq0sCv18kcJ93HXDC2PD+PUf3q3k+UHT7QVjja7KmZwlHPu4Say5Wc5iEfAQwmaIMTQxMvOzLGcF0weYCJ1N+5Qn7kyDEJ28QbqUo8XZHkMGUoGomSD4TLxPE6hVB8nE/n5GcLnEUfsfj+3fjV+TShtV5NOa7H5U2li5taq/Avm3C1AXmjCdMVANVWgN+9wGVPxTg92Tl19ct6QFQe0lGr6bnuRJYGm5Z8NUKezUxev8OzO487gvUMOIWqfo+Vlv3cbug7MvesK16vVcFmg8kADVcsA/Mo3toCEJB7ZURzZYP2T08A6EXLxS03BoXWEcsHhaq6Bapu4Ji2wjKFVLuBYTxPOuqpP3NGmQn4HRewQsGU8HrCRUD3cpfUZhqm4vAkLskmjbnalHxjCScFYKN/B/h42G/UdYlxgGXZkvDfaSpnBS4nScFwDi6zAPDt5x+GkTtyGkCq2WvWejkmfJ/zs2UUPacJBkIO/3u2ekpej3m7IczVt+16nbVbFt5LfYjucnNZn97eUE+xKPRbwBC/epJKUZeMiHZ1LCnvsfzIq3pZTOvlNV4u7IaY4dBLoQNqHMwTVm9oY5uN1f/qKP2c13iiL9wUiwG8O8NgOt+xjN2IC3rgc7jGXCMZ+pglG6nFwAtvRTtuPJSgWyBm85AwNEuN4Yi02wHRUoea3pt57FqL2Kx/IsS/oJfT197yl8zyk19oJr6YIelD5qz9OprNJkmN22bvdal/DLMjmWAVl8hK6z4u1kHlSOToPMmp+JuR/+h6z+AdptTW/5DiUKAbN9eW8hu3xPlb4Qf8aiLxxxlGnisPc/UQ5FpyParcRSZiue4xDT3Ej0tZsvr8MDovJpdQKakyPxb0Iwx4RHiIL8QDYAdawI1BTdsc/uFhRtEb7/30br4Hw==7ZjbctowEIafhst2fMI4lw2QdtrS6QzTNrlU8NpWI1seWQa7T18JSz5gIKQFEjK5wvtLK0u7+7EaD+xxXHxkKI1m1AcysAy/GNiTgWWZhu2JH6mUlTJ0RpUQMuyrSY0wx39Aeyo1xz5knYmcUsJx2hUXNElgwTsaYoyuutMCSrpvTVEIPWG+QKSv/sI+jyrVs0aN/glwGOk3m+5VNRIjPVmdJIuQT1ctyZ4O7DGjlFdPcTEGIoOn41L53ewYrTfGIOGHOMROdmM8/PzxxboNZt7sw+frRfhOrZLxUh8YfHF+ZVLGIxrSBJFpo14zmic+yFUNYTVzvlKaCtEU4m/gvFTJRDmnQop4TNQoFJjfSvf3Q2XdtUYmhVp5bZTaSDgrW07SvGuPNW5rS/tV55OH2hk2HQOaswXsiZWlyg+xEPieeU6dXEEF0BjEfoQfA4I4Xnb3gVR5hvW8JoPiQSXxCQlVm1wikqs3XUOIE0kgZJzRsp9wQgRMMrGrCHOYp2gdhZXguZs2lKUVYQEuZPpVbJfAOBT7o9uPhnKwPUWH+nvQsKxarCkpamGmtaOHz37j4WAe/rfOlet3isWb64pwrM2K2Mh1xZ/y2kh3vY1/rwCnB9A3ynEgT3YPAWUw2cFRUwbm4ywFmJAxJZStfW1/CJ7vCF0u/QCtEc+6t133SLC53dCadp8209qCm3Mq3IaXgtsRsTH1NeaxPuKehC93owgcw+ouUR3gZHy5Pb5qoMQlLk4JcHjZPcoxredtUqNLoeYFNCnvQNhMY3sNnOfW5u1uOijgwF5Pz7GMPjzn7TlXb/Qc3quMQ/HZUQXnwUdvcxs/GRcpegXcjJ6bG31v2RbkPMFJxpFc5OIjbTuni7Qwmw8+1Y2q+WxmT/8C ================================================ FILE: docs/developers/plugins.md ================================================ # Plugins Plugins are the most efficient way to customize or change the default behavior of a chart. They have been introduced at [version 2.1.0](https://github.com/chartjs/Chart.js/releases/tag/2.1.0) (global plugins only) and extended at [version 2.5.0](https://github.com/chartjs/Chart.js/releases/tag/v2.5.0) (per chart plugins and options). ## Using plugins Plugins can be shared between chart instances: ```javascript const plugin = { /* plugin implementation */ }; // chart1 and chart2 use "plugin" const chart1 = new Chart(ctx, { plugins: [plugin] }); const chart2 = new Chart(ctx, { plugins: [plugin] }); // chart3 doesn't use "plugin" const chart3 = new Chart(ctx, {}); ``` Plugins can also be defined directly in the chart `plugins` config (a.k.a. *inline plugins*): :::warning *inline* plugins are not registered. Some plugins require registering, i.e. can't be used *inline*. ::: ```javascript const chart = new Chart(ctx, { plugins: [{ beforeInit: function(chart, args, options) { //.. } }] }); ``` However, this approach is not ideal when the customization needs to apply to many charts. ## Global plugins Plugins can be registered globally to be applied on all charts (a.k.a. *global plugins*): ```javascript Chart.register({ // plugin implementation }); ``` :::warning *inline* plugins can't be registered globally. ::: ## Configuration ### Plugin ID Plugins must define a unique id in order to be configurable. This id should follow the [npm package name convention](https://docs.npmjs.com/files/package.json#name): - can't start with a dot or an underscore - can't contain any non-URL-safe characters - can't contain uppercase letters - should be something short, but also reasonably descriptive If a plugin is intended to be released publicly, you may want to check the [registry](https://www.npmjs.com/search?q=chartjs-plugin-) to see if there's something by that name already. Note that in this case, the package name should be prefixed by `chartjs-plugin-` to appear in Chart.js plugin registry. ### Plugin options Plugin options are located under the `options.plugins` config and are scoped by the plugin ID: `options.plugins.{plugin-id}`. ```javascript const chart = new Chart(ctx, { options: { foo: { ... }, // chart 'foo' option plugins: { p1: { foo: { ... }, // p1 plugin 'foo' option bar: { ... } }, p2: { foo: { ... }, // p2 plugin 'foo' option bla: { ... } } } } }); ``` #### Disable plugins To disable a global plugin for a specific chart instance, the plugin options must be set to `false`: ```javascript Chart.register({ id: 'p1', // ... }); const chart = new Chart(ctx, { options: { plugins: { p1: false // disable plugin 'p1' for this instance } } }); ``` To disable all plugins for a specific chart instance, set `options.plugins` to `false`: ```javascript const chart = new Chart(ctx, { options: { plugins: false // all plugins are disabled for this instance } }); ``` #### Plugin defaults You can set default values for your plugin options in the `defaults` entry of your plugin object. In the example below the canvas will always have a lightgreen backgroundColor unless the user overrides this option in `options.plugins.custom_canvas_background_color.color`. ```javascript const plugin = { id: 'custom_canvas_background_color', beforeDraw: (chart, args, options) => { const {ctx} = chart; ctx.save(); ctx.globalCompositeOperation = 'destination-over'; ctx.fillStyle = options.color; ctx.fillRect(0, 0, chart.width, chart.height); ctx.restore(); }, defaults: { color: 'lightGreen' } } ``` ## Plugin Core API Read more about the [existing plugin extension hooks](../api/interfaces/Plugin). ### Chart Initialization Plugins are notified during the initialization process. These hooks can be used to set up data needed for the plugin to operate. ![Chart.js init flowchart](./init_flowchart.png) ### Chart Update Plugins are notified throughout the update process. ![Chart.js update flowchart](./update_flowchart.png) ### Scale Update Plugins are notified throughout the scale update process. ![Chart.js scale update flowchart](./scale_flowchart.png) ### Rendering Plugins can interact with the chart throughout the render process. The rendering process is documented in the flowchart below. Each of the green processes is a plugin notification. The red lines indicate how cancelling part of the render process can occur when a plugin returns `false` from a hook. Not all hooks are cancelable, however, in general most `before*` hooks can be cancelled. ![Chart.js render pipeline flowchart](./render_flowchart.png) ### Event Handling Plugins can interact with the chart during the event handling process. The event handling flow is documented in the flowchart below. Each of the green processes is a plugin notification. If a plugin makes changes that require a re-render, the plugin can set `args.changed` to `true` to indicate that a render is needed. The built-in tooltip plugin uses this method to indicate when the tooltip has changed. ![Chart.js event handling flowchart](./event_flowchart.png) ### Chart destroy Plugins are notified during the destroy process. These hooks can be used to destroy things that the plugin made and used during its life. The `destroy` hook has been deprecated since Chart.js version 3.7.0, use the `afterDestroy` hook instead. ![Chart.js destroy flowchart](./destroy_flowchart.png) ## TypeScript Typings If you want your plugin to be statically typed, you must provide a `.d.ts` TypeScript declaration file. Chart.js provides a way to augment built-in types with user-defined ones, by using the concept of "declaration merging". When adding a plugin, `PluginOptionsByType` must contain the declarations for the plugin. For example, to provide typings for the [`canvas backgroundColor plugin`](../configuration/canvas-background.md), you would add a `.d.ts` containing: ```ts import {ChartType, Plugin} from 'chart.js'; declare module 'chart.js' { interface PluginOptionsByType { customCanvasBackgroundColor?: { color?: string } } } ``` ================================================ FILE: docs/developers/publishing.md ================================================ # Publishing an extension If you are planning on publishing an extension for Chart.js, here are some pointers. ## Awesome You'd probably want your extension to be listed in the [awesome](https://github.com/chartjs/awesome). Note the minimum extension age requirement of 30 days. ## ESM If you are utilizing ESM, you probably still want to publish a UMD bundle of your extension. Because Chart.js v3 is tree shakeable, the interface is a bit different. UMD package's global `Chart` includes everything, while ESM package exports all the things separately. Fortunately, most of the exports can be mapped automatically by the bundlers. But not the helpers. In UMD, helpers are available through `Chart.helpers`. In ESM, they are imported from `chart.js/helpers`. For example `import {isNullOrUndef} from 'chart.js/helpers'` is available at `Chart.helpers.isNullOrUndef` for UMD. ### Rollup `output.globals` can be used to convert the helpers. ```js module.exports = { // ... output: { globals: { 'chart.js': 'Chart', 'chart.js/helpers': 'Chart.helpers' } } }; ``` ================================================ FILE: docs/developers/updates.md ================================================ # Updating Charts It's pretty common to want to update charts after they've been created. When the chart data or options are changed, Chart.js will animate to the new data values and options. ## Adding or Removing Data Adding and removing data is supported by changing the data array. To add data, just add data into the data array as seen in this example, to remove it you can pop it again. ```javascript function addData(chart, label, newData) { chart.data.labels.push(label); chart.data.datasets.forEach((dataset) => { dataset.data.push(newData); }); chart.update(); } function removeData(chart) { chart.data.labels.pop(); chart.data.datasets.forEach((dataset) => { dataset.data.pop(); }); chart.update(); } ``` ## Updating Options To update the options, mutating the `options` property in place or passing in a new options object are supported. - If the options are mutated in place, other option properties would be preserved, including those calculated by Chart.js. - If created as a new object, it would be like creating a new chart with the options - old options would be discarded. ```javascript function updateConfigByMutating(chart) { chart.options.plugins.title.text = 'new title'; chart.update(); } function updateConfigAsNewObject(chart) { chart.options = { responsive: true, plugins: { title: { display: true, text: 'Chart.js' } }, scales: { x: { display: true }, y: { display: true } } }; chart.update(); } ``` Scales can be updated separately without changing other options. To update the scales, pass in an object containing all the customization including those unchanged ones. Variables referencing any one from `chart.scales` would be lost after updating scales with a new `id` or the changed `type`. ```javascript function updateScales(chart) { let xScale = chart.scales.x; let yScale = chart.scales.y; chart.options.scales = { newId: { display: true }, y: { display: true, type: 'logarithmic' } }; chart.update(); // need to update the reference xScale = chart.scales.newId; yScale = chart.scales.y; } ``` You can update a specific scale by its id as well. ```javascript function updateScale(chart) { chart.options.scales.y = { type: 'logarithmic' }; chart.update(); } ``` Code sample for updating options can be found in [line-datasets.html](https://www.chartjs.org/docs/latest/samples/area/line-datasets.html). ## Preventing Animations Sometimes when a chart updates, you may not want an animation. To achieve this you can call `update` with `'none'` as mode. ```javascript myChart.update('none'); ``` ================================================ FILE: docs/general/accessibility.md ================================================ # Accessibility Chart.js charts are rendered on user provided `canvas` elements. Thus, it is up to the user to create the `canvas` element in a way that is accessible. The `canvas` element has support in all browsers and will render on screen but the `canvas` content will not be accessible to screen readers. With `canvas`, the accessibility has to be added with ARIA attributes on the `canvas` element or added using internal fallback content placed within the opening and closing canvas tags. This [website](http://pauljadam.com/demos/canvas.html) has a more detailed explanation of `canvas` accessibility as well as in depth examples. ## Examples These are some examples of **accessible** `canvas` elements. By setting the `role` and `aria-label`, this `canvas` now has an accessible name. ```html ``` This `canvas` element has a text alternative via fallback content. ```html

Hello Fallback World

``` These are some bad examples of **inaccessible** `canvas` elements. This `canvas` element does not have an accessible name or role. ```html ``` This `canvas` element has inaccessible fallback content. ```html Your browser does not support the canvas element. ``` ================================================ FILE: docs/general/colors.md ================================================ # Colors Charts support three color options: * for geometric elements, you can change *background* and *border* colors; * for textual elements, you can change the *font* color. Also, you can change the whole [canvas background](../configuration/canvas-background.md). ## Default colors If a color is not specified, a global default color from `Chart.defaults` is used: | Name | Type | Description | Default value | ---- | ---- | ----------- | ------------- | `backgroundColor` | [`Color`](../api/#color) | Background color | `rgba(0, 0, 0, 0.1)` | `borderColor` | [`Color`](../api/#color) | Border color | `rgba(0, 0, 0, 0.1)` | `color` | [`Color`](../api/#color) | Font color | `#666` You can reset default colors by updating these properties of `Chart.defaults`: ```javascript Chart.defaults.backgroundColor = '#9BD0F5'; Chart.defaults.borderColor = '#36A2EB'; Chart.defaults.color = '#000'; ``` ### Per-dataset color settings If your chart has multiple datasets, using default colors would make individual datasets indistinguishable. In that case, you can set `backgroundColor` and `borderColor` for each dataset: ```javascript const data = { labels: ['A', 'B', 'C'], datasets: [ { label: 'Dataset 1', data: [1, 2, 3], borderColor: '#36A2EB', backgroundColor: '#9BD0F5', }, { label: 'Dataset 2', data: [2, 3, 4], borderColor: '#FF6384', backgroundColor: '#FFB1C1', } ] }; ``` However, setting colors for each dataset might require additional work that you'd rather not do. In that case, consider using the following plugins with pre-defined or generated palettes. ### Default color palette If you don't have any preference for colors, you can use the built-in `Colors` plugin. It will cycle through a palette of seven Chart.js brand colors:
![Colors plugin palette](./colors-plugin-palette.png)
All you need is to import and register the plugin: ```javascript import { Colors } from 'chart.js'; Chart.register(Colors); ``` :::tip Note If you are using the UMD version of Chart.js, this plugin will be enabled by default. You can disable it by setting the `enabled` option to `false`: ```js const options = { plugins: { colors: { enabled: false } } }; ``` ::: ### Dynamic datasets at runtime By default, the colors plugin only works when you initialize the chart without any colors for the border or background specified. If you want to force the colors plugin to always color your datasets, for example, when using dynamic datasets at runtime you will need to set the `forceOverride` option to true: ```js const options = { plugins: { colors: { forceOverride: true } } }; ``` ### Advanced color palettes See the [awesome list](https://github.com/chartjs/awesome#plugins) for plugins that would give you more flexibility defining color palettes. ## Color formats You can specify the color as a string in either of the following notations: | Notation | Example | Example with transparency | -------- | ------- | ------------------------- | [Hexadecimal](https://developer.mozilla.org/en-US/docs/Web/CSS/hex-color) | `#36A2EB` | `#36A2EB80` | [RGB](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgb) or [RGBA](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/rgba) | `rgb(54, 162, 235)` | `rgba(54, 162, 235, 0.5)` | [HSL](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsl) or [HSLA](https://developer.mozilla.org/en-US/docs/Web/CSS/color_value/hsla) | `hsl(204, 82%, 57%)` | `hsla(204, 82%, 57%, 0.5)` Alternatively, you can pass a [CanvasPattern](https://developer.mozilla.org/en-US/docs/Web/API/CanvasPattern) or [CanvasGradient](https://developer.mozilla.org/en/docs/Web/API/CanvasGradient) object instead of a string color to achieve some interesting effects. ## Patterns and Gradients For example, you can fill a dataset with a pattern from an image. ```javascript const img = new Image(); img.src = 'https://example.com/my_image.png'; img.onload = () => { const ctx = document.getElementById('canvas').getContext('2d'); const fillPattern = ctx.createPattern(img, 'repeat'); const chart = new Chart(ctx, { data: { labels: ['Item 1', 'Item 2', 'Item 3'], datasets: [{ data: [10, 20, 30], backgroundColor: fillPattern }] } }); }; ``` Pattern fills can help viewers with vision deficiencies (e.g., color-blindness or partial sight) [more easily understand your data](http://betweentwobrackets.com/data-graphics-and-colour-vision/). You can use the [Patternomaly](https://github.com/ashiguruma/patternomaly) library to generate patterns to fill datasets: ```javascript const chartData = { datasets: [{ data: [45, 25, 20, 10], backgroundColor: [ pattern.draw('square', '#ff6384'), pattern.draw('circle', '#36a2eb'), pattern.draw('diamond', '#cc65fe'), pattern.draw('triangle', '#ffce56') ] }], labels: ['Red', 'Blue', 'Purple', 'Yellow'] }; ``` ================================================ FILE: docs/general/data-structures.md ================================================ # Data structures The `data` property of a dataset can be passed in various formats. By default, that `data` is parsed using the associated chart type and scales. If the `labels` property of the main `data` property is used, it has to contain the same amount of elements as the dataset with the most values. These labels are used to label the index axis (default `x` axis). The values for the labels have to be provided in an array. The provided labels can be of the type string or number to be rendered correctly. If you want multiline labels, you can provide an array with each line as one entry in the array. ## Primitive[] ```javascript const cfg = { type: 'bar', data: { datasets: [{ data: [20, 10], }], labels: ['a', 'b'] } } ``` When `data` is an array of numbers, values from the `labels` array at the same index are used for the index axis (`x` for vertical, `y` for horizontal charts). ## Array[] ```javascript const cfg = { type: 'line', data: { datasets: [{ data: [[10, 20], [15, null], [20, 10]] }] } } ``` When `data` is an array of arrays (or what TypeScript would call tuples), the first element of each tuple is the index (`x` for vertical, `y` for horizontal charts) and the second element is the value (`y` by default). ## Object[] ```javascript const cfg = { type: 'line', data: { datasets: [{ data: [{x: 10, y: 20}, {x: 15, y: null}, {x: 20, y: 10}] }] } } ``` ```javascript const cfg = { type: 'line', data: { datasets: [{ data: [{x: '2016-12-25', y: 20}, {x: '2016-12-26', y: 10}] }] } } ``` ```javascript const cfg = { type: 'bar', data: { datasets: [{ data: [{x: 'Sales', y: 20}, {x: 'Revenue', y: 10}] }] } } ``` This is also the internal format used for parsed data. In this mode, parsing can be disabled by specifying `parsing: false` at chart options or dataset. If parsing is disabled, data must be sorted and in the formats the associated chart type and scales use internally. The values provided must be parsable by the associated scales or in the internal format of the associated scales. For example, the `category` scale uses integers as an internal format, where each integer represents an index in the labels array; but, if parsing is enabled, it can also parse string labels. `null` can be used for skipped values. ## Object[] using custom properties ```javascript const cfg = { type: 'bar', data: { datasets: [{ data: [{id: 'Sales', nested: {value: 1500}}, {id: 'Purchases', nested: {value: 500}}] }] }, options: { parsing: { xAxisKey: 'id', yAxisKey: 'nested.value' } } } ``` When using the pie/doughnut, radar or polarArea chart type, the `parsing` object should have a `key` item that points to the value to look at. In this example, the doughnut chart will show two items with values 1500 and 500. ```javascript const cfg = { type: 'doughnut', data: { datasets: [{ data: [{id: 'Sales', nested: {value: 1500}}, {id: 'Purchases', nested: {value: 500}}] }] }, options: { parsing: { key: 'nested.value' } } } ``` If the key contains a dot, it needs to be escaped with a double slash: ```javascript const cfg = { type: 'line', data: { datasets: [{ data: [{'data.key': 'one', 'data.value': 20}, {'data.key': 'two', 'data.value': 30}] }] }, options: { parsing: { xAxisKey: 'data\\.key', yAxisKey: 'data\\.value' } } } ``` :::warning When using object notation in a radar chart, you still need a `labels` array with labels for the chart to show correctly. ::: ## Object ```javascript const cfg = { type: 'line', data: { datasets: [{ data: { January: 10, February: 20 } }] } } ``` In this mode, the property name is used for the `index` scale and value for the `value` scale. For vertical charts, the index scale is `x` and value scale is `y`. ## Dataset Configuration | Name | Type | Description | ---- | ---- | ----------- | `label` | `string` | The label for the dataset which appears in the legend and tooltips. | `clip` | `number`\|`object` | How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. 0 = clip at chartArea. Clipping can also be configured per side: clip: {left: 5, top: false, right: -2, bottom: 0} | `order` | `number` | The drawing order of dataset. Also affects order for stacking, tooltip and legend. | `stack` | `string` | The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). Defaults to dataset `type`. | `parsing` | `boolean`\|`object` | How to parse the dataset. The parsing can be disabled by specifying parsing: false at chart options or dataset. If parsing is disabled, data must be sorted and in the formats the associated chart type and scales use internally. | `hidden` | `boolean` | Configure the visibility of the dataset. Using `hidden: true` will hide the dataset from being rendered in the Chart. ### parsing ```javascript const data = [{x: 'Jan', net: 100, cogs: 50, gm: 50}, {x: 'Feb', net: 120, cogs: 55, gm: 75}]; const cfg = { type: 'bar', data: { labels: ['Jan', 'Feb'], datasets: [{ label: 'Net sales', data: data, parsing: { yAxisKey: 'net' } }, { label: 'Cost of goods sold', data: data, parsing: { yAxisKey: 'cogs' } }, { label: 'Gross margin', data: data, parsing: { yAxisKey: 'gm' } }] }, }; ``` ## TypeScript When using TypeScript, if you want to use a data structure that is not the default data structure, you will need to pass it to the type interface when instantiating the data variable. ```ts import {ChartData} from 'chart.js'; const datasets: ChartData <'bar', {key: string, value: number} []> = { datasets: [{ data: [{key: 'Sales', value: 20}, {key: 'Revenue', value: 10}], parsing: { xAxisKey: 'key', yAxisKey: 'value' } }], }; ``` ================================================ FILE: docs/general/fonts.md ================================================ # Fonts There are special global settings that can change all the fonts on the chart. These options are in `Chart.defaults.font`. The global font settings only apply when more specific options are not included in the config. For example, in this chart, the text will have a font size of 16px except for the labels in the legend. ```javascript Chart.defaults.font.size = 16; let chart = new Chart(ctx, { type: 'line', data: data, options: { plugins: { legend: { labels: { // This more specific font property overrides the global property font: { size: 14 } } } } } }); ``` | Name | Type | Default | Description | ---- | ---- | ------- | ----------- | `family` | `string` | `"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"` | Default font family for all text, follows CSS font-family options. | `size` | `number` | `12` | Default font size (in px) for text. Does not apply to radialLinear scale point labels. | `style` | `string` | `'normal'` | Default font style. Does not apply to tooltip title or footer. Does not apply to chart title. Follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit). | `weight` | `normal` \| `bold` \| `lighter` \| `bolder` \| `number` | `undefined` | Default font weight (boldness). (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight)). | `lineHeight` | `number`\|`string` | `1.2` | Height of an individual line of text (see [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/line-height)). ## Missing Fonts If a font is specified for a chart that does exist on the system, the browser will not apply the font when it is set. If you notice odd fonts appearing in your charts, check that the font you are applying exists on your system. See [issue 3318](https://github.com/chartjs/Chart.js/issues/3318) for more details. ## Loading Fonts If a font is not cached and needs to be loaded, charts that use the font will need to be updated once the font is loaded. This can be accomplished using the [Font Loading APIs](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Font_Loading_API). See [issue 8020](https://github.com/chartjs/Chart.js/issues/8020) for more details. ================================================ FILE: docs/general/options.md ================================================ # Options ## Option resolution Options are resolved from top to bottom, using a context dependent route. ### Chart level options * options * overrides[`config.type`] * defaults ### Dataset level options `dataset.type` defaults to `config.type`, if not specified. * dataset * options.datasets[`dataset.type`] * options * overrides[`config.type`].datasets[`dataset.type`] * defaults.datasets[`dataset.type`] * defaults ### Dataset animation options * dataset.animation * options.datasets[`dataset.type`].animation * options.animation * overrides[`config.type`].datasets[`dataset.type`].animation * defaults.datasets[`dataset.type`].animation * defaults.animation ### Dataset element level options Each scope is looked up with `elementType` prefix in the option name first, then without the prefix. For example, `radius` for `point` element is looked up using `pointRadius` and if that does not hit, then `radius`. * dataset * options.datasets[`dataset.type`] * options.datasets[`dataset.type`].elements[`elementType`] * options.elements[`elementType`] * options * overrides[`config.type`].datasets[`dataset.type`] * overrides[`config.type`].datasets[`dataset.type`].elements[`elementType`] * defaults.datasets[`dataset.type`] * defaults.datasets[`dataset.type`].elements[`elementType`] * defaults.elements[`elementType`] * defaults ### Scale options * options.scales * overrides[`config.type`].scales * defaults.scales * defaults.scale ### Plugin options A plugin can provide `additionalOptionScopes` array of paths to additionally look for its options in. For root scope, use empty string: `''`. Most core plugins also take options from root scope. * options.plugins[`plugin.id`] * (options.[`...plugin.additionalOptionScopes`]) * overrides[`config.type`].plugins[`plugin.id`] * defaults.plugins[`plugin.id`] * (defaults.[`...plugin.additionalOptionScopes`]) ## Scriptable Options Scriptable options also accept a function which is called for each of the underlying data values and that takes the unique argument `context` representing contextual information (see [option context](options.md#option-context)). A resolver is passed as second parameter, that can be used to access other options in the same context. :::tip Note The `context` argument should be validated in the scriptable function, because the function can be invoked in different contexts. The `type` field is a good candidate for this validation. ::: Example: ```javascript color: function(context) { const index = context.dataIndex; const value = context.dataset.data[index]; return value < 0 ? 'red' : // draw negative values in red index % 2 ? 'blue' : // else, alternate values in blue and green 'green'; }, borderColor: function(context, options) { const color = options.color; // resolve the value of another scriptable option: 'red', 'blue' or 'green' return Chart.helpers.color(color).lighten(0.2); } ``` ## Indexable Options Indexable options also accept an array in which each item corresponds to the element at the same index. Note that if there are less items than data, the items are looped over. In many cases, using a [function](#scriptable-options) is more appropriate if supported. Example: ```javascript color: [ 'red', // color for data at index 0 'blue', // color for data at index 1 'green', // color for data at index 2 'black', // color for data at index 3 //... ] ``` ## Option Context The option context is used to give contextual information when resolving options and currently only applies to [scriptable options](#scriptable-options). The object is preserved, so it can be used to store and pass information between calls. There are multiple levels of context objects: * `chart` * `dataset` * `data` * `scale` * `tick` * `pointLabel` (only used in the radial linear scale) * `tooltip` Each level inherits its parent(s) and any contextual information stored in the parent is available through the child. The context object contains the following properties: ### chart * `chart`: the associated chart * `type`: `'chart'` ### dataset In addition to [chart](#chart) * `active`: true if an element is active (hovered) * `dataset`: dataset at index `datasetIndex` * `datasetIndex`: index of the current dataset * `index`: same as `datasetIndex` * `mode`: the update mode * `type`: `'dataset'` ### data In addition to [dataset](#dataset) * `active`: true if an element is active (hovered) * `dataIndex`: index of the current data * `parsed`: the parsed data values for the given `dataIndex` and `datasetIndex` * `raw`: the raw data values for the given `dataIndex` and `datasetIndex` * `element`: the element (point, arc, bar, etc.) for this data * `index`: same as `dataIndex` * `type`: `'data'` ### scale In addition to [chart](#chart) * `scale`: the associated scale * `type`: `'scale'` ### tick In addition to [scale](#scale) * `tick`: the associated tick object * `index`: tick index * `type`: `'tick'` ### pointLabel In addition to [scale](#scale) * `label`: the associated label value * `index`: label index * `type`: `'pointLabel'` ### tooltip In addition to [chart](#chart) * `tooltip`: the tooltip object * `tooltipItems`: the items the tooltip is displaying ================================================ FILE: docs/general/padding.md ================================================ # Padding Padding values in Chart options can be supplied in a couple of different formats. ## Number If this value is a number, it is applied to all sides (left, top, right, bottom). For example, defining a 20px padding to all sides of the chart: ```javascript let chart = new Chart(ctx, { type: 'line', data: data, options: { layout: { padding: 20 } } }); ``` ## {top, left, bottom, right} object If this value is an object, the `left` property defines the left padding. Similarly, the `right`, `top` and `bottom` properties can also be specified. Omitted properties default to `0`. Let's say you wanted to add 50px of padding to the left side of the chart canvas, you would do: ```javascript let chart = new Chart(ctx, { type: 'line', data: data, options: { layout: { padding: { left: 50 } } } }); ``` ## {x, y} object This is a shorthand for defining left/right and top/bottom to the same values. For example, 10px left / right and 4px top / bottom padding on a Radial Linear Axis [tick backdropPadding](../axes/radial/linear.md#linear-radial-axis-specific-tick-options): ```javascript let chart = new Chart(ctx, { type: 'radar', data: data, options: { scales: { r: { ticks: { backdropPadding: { x: 10, y: 4 } } } } }); ``` ================================================ FILE: docs/general/performance.md ================================================ # Performance Chart.js charts are rendered on `canvas` elements, which makes rendering quite fast. For large datasets or performance sensitive applications, you may wish to consider the tips below. ## Data structure and format ### Parsing Provide prepared data in the internal format accepted by the dataset and scales, and set `parsing: false`. See [Data structures](data-structures.md) for more information. ### Data normalization Chart.js is fastest if you provide data with indices that are unique, sorted, and consistent across datasets and provide the `normalized: true` option to let Chart.js know that you have done so. Even without this option, it can sometimes still be faster to provide sorted data. ### Decimation Decimating your data will achieve the best results. When there is a lot of data to display on the graph, it doesn't make sense to show tens of thousands of data points on a graph that is only a few hundred pixels wide. The [decimation plugin](../configuration/decimation.md) can be used with line charts to decimate data before the chart is rendered. This will provide the best performance since it will reduce the memory needed to render the chart. Line charts are able to do [automatic data decimation during draw](#automatic-data-decimation-during-draw), when certain conditions are met. You should still consider decimating data yourself before passing it in for maximum performance since the automatic decimation occurs late in the chart life cycle. ## Tick Calculation ### Rotation [Specify a rotation value](../axes/cartesian/index.md#tick-configuration) by setting `minRotation` and `maxRotation` to the same value, which avoids the chart from having to automatically determine a value to use. ### Sampling Set the [`ticks.sampleSize`](../axes/cartesian/index.md#tick-configuration) option. This will determine how large your labels are by looking at only a subset of them in order to render axes more quickly. This works best if there is not a large variance in the size of your labels. ## Disable Animations If your charts have long render times, it is a good idea to disable animations. Doing so will mean that the chart needs to only be rendered once during an update instead of multiple times. This will have the effect of reducing CPU usage and improving general page performance. Line charts use Path2D caching when animations are disabled and Path2D is available. To disable animations ```javascript new Chart(ctx, { type: 'line', data: data, options: { animation: false } }); ``` ## Specify `min` and `max` for scales If you specify the `min` and `max`, the scale does not have to compute the range from the data. ```javascript new Chart(ctx, { type: 'line', data: data, options: { scales: { x: { type: 'time', min: new Date('2019-01-01').valueOf(), max: new Date('2019-12-31').valueOf() }, y: { type: 'linear', min: 0, max: 100 } } } }); ``` ## Parallel rendering with web workers As of 2023, modern browser have the ability to [transfer rendering control of a canvas](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/transferControlToOffscreen) to a web worker. Web workers can use the [OffscreenCanvas API](https://developer.mozilla.org/en-US/docs/Web/API/OffscreenCanvas) to render from a web worker onto canvases in the DOM. Chart.js is a canvas-based library and supports rendering in a web worker - just pass an OffscreenCanvas into the Chart constructor instead of a Canvas element. By moving all Chart.js calculations onto a separate thread, the main thread can be freed up for other uses. Some tips and tricks when using Chart.js in a web worker: * Transferring data between threads can be expensive, so ensure that your config and data objects are as small as possible. Try generating them on the worker side if you can (workers can make HTTP requests!) or passing them to your worker as ArrayBuffers, which can be transferred quickly from one thread to another. * You can't transfer functions between threads, so if your config object includes functions you'll have to strip them out before transferring and then add them back later. * You can't access the DOM from worker threads, so Chart.js plugins that use the DOM (including any mouse interactions) will likely not work. * Ensure that you have a fallback if you support older browsers. * Resizing the chart must be done manually. See an example in the worker code below. Example main thread code: ```javascript const config = {}; const canvas = new HTMLCanvasElement(); const offscreenCanvas = canvas.transferControlToOffscreen(); const worker = new Worker('worker.js'); worker.postMessage({canvas: offscreenCanvas, config}, [offscreenCanvas]); ``` Example worker code, in `worker.js`: ```javascript onmessage = function(event) { const {canvas, config} = event.data; const chart = new Chart(canvas, config); // Resizing the chart must be done manually, since OffscreenCanvas does not include event listeners. canvas.width = 100; canvas.height = 100; chart.resize(); }; ``` ## Line Charts ### Leave Bézier curves disabled If you are drawing lines on your chart, disabling Bézier curves will improve render times since drawing a straight line is more performant than a Bézier curve. Bézier curves are disabled by default. ### Automatic data decimation during draw Line element will automatically decimate data, when `tension`, `stepped`, and `borderDash` are left set to their default values (`false`, `0`, and `[]` respectively). This improves rendering speed by skipping drawing of invisible line segments. ### Enable spanGaps If you have a lot of data points, it can be more performant to enable `spanGaps`. This disables segmentation of the line, which can be an unneeded step. To enable `spanGaps`: ```javascript new Chart(ctx, { type: 'line', data: { datasets: [{ spanGaps: true // enable for a single dataset }] }, options: { spanGaps: true // enable for all datasets } }); ``` ### Disable Line Drawing If you have a lot of data points, it can be more performant to disable rendering of the line for a dataset and only draw points. Doing this means that there is less to draw on the canvas which will improve render performance. To disable lines: ```javascript new Chart(ctx, { type: 'line', data: { datasets: [{ showLine: false // disable for a single dataset }] }, options: { showLine: false // disable for all datasets } }); ``` ### Disable Point Drawing If you have a lot of data points, it can be more performant to disable rendering of the points for a dataset and only draw lines. Doing this means that there is less to draw on the canvas which will improve render performance. To disable point drawing: ```javascript new Chart(ctx, { type: 'line', data: { datasets: [{ pointRadius: 0 // disable for a single dataset }] }, options: { datasets: { line: { pointRadius: 0 // disable for all `'line'` datasets } }, elements: { point: { radius: 0 // default to disabled in all datasets } } } }); ``` ## When transpiling with Babel, consider using `loose` mode Babel 7.9 changed the way classes are constructed. It is slow, unless used with `loose` mode. [More information](https://github.com/babel/babel/issues/11356) ================================================ FILE: docs/getting-started/index.md ================================================ # Getting Started Let's get started with Chart.js! * **[Follow a step-by-step guide](./usage) to get up to speed with Chart.js** * [Install Chart.js](./installation) from npm or a CDN * [Integrate Chart.js](./integration) with bundlers, loaders, and front-end frameworks * [Use Chart.js from Node.js](./using-from-node-js) Alternatively, see the example below or check [samples](../samples). ## Create a Chart In this example, we create a bar chart for a single dataset and render it on an HTML page. Add this code snippet to your page: ```html
``` You should get a chart like this: ![demo](./preview.png) Let's break this code down. First, we need to have a canvas in our page. It's recommended to give the chart its own container for [responsiveness](../configuration/responsive.md). ```html
``` Now that we have a canvas, we can include Chart.js from a CDN. ```html ``` Finally, we can create a chart. We add a script that acquires the `myChart` canvas element and instantiates `new Chart` with desired configuration: `bar` chart type, labels, data points, and options. ```html ``` You can see all the ways to use Chart.js in the [step-by-step guide](./usage). ================================================ FILE: docs/getting-started/installation.md ================================================ # Installation ## npm [![npm](https://img.shields.io/npm/v/chart.js.svg?style=flat-square&maxAge=600)](https://npmjs.com/package/chart.js) [![npm](https://img.shields.io/npm/dm/chart.js.svg?style=flat-square&maxAge=600)](https://npmjs.com/package/chart.js) ```sh npm install chart.js ``` ## CDN ### CDNJS [![cdnjs](https://img.shields.io/cdnjs/v/Chart.js.svg?style=flat-square&maxAge=600)](https://cdnjs.com/libraries/Chart.js) Chart.js built files are available on [CDNJS](https://cdnjs.com/): ### jsDelivr [![jsdelivr](https://img.shields.io/npm/v/chart.js.svg?label=jsdelivr&style=flat-square&maxAge=600)](https://cdn.jsdelivr.net/npm/chart.js@latest/dist/) [![jsdelivr hits](https://data.jsdelivr.com/v1/package/npm/chart.js/badge)](https://www.jsdelivr.com/package/npm/chart.js) Chart.js built files are also available through [jsDelivr](https://www.jsdelivr.com/): ## GitHub [![github](https://img.shields.io/github/release/chartjs/Chart.js.svg?style=flat-square&maxAge=600)](https://github.com/chartjs/Chart.js/releases/latest) You can download the latest version of [Chart.js on GitHub](https://github.com/chartjs/Chart.js/releases/latest). If you download or clone the repository, you must [build](../developers/contributing.md#building-and-testing) Chart.js to generate the dist files. Chart.js no longer comes with prebuilt release versions, so an alternative option to downloading the repo is **strongly** advised. ================================================ FILE: docs/getting-started/integration.md ================================================ # Integration Chart.js can be integrated with plain JavaScript or with different module loaders. The examples below show how to load Chart.js in different systems. If you're using a front-end framework (e.g., React, Angular, or Vue), please see [available integrations](https://github.com/chartjs/awesome#integrations). ## Script Tag ```html ``` ## Bundlers (Webpack, Rollup, etc.) Chart.js is tree-shakeable, so it is necessary to import and register the controllers, elements, scales and plugins you are going to use. ### Quick start If you don't care about the bundle size, you can use the `auto` package ensuring all features are available: ```javascript import Chart from 'chart.js/auto'; ``` ### Bundle optimization When optimizing the bundle, you need to import and register the components that are needed in your application. The options are categorized into controllers, elements, plugins, scales. You can pick and choose many of these, e.g. if you are not going to use tooltips, don't import and register the `Tooltip` plugin. But each type of chart has its own bare-minimum requirements (typically the type's controller, element(s) used by that controller and scale(s)): * Bar chart * `BarController` * `BarElement` * Default scales: `CategoryScale` (x), `LinearScale` (y) * Bubble chart * `BubbleController` * `PointElement` * Default scales: `LinearScale` (x/y) * Doughnut chart * `DoughnutController` * `ArcElement` * Not using scales * Line chart * `LineController` * `LineElement` * `PointElement` * Default scales: `CategoryScale` (x), `LinearScale` (y) * Pie chart * `PieController` * `ArcElement` * Not using scales * PolarArea chart * `PolarAreaController` * `ArcElement` * Default scale: `RadialLinearScale` (r) * Radar chart * `RadarController` * `LineElement` * `PointElement` * Default scale: `RadialLinearScale` (r) * Scatter chart * `ScatterController` * `PointElement` * Default scales: `LinearScale` (x/y) Available plugins: * [`Decimation`](../configuration/decimation.md) * `Filler` - used to fill area described by `LineElement`, see [Area charts](../charts/area.md) * [`Legend`](../configuration/legend.md) * [`SubTitle`](../configuration/subtitle.md) * [`Title`](../configuration/title.md) * [`Tooltip`](../configuration/tooltip.md) Available scales: * Cartesian scales (x/y) * [`CategoryScale`](../axes/cartesian/category.md) * [`LinearScale`](../axes/cartesian/linear.md) * [`LogarithmicScale`](../axes/cartesian/logarithmic.md) * [`TimeScale`](../axes/cartesian/time.md) * [`TimeSeriesScale`](../axes/cartesian/timeseries.md) * Radial scales (r) * [`RadialLinearScale`](../axes/radial/linear.md) ### Helper functions If you want to use the helper functions, you will need to import these separately from the helpers package and use them as stand-alone functions. Example of [Converting Events to Data Values](../configuration/interactions.md#converting-events-to-data-values) using bundlers. ```javascript import Chart from 'chart.js/auto'; import { getRelativePosition } from 'chart.js/helpers'; const chart = new Chart(ctx, { type: 'line', data: data, options: { onClick: (e) => { const canvasPosition = getRelativePosition(e, chart); // Substitute the appropriate scale IDs const dataX = chart.scales.x.getValueForPixel(canvasPosition.x); const dataY = chart.scales.y.getValueForPixel(canvasPosition.y); } } }); ``` ## CommonJS Because Chart.js is an ESM library, in CommonJS modules you should use a dynamic `import`: ```javascript const { Chart } = await import('chart.js'); ``` ## RequireJS **Important:** RequireJS can load only [AMD modules](https://requirejs.org/docs/whyamd.html), so be sure to require one of the UMD builds instead (i.e. `dist/chart.umd.min.js`). ```javascript require(['path/to/chartjs/dist/chart.umd.min.js'], function(Chart){ const myChart = new Chart(ctx, {...}); }); ``` :::tip Note In order to use the time scale, you need to make sure [one of the available date adapters](https://github.com/chartjs/awesome#adapters) and corresponding date library are fully loaded **after** requiring Chart.js. For this you can use nested requires: ```javascript require(['chartjs'], function(Chart) { require(['moment'], function() { require(['chartjs-adapter-moment'], function() { new Chart(ctx, {...}); }); }); }); ``` ::: ================================================ FILE: docs/getting-started/usage.md ================================================ # Step-by-step guide Follow this guide to get familiar with all major concepts of Chart.js: chart types and elements, datasets, customization, plugins, components, and tree-shaking. Don't hesitate to follow the links in the text. We'll build a Chart.js data visualization with a couple of charts from scratch: ![result](./usage-8.png) ## Build a new application with Chart.js In a new folder, create the `package.json` file with the following contents: ```json { "name": "chartjs-example", "version": "1.0.0", "license": "MIT", "scripts": { "dev": "parcel src/index.html", "build": "parcel build src/index.html" }, "devDependencies": { "parcel": "^2.6.2" }, "dependencies": { "@cubejs-client/core": "^0.31.0", "chart.js": "^4.0.0" } } ``` Modern front-end applications often use JavaScript module bundlers, so we’ve picked [Parcel](https://parceljs.org) as a nice zero-configuration build tool. We’re also installing Chart.js v4 and a JavaScript client for [Cube](https://cube.dev/?ref=eco-chartjs), an open-source API for data apps we’ll use to fetch real-world data (more on that later). Run `npm install`, `yarn install`, or `pnpm install` to install the dependencies, then create the `src` folder. Inside that folder, we’ll need a very simple `index.html` file: ```html Chart.js example
``` As you can see, Chart.js requires minimal markup: a `canvas` tag with an `id` by which we’ll reference the chart later. By default, Chart.js charts are [responsive](../configuration/responsive.md) and take the whole enclosing container. So, we set the width of the `div` to control chart width. Lastly, let’s create the `src/acquisitions.js` file with the following contents: ```jsx import Chart from 'chart.js/auto' (async function() { const data = [ { year: 2010, count: 10 }, { year: 2011, count: 20 }, { year: 2012, count: 15 }, { year: 2013, count: 25 }, { year: 2014, count: 22 }, { year: 2015, count: 30 }, { year: 2016, count: 28 }, ]; new Chart( document.getElementById('acquisitions'), { type: 'bar', data: { labels: data.map(row => row.year), datasets: [ { label: 'Acquisitions by year', data: data.map(row => row.count) } ] } } ); })(); ``` Let’s walk through this code: - We import `Chart`, the main Chart.js class, from the special `chart.js/auto` path. It loads [all available Chart.js components](./integration) (which is very convenient) but disallows tree-shaking. We’ll address that later. - We instantiate a new `Chart` instance and provide two arguments: the canvas element where the chart would be rendered and the options object. - We just need to provide a chart type (`bar`) and provide `data` which consists of `labels` (often, numeric or textual descriptions of data points) and an array of `datasets` (Chart.js supports multiple datasets for most chart types). Each dataset is designated with a `label` and contains an array of data points. - For now, we only have a few entries of dummy data. So, we extract `year` and `count` properties to produce the arrays of `labels` and data points within the only dataset. Time to run the example with `npm run dev`, `yarn dev`, or `pnpm dev` and navigate to [localhost:1234](http://localhost:1234) in your web browser: ![result](./usage-1.png) With just a few lines of code, we’ve got a chart with a lot of features: a [legend](../configuration/legend.md), [grid lines](../samples/scale-options/grid.md), [ticks](../samples/scale-options/ticks.md), and [tooltips](../configuration/tooltip.md) shown on hover. Refresh the web page a few times to see that the chart is also [animated](../configuration/animations.md#animations). Try clicking on the “Acquisitions by year” label to see that you’re also able to toggle datasets visibility (especially useful when you have multiple datasets). ### Simple customizations Let’s see how Chart.js charts can be customized. First, let’s turn off the animations so the chart appears instantly. Second, let’s hide the legend and tooltips since we have only one dataset and pretty trivial data. Replace the `new Chart(...);` invocation in `src/acquisitions.js` with the following snippet: ```jsx new Chart( document.getElementById('acquisitions'), { type: 'bar', options: { animation: false, plugins: { legend: { display: false }, tooltip: { enabled: false } } }, data: { labels: data.map(row => row.year), datasets: [ { label: 'Acquisitions by year', data: data.map(row => row.count) } ] } } ); ``` As you can see, we’ve added the `options` property to the second argument—that’s how you can specify all kinds of customization options for Chart.js. The [animation is disabled](../configuration/animations.md#disabling-animation) with a boolean flag provided via `animation`. Most chart-wide options (e.g., [responsiveness](../configuration/responsive.md) or [device pixel ratio](../configuration/device-pixel-ratio.md)) are configured like this. The legend and tooltips are hidden with boolean flags provided under the respective sections in `plugins`. Note that some of Chart.js features are extracted into plugins: self-contained, separate pieces of code. A few of them are available as a part of [Chart.js distribution](https://github.com/chartjs/Chart.js/tree/master/src/plugins), other plugins are maintained independently and can be located in the [awesome list](https://github.com/chartjs/awesome) of plugins, framework integrations, and additional chart types. You should be able to see the updated minimalistic chart in your browser. ### Real-world data With hardcoded, limited-size, unrealistic data, it’s hard to show the full potential of Chart.js. Let’s quickly connect to a data API to make our example application closer to a production use case. Let’s create the `src/api.js` file with the following contents: ```jsx import { CubejsApi } from '@cubejs-client/core'; const apiUrl = 'https://heavy-lansford.gcp-us-central1.cubecloudapp.dev/cubejs-api/v1'; const cubeToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjEwMDAwMDAwMDAsImV4cCI6NTAwMDAwMDAwMH0.OHZOpOBVKr-sCwn8sbZ5UFsqI3uCs6e4omT7P6WVMFw'; const cubeApi = new CubejsApi(cubeToken, { apiUrl }); export async function getAquisitionsByYear() { const acquisitionsByYearQuery = { dimensions: [ 'Artworks.yearAcquired', ], measures: [ 'Artworks.count' ], filters: [ { member: 'Artworks.yearAcquired', operator: 'set' } ], order: { 'Artworks.yearAcquired': 'asc' } }; const resultSet = await cubeApi.load(acquisitionsByYearQuery); return resultSet.tablePivot().map(row => ({ year: parseInt(row['Artworks.yearAcquired']), count: parseInt(row['Artworks.count']) })); } export async function getDimensions() { const dimensionsQuery = { dimensions: [ 'Artworks.widthCm', 'Artworks.heightCm' ], measures: [ 'Artworks.count' ], filters: [ { member: 'Artworks.classification', operator: 'equals', values: [ 'Painting' ] }, { member: 'Artworks.widthCm', operator: 'set' }, { member: 'Artworks.widthCm', operator: 'lt', values: [ '500' ] }, { member: 'Artworks.heightCm', operator: 'set' }, { member: 'Artworks.heightCm', operator: 'lt', values: [ '500' ] } ] }; const resultSet = await cubeApi.load(dimensionsQuery); return resultSet.tablePivot().map(row => ({ width: parseInt(row['Artworks.widthCm']), height: parseInt(row['Artworks.heightCm']), count: parseInt(row['Artworks.count']) })); } ``` Let’s see what’s happening there: - We `import` the JavaScript client library for [Cube](https://cube.dev/?ref=eco-chartjs), an open-source API for data apps, configure it with the API URL (`apiUrl`) and the authentication token (`cubeToken`), and finally instantiate the client (`cubeApi`). - Cube API is hosted in [Cube Cloud](https://cube.dev/cloud/?ref=eco-chartjs) and connected to a database with a [public dataset](https://github.com/MuseumofModernArt/collection) of ~140,000 records representing all of the artworks in the collection of the [Museum of Modern Art](https://www.moma.org) in New York, USA. Certainly, a more real-world dataset than what we’ve got now. - We define a couple of asynchronous functions to fetch data from the API: `getAquisitionsByYear` and `getDimensions`. The first one returns the number of artworks by the year of acquisition, the other returns the number of artworks for every width-height pair (we’ll need it for another chart). - Let’s take a look at `getAquisitionsByYear`. First, we create a declarative, JSON-based query in the `acquisitionsByYearQuery` variable. As you can see, we specify that for every `yearAcquired` we’d like to get the `count` of artworks; `yearAcquired` has to be set (i.e., not undefined); the result set would be sorted by `yearAcquired` in the ascending order. - Second, we fetch the `resultSet` by calling `cubeApi.load` and map it to an array of objects with desired `year` and `count` properties. Now, let’s deliver the real-world data to our chart. Please apply a couple of changes to `src/acquisitions.js`: add an import and replace the definition of the `data` variable. ```jsx import { getAquisitionsByYear } from './api' // ... const data = await getAquisitionsByYear(); ``` Done! Now, our chart with real-world data looks like this. Looks like something interesting happened in 1964, 1968, and 2008! ![result](./usage-2.png) We’re done with the bar chart. Let’s try another Chart.js chart type. ### Further customizations Chart.js supports many common chart types. For instance, [Bubble chart](../charts/bubble.md) allows to display three dimensions of data at the same time: locations on `x` and `y` axes represent two dimensions, and the third dimension is represented by the size of the individual bubbles. To create the chart, stop the already running application, then go to `src/index.html`, and uncomment the following two lines: ```html

``` Then, create the `src/dimensions.js` file with the following contents: ```jsx import Chart from 'chart.js/auto' import { getDimensions } from './api' (async function() { const data = await getDimensions(); new Chart( document.getElementById('dimensions'), { type: 'bubble', data: { labels: data.map(x => x.year), datasets: [ { label: 'Dimensions', data: data.map(row => ({ x: row.width, y: row.height, r: row.count })) } ] } } ); })(); ``` Probably, everything is pretty straightforward there: we get data from the API and render a new chart with the `bubble` type, passing three dimensions of data as `x`, `y`, and `r` (radius) properties. Now, reset caches with `rm -rf .parcel-cache` and start the application again with `npm run dev`, `yarn dev`, or `pnpm dev`. We can review the new chart now: ![result](./usage-3.png) Well, it doesn’t look pretty. First of all, the chart is not square. Artworks’ width and height are equally important so we’d like to make the chart width equal to its height as well. By default, Chart.js charts have the [aspect ratio](../configuration/responsive.md) of either 1 (for all radial charts, e.g., a doughnut chart) or 2 (for all the rest). Let’s modify the aspect ratio for our chart: ```jsx // ... new Chart( document.getElementById('dimensions'), { type: 'bubble', options: { aspectRatio: 1, }, // ... ``` Looks much better now: ![result](./usage-4.png) However, it’s still not ideal. The horizontal axis spans from 0 to 500 while the vertical axis spans from 0 to 450. By default, Chart.js automatically adjusts the range (minimum and maximum values) of the axes to the values provided in the dataset, so the chart “fits” your data. Apparently, MoMa collection doesn’t have artworks in the range of 450 to 500 cm in height. Let’s modify the [axes configuration](../axes/) for our chart to account for that: ```jsx // ... new Chart( document.getElementById('dimensions'), { type: 'bubble', options: { aspectRatio: 1, scales: { x: { max: 500 }, y: { max: 500 } } }, // ... ``` Great! Behold the updated chart: ![result](./usage-5.png) However, there’s one more nitpick: what are these numbers? It’s not very obvious that the units are centimetres. Let’s apply a [custom tick format](../axes/labelling.md#creating-custom-tick-formats) to both axes to make things clear. We’ll provide a callback function that would be called to format each tick value. Here’s the updated axes configuration: ```jsx // ... new Chart( document.getElementById('dimensions'), { type: 'bubble', options: { aspectRatio: 1, scales: { x: { max: 500, ticks: { callback: value => `${value / 100} m` } }, y: { max: 500, ticks: { callback: value => `${value / 100} m` } } } }, // ... ``` Perfect, now we have proper units on both axes: ![result](./usage-6.png) ### Multiple datasets Chart.js plots each dataset independently and allows to apply custom styles to them. Take a look at the chart: there’s a visible “line” of bubbles with equal `x` and `y` coordinates representing square artworks. It would be cool to put these bubbles in their own dataset and paint them differently. Also, we can separate “taller” artworks from “wider” ones and paint them differently, too. Here’s how we can do that. Replace the `datasets` with the following code: ```jsx // ... datasets: [ { label: 'width = height', data: data .filter(row => row.width === row.height) .map(row => ({ x: row.width, y: row.height, r: row.count })) }, { label: 'width > height', data: data .filter(row => row.width > row.height) .map(row => ({ x: row.width, y: row.height, r: row.count })) }, { label: 'width < height', data: data .filter(row => row.width < row.height) .map(row => ({ x: row.width, y: row.height, r: row.count })) } ] // .. ``` As you can see, we define three datasets with different labels. Each dataset gets its own slice of data extracted with `filter`. Now they are visually distinct and, as you already know, you can toggle their visibility independently. ![result](./usage-7.png) Here we rely on the default color palette. However, keep in mind every chart type supports a lot of [dataset options](../charts/bubble.md#dataset-properties) that you can feel free to customize. ### Plugins Another—and very powerful!—way to customize Chart.js charts is to use plugins. You can find some in the [plugin directory](https://github.com/chartjs/awesome#plugins) or create your own, ad-hoc ones. In Chart.js ecosystem, it’s idiomatic and expected to fine tune charts with plugins. For example, you can customize [canvas background](../configuration/canvas-background.md) or [add a border](../samples/plugins/chart-area-border.md) to it with simple ad-hoc plugins. Let’s try the latter. Plugins have an [extensive API](../developers/plugins.md) but, in a nutshell, a plugin is defined as an object with a `name` and one or more callback functions defined in the extension points. Insert the following snippet before and in place of the `new Chart(...);` invocation in `src/dimensions.js`: ```jsx // ... const chartAreaBorder = { id: 'chartAreaBorder', beforeDraw(chart, args, options) { const { ctx, chartArea: { left, top, width, height } } = chart; ctx.save(); ctx.strokeStyle = options.borderColor; ctx.lineWidth = options.borderWidth; ctx.setLineDash(options.borderDash || []); ctx.lineDashOffset = options.borderDashOffset; ctx.strokeRect(left, top, width, height); ctx.restore(); } }; new Chart( document.getElementById('dimensions'), { type: 'bubble', plugins: [ chartAreaBorder ], options: { plugins: { chartAreaBorder: { borderColor: 'red', borderWidth: 2, borderDash: [ 5, 5 ], borderDashOffset: 2, } }, aspectRatio: 1, // ... ``` As you can see, in this `chartAreaBorder` plugin, we acquire the canvas context, save its current state, apply styles, draw a rectangular shape around the chart area, and restore the canvas state. We’re also passing the plugin in `plugins` so it’s only applied to this particular chart. We also pass the plugin options in `options.plugins.chartAreaBorder`; we could surely hardcode them in the plugin source code but it’s much more reusable this way. Our bubble chart looks fancier now: ![result](./usage-8.png) ### Tree-shaking In production, we strive to ship as little code as possible, so the end users can load our data applications faster and have better experience. For that, we’ll need to apply [tree-shaking](https://cube.dev/blog/how-to-build-tree-shakeable-javascript-libraries/?ref=eco-chartjs) which is fancy term for removing unused code from the JavaScript bundle. Chart.js fully supports tree-shaking with its component design. You can register all Chart.js components at once (which is convenient when you’re prototyping) and get them bundled with your application. Or, you can register only necessary components and get a minimal bundle, much less in size. Let’s inspect our example application. What’s the bundle size? You can stop the application and run `npm run build`, or `yarn build`, or `pnpm build`. In a few moments, you’ll get something like this: ```bash % yarn build yarn run v1.22.17 $ parcel build src/index.html ✨ Built in 88ms dist/index.html 381 B 164ms dist/index.74a47636.js 265.48 KB 1.25s dist/index.ba0c2e17.js 881 B 63ms ✨ Done in 0.51s. ``` We can see that Chart.js and other dependencies were bundled together in a single 265 KB file. To reduce the bundle size, we’ll need to apply a couple of changes to `src/acquisitions.js` and `src/dimensions.js`. First, we’ll need to remove the following import statement from both files: `import Chart from 'chart.js/auto'`. Instead, let’s load only necessary components and “register” them with Chart.js using `Chart.register(...)`. Here’s what we need in `src/acquisitions.js`: ```jsx import { Chart, Colors, BarController, CategoryScale, LinearScale, BarElement, Legend } from 'chart.js' Chart.register( Colors, BarController, BarElement, CategoryScale, LinearScale, Legend ); ``` And here’s the snippet for `src/dimensions.js`: ```jsx import { Chart, Colors, BubbleController, CategoryScale, LinearScale, PointElement, Legend } from 'chart.js' Chart.register( Colors, BubbleController, PointElement, CategoryScale, LinearScale, Legend ); ``` You can see that, in addition to the `Chart` class, we’re also loading a controller for the chart type, scales, and other chart elements (e.g., bars or points). You can look all available components up in the [documentation](./integration.md#bundle-optimization). Alternatively, you can follow Chart.js advice in the console. For example, if you forget to import `BarController` for your bar chart, you’ll see the following message in the browser console: ``` Unhandled Promise Rejection: Error: "bar" is not a registered controller. ``` Remember to carefully check for imports from `chart.js/auto` when preparing your application for production. It takes only one import like this to effectively disable tree-shaking. Now, let’s inspect our application once again. Run `yarn build` and you’ll get something like this: ```bash % yarn build yarn run v1.22.17 $ parcel build src/index.html ✨ Built in 88ms dist/index.html 381 B 176ms dist/index.5888047.js 208.66 KB 1.23s dist/index.dcb2e865.js 932 B 58ms ✨ Done in 0.51s. ``` By importing and registering only select components, we’ve removed more than 56 KB of unnecessary code. Given that other dependencies take ~50 KB in the bundle, tree-shaking helps remove ~25% of Chart.js code from the bundle for our example application. ## Next steps Now you’re familiar with all major concepts of Chart.js: chart types and elements, datasets, customization, plugins, components, and tree-shaking. Feel free to review many [examples of charts](../samples/information.md) in the documentation and check the [awesome list](https://github.com/chartjs/awesome) of Chart.js plugins and additional chart types as well as [framework integrations](https://github.com/chartjs/awesome#integrations) (e.g., React, Vue, Svelte, etc.). Also, don’t hesitate to join [Chart.js Discord](https://discord.gg/HxEguTK6av) and follow [Chart.js on Twitter](https://twitter.com/chartjs). Have fun and good luck building with Chart.js! ================================================ FILE: docs/getting-started/using-from-node-js.md ================================================ # Using from Node.js You can use Chart.js in Node.js for server-side generation of plots with help from an NPM package such as [node-canvas](https://github.com/Automattic/node-canvas) or [skia-canvas](https://skia-canvas.org/). Sample usage: ```js import {CategoryScale, Chart, LinearScale, LineController, LineElement, PointElement} from 'chart.js'; import {Canvas} from 'skia-canvas'; import fsp from 'node:fs/promises'; Chart.register([ CategoryScale, LineController, LineElement, LinearScale, PointElement ]); const canvas = new Canvas(400, 300); const chart = new Chart( canvas, // TypeScript needs "as any" here { type: 'line', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [{ label: '# of Votes', data: [12, 19, 3, 5, 2, 3], borderColor: 'red' }] } } ); const pngBuffer = await canvas.toBuffer('png', {matte: 'white'}); await fsp.writeFile('output.png', pngBuffer); chart.destroy(); ``` ================================================ FILE: docs/index.md ================================================ # Chart.js Welcome to Chart.js! * **[Get started with Chart.js](./getting-started/) — best if you're new to Chart.js** * Migrate from [Chart.js v3](./migration/v4-migration.md) or [Chart.js v2](./migration/v3-migration.md) * Join the community on [Discord](https://discord.gg/HxEguTK6av) and [Twitter](https://twitter.com/chartjs) * Post a question tagged with `chart.js` on [Stack Overflow](https://stackoverflow.com/questions/tagged/chart.js) * [Contribute to Chart.js](./developers/contributing.md) ## Why Chart.js Among [many charting libraries](https://awesome.cube.dev/?tools=charts&ref=eco-chartjs) for JavaScript application developers, Chart.js is currently the most popular one according to [GitHub stars](https://github.com/chartjs/Chart.js) (~60,000) and [npm downloads](https://www.npmjs.com/package/chart.js) (~2,400,000 weekly). Chart.js was created and [announced](https://twitter.com/_nnnick/status/313599208387137536) in 2013 but has come a long way since then. It’s open-source, licensed under the very permissive [MIT license](https://github.com/chartjs/Chart.js/blob/master/LICENSE.md), and maintained by an active community. ### Features Chart.js provides a set of frequently used chart types, plugins, and customization options. In addition to a reasonable set of [built-in chart types](./charts/area.md), you can use additional community-maintained [chart types](https://github.com/chartjs/awesome#charts). On top of that, it’s possible to combine several chart types into a [mixed chart](./charts/mixed.md) (essentially, blending multiple chart types into one on the same canvas). Chart.js is highly customizable with [custom plugins](https://github.com/chartjs/awesome#plugins) to create annotations, zoom, or drag-and-drop functionalities to name a few things. ### Defaults Chart.js comes with a sound default configuration, making it very easy to start with and get an app that is ready for production. Chances are you will get a very appealing chart even if you don’t specify any options at all. For instance, Chart.js has animations turned on by default, so you can instantly bring attention to the story you’re telling with the data. ### Integrations Chart.js comes with built-in TypeScript typings and is compatible with all popular [JavaScript frameworks](https://github.com/chartjs/awesome#javascript) including [React](https://github.com/reactchartjs/react-chartjs-2), [Vue](https://github.com/apertureless/vue-chartjs/), [Svelte](https://github.com/SauravKanchan/svelte-chartjs), and [Angular](https://github.com/valor-software/ng2-charts). You can use Chart.js directly or leverage well-maintained wrapper packages that allow for a more native integration with your frameworks of choice. ### Developer experience Chart.js has very thorough documentation (yes, you're reading it), [API reference](./api/), and [examples](./samples/information.md). Maintainers and community members eagerly engage in conversations on [Discord](https://discord.gg/HxEguTK6av), [GitHub Discussions](https://github.com/chartjs/Chart.js/discussions), and [Stack Overflow](https://stackoverflow.com/questions/tagged/chart.js) where more than 11,000 questions are tagged with `chart.js`. ### Canvas rendering Chart.js renders chart elements on an HTML5 canvas unlike several others, mostly D3.js-based, charting libraries that render as SVG. Canvas rendering makes Chart.js very performant, especially for large datasets and complex visualizations that would otherwise require thousands of SVG nodes in the DOM tree. At the same time, canvas rendering disallows CSS styling, so you will have to use built-in options for that, or create a custom plugin or chart type to render everything to your liking. ### Performance Chart.js is very well suited for large datasets. Such datasets can be efficiently ingested using the internal format, so you can skip data [parsing](./general/performance.md#parsing) and [normalization](./general/performance.md#data-normalization). Alternatively, [data decimation](./configuration/decimation.md) can be configured to sample the dataset and reduce its size before rendering. In the end, the canvas rendering that Chart.js uses reduces the toll on your DOM tree in comparison to SVG rendering. Also, tree-shaking support allows you to include minimal parts of Chart.js code in your bundle, reducing bundle size and page load time. ### Community Chart.js is [actively developed](https://github.com/chartjs/Chart.js/pulls?q=is%3Apr+is%3Aclosed) and maintained by the community. With minor [releases](https://github.com/chartjs/Chart.js/releases) on an approximately bi-monthly basis and major releases with breaking changes every couple of years, Chart.js keeps the balance between adding new features and making it a hassle to keep up with them. ================================================ FILE: docs/migration/v3-migration.md ================================================ # 3.x Migration Guide Chart.js 3.0 introduces a number of breaking changes. Chart.js 2.0 was released in April 2016. In the years since then, as Chart.js has grown in popularity and feature set, we've learned some lessons about how to better create a charting library. In order to improve performance, offer new features, and improve maintainability, it was necessary to break backwards compatibility, but we aimed to do so only when worth the benefit. Some major highlights of v3 include: * Large [performance](../general/performance.md) improvements including the ability to skip data parsing and render charts in parallel via webworkers * Additional configurability and scriptable options with better defaults * Completely rewritten animation system * Rewritten filler plugin with numerous bug fixes * Documentation migrated from GitBook to Vuepress * API documentation generated and verified by TypeDoc * No more CSS injection * Tons of bug fixes * Tree shaking ## End user migration ### Setup and installation * Distributed files are now in lower case. For example: `dist/chart.js`. * Chart.js is no longer providing the `Chart.bundle.js` and `Chart.bundle.min.js`. Please see the [installation](../getting-started/installation.md) and [integration](../getting-started/integration.md) docs for details on the recommended way to setup Chart.js if you were using these builds. * `moment` is no longer specified as an npm dependency. If you are using the `time` or `timeseries` scales, you must include one of [the available adapters](https://github.com/chartjs/awesome#adapters) and corresponding date library. You no longer need to exclude moment from your build. * The `Chart` constructor will throw an error if the canvas/context provided is already in use * Chart.js 3 is tree-shakeable. So if you are using it as an `npm` module in a project and want to make use of this feature, you need to import and register the controllers, elements, scales and plugins you want to use, for a list of all the available items to import see [integration](../getting-started/integration.md#bundlers-webpack-rollup-etc). You will not have to call `register` if importing Chart.js via a `script` tag or from the [`auto`](../getting-started/integration.md#bundlers-webpack-rollup-etc) register path as an `npm` module, in this case you will not get the tree shaking benefits. Here is an example of registering components: ```javascript import { Chart, LineController, LineElement, PointElement, LinearScale, Title } from `chart.js` Chart.register(LineController, LineElement, PointElement, LinearScale, Title); const chart = new Chart(ctx, { type: 'line', // data: ... options: { plugins: { title: { display: true, text: 'Chart Title' } }, scales: { x: { type: 'linear' }, y: { type: 'linear' } } } }) ``` ### Chart types * `horizontalBar` chart type was removed. Horizontal bar charts can be configured using the new [`indexAxis`](../charts/bar.md#horizontal-bar-chart) option ### Options A number of changes were made to the configuration options passed to the `Chart` constructor. Those changes are documented below. #### Generic changes * Indexable options are now looping. `backgroundColor: ['red', 'green']` will result in alternating `'red'` / `'green'` if there are more than 2 data points. * The input properties of object data can now be freely specified, see [data structures](../general/data-structures.md) for details. * Most options are resolved utilizing proxies, instead of merging with defaults. In addition to easily enabling different resolution routes for different contexts, it allows using other resolved options in scriptable options. * Options are by default scriptable and indexable, unless disabled for some reason. * Scriptable options receive a option resolver as second parameter for accessing other options in same context. * Resolution falls to upper scopes, if no match is found earlier. See [options](../general/options.md) for details. #### Specific changes * `elements.rectangle` is now `elements.bar` * `hover.animationDuration` is now configured in `animation.active.duration` * `responsiveAnimationDuration` is now configured in `animation.resize.duration` * Polar area `elements.arc.angle` is now configured in degrees instead of radians. * Polar area `startAngle` option is now consistent with `Radar`, 0 is at top and value is in degrees. Default is changed from `-½π` to `0`. * Doughnut `rotation` option is now in degrees and 0 is at top. Default is changed from `-½π` to `0`. * Doughnut `circumference` option is now in degrees. Default is changed from `2π` to `360`. * Doughnut `cutoutPercentage` was renamed to `cutout`and accepts pixels as number and percent as string ending with `%`. * `scale` option was removed in favor of `options.scales.r` (or any other scale id, with `axis: 'r'`) * `scales.[x/y]Axes` arrays were removed. Scales are now configured directly to `options.scales` object with the object key being the scale Id. * `scales.[x/y]Axes.barPercentage` was moved to dataset option `barPercentage` * `scales.[x/y]Axes.barThickness` was moved to dataset option `barThickness` * `scales.[x/y]Axes.categoryPercentage` was moved to dataset option `categoryPercentage` * `scales.[x/y]Axes.maxBarThickness` was moved to dataset option `maxBarThickness` * `scales.[x/y]Axes.minBarLength` was moved to dataset option `minBarLength` * `scales.[x/y]Axes.scaleLabel` was renamed to `scales[id].title` * `scales.[x/y]Axes.scaleLabel.labelString` was renamed to `scales[id].title.text` * `scales.[x/y]Axes.ticks.beginAtZero` was renamed to `scales[id].beginAtZero` * `scales.[x/y]Axes.ticks.max` was renamed to `scales[id].max` * `scales.[x/y]Axes.ticks.min` was renamed to `scales[id].min` * `scales.[x/y]Axes.ticks.reverse` was renamed to `scales[id].reverse` * `scales.[x/y]Axes.ticks.suggestedMax` was renamed to `scales[id].suggestedMax` * `scales.[x/y]Axes.ticks.suggestedMin` was renamed to `scales[id].suggestedMin` * `scales.[x/y]Axes.ticks.unitStepSize` was removed. Use `scales[id].ticks.stepSize` * `scales.[x/y]Axes.ticks.userCallback` was renamed to `scales[id].ticks.callback` * `scales.[x/y]Axes.time.format` was renamed to `scales[id].time.parser` * `scales.[x/y]Axes.time.max` was renamed to `scales[id].max` * `scales.[x/y]Axes.time.min` was renamed to `scales[id].min` * `scales.[x/y]Axes.zeroLine*` options of axes were removed. Use scriptable scale options instead. * The dataset option `steppedLine` was removed. Use `stepped` * The chart option `showLines` was renamed to `showLine` to match the dataset option. * The chart option `startAngle` was moved to `radial` scale options. * To override the platform class used in a chart instance, pass `platform: PlatformClass` in the config object. Note that the class should be passed, not an instance of the class. * `aspectRatio` defaults to 1 for doughnut, pie, polarArea, and radar charts * `TimeScale` does not read `t` from object data by default anymore. The default property is `x` or `y`, depending on the orientation. See [data structures](../general/data-structures.md) for details on how to change the default. * `tooltips` namespace was renamed to `tooltip` to match the plugin name * `legend`, `title` and `tooltip` namespaces were moved from `options` to `options.plugins`. * `tooltips.custom` was renamed to `plugins.tooltip.external` #### Defaults * `global` namespace was removed from `defaults`. So `Chart.defaults.global` is now `Chart.defaults` * Dataset controller defaults were relocate to `overrides`. For example `Chart.defaults.line` is now `Chart.overrides.line` * `default` prefix was removed from defaults. For example `Chart.defaults.global.defaultColor` is now `Chart.defaults.color` * `defaultColor` was split to `color`, `borderColor` and `backgroundColor` * `defaultFontColor` was renamed to `color` * `defaultFontFamily` was renamed to `font.family` * `defaultFontSize` was renamed to `font.size` * `defaultFontStyle` was renamed to `font.style` * `defaultLineHeight` was renamed to `font.lineHeight` * Horizontal Bar default tooltip mode was changed from `'index'` to `'nearest'` to match vertical bar charts * `legend`, `title` and `tooltip` namespaces were moved from `Chart.defaults` to `Chart.defaults.plugins`. * `elements.line.fill` default changed from `true` to `false`. * Line charts no longer override the default `interaction` mode. Default is changed from `'index'` to `'nearest'`. #### Scales The configuration options for scales is the largest change in v3. The `xAxes` and `yAxes` arrays were removed and axis options are individual scales now keyed by scale ID. The v2 configuration below is shown with it's new v3 configuration ```javascript options: { scales: { xAxes: [{ id: 'x', type: 'time', display: true, title: { display: true, text: 'Date' }, ticks: { major: { enabled: true }, font: function(context) { if (context.tick && context.tick.major) { return { weight: 'bold', color: '#FF0000' }; } } } }], yAxes: [{ id: 'y', display: true, title: { display: true, text: 'value' } }] } } ``` And now, in v3: ```javascript options: { scales: { x: { type: 'time', display: true, title: { display: true, text: 'Date' }, ticks: { major: { enabled: true }, color: (context) => context.tick && context.tick.major && '#FF0000', font: function(context) { if (context.tick && context.tick.major) { return { weight: 'bold' }; } } } }, y: { display: true, title: { display: true, text: 'value' } } } } ``` * The time scale option `distribution: 'series'` was removed and a new scale type `timeseries` was introduced in its place * In the time scale, `autoSkip` is now enabled by default for consistency with the other scales #### Animations Animation system was completely rewritten in Chart.js v3. Each property can now be animated separately. Please see [animations](../configuration/animations.md) docs for details. #### Customizability * `custom` attribute of elements was removed. Please use scriptable options * The `hover` property of scriptable options `context` object was renamed to `active` to align it with the datalabels plugin. #### Interactions * To allow DRY configuration, a root options scope for common interaction options was added. `options.hover` and `options.plugins.tooltip` now both extend from `options.interaction`. Defaults are defined at `defaults.interaction` level, so by default hover and tooltip interactions share the same mode etc. * `interactions` are now limited to the chart area + allowed overflow * `{mode: 'label'}` was replaced with `{mode: 'index'}` * `{mode: 'single'}` was replaced with `{mode: 'nearest', intersect: true}` * `modes['X-axis']` was replaced with `{mode: 'index', intersect: false}` * `options.onClick` is now limited to the chart area * `options.onClick` and `options.onHover` now receive the `chart` instance as a 3rd argument * `options.onHover` now receives a wrapped `event` as the first parameter. The previous first parameter value is accessible via `event.native`. * `options.hover.onHover` was removed, use `options.onHover`. #### Ticks * `options.gridLines` was renamed to `options.grid` * `options.gridLines.offsetGridLines` was renamed to `options.grid.offset`. * `options.gridLines.tickMarkLength` was renamed to `options.grid.tickLength`. * `options.ticks.fixedStepSize` is no longer used. Use `options.ticks.stepSize`. * `options.ticks.major` and `options.ticks.minor` were replaced with scriptable options for tick fonts. * `Chart.Ticks.formatters.linear` was renamed to `Chart.Ticks.formatters.numeric`. * `options.ticks.backdropPaddingX` and `options.ticks.backdropPaddingY` were replaced with `options.ticks.backdropPadding` in the radial linear scale. #### Tooltip * `xLabel` and `yLabel` were removed. Please use `label` and `formattedValue` * The `filter` option will now be passed additional parameters when called and should have the method signature `function(tooltipItem, index, tooltipItems, data)` * The `custom` callback now takes a context object that has `tooltip` and `chart` properties * All properties of tooltip model related to the tooltip options have been moved to reside within the `options` property. * The callbacks no longer are given a `data` parameter. The tooltip item parameter contains the chart and dataset instead * The tooltip item's `index` parameter was renamed to `dataIndex` and `value` was renamed to `formattedValue` * The `xPadding` and `yPadding` options were merged into a single `padding` object ## Developer migration While the end-user migration for Chart.js 3 is fairly straight-forward, the developer migration can be more complicated. Please reach out for help in the #dev [Discord](https://discord.gg/HxEguTK6av) channel if tips on migrating would be helpful. Some of the biggest things that have changed: * There is a completely rewritten and more performant animation system. * `Element._model` and `Element._view` are no longer used and properties are now set directly on the elements. You will have to use the method `getProps` to access these properties inside most methods such as `inXRange`/`inYRange` and `getCenterPoint`. Please take a look at [the Chart.js-provided elements](https://github.com/chartjs/Chart.js/tree/master/src/elements) for examples. * When building the elements in a controller, it's now suggested to call `updateElement` to provide the element properties. There are also methods such as `getSharedOptions` and `includeOptions` that have been added to skip redundant computation. Please take a look at [the Chart.js-provided controllers](https://github.com/chartjs/Chart.js/tree/master/src/controllers) for examples. * Scales introduced a new parsing API. This API takes user data and converts it into a more standard format. E.g. it allows users to provide numeric data as a `string` and converts it to a `number` where necessary. Previously this was done on the fly as charts were rendered. Now it's done up front with the ability to skip it for better performance if users provide data in the correct format. If you're using standard data format like `x`/`y` you may not need to do anything. If you're using a custom data format you will have to override some of the parse methods in `core.datasetController.js`. An example can be found in [chartjs-chart-financial](https://github.com/chartjs/chartjs-chart-financial), which uses an `{o, h, l, c}` data format. A few changes were made to controllers that are more straight-forward, but will affect all controllers: * Options: * `global` was removed from the defaults namespace as it was unnecessary and sometimes inconsistent * Dataset defaults are now under the chart type options instead of vice-versa. This was not able to be done when introduced in 2.x for backwards compatibility. Fixing it removes the biggest stumbling block that new chart developers encountered * Scale default options need to be updated as described in the end user migration section (e.g. `x` instead of `xAxes` and `y` instead of `yAxes`) * `updateElement` was changed to `updateElements` and has a new method signature as described below. This provides performance enhancements such as allowing easier reuse of computations that are common to all elements and reducing the number of function calls ### Removed The following properties and methods were removed: #### Removed from Chart * `Chart.animationService` * `Chart.active` * `Chart.borderWidth` * `Chart.chart.chart` * `Chart.Bar`. New charts are created via `new Chart` and providing the appropriate `type` parameter * `Chart.Bubble`. New charts are created via `new Chart` and providing the appropriate `type` parameter * `Chart.Chart` * `Chart.Controller` * `Chart.Doughnut`. New charts are created via `new Chart` and providing the appropriate `type` parameter * `Chart.innerRadius` now lives on doughnut, pie, and polarArea controllers * `Chart.lastActive` * `Chart.Legend` was moved to `Chart.plugins.legend._element` and made private * `Chart.Line`. New charts are created via `new Chart` and providing the appropriate `type` parameter * `Chart.LinearScaleBase` now must be imported and cannot be accessed off the `Chart` object * `Chart.offsetX` * `Chart.offsetY` * `Chart.outerRadius` now lives on doughnut, pie, and polarArea controllers * `Chart.plugins` was replaced with `Chart.registry`. Plugin defaults are now in `Chart.defaults.plugins[id]`. * `Chart.plugins.register` was replaced by `Chart.register`. * `Chart.PolarArea`. New charts are created via `new Chart` and providing the appropriate `type` parameter * `Chart.prototype.generateLegend` * `Chart.platform`. It only contained `disableCSSInjection`. CSS is never injected in v3. * `Chart.PluginBase` * `Chart.Radar`. New charts are created via `new Chart` and providing the appropriate `type` parameter * `Chart.radiusLength` * `Chart.scaleService` was replaced with `Chart.registry`. Scale defaults are now in `Chart.defaults.scales[type]`. * `Chart.Scatter`. New charts are created via `new Chart` and providing the appropriate `type` parameter * `Chart.types` * `Chart.Title` was moved to `Chart.plugins.title._element` and made private * `Chart.Tooltip` is now provided by the tooltip plugin. The positioners can be accessed from `tooltipPlugin.positioners` * `ILayoutItem.minSize` #### Removed from Dataset Controllers * `BarController.getDatasetMeta().bar` * `DatasetController.addElementAndReset` * `DatasetController.createMetaData` * `DatasetController.createMetaDataset` * `DoughnutController.getRingIndex` #### Removed from Elements * `Element.getArea` * `Element.height` * `Element.hidden` was replaced by chart level status, usable with `getDataVisibility(index)` / `toggleDataVisibility(index)` * `Element.initialize` * `Element.inLabelRange` * `Line.calculatePointY` #### Removed from Helpers * `helpers.addEvent` * `helpers.aliasPixel` * `helpers.arrayEquals` * `helpers.configMerge` * `helpers.findIndex` * `helpers.findNextWhere` * `helpers.findPreviousWhere` * `helpers.extend`. Use `Object.assign` instead * `helpers.getValueAtIndexOrDefault`. Use `helpers.resolve` instead. * `helpers.indexOf` * `helpers.lineTo` * `helpers.longestText` was made private * `helpers.max` * `helpers.measureText` was made private * `helpers.min` * `helpers.nextItem` * `helpers.niceNum` * `helpers.numberOfLabelLines` * `helpers.previousItem` * `helpers.removeEvent` * `helpers.roundedRect` * `helpers.scaleMerge` * `helpers.where` #### Removed from Layout * `Layout.defaults` #### Removed from Scales * `LinearScaleBase.handleDirectionalChanges` * `LogarithmicScale.minNotZero` * `Scale.getRightValue` * `Scale.longestLabelWidth` * `Scale.longestTextCache` is now private * `Scale.margins` is now private * `Scale.mergeTicksOptions` * `Scale.ticksAsNumbers` * `Scale.tickValues` is now private * `TimeScale.getLabelCapacity` is now private * `TimeScale.tickFormatFunction` is now private #### Removed from Plugins (Legend, Title, and Tooltip) * `IPlugin.afterScaleUpdate`. Use `afterLayout` instead * `Legend.margins` is now private * Legend `onClick`, `onHover`, and `onLeave` options now receive the legend as the 3rd argument in addition to implicitly via `this` * Legend `onClick`, `onHover`, and `onLeave` options now receive a wrapped `event` as the first parameter. The previous first parameter value is accessible via `event.native`. * `Title.margins` is now private * The tooltip item's `x` and `y` attributes were replaced by `element`. You can use `element.x` and `element.y` or `element.tooltipPosition()` instead. #### Removal of Public APIs The following public APIs were removed. * `getElementAtEvent` is replaced with `chart.getElementsAtEventForMode(e, 'nearest', { intersect: true }, false)` * `getElementsAtEvent` is replaced with `chart.getElementsAtEventForMode(e, 'index', { intersect: true }, false)` * `getElementsAtXAxis` is replaced with `chart.getElementsAtEventForMode(e, 'index', { intersect: false }, false)` * `getDatasetAtEvent` is replaced with `chart.getElementsAtEventForMode(e, 'dataset', { intersect: true }, false)` #### Removal of private APIs The following private APIs were removed. * `Chart._bufferedRender` * `Chart._updating` * `Chart.data.datasets[datasetIndex]._meta` * `DatasetController._getIndexScaleId` * `DatasetController._getIndexScale` * `DatasetController._getValueScaleId` * `DatasetController._getValueScale` * `Element._ctx` * `Element._model` * `Element._view` * `LogarithmicScale._valueOffset` * `TimeScale.getPixelForOffset` * `TimeScale.getLabelWidth` * `Tooltip._lastActive` ### Renamed The following properties were renamed during v3 development: * `Chart.Animation.animationObject` was renamed to `Chart.Animation` * `Chart.Animation.chartInstance` was renamed to `Chart.Animation.chart` * `Chart.canvasHelpers` was merged with `Chart.helpers` * `Chart.elements.Arc` was renamed to `Chart.elements.ArcElement` * `Chart.elements.Line` was renamed to `Chart.elements.LineElement` * `Chart.elements.Point` was renamed to `Chart.elements.PointElement` * `Chart.elements.Rectangle` was renamed to `Chart.elements.BarElement` * `Chart.layoutService` was renamed to `Chart.layouts` * `Chart.pluginService` was renamed to `Chart.plugins` * `helpers.callCallback` was renamed to `helpers.callback` * `helpers.drawRoundedRectangle` was renamed to `helpers.roundedRect` * `helpers.getValueOrDefault` was renamed to `helpers.valueOrDefault` * `LayoutItem.fullWidth` was renamed to `LayoutItem.fullSize` * `Point.controlPointPreviousX` was renamed to `Point.cp1x` * `Point.controlPointPreviousY` was renamed to `Point.cp1y` * `Point.controlPointNextX` was renamed to `Point.cp2x` * `Point.controlPointNextY` was renamed to `Point.cp2y` * `Scale.calculateTickRotation` was renamed to `Scale.calculateLabelRotation` * `Tooltip.options.legendColorBackgroupd` was renamed to `Tooltip.options.multiKeyBackground` #### Renamed private APIs The private APIs listed below were renamed: * `BarController.calculateBarIndexPixels` was renamed to `BarController._calculateBarIndexPixels` * `BarController.calculateBarValuePixels` was renamed to `BarController._calculateBarValuePixels` * `BarController.getStackCount` was renamed to `BarController._getStackCount` * `BarController.getStackIndex` was renamed to `BarController._getStackIndex` * `BarController.getRuler` was renamed to `BarController._getRuler` * `Chart.destroyDatasetMeta` was renamed to `Chart._destroyDatasetMeta` * `Chart.drawDataset` was renamed to `Chart._drawDataset` * `Chart.drawDatasets` was renamed to `Chart._drawDatasets` * `Chart.eventHandler` was renamed to `Chart._eventHandler` * `Chart.handleEvent` was renamed to `Chart._handleEvent` * `Chart.initialize` was renamed to `Chart._initialize` * `Chart.resetElements` was renamed to `Chart._resetElements` * `Chart.unbindEvents` was renamed to `Chart._unbindEvents` * `Chart.updateDataset` was renamed to `Chart._updateDataset` * `Chart.updateDatasets` was renamed to `Chart._updateDatasets` * `Chart.updateLayout` was renamed to `Chart._updateLayout` * `DatasetController.destroy` was renamed to `DatasetController._destroy` * `DatasetController.insertElements` was renamed to `DatasetController._insertElements` * `DatasetController.onDataPop` was renamed to `DatasetController._onDataPop` * `DatasetController.onDataPush` was renamed to `DatasetController._onDataPush` * `DatasetController.onDataShift` was renamed to `DatasetController._onDataShift` * `DatasetController.onDataSplice` was renamed to `DatasetController._onDataSplice` * `DatasetController.onDataUnshift` was renamed to `DatasetController._onDataUnshift` * `DatasetController.removeElements` was renamed to `DatasetController._removeElements` * `DatasetController.resyncElements` was renamed to `DatasetController._resyncElements` * `LayoutItem.isFullWidth` was renamed to `LayoutItem.isFullSize` * `RadialLinearScale.setReductions` was renamed to `RadialLinearScale._setReductions` * `RadialLinearScale.pointLabels` was renamed to `RadialLinearScale._pointLabels` * `Scale.handleMargins` was renamed to `Scale._handleMargins` ### Changed The APIs listed in this section have changed in signature or behaviour from version 2. #### Changed in Scales * `Scale.getLabelForIndex` was replaced by `scale.getLabelForValue` * `Scale.getPixelForValue` now only requires one parameter. For the `TimeScale` that parameter must be millis since the epoch. As a performance optimization, it may take an optional second parameter, giving the index of the data point. ##### Changed in Ticks * `Scale.afterBuildTicks` now has no parameters like the other callbacks * `Scale.buildTicks` is now expected to return tick objects * `Scale.convertTicksToLabels` was renamed to `generateTickLabels`. It is now expected to set the label property on the ticks given as input * `Scale.ticks` now contains objects instead of strings * When the `autoSkip` option is enabled, `Scale.ticks` now contains only the non-skipped ticks instead of all ticks. * Ticks are now always generated in monotonically increasing order ##### Changed in Time Scale * `getValueForPixel` now returns milliseconds since the epoch #### Changed in Controllers ##### Core Controller * The first parameter to `updateHoverStyle` is now an array of objects containing the `element`, `datasetIndex`, and `index` * The signature or `resize` changed, the first `silent` parameter was removed. ##### Dataset Controllers * `updateElement` was replaced with `updateElements` now taking the elements to update, the `start` index, `count`, and `mode` * `setHoverStyle` and `removeHoverStyle` now additionally take the `datasetIndex` and `index` #### Changed in Interactions * Interaction mode methods now return an array of objects containing the `element`, `datasetIndex`, and `index` #### Changed in Layout * `ILayoutItem.update` no longer has a return value #### Changed in Helpers All helpers are now exposed in a flat hierarchy, e.g., `Chart.helpers.canvas.clipArea` -> `Chart.helpers.clipArea` ##### Canvas Helper * The second parameter to `drawPoint` is now the full options object, so `style`, `rotation`, and `radius` are no longer passed explicitly * `helpers.getMaximumHeight` was replaced by `helpers.dom.getMaximumSize` * `helpers.getMaximumWidth` was replaced by `helpers.dom.getMaximumSize` * `helpers.clear` was renamed to `helpers.clearCanvas` and now takes `canvas` and optionally `ctx` as parameter(s). * `helpers.retinaScale` accepts optional third parameter `forceStyle`, which forces overriding current canvas style. `forceRatio` no longer falls back to `window.devicePixelRatio`, instead it defaults to `1`. #### Changed in Platform * `Chart.platform` is no longer the platform object used by charts. Every chart instance now has a separate platform instance. * `Chart.platforms` is an object that contains two usable platform classes, `BasicPlatform` and `DomPlatform`. It also contains `BasePlatform`, a class that all platforms must extend from. * If the canvas passed in is an instance of `OffscreenCanvas`, the `BasicPlatform` is automatically used. * `isAttached` method was added to platform. #### Changed in IPlugin interface * All plugin hooks have unified signature with 3 arguments: `chart`, `args` and `options`. This means change in signature for these hooks: `beforeInit`, `afterInit`, `reset`, `beforeLayout`, `afterLayout`, `beforeRender`, `afterRender`, `beforeDraw`, `afterDraw`, `beforeDatasetsDraw`, `afterDatasetsDraw`, `beforeEvent`, `afterEvent`, `resize`, `destroy`. * `afterDatasetsUpdate`, `afterUpdate`, `beforeDatasetsUpdate`, and `beforeUpdate` now receive `args` object as second argument. `options` argument is always the last and thus was moved from 2nd to 3rd place. * `afterEvent` and `beforeEvent` now receive a wrapped `event` as the `event` property of the second argument. The native event is available via `args.event.native`. * Initial `resize` is no longer silent. Meaning that `resize` event can fire between `beforeInit` and `afterInit` * New hooks: `install`, `start`, `stop`, and `uninstall` * `afterEvent` should notify about changes that need a render by setting `args.changed` to true. Because the `args` are shared with all plugins, it should only be set to true and not false. ================================================ FILE: docs/migration/v4-migration.md ================================================ # 4.x Migration Guide Chart.js 4.0 introduces a number of breaking changes. We tried keeping the amount of breaking changes to a minimum. For some features and bug fixes it was necessary to break backwards compatibility, but we aimed to do so only when worth the benefit. ## End user migration ### Charts * Charts don't override the default tooltip callbacks, so all chart types have the same-looking tooltips. * Default scale override has been removed if the configured scale starts with `x`/`y`. Defining `xAxes` in your config will now create a second scale instead of overriding the default `x` axis. ### Options A number of changes were made to the configuration options passed to the `Chart` constructor. Those changes are documented below. #### Specific changes * The radialLinear grid indexable and scriptable options don't decrease the index of the specified grid line anymore. * The `destroy` plugin hook has been removed and replaced with `afterDestroy`. * Ticks callback on time scale now receives timestamp instead of a formatted label. * `scales[id].grid.drawBorder` has been renamed to `scales[id].border.display`. * `scales[id].grid.borderWidth` has been renamed to `scales[id].border.width`. * `scales[id].grid.borderColor` has been renamed to `scales[id].border.color`. * `scales[id].grid.borderDash` has been renamed to `scales[id].border.dash`. * `scales[id].grid.borderDashOffset` has been renamed to `scales[id].border.dashOffset`. * The z index for the border of a scale is now configurable instead of being 1 higher as the grid z index. * Linear scales now add and subtracts `5%` of the max value to the range if the min and max are the same instead of `1`. * If the tooltip callback returns `undefined`, then the default callback will be used. * `maintainAspectRatio` respects container height. * Time and timeseries scales use `ticks.stepSize` instead of `time.stepSize`, which has been removed. * `maxTickslimit` won't be used for the ticks in `autoSkip` if the determined max ticks is less then the `maxTicksLimit`. * `dist/chart.js` has been removed. * `dist/chart.min.js` has been renamed to `dist/chart.umd.min.js` (and before 4.5.0 `dist/chart.umd.js`). * `dist/chart.esm.js` has been renamed to `dist/chart.js`. #### Type changes * The order of the `ChartMeta` parameters have been changed from `` to ``. ### General * Chart.js becomes an [ESM-only package](https://nodejs.org/api/esm.html) ([the UMD bundle is still available](../getting-started/installation.md#cdn)). To use Chart.js, your project should also be an ES module. Make sure to have this in your `package.json`: ```json { "type": "module" } ``` If you are experiencing problems with [Jest](https://jestjs.io), follow its [documentation](https://jestjs.io/docs/ecmascript-modules) to enable the ESM support. Or, we can recommend you migrating to [Vitest](https://vitest.dev/). Vitest has the ESM support out of the box and [almost the same API as Jest](https://vitest.dev/guide/migration.html#migrating-from-jest). See an [example of migration](https://github.com/reactchartjs/react-chartjs-2/commit/7f3ec96101d21e43cae8cbfe5e09a46a17cff1ef). * Removed fallback to `fontColor` for the legend text and strikethrough color. * Removed `config._chart` fallback for `this.chart` in the filler plugin. * Removed `this._chart` in the filler plugin. ================================================ FILE: docs/package.json ================================================ { "name": "docs", "private": "true", "version": "4.0.0-dev", "license": "MIT", "type": "module", "scripts": { "build": "vuepress build --no-cache", "dev": "vuepress dev --no-cache" }, "devDependencies": { "@simonbrunel/vuepress-plugin-versions": "^0.2.0", "@vuepress/plugin-google-analytics": "^1.9.7", "@vuepress/plugin-html-redirect": "^0.1.2", "markdown-it": "^12.3.2", "markdown-it-include": "^2.0.0", "typedoc": "^0.23.10", "typedoc-plugin-markdown": "^3.13.4", "typescript": "^4.7.4", "vue": "^2.6.14", "vue-tabs-component": "^1.5.0", "vuepress": "^1.9.7", "vuepress-plugin-code-copy": "^1.0.6", "vuepress-plugin-flexsearch": "^0.3.0", "vuepress-plugin-redirect": "^1.2.5", "vuepress-plugin-tabs": "^0.3.0", "vuepress-plugin-typedoc": "^0.11.0", "vuepress-theme-chartjs": "^0.2.0", "webpack": "^4.46.0" } } ================================================ FILE: docs/samples/.eslintrc.yml ================================================ rules: no-console: "off" ================================================ FILE: docs/samples/advanced/data-decimation.md ================================================ # Data Decimation This example shows how to use the built-in data decimation to reduce the number of points drawn on the graph for improved performance. ```js chart-editor // const actions = [ { name: 'No decimation (default)', handler(chart) { chart.options.plugins.decimation.enabled = false; chart.update(); } }, { name: 'min-max decimation', handler(chart) { chart.options.plugins.decimation.algorithm = 'min-max'; chart.options.plugins.decimation.enabled = true; chart.update(); }, }, { name: 'LTTB decimation (50 samples)', handler(chart) { chart.options.plugins.decimation.algorithm = 'lttb'; chart.options.plugins.decimation.enabled = true; chart.options.plugins.decimation.samples = 50; chart.update(); } }, { name: 'LTTB decimation (500 samples)', handler(chart) { chart.options.plugins.decimation.algorithm = 'lttb'; chart.options.plugins.decimation.enabled = true; chart.options.plugins.decimation.samples = 500; chart.update(); } } ]; // // const NUM_POINTS = 100000; Utils.srand(10); // parseISODate returns a luxon date object to work with in the samples // We will create points every 30s starting from this point in time const start = Utils.parseISODate('2021-04-01T00:00:00Z').toMillis(); const pointData = []; for (let i = 0; i < NUM_POINTS; ++i) { // Most data will be in the range [0, 20) but some rare data will be in the range [0, 100) const max = Math.random() < 0.001 ? 100 : 20; pointData.push({x: start + (i * 30000), y: Utils.rand(0, max)}); } const data = { datasets: [{ borderColor: Utils.CHART_COLORS.red, borderWidth: 1, data: pointData, label: 'Large Dataset', radius: 0, }] }; // // const decimation = { enabled: false, algorithm: 'min-max', }; // // const config = { type: 'line', data: data, options: { // Turn off animations and data parsing for performance animation: false, parsing: false, interaction: { mode: 'nearest', axis: 'x', intersect: false }, plugins: { decimation: decimation, }, scales: { x: { type: 'time', ticks: { source: 'auto', // Disabled rotation for performance maxRotation: 0, autoSkip: true, } } } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Data Decimation](../../configuration/decimation.md) * [Line](../../charts/line.md) * [Time Scale](../../axes/cartesian/time.md) ================================================ FILE: docs/samples/advanced/derived-axis-type.md ================================================ # Derived Axis Type ```js chart-editor // const DATA_COUNT = 12; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 1000}; const labels = Utils.months({count: DATA_COUNT}); const data = { labels: labels, datasets: [ { label: 'My First dataset', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), fill: false, } ], }; // // const config = { type: 'line', data, options: { responsive: true, scales: { x: { display: true, }, y: { display: true, type: 'log2', } } } }; // module.exports = { actions: [], config: config, }; ``` ## Log2 axis implementation <<< @/scripts/log2.js ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [New Axes](../../developers/axes.md) ================================================ FILE: docs/samples/advanced/derived-chart-type.md ================================================ # Derived Chart Type ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100, rmin: 1, rmax: 20}; const data = { datasets: [ { label: 'My First dataset', backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), borderColor: Utils.CHART_COLORS.blue, borderWidth: 1, boxStrokeStyle: 'red', data: Utils.bubbles(NUMBER_CFG) } ], }; // // const config = { type: 'derivedBubble', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Derived Chart Type' }, } } }; // module.exports = { actions: [], config: config, }; ``` ## DerivedBubble Implementation <<< @/scripts/derived-bubble.js ## Docs * [Bubble Chart](../../charts/bubble.md) * [New Charts](../../developers/charts.md) ================================================ FILE: docs/samples/advanced/linear-gradient.md ================================================ # Linear Gradient ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(-100, 100)); } chart.update(); } } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // let width, height, gradient; function getGradient(ctx, chartArea) { const chartWidth = chartArea.right - chartArea.left; const chartHeight = chartArea.bottom - chartArea.top; if (!gradient || width !== chartWidth || height !== chartHeight) { // Create the gradient because this is either the first render // or the size of the chart has changed width = chartWidth; height = chartHeight; gradient = ctx.createLinearGradient(0, chartArea.bottom, 0, chartArea.top); gradient.addColorStop(0, Utils.CHART_COLORS.blue); gradient.addColorStop(0.5, Utils.CHART_COLORS.yellow); gradient.addColorStop(1, Utils.CHART_COLORS.red); } return gradient; } // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: function(context) { const chart = context.chart; const {ctx, chartArea} = chart; if (!chartArea) { // This case happens on initial chart load return; } return getGradient(ctx, chartArea); }, }, ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Colors](../../general/colors.md) * [Patterns and Gradients](../../general/colors.md#patterns-and-gradients) * [Data structures (`labels`)](../../general/data-structures.md) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) * [Line](../../charts/line.md) ================================================ FILE: docs/samples/advanced/programmatic-events.md ================================================ # Programmatic Event Triggers ```js chart-editor // function triggerHover(chart) { if (chart.getActiveElements().length > 0) { chart.setActiveElements([]); } else { chart.setActiveElements([ { datasetIndex: 0, index: 0, }, { datasetIndex: 1, index: 0, } ]); } chart.update(); } // // function triggerTooltip(chart) { const tooltip = chart.tooltip; if (tooltip.getActiveElements().length > 0) { tooltip.setActiveElements([], {x: 0, y: 0}); } else { const chartArea = chart.chartArea; tooltip.setActiveElements([ { datasetIndex: 0, index: 2, }, { datasetIndex: 1, index: 2, } ], { x: (chartArea.left + chartArea.right) / 2, y: (chartArea.top + chartArea.bottom) / 2, }); } chart.update(); } // // const actions = [ { name: 'Trigger Hover', handler: triggerHover }, { name: 'Trigger Tooltip', handler: triggerTooltip } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), hoverBorderWidth: 5, hoverBorderColor: 'green', }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), hoverBorderWidth: 5, hoverBorderColor: 'green', } ] }; // // const config = { type: 'bar', data: data, options: { }, }; // module.exports = { actions: actions, config: config, }; ``` ## API * [Chart](../../api/classes/Chart.md) * [`setActiveElements`](../../api/classes/Chart.md#setactiveelements) * [TooltipModel](../../api/interfaces/TooltipModel.md) * [`setActiveElements`](../../api/interfaces/TooltipModel.md#setactiveelements) ## Docs * [Bar](../../charts/bar.md) * [Interactions (`hoverBorderColor`)](../../charts/bar.md#interactions) * [Interactions](../../configuration/interactions.md) * [Tooltip](../../configuration/tooltip.md) ================================================ FILE: docs/samples/advanced/progress-bar.md ================================================ # Animation Progress Bar ## Initial animation ## Other animations ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const dsColor = Utils.namedColor(chart.data.datasets.length); const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(-100, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const initProgress = document.getElementById('initialProgress'); const progress = document.getElementById('animationProgress'); const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // const config = { type: 'line', data: data, options: { animation: { duration: 2000, onProgress: function(context) { if (context.initial) { initProgress.value = context.currentStep / context.numSteps; } else { progress.value = context.currentStep / context.numSteps; } }, onComplete: function(context) { if (context.initial) { console.log('Initial animation finished'); } else { console.log('animation finished'); } } }, interaction: { mode: 'nearest', axis: 'x', intersect: false }, plugins: { title: { display: true, text: 'Chart.js Line Chart - Animation Progress Bar' } }, }, }; // module.exports = { actions: actions, config: config, output: 'console.log output is displayed here' }; ``` ## Docs * [Animations](../../configuration/animations.md) * [Animation Callbacks](../../configuration/animations.md#animation-callbacks) * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) ================================================ FILE: docs/samples/advanced/radial-gradient.md ================================================ # Radial Gradient ```js chart-editor // const DATA_COUNT = 5; Utils.srand(110); const chartColors = Utils.CHART_COLORS; const colors = [chartColors.red, chartColors.orange, chartColors.yellow, chartColors.green, chartColors.blue]; const cache = new Map(); let width = null; let height = null; const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = generateData(); }); chart.update(); } }, ]; // // function createRadialGradient3(context, c1, c2, c3) { const chartArea = context.chart.chartArea; if (!chartArea) { // This case happens on initial chart load return; } const chartWidth = chartArea.right - chartArea.left; const chartHeight = chartArea.bottom - chartArea.top; if (width !== chartWidth || height !== chartHeight) { cache.clear(); } let gradient = cache.get(c1 + c2 + c3); if (!gradient) { // Create the gradient because this is either the first render // or the size of the chart has changed width = chartWidth; height = chartHeight; const centerX = (chartArea.left + chartArea.right) / 2; const centerY = (chartArea.top + chartArea.bottom) / 2; const r = Math.min( (chartArea.right - chartArea.left) / 2, (chartArea.bottom - chartArea.top) / 2 ); const ctx = context.chart.ctx; gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, r); gradient.addColorStop(0, c1); gradient.addColorStop(0.5, c2); gradient.addColorStop(1, c3); cache.set(c1 + c2 + c3, gradient); } return gradient; } // // function generateData() { return Utils.numbers({ count: DATA_COUNT, min: 0, max: 100 }); } const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [{ data: generateData() }] }; // // const config = { type: 'polarArea', data: data, options: { plugins: { legend: false, tooltip: false, }, elements: { arc: { backgroundColor: function(context) { let c = colors[context.dataIndex]; if (!c) { return; } if (context.active) { c = helpers.getHoverColor(c); } const mid = helpers.color(c).desaturate(0.2).darken(0.2).rgbString(); const start = helpers.color(c).lighten(0.2).rotate(270).rgbString(); const end = helpers.color(c).lighten(0.1).rgbString(); return createRadialGradient3(context, start, mid, end); }, } } } }; // module.exports = { actions, config, }; ``` ## Docs * [Polar Area Chart](../../charts/polar.md) * [Styling](../../charts/polar.md#styling) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) ================================================ FILE: docs/samples/animations/delay.md ================================================ # Delay ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), backgroundColor: Utils.CHART_COLORS.red, }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), backgroundColor: Utils.CHART_COLORS.blue, }, { label: 'Dataset 3', data: Utils.numbers(NUMBER_CFG), backgroundColor: Utils.CHART_COLORS.green, }, ] }; // // let delayed; const config = { type: 'bar', data: data, options: { animation: { onComplete: () => { delayed = true; }, delay: (context) => { let delay = 0; if (context.type === 'data' && context.mode === 'default' && !delayed) { delay = context.dataIndex * 300 + context.datasetIndex * 100; } return delay; }, }, scales: { x: { stacked: true, }, y: { stacked: true } } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Animations](../../configuration/animations.md) * [animation (`delay`)](../../configuration/animations.md#animation) * [Animation Callbacks](../../configuration/animations.md#animation-callbacks) * [Bar](../../charts/bar.md) * [Stacked Bar Chart](../../charts/bar.md#stacked-bar-chart) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) ================================================ FILE: docs/samples/animations/drop.md ================================================ # Drop ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const dsColor = Utils.namedColor(chart.data.datasets.length); const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(-100, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', animations: { y: { duration: 2000, delay: 500 } }, data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), fill: 1, tension: 0.5 }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // const config = { type: 'line', data: data, options: { animations: { y: { easing: 'easeInOutElastic', from: (ctx) => { if (ctx.type === 'data') { if (ctx.mode === 'default' && !ctx.dropped) { ctx.dropped = true; return 0; } } } } }, }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Area](../../charts/area.md) * [Animations](../../configuration/animations.md) * [animation (`easing`)](../../configuration/animations.md#animation) * [animations (`from`)](../../configuration/animations.md#animations-2) * [Line](../../charts/line.md) * [Line Styling](../../charts/line.md#line-styling) * `fill` * `tension` * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) ================================================ FILE: docs/samples/animations/loop.md ================================================ # Loop ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const dsColor = Utils.namedColor(chart.data.datasets.length); const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(-100, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: DATA_COUNT}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), tension: 0.4, }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), tension: 0.2, } ] }; // // const config = { type: 'line', data: data, options: { animations: { radius: { duration: 400, easing: 'linear', loop: (context) => context.active } }, hoverRadius: 12, hoverBackgroundColor: 'yellow', interaction: { mode: 'nearest', intersect: false, axis: 'x' }, plugins: { tooltip: { enabled: false } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Animations](../../configuration/animations.md) * [animation](../../configuration/animations.md#animation) * `duration` * `easing` * **`loop`** * [Default animations (`radius`)](../../configuration/animations.md#default-animations) * [Data structures (`labels`)](../../general/data-structures.md) * [Elements](../../configuration/elements.md) * [Point Configuration](../../configuration/elements.md#point-configuration) * `hoverRadius` * `hoverBackgroundColor` * [Line](../../charts/line.md) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) * [Tooltip (`enabled`)](../../configuration/tooltip.md) ================================================ FILE: docs/samples/animations/progressive-line-easing.md ================================================ # Progressive Line With Easing ```js chart-editor // const data = []; const data2 = []; let prev = 100; let prev2 = 80; for (let i = 0; i < 1000; i++) { prev += 5 - Math.random() * 10; data.push({x: i, y: prev}); prev2 += 5 - Math.random() * 10; data2.push({x: i, y: prev2}); } // // let easing = helpers.easingEffects.easeOutQuad; let restart = false; const totalDuration = 5000; const duration = (ctx) => easing(ctx.index / data.length) * totalDuration / data.length; const delay = (ctx) => easing(ctx.index / data.length) * totalDuration; const previousY = (ctx) => ctx.index === 0 ? ctx.chart.scales.y.getPixelForValue(100) : ctx.chart.getDatasetMeta(ctx.datasetIndex).data[ctx.index - 1].getProps(['y'], true).y; const animation = { x: { type: 'number', easing: 'linear', duration: duration, from: NaN, // the point is initially skipped delay(ctx) { if (ctx.type !== 'data' || ctx.xStarted) { return 0; } ctx.xStarted = true; return delay(ctx); } }, y: { type: 'number', easing: 'linear', duration: duration, from: previousY, delay(ctx) { if (ctx.type !== 'data' || ctx.yStarted) { return 0; } ctx.yStarted = true; return delay(ctx); } } }; // // const config = { type: 'line', data: { datasets: [{ borderColor: Utils.CHART_COLORS.red, borderWidth: 1, radius: 0, data: data, }, { borderColor: Utils.CHART_COLORS.blue, borderWidth: 1, radius: 0, data: data2, }] }, options: { animation, interaction: { intersect: false }, plugins: { legend: false, title: { display: true, text: () => easing.name } }, scales: { x: { type: 'linear' } } } }; // // function restartAnims(chart) { chart.stop(); const meta0 = chart.getDatasetMeta(0); const meta1 = chart.getDatasetMeta(1); for (let i = 0; i < data.length; i++) { const ctx0 = meta0.controller.getContext(i); const ctx1 = meta1.controller.getContext(i); ctx0.xStarted = ctx0.yStarted = false; ctx1.xStarted = ctx1.yStarted = false; } chart.update(); } const actions = [ { name: 'easeOutQuad', handler(chart) { easing = helpers.easingEffects.easeOutQuad; restartAnims(chart); } }, { name: 'easeOutCubic', handler(chart) { easing = helpers.easingEffects.easeOutCubic; restartAnims(chart); } }, { name: 'easeOutQuart', handler(chart) { easing = helpers.easingEffects.easeOutQuart; restartAnims(chart); } }, { name: 'easeOutQuint', handler(chart) { easing = helpers.easingEffects.easeOutQuint; restartAnims(chart); } }, { name: 'easeInQuad', handler(chart) { easing = helpers.easingEffects.easeInQuad; restartAnims(chart); } }, { name: 'easeInCubic', handler(chart) { easing = helpers.easingEffects.easeInCubic; restartAnims(chart); } }, { name: 'easeInQuart', handler(chart) { easing = helpers.easingEffects.easeInQuart; restartAnims(chart); } }, { name: 'easeInQuint', handler(chart) { easing = helpers.easingEffects.easeInQuint; restartAnims(chart); } }, ]; // module.exports = { config, actions }; ``` ## Api * [Chart](../../api/classes/Chart.md) * [`getDatasetMeta`](../../api/classes/Chart.md#getdatasetmeta) * [Scale](../../api/classes/Scale.md) * [`getPixelForValue`](../../api/classes/Scale.md#getpixelforvalue) ## Docs * [Animations](../../configuration/animations.md) * [animation](../../configuration/animations.md#animation) * `delay` * `duration` * `easing` * `loop` * [Easing](../../configuration/animations.md#easing) * [Line](../../charts/line.md) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) * [Data Context](../../general/options.md#data) ================================================ FILE: docs/samples/animations/progressive-line.md ================================================ # Progressive Line ```js chart-editor // const data = []; const data2 = []; let prev = 100; let prev2 = 80; for (let i = 0; i < 1000; i++) { prev += 5 - Math.random() * 10; data.push({x: i, y: prev}); prev2 += 5 - Math.random() * 10; data2.push({x: i, y: prev2}); } // // const totalDuration = 10000; const delayBetweenPoints = totalDuration / data.length; const previousY = (ctx) => ctx.index === 0 ? ctx.chart.scales.y.getPixelForValue(100) : ctx.chart.getDatasetMeta(ctx.datasetIndex).data[ctx.index - 1].getProps(['y'], true).y; const animation = { x: { type: 'number', easing: 'linear', duration: delayBetweenPoints, from: NaN, // the point is initially skipped delay(ctx) { if (ctx.type !== 'data' || ctx.xStarted) { return 0; } ctx.xStarted = true; return ctx.index * delayBetweenPoints; } }, y: { type: 'number', easing: 'linear', duration: delayBetweenPoints, from: previousY, delay(ctx) { if (ctx.type !== 'data' || ctx.yStarted) { return 0; } ctx.yStarted = true; return ctx.index * delayBetweenPoints; } } }; // // const config = { type: 'line', data: { datasets: [{ borderColor: Utils.CHART_COLORS.red, borderWidth: 1, radius: 0, data: data, }, { borderColor: Utils.CHART_COLORS.blue, borderWidth: 1, radius: 0, data: data2, }] }, options: { animation, interaction: { intersect: false }, plugins: { legend: false }, scales: { x: { type: 'linear' } } } }; // module.exports = { config }; ``` ## Api * [Chart](../../api/classes/Chart.md) * [`getDatasetMeta`](../../api/classes/Chart.md#getdatasetmeta) * [Scale](../../api/classes/Scale.md) * [`getPixelForValue`](../../api/classes/Scale.md#getpixelforvalue) ## Docs * [Animations](../../configuration/animations.md) * [animation](../../configuration/animations.md#animation) * `delay` * `duration` * `easing` * `loop` * [Line](../../charts/line.md) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) * [Data Context](../../general/options.md#data) ================================================ FILE: docs/samples/area/line-boundaries.md ================================================ # Line Chart Boundaries ```js chart-editor // const inputs = { min: -100, max: 100, count: 8, decimals: 2, continuity: 1 }; const generateLabels = () => { return Utils.months({count: inputs.count}); }; const generateData = () => (Utils.numbers(inputs)); // // const data = { labels: generateLabels(), datasets: [ { label: 'Dataset', data: generateData(), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red), fill: false } ] }; // // let smooth = false; const actions = [ { name: 'Fill: false (default)', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.fill = false; }); chart.update(); } }, { name: 'Fill: origin', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.fill = 'origin'; }); chart.update(); } }, { name: 'Fill: start', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.fill = 'start'; }); chart.update(); } }, { name: 'Fill: end', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.fill = 'end'; }); chart.update(); } }, { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = generateData(); }); chart.update(); } }, { name: 'Smooth', handler(chart) { smooth = !smooth; chart.options.elements.line.tension = smooth ? 0.4 : 0; chart.update(); } } ]; // // const config = { type: 'line', data: data, options: { plugins: { filler: { propagate: false, }, title: { display: true, text: (ctx) => 'Fill: ' + ctx.chart.data.datasets[0].fill } }, interaction: { intersect: false, } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Area](../../charts/area.md) * [Filling modes](../../charts/area.md#filling-modes) * Boundary: `'start'`, `'end'`, `'origin'` * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) ================================================ FILE: docs/samples/area/line-datasets.md ================================================ # Line Chart Datasets ```js chart-editor // const inputs = { min: 20, max: 80, count: 8, decimals: 2, continuity: 1 }; const generateLabels = () => { return Utils.months({count: inputs.count}); }; const generateData = () => (Utils.numbers(inputs)); Utils.srand(42); // // const data = { labels: generateLabels(), datasets: [ { label: 'D0', data: generateData(), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red), hidden: true }, { label: 'D1', data: generateData(), borderColor: Utils.CHART_COLORS.orange, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange), fill: '-1' }, { label: 'D2', data: generateData(), borderColor: Utils.CHART_COLORS.yellow, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.yellow), hidden: true, fill: 1 }, { label: 'D3', data: generateData(), borderColor: Utils.CHART_COLORS.green, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green), fill: '-1' }, { label: 'D4', data: generateData(), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue), fill: '-1' }, { label: 'D5', data: generateData(), borderColor: Utils.CHART_COLORS.grey, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.grey), fill: '+2' }, { label: 'D6', data: generateData(), borderColor: Utils.CHART_COLORS.purple, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.purple), fill: false }, { label: 'D7', data: generateData(), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red), fill: 8 }, { label: 'D8', data: generateData(), borderColor: Utils.CHART_COLORS.orange, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange), fill: 'end', hidden: true }, { label: 'D9', data: generateData(), borderColor: Utils.CHART_COLORS.yellow, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.yellow), fill: {above: 'blue', below: 'red', target: {value: 350}} } ] }; // // let smooth = false; let propagate = false; const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = generateData(); }); chart.update(); } }, { name: 'Propagate', handler(chart) { propagate = !propagate; chart.options.plugins.filler.propagate = propagate; chart.update(); } }, { name: 'Smooth', handler(chart) { smooth = !smooth; chart.options.elements.line.tension = smooth ? 0.4 : 0; chart.update(); } } ]; // // const config = { type: 'line', data: data, options: { scales: { y: { stacked: true } }, plugins: { filler: { propagate: false }, 'samples-filler-analyser': { target: 'chart-analyser' } }, interaction: { intersect: false, }, }, }; // module.exports = { actions: actions, config: config, }; ```
## Docs * [Area](../../charts/area.md) * [Filling modes](../../charts/area.md#filling-modes) * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Axes scales](../../axes/) * [Common options to all axes (`stacked`)](../../axes/#common-options-to-all-axes) ================================================ FILE: docs/samples/area/line-drawtime.md ================================================ # Line Chart drawTime ```js chart-editor // const inputs = { min: -100, max: 100, count: 8, decimals: 2, continuity: 1 }; const generateLabels = () => { return Utils.months({count: inputs.count}); }; Utils.srand(3); const generateData = () => (Utils.numbers(inputs)); // // const data = { labels: generateLabels(), datasets: [ { label: 'Dataset 1', data: generateData(), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.CHART_COLORS.red, fill: true }, { label: 'Dataset 2', data: generateData(), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue), fill: true } ] }; // // let smooth = false; const actions = [ { name: 'drawTime: beforeDatasetDraw (default)', handler: (chart) => { chart.options.plugins.filler.drawTime = 'beforeDatasetDraw'; chart.update(); } }, { name: 'drawTime: beforeDatasetsDraw', handler: (chart) => { chart.options.plugins.filler.drawTime = 'beforeDatasetsDraw'; chart.update(); } }, { name: 'drawTime: beforeDraw', handler: (chart) => { chart.options.plugins.filler.drawTime = 'beforeDraw'; chart.update(); } }, { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = generateData(); }); chart.update(); } }, { name: 'Smooth', handler(chart) { smooth = !smooth; chart.options.elements.line.tension = smooth ? 0.4 : 0; chart.update(); } } ]; // // const config = { type: 'line', data: data, options: { plugins: { filler: { propagate: false, }, title: { display: true, text: (ctx) => 'drawTime: ' + ctx.chart.options.plugins.filler.drawTime } }, pointBackgroundColor: '#fff', radius: 10, interaction: { intersect: false, } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Area](../../charts/area.md) * [Configuration (`drawTime`)](../../charts/area.md#configuration) * [Line](../../charts/line.md) * [Line Styling (`tension`)](../../charts/line.md#line-styling) * [Data structures (`labels`)](../../general/data-structures.md) ================================================ FILE: docs/samples/area/line-stacked.md ================================================ # Line Chart Stacked ```js chart-editor // const actions = [ { name: 'Stacked: true', handler: (chart) => { chart.options.scales.y.stacked = true; chart.update(); } }, { name: 'Stacked: false (default)', handler: (chart) => { chart.options.scales.y.stacked = false; chart.update(); } }, { name: 'Stacked Single', handler: (chart) => { chart.options.scales.y.stacked = 'single'; chart.update(); } }, { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const dsColor = Utils.namedColor(chart.data.datasets.length); const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: dsColor, borderColor: dsColor, fill: true, data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(-100, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'My First dataset', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.CHART_COLORS.red, fill: true }, { label: 'My Second dataset', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.CHART_COLORS.blue, fill: true }, { label: 'My Third dataset', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.green, backgroundColor: Utils.CHART_COLORS.green, fill: true }, { label: 'My Fourth dataset', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.yellow, backgroundColor: Utils.CHART_COLORS.yellow, fill: true } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { title: { display: true, text: (ctx) => 'Chart.js Line Chart - stacked=' + ctx.chart.options.scales.y.stacked }, tooltip: { mode: 'index' }, }, interaction: { mode: 'nearest', axis: 'x', intersect: false }, scales: { x: { title: { display: true, text: 'Month' } }, y: { stacked: true, title: { display: true, text: 'Value' } } } } }; // module.exports = { actions: actions, config: config }; ``` ## Docs * [Area](../../charts/area.md) * [Filling modes](../../charts/area.md#filling-modes) * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Axes scales](../../axes/) * [Common options to all axes (`stacked`)](../../axes/#common-options-to-all-axes) ================================================ FILE: docs/samples/area/radar.md ================================================ # Radar Chart Stacked ```js chart-editor // const inputs = { min: 8, max: 16, count: 8, decimals: 2, continuity: 1 }; const generateLabels = () => { return Utils.months({count: inputs.count}); }; const generateData = () => { const values = Utils.numbers(inputs); inputs.from = values; return values; }; const labels = Utils.months({count: 8}); const data = { labels: generateLabels(), datasets: [ { label: 'D0', data: generateData(), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red), }, { label: 'D1', data: generateData(), borderColor: Utils.CHART_COLORS.orange, hidden: true, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange), fill: '-1' }, { label: 'D2', data: generateData(), borderColor: Utils.CHART_COLORS.yellow, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.yellow), fill: 1 }, { label: 'D3', data: generateData(), borderColor: Utils.CHART_COLORS.green, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green), fill: false }, { label: 'D4', data: generateData(), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue), fill: '-1' }, { label: 'D5', data: generateData(), borderColor: Utils.CHART_COLORS.purple, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.purple), fill: '-1' }, { label: 'D6', data: generateData(), borderColor: Utils.CHART_COLORS.grey, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.grey), fill: {value: 85} } ] }; // // let smooth = false; let propagate = false; const actions = [ { name: 'Randomize', handler(chart) { inputs.from = []; chart.data.datasets.forEach(dataset => { dataset.data = generateData(); }); chart.update(); } }, { name: 'Propagate', handler(chart) { propagate = !propagate; chart.options.plugins.filler.propagate = propagate; chart.update(); } }, { name: 'Smooth', handler(chart) { smooth = !smooth; chart.options.elements.line.tension = smooth ? 0.4 : 0; chart.update(); } } ]; // // const config = { type: 'radar', data: data, options: { plugins: { filler: { propagate: false }, 'samples-filler-analyser': { target: 'chart-analyser' } }, interaction: { intersect: false } } }; // module.exports = { actions: actions, config: config }; ```
## Docs * [Area](../../charts/area.md) * [Filling modes](../../charts/area.md#filling-modes) * [`propagate`](../../charts/area.md#propagate) * [Radar](../../charts/radar.md) * [Data structures (`labels`)](../../general/data-structures.md) ================================================ FILE: docs/samples/bar/border-radius.md ================================================ # Bar Chart Border Radius ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Fully Rounded', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), borderWidth: 2, borderRadius: Number.MAX_VALUE, borderSkipped: false, }, { label: 'Small Radius', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), borderWidth: 2, borderRadius: 5, borderSkipped: false, } ] }; // // const config = { type: 'bar', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Bar Chart' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Bar](../../charts/bar.md) * [`borderRadius`](../../charts/bar.md#borderradius) * [Data structures (`labels`)](../../general/data-structures.md) ================================================ FILE: docs/samples/bar/floating.md ================================================ # Floating Bars Using `[number, number][]` as the type for `data` to define the beginning and end value for each bar. This is instead of having every bar start at 0. ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = chart.data.labels.map(() => { return [Utils.rand(-100, 100), Utils.rand(-100, 100)]; }); }); chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: labels.map(() => { return [Utils.rand(-100, 100), Utils.rand(-100, 100)]; }), backgroundColor: Utils.CHART_COLORS.red, }, { label: 'Dataset 2', data: labels.map(() => { return [Utils.rand(-100, 100), Utils.rand(-100, 100)]; }), backgroundColor: Utils.CHART_COLORS.blue, }, ] }; // // const config = { type: 'bar', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Floating Bar Chart' } } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Bar](../../charts/bar.md) * [Data structures (`labels`)](../../general/data-structures.md) ================================================ FILE: docs/samples/bar/horizontal.md ================================================ # Horizontal Bar Chart ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const dsColor = Utils.namedColor(chart.data.datasets.length); const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, borderWidth: 1, data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(-100, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // const config = { type: 'bar', data: data, options: { indexAxis: 'y', // Elements options apply to all of the options unless overridden in a dataset // In this case, we are setting the border of each horizontal bar to be 2px wide elements: { bar: { borderWidth: 2, } }, responsive: true, plugins: { legend: { position: 'right', }, title: { display: true, text: 'Chart.js Horizontal Bar Chart' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Bar](../../charts/bar.md) * [Horizontal Bar Chart](../../charts/bar.md#horizontal-bar-chart) ================================================ FILE: docs/samples/bar/stacked-groups.md ================================================ # Stacked Bar Chart with Groups Using the `stack` property to divide datasets into multiple stacks. ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), backgroundColor: Utils.CHART_COLORS.red, stack: 'Stack 0', }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), backgroundColor: Utils.CHART_COLORS.blue, stack: 'Stack 0', }, { label: 'Dataset 3', data: Utils.numbers(NUMBER_CFG), backgroundColor: Utils.CHART_COLORS.green, stack: 'Stack 1', }, ] }; // // const config = { type: 'bar', data: data, options: { plugins: { title: { display: true, text: 'Chart.js Bar Chart - Stacked' }, }, responsive: true, interaction: { intersect: false, }, scales: { x: { stacked: true, }, y: { stacked: true } } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Bar](../../charts/bar.md) * [Stacked Bar Chart](../../charts/bar.md#stacked-bar-chart) * [Data structures (`labels`)](../../general/data-structures.md) * [Dataset Configuration (`stack`)](../../general/data-structures.md#dataset-configuration) ================================================ FILE: docs/samples/bar/stacked.md ================================================ # Stacked Bar Chart ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), backgroundColor: Utils.CHART_COLORS.red, }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), backgroundColor: Utils.CHART_COLORS.blue, }, { label: 'Dataset 3', data: Utils.numbers(NUMBER_CFG), backgroundColor: Utils.CHART_COLORS.green, }, ] }; // // const config = { type: 'bar', data: data, options: { plugins: { title: { display: true, text: 'Chart.js Bar Chart - Stacked' }, }, responsive: true, scales: { x: { stacked: true, }, y: { stacked: true } } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Bar](../../charts/bar.md) * [Stacked Bar Chart](../../charts/bar.md#stacked-bar-chart) ================================================ FILE: docs/samples/bar/vertical.md ================================================ # Vertical Bar Chart ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const dsColor = Utils.namedColor(chart.data.datasets.length); const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, borderWidth: 1, data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(-100, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // const config = { type: 'bar', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Bar Chart' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Bar](../../charts/bar.md) * [Data structures (`labels`)](../../general/data-structures.md) ================================================ FILE: docs/samples/information.md ================================================ # Chart.js Samples You can navigate through the samples via the sidebar. Alternatively, you can run them locally. To do so, clone the [Chart.js repository](https://github.com/chartjs/Chart.js) from GitHub, run `pnpm ci` to install all packages, then run `pnpm run docs:dev` to build the documentation. As soon as the build is done, you can go to [localhost:8080/samples](http://localhost:8080/samples/) to see the samples. ## Out of the box working samples These samples are made for demonstration purposes only. They won't work out of the box if you copy paste them into your own website. This is because of how the docs are getting built. Some boilerplate code gets hidden. For a sample that can be copied and pasted and used directly you can check the [usage page](../getting-started/usage.md). ## Autogenerated data The data used in the samples is autogenerated using custom functions. These functions do not ship with the library, for more information about this you can check the [utils page](./utils.md). ## Actions block The samples have an `actions` code block. These actions are not part of Chart.js. They are internally transformed to separate buttons together with `onClick` listeners by a plugin we use in the documentation. To implement such actions yourself you can make some buttons and add `onClick` event listeners to them. Then in these event listeners you can call your variable in which you made the chart and do the logic that the button is supposed to do. ================================================ FILE: docs/samples/legend/events.md ================================================ # Events This sample demonstrates how to use the event hooks to highlight chart elements. ```js chart-editor // const data = { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [{ label: '# of Votes', data: [12, 19, 3, 5, 2, 3], borderWidth: 1, backgroundColor: ['#CB4335', '#1F618D', '#F1C40F', '#27AE60', '#884EA0', '#D35400'], }] }; // // // Append '4d' to the colors (alpha channel), except for the hovered index function handleHover(evt, item, legend) { legend.chart.data.datasets[0].backgroundColor.forEach((color, index, colors) => { colors[index] = index === item.index || color.length === 9 ? color : color + '4D'; }); legend.chart.update(); } // // // Removes the alpha channel from background colors function handleLeave(evt, item, legend) { legend.chart.data.datasets[0].backgroundColor.forEach((color, index, colors) => { colors[index] = color.length === 9 ? color.slice(0, -2) : color; }); legend.chart.update(); } // // const config = { type: 'pie', data: data, options: { plugins: { legend: { onHover: handleHover, onLeave: handleLeave } } } }; // module.exports = { config }; ``` ## Docs * [Doughnut and Pie Charts](../../charts/doughnut.md) * [Legend](../../configuration/legend.md) * `onHover` * `onLeave` ================================================ FILE: docs/samples/legend/html.md ================================================ # HTML Legend This example shows how to create a custom HTML legend using a plugin and connect it to the chart in lieu of the default on-canvas legend. For an html legend to work you need to place an empty div at your web page with the ID you provide in the options to bind to like so: `
`.
```js chart-editor // const getOrCreateLegendList = (chart, id) => { const legendContainer = document.getElementById(id); let listContainer = legendContainer.querySelector('ul'); if (!listContainer) { listContainer = document.createElement('ul'); listContainer.style.display = 'flex'; listContainer.style.flexDirection = 'row'; listContainer.style.margin = 0; listContainer.style.padding = 0; legendContainer.appendChild(listContainer); } return listContainer; }; const htmlLegendPlugin = { id: 'htmlLegend', afterUpdate(chart, args, options) { const ul = getOrCreateLegendList(chart, options.containerID); // Remove old legend items while (ul.firstChild) { ul.firstChild.remove(); } // Reuse the built-in legendItems generator const items = chart.options.plugins.legend.labels.generateLabels(chart); items.forEach(item => { const li = document.createElement('li'); li.style.alignItems = 'center'; li.style.cursor = 'pointer'; li.style.display = 'flex'; li.style.flexDirection = 'row'; li.style.marginLeft = '10px'; li.onclick = () => { const {type} = chart.config; if (type === 'pie' || type === 'doughnut') { // Pie and doughnut charts only have a single dataset and visibility is per item chart.toggleDataVisibility(item.index); } else { chart.setDatasetVisibility(item.datasetIndex, !chart.isDatasetVisible(item.datasetIndex)); } chart.update(); }; // Color box const boxSpan = document.createElement('span'); boxSpan.style.background = item.fillStyle; boxSpan.style.borderColor = item.strokeStyle; boxSpan.style.borderWidth = item.lineWidth + 'px'; boxSpan.style.display = 'inline-block'; boxSpan.style.flexShrink = 0; boxSpan.style.height = '20px'; boxSpan.style.marginRight = '10px'; boxSpan.style.width = '20px'; // Text const textContainer = document.createElement('p'); textContainer.style.color = item.fontColor; textContainer.style.margin = 0; textContainer.style.padding = 0; textContainer.style.textDecoration = item.hidden ? 'line-through' : ''; const text = document.createTextNode(item.text); textContainer.appendChild(text); li.appendChild(boxSpan); li.appendChild(textContainer); ul.appendChild(li); }); } }; // // const NUM_DATA = 7; const NUM_CFG = {count: NUM_DATA, min: 0, max: 100}; const data = { labels: Utils.months({count: NUM_DATA}), datasets: [ { label: 'Dataset: 1', data: Utils.numbers(NUM_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), fill: false, }, { label: 'Dataset: 1', data: Utils.numbers(NUM_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), fill: false, }, ], }; // // const config = { type: 'line', data: data, options: { plugins: { htmlLegend: { // ID of the container to put the legend in containerID: 'legend-container', }, legend: { display: false, } } }, plugins: [htmlLegendPlugin], }; // module.exports = { actions: [], config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Legend](../../configuration/legend.md) * `display: false` * [Plugins](../../developers/plugins.md) ================================================ FILE: docs/samples/legend/point-style.md ================================================ # Point Style This sample show how to use the dataset point style in the legend instead of a rectangle to identify each dataset.. ```js chart-editor // const actions = [ { name: 'Toggle Point Style', handler(chart) { chart.options.plugins.legend.labels.usePointStyle = !chart.options.plugins.legend.labels.usePointStyle; chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), borderWidth: 1, pointStyle: 'rectRot', pointRadius: 5, pointBorderColor: 'rgb(0, 0, 0)' }, ] }; // // const config = { type: 'line', data: data, options: { plugins: { legend: { labels: { usePointStyle: true, }, } } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Legend](../../configuration/legend.md) * [Legend Label Configuration](../../configuration/legend.md#legend-label-configuration) * `usePointStyle` * [Elements](../../configuration/elements.md) * [Point Configuration](../../configuration/elements.md#point-configuration) * [Point Styles](../../configuration/elements.md#point-styles) ================================================ FILE: docs/samples/legend/position.md ================================================ # Position This sample show how to change the position of the chart legend. ```js chart-editor // const actions = [ { name: 'Position: top', handler(chart) { chart.options.plugins.legend.position = 'top'; chart.update(); } }, { name: 'Position: right', handler(chart) { chart.options.plugins.legend.position = 'right'; chart.update(); } }, { name: 'Position: bottom', handler(chart) { chart.options.plugins.legend.position = 'bottom'; chart.update(); } }, { name: 'Position: left', handler(chart) { chart.options.plugins.legend.position = 'left'; chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, ] }; // // const config = { type: 'line', data: data, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Legend](../../configuration/legend.md) * [Position](../../configuration/legend.md#position) ================================================ FILE: docs/samples/legend/title.md ================================================ # Alignment and Title Position This sample show how to configure the alignment and title position of the chart legend. ```js chart-editor // const actions = [ { name: 'Title Position: start', handler(chart) { chart.options.plugins.legend.align = 'start'; chart.options.plugins.legend.title.position = 'start'; chart.update(); } }, { name: 'Title Position: center (default)', handler(chart) { chart.options.plugins.legend.align = 'center'; chart.options.plugins.legend.title.position = 'center'; chart.update(); } }, { name: 'Title Position: end', handler(chart) { chart.options.plugins.legend.align = 'end'; chart.options.plugins.legend.title.position = 'end'; chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, ] }; // // const config = { type: 'line', data: data, options: { plugins: { legend: { title: { display: true, text: 'Legend Title', } } } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Legend](../../configuration/legend.md) ================================================ FILE: docs/samples/line/interpolation.md ================================================ # Interpolation Modes ```js chart-editor // const DATA_COUNT = 12; const labels = []; for (let i = 0; i < DATA_COUNT; ++i) { labels.push(i.toString()); } const datapoints = [0, 20, 20, 60, 60, 120, NaN, 180, 120, 125, 105, 110, 170]; const data = { labels: labels, datasets: [ { label: 'Cubic interpolation (monotone)', data: datapoints, borderColor: Utils.CHART_COLORS.red, fill: false, cubicInterpolationMode: 'monotone', tension: 0.4 }, { label: 'Cubic interpolation', data: datapoints, borderColor: Utils.CHART_COLORS.blue, fill: false, tension: 0.4 }, { label: 'Linear interpolation (default)', data: datapoints, borderColor: Utils.CHART_COLORS.green, fill: false } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Chart.js Line Chart - Cubic interpolation mode' }, }, interaction: { intersect: false, }, scales: { x: { display: true, title: { display: true } }, y: { display: true, title: { display: true, text: 'Value' }, suggestedMin: -10, suggestedMax: 200 } } }, }; // module.exports = { actions: [], config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [`cubicInterpolationMode`](../../charts/line.md#cubicinterpolationmode) * [Line Styling (`tension`)](../../charts/line.md#line-styling) ================================================ FILE: docs/samples/line/line.md ================================================ # Line Chart ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const dsColor = Utils.namedColor(chart.data.datasets.length); const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(-100, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Line Chart' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) ================================================ FILE: docs/samples/line/multi-axis.md ================================================ # Multi Axis Line Chart ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), yAxisID: 'y', }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), yAxisID: 'y1', } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, interaction: { mode: 'index', intersect: false, }, stacked: false, plugins: { title: { display: true, text: 'Chart.js Line Chart - Multi Axis' } }, scales: { y: { type: 'linear', display: true, position: 'left', }, y1: { type: 'linear', display: true, position: 'right', // grid line settings grid: { drawOnChartArea: false, // only want the grid lines for one axis to show up }, }, } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Axes scales](../../axes/) * [Cartesian Axes](../../axes/cartesian/) * [Axis Position](../../axes/cartesian/#axis-position) * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) ================================================ FILE: docs/samples/line/point-styling.md ================================================ # Point Styling ```js chart-editor // const actions = [ { name: 'pointStyle: circle (default)', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.pointStyle = 'circle'; }); chart.update(); } }, { name: 'pointStyle: cross', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.pointStyle = 'cross'; }); chart.update(); } }, { name: 'pointStyle: crossRot', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.pointStyle = 'crossRot'; }); chart.update(); } }, { name: 'pointStyle: dash', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.pointStyle = 'dash'; }); chart.update(); } }, { name: 'pointStyle: line', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.pointStyle = 'line'; }); chart.update(); } }, { name: 'pointStyle: rect', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.pointStyle = 'rect'; }); chart.update(); } }, { name: 'pointStyle: rectRounded', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.pointStyle = 'rectRounded'; }); chart.update(); } }, { name: 'pointStyle: rectRot', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.pointStyle = 'rectRot'; }); chart.update(); } }, { name: 'pointStyle: star', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.pointStyle = 'star'; }); chart.update(); } }, { name: 'pointStyle: triangle', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.pointStyle = 'triangle'; }); chart.update(); } }, { name: 'pointStyle: false', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.pointStyle = false; }); chart.update(); } } ]; // // const data = { labels: ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6'], datasets: [ { label: 'Dataset', data: Utils.numbers({count: 6, min: -100, max: 100}), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), pointStyle: 'circle', pointRadius: 10, pointHoverRadius: 15 } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { title: { display: true, text: (ctx) => 'Point Style: ' + ctx.chart.data.datasets[0].pointStyle, } } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Point Styling](../../charts/line.md#point-styling) ================================================ FILE: docs/samples/line/segments.md ================================================ # Line Segment Styling Using helper functions to style each segment. Gaps in the data ('skipped') are set to dashed lines and segments with values going 'down' are set to a different color. ```js chart-editor // const skipped = (ctx, value) => ctx.p0.skip || ctx.p1.skip ? value : undefined; const down = (ctx, value) => ctx.p0.parsed.y > ctx.p1.parsed.y ? value : undefined; // // const genericOptions = { fill: false, interaction: { intersect: false }, radius: 0, }; // // const config = { type: 'line', data: { labels: Utils.months({count: 7}), datasets: [{ label: 'My First Dataset', data: [65, 59, NaN, 48, 56, 57, 40], borderColor: 'rgb(75, 192, 192)', segment: { borderColor: ctx => skipped(ctx, 'rgb(0,0,0,0.2)') || down(ctx, 'rgb(192,75,75)'), borderDash: ctx => skipped(ctx, [6, 6]), }, spanGaps: true }] }, options: genericOptions }; // module.exports = { actions: [], config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Line Styling](../../charts/line.md#line-styling) * [Segment](../../charts/line.md#segment) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) ================================================ FILE: docs/samples/line/stepped.md ================================================ # Stepped Line Charts ```js chart-editor // const actions = [ { name: 'Step: false (default)', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.stepped = false; }); chart.update(); } }, { name: 'Step: true', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.stepped = true; }); chart.update(); } }, { name: 'Step: before', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.stepped = 'before'; }); chart.update(); } }, { name: 'Step: after', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.stepped = 'after'; }); chart.update(); } }, { name: 'Step: middle', handler: (chart) => { chart.data.datasets.forEach(dataset => { dataset.stepped = 'middle'; }); chart.update(); } } ]; // // const data = { labels: ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5', 'Day 6'], datasets: [ { label: 'Dataset', data: Utils.numbers({count: 6, min: -100, max: 100}), borderColor: Utils.CHART_COLORS.red, fill: false, stepped: true, } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, interaction: { intersect: false, axis: 'x' }, plugins: { title: { display: true, text: (ctx) => 'Step ' + ctx.chart.data.datasets[0].stepped + ' Interpolation', } } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Stepped](../../charts/line.md#stepped) ================================================ FILE: docs/samples/line/styling.md ================================================ # Line Styling ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: DATA_COUNT}); const data = { labels: labels, datasets: [ { label: 'Unfilled', fill: false, backgroundColor: Utils.CHART_COLORS.blue, borderColor: Utils.CHART_COLORS.blue, data: Utils.numbers(NUMBER_CFG), }, { label: 'Dashed', fill: false, backgroundColor: Utils.CHART_COLORS.green, borderColor: Utils.CHART_COLORS.green, borderDash: [5, 5], data: Utils.numbers(NUMBER_CFG), }, { label: 'Filled', backgroundColor: Utils.CHART_COLORS.red, borderColor: Utils.CHART_COLORS.red, data: Utils.numbers(NUMBER_CFG), fill: true, } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Chart.js Line Chart' }, }, interaction: { mode: 'index', intersect: false }, scales: { x: { display: true, title: { display: true, text: 'Month' } }, y: { display: true, title: { display: true, text: 'Value' } } } }, }; // module.exports = { actions: [], config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Line Styling](../../charts/line.md#line-styling) ================================================ FILE: docs/samples/other-charts/bubble.md ================================================ # Bubble ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, rmin: 5, rmax: 15, min: 0, max: 100}; const data = { datasets: [ { label: 'Dataset 1', data: Utils.bubbles(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.bubbles(NUMBER_CFG), borderColor: Utils.CHART_COLORS.orange, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange, 0.5), } ] }; // // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.bubbles({count: DATA_COUNT, rmin: 5, rmax: 15, min: 0, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const chartData = chart.data; const dsColor = Utils.namedColor(chartData.datasets.length); const newDataset = { label: 'Dataset ' + (chartData.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, data: Utils.bubbles({count: DATA_COUNT, rmin: 5, rmax: 15, min: 0, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const chartData = chart.data; if (chartData.datasets.length > 0) { for (let index = 0; index < chartData.datasets.length; ++index) { chartData.datasets[index].data.push(Utils.bubbles({count: 1, rmin: 5, rmax: 15, min: 0, max: 100})[0]); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const config = { type: 'bubble', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Bubble Chart' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Bubble](../../charts/bubble.md) ================================================ FILE: docs/samples/other-charts/combo-bar-line.md ================================================ # Combo bar/line ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const dsColor = Utils.namedColor(chart.data.datasets.length); const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, borderWidth: 1, data: Utils.numbers({count: data.labels.length, min: -100, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(-100, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), order: 1 }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), type: 'line', order: 0 } ] }; // // const config = { type: 'bar', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Combined Line/Bar Chart' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Bar](../../charts/bar.md) * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) ================================================ FILE: docs/samples/other-charts/doughnut.md ================================================ # Doughnut ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: [], data: [], }; for (let i = 0; i < data.labels.length; i++) { newDataset.data.push(Utils.numbers({count: 1, min: 0, max: 100})); const colorIndex = i % Object.keys(Utils.CHART_COLORS).length; newDataset.backgroundColor.push(Object.values(Utils.CHART_COLORS)[colorIndex]); } chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels.push('data #' + (data.labels.length + 1)); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(0, 100)); } chart.update(); } } }, { name: 'Hide(0)', handler(chart) { chart.hide(0); } }, { name: 'Show(0)', handler(chart) { chart.show(0); } }, { name: 'Hide (0, 1)', handler(chart) { chart.hide(0, 1); } }, { name: 'Show (0, 1)', handler(chart) { chart.show(0, 1); } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 5; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const data = { labels: ['Red', 'Orange', 'Yellow', 'Green', 'Blue'], datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), backgroundColor: Object.values(Utils.CHART_COLORS), } ] }; // // const config = { type: 'doughnut', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Doughnut Chart' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Doughnut and Pie Charts](../../charts/doughnut.md) ================================================ FILE: docs/samples/other-charts/multi-series-pie.md ================================================ # Multi Series Pie ```js chart-editor // const DATA_COUNT = 5; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: ['Overall Yay', 'Overall Nay', 'Group A Yay', 'Group A Nay', 'Group B Yay', 'Group B Nay', 'Group C Yay', 'Group C Nay'], datasets: [ { backgroundColor: ['#AAA', '#777'], data: [21, 79] }, { backgroundColor: ['hsl(0, 100%, 60%)', 'hsl(0, 100%, 35%)'], data: [33, 67] }, { backgroundColor: ['hsl(100, 100%, 60%)', 'hsl(100, 100%, 35%)'], data: [20, 80] }, { backgroundColor: ['hsl(180, 100%, 60%)', 'hsl(180, 100%, 35%)'], data: [10, 90] } ] }; // // const config = { type: 'pie', data: data, options: { responsive: true, plugins: { legend: { labels: { generateLabels: function(chart) { // Get the default label list const original = Chart.overrides.pie.plugins.legend.labels.generateLabels; const labelsOriginal = original.call(this, chart); // Build an array of colors used in the datasets of the chart let datasetColors = chart.data.datasets.map(function(e) { return e.backgroundColor; }); datasetColors = datasetColors.flat(); // Modify the color and hide state of each label labelsOriginal.forEach(label => { // There are twice as many labels as there are datasets. This converts the label index into the corresponding dataset index label.datasetIndex = (label.index - label.index % 2) / 2; // The hidden state must match the dataset's hidden state label.hidden = !chart.isDatasetVisible(label.datasetIndex); // Change the color to match the dataset label.fillStyle = datasetColors[label.index]; }); return labelsOriginal; } }, onClick: function(mouseEvent, legendItem, legend) { // toggle the visibility of the dataset from what it currently is legend.chart.getDatasetMeta( legendItem.datasetIndex ).hidden = legend.chart.isDatasetVisible(legendItem.datasetIndex); legend.chart.update(); } }, tooltip: { callbacks: { title: function(context) { const labelIndex = (context[0].datasetIndex * 2) + context[0].dataIndex; return context[0].chart.data.labels[labelIndex] + ': ' + context[0].formattedValue; } } } } }, }; // module.exports = { config: config, }; ``` ## Docs * [Doughnut and Pie Charts](../../charts/doughnut.md) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) ================================================ FILE: docs/samples/other-charts/pie.md ================================================ # Pie ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: [], data: [], }; for (let i = 0; i < data.labels.length; i++) { newDataset.data.push(Utils.numbers({count: 1, min: 0, max: 100})); const colorIndex = i % Object.keys(Utils.CHART_COLORS).length; newDataset.backgroundColor.push(Object.values(Utils.CHART_COLORS)[colorIndex]); } chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels.push('data #' + (data.labels.length + 1)); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(0, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 5; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const data = { labels: ['Red', 'Orange', 'Yellow', 'Green', 'Blue'], datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), backgroundColor: Object.values(Utils.CHART_COLORS), } ] }; // // const config = { type: 'pie', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Pie Chart' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Doughnut and Pie Charts](../../charts/doughnut.md) ================================================ FILE: docs/samples/other-charts/polar-area-center-labels.md ================================================ # Polar area centered point labels ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); }); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels.push('data #' + (data.labels.length + 1)); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(0, 100)); } chart.update(); } } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 5; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = ['Red', 'Orange', 'Yellow', 'Green', 'Blue']; const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), backgroundColor: [ Utils.transparentize(Utils.CHART_COLORS.red, 0.5), Utils.transparentize(Utils.CHART_COLORS.orange, 0.5), Utils.transparentize(Utils.CHART_COLORS.yellow, 0.5), Utils.transparentize(Utils.CHART_COLORS.green, 0.5), Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), ] } ] }; // // const config = { type: 'polarArea', data: data, options: { responsive: true, scales: { r: { pointLabels: { display: true, centerPointLabels: true, font: { size: 18 } } } }, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Polar Area Chart With Centered Point Labels' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Polar Area Chart](../../charts/polar.md) * [Linear Radial Axis](../../axes/radial/linear.md) * [Point Label Options (`centerPointLabels`)](../../axes/radial/linear.md#point-label-options) ================================================ FILE: docs/samples/other-charts/polar-area.md ================================================ # Polar area ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); }); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels.push('data #' + (data.labels.length + 1)); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(0, 100)); } chart.update(); } } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 5; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = ['Red', 'Orange', 'Yellow', 'Green', 'Blue']; const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), backgroundColor: [ Utils.transparentize(Utils.CHART_COLORS.red, 0.5), Utils.transparentize(Utils.CHART_COLORS.orange, 0.5), Utils.transparentize(Utils.CHART_COLORS.yellow, 0.5), Utils.transparentize(Utils.CHART_COLORS.green, 0.5), Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), ] } ] }; // // const config = { type: 'polarArea', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Polar Area Chart' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Polar Area Chart](../../charts/polar.md) * [Radial linear scale](../../axes/radial/linear.md) ================================================ FILE: docs/samples/other-charts/radar-skip-points.md ================================================ # Radar skip points ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach((dataset, i) => { const data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); if (i === 0) { data[0] = null; } else if (i === 1) { data[Number.parseInt(data.length / 2, 10)] = null; } else { data[data.length - 1] = null; } dataset.data = data; }); chart.update(); } } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = Utils.months({count: 7}); const dataFirstSkip = Utils.numbers(NUMBER_CFG); const dataMiddleSkip = Utils.numbers(NUMBER_CFG); const dataLastSkip = Utils.numbers(NUMBER_CFG); dataFirstSkip[0] = null; dataMiddleSkip[Number.parseInt(dataMiddleSkip.length / 2, 10)] = null; dataLastSkip[dataLastSkip.length - 1] = null; const data = { labels: labels, datasets: [ { label: 'Skip first dataset', data: dataFirstSkip, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Skip mid dataset', data: dataMiddleSkip, borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), }, { label: 'Skip last dataset', data: dataLastSkip, borderColor: Utils.CHART_COLORS.green, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green, 0.5), } ] }; // // const config = { type: 'radar', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Chart.js Radar Skip Points Chart' } } }, }; // module.exports = { actions: actions, config: config }; ``` ## Docs * [Radar](../../charts/radar.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Radial linear scale](../../axes/radial/linear.md) ================================================ FILE: docs/samples/other-charts/radar.md ================================================ # Radar ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const dsColor = Utils.namedColor(chart.data.datasets.length); const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, data: Utils.numbers({count: data.labels.length, min: 0, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(0, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // const config = { type: 'radar', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Chart.js Radar Chart' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Radar](../../charts/radar.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Radial linear scale](../../axes/radial/linear.md) ================================================ FILE: docs/samples/other-charts/scatter-multi-axis.md ================================================ # Scatter - Multi axis ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, rmin: 1, rmax: 1, min: -100, max: 100}; const data = { datasets: [ { label: 'Dataset 1', data: Utils.bubbles(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), yAxisID: 'y', }, { label: 'Dataset 2', data: Utils.bubbles(NUMBER_CFG), borderColor: Utils.CHART_COLORS.orange, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange, 0.5), yAxisID: 'y2', } ] }; // // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.bubbles({count: DATA_COUNT, rmin: 1, rmax: 1, min: -100, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const chartData = chart.data; const dsColor = Utils.namedColor(chartData.datasets.length); const newDataset = { label: 'Dataset ' + (chartData.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, data: Utils.bubbles({count: DATA_COUNT, rmin: 1, rmax: 1, min: -100, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const chartData = chart.data; if (chartData.datasets.length > 0) { for (let index = 0; index < chartData.datasets.length; ++index) { chartData.datasets[index].data.push(Utils.bubbles({count: 1, rmin: 1, rmax: 1, min: -100, max: 100})[0]); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const config = { type: 'scatter', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Scatter Multi Axis Chart' } }, scales: { y: { type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance position: 'left', ticks: { color: Utils.CHART_COLORS.red } }, y2: { type: 'linear', // only linear but allow scale type registration. This allows extensions to exist solely for log scale for instance position: 'right', reverse: true, ticks: { color: Utils.CHART_COLORS.blue }, grid: { drawOnChartArea: false // only want the grid lines for one axis to show up } } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Scatter](../../charts/scatter.md) * [Cartesian Axes](../../axes/cartesian/) * [Axis Position](../../axes/cartesian/#axis-position) ================================================ FILE: docs/samples/other-charts/scatter.md ================================================ # Scatter ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, rmin: 1, rmax: 1, min: 0, max: 100}; const data = { datasets: [ { label: 'Dataset 1', data: Utils.bubbles(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.bubbles(NUMBER_CFG), borderColor: Utils.CHART_COLORS.orange, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.orange, 0.5), } ] }; // // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.bubbles({count: DATA_COUNT, rmin: 1, rmax: 1, min: 0, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const chartData = chart.data; const dsColor = Utils.namedColor(chartData.datasets.length); const newDataset = { label: 'Dataset ' + (chartData.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, data: Utils.bubbles({count: DATA_COUNT, rmin: 1, rmax: 1, min: 0, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const chartData = chart.data; if (chartData.datasets.length > 0) { for (let index = 0; index < chartData.datasets.length; ++index) { chartData.datasets[index].data.push(Utils.bubbles({count: 1, rmin: 1, rmax: 1, min: 0, max: 100})[0]); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const config = { type: 'scatter', data: data, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: 'Chart.js Scatter Chart' } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Scatter](../../charts/scatter.md) ================================================ FILE: docs/samples/other-charts/stacked-bar-line.md ================================================ # Stacked bar/line ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const dsColor = Utils.namedColor(chart.data.datasets.length); const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: Utils.transparentize(dsColor, 0.5), borderColor: dsColor, borderWidth: 1, stack: 'combined', data: Utils.numbers({count: data.labels.length, min: 0, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(0, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), stack: 'combined', type: 'bar' }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), stack: 'combined' } ] }; // // const config = { type: 'line', data: data, options: { plugins: { title: { display: true, text: 'Chart.js Stacked Line/Bar Chart' } }, scales: { y: { stacked: true } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Axes scales](../../axes/) * [Common options to all axes (`stacked`)](../../axes/#common-options-to-all-axes) * [Stacking](../../axes/#stacking) * [Bar](../../charts/bar.md) * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Dataset Configuration (`stack`)](../../general/data-structures.md#dataset-configuration) ================================================ FILE: docs/samples/plugins/chart-area-border.md ================================================ # Chart Area Border ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // const chartAreaBorder = { id: 'chartAreaBorder', beforeDraw(chart, args, options) { const {ctx, chartArea: {left, top, width, height}} = chart; ctx.save(); ctx.strokeStyle = options.borderColor; ctx.lineWidth = options.borderWidth; ctx.setLineDash(options.borderDash || []); ctx.lineDashOffset = options.borderDashOffset; ctx.strokeRect(left, top, width, height); ctx.restore(); } }; // // const config = { type: 'line', data: data, options: { plugins: { chartAreaBorder: { borderColor: 'red', borderWidth: 2, borderDash: [5, 5], borderDashOffset: 2, } } }, plugins: [chartAreaBorder] }; // module.exports = { config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Plugins](../../developers/plugins.md) ================================================ FILE: docs/samples/plugins/doughnut-empty-state.md ================================================ # Doughnut Empty State ```js chart-editor // const data = { labels: [], datasets: [ { label: 'Dataset 1', data: [] } ] }; // // const plugin = { id: 'emptyDoughnut', afterDraw(chart, args, options) { const {datasets} = chart.data; const {color, width, radiusDecrease} = options; let hasData = false; for (let i = 0; i < datasets.length; i += 1) { const dataset = datasets[i]; hasData |= dataset.data.length > 0; } if (!hasData) { const {chartArea: {left, top, right, bottom}, ctx} = chart; const centerX = (left + right) / 2; const centerY = (top + bottom) / 2; const r = Math.min(right - left, bottom - top) / 2; ctx.beginPath(); ctx.lineWidth = width || 2; ctx.strokeStyle = color || 'rgba(255, 128, 0, 0.5)'; ctx.arc(centerX, centerY, (r - radiusDecrease || 0), 0, 2 * Math.PI); ctx.stroke(); } } }; // // const config = { type: 'doughnut', data: data, options: { plugins: { emptyDoughnut: { color: 'rgba(255, 128, 0, 0.5)', width: 2, radiusDecrease: 20 } } }, plugins: [plugin] }; // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.points(NUMBER_CFG); }); chart.update(); } }, ]; module.exports = { actions, config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Plugins](../../developers/plugins.md) * [Doughnut and Pie Charts](../../charts/doughnut.md) ================================================ FILE: docs/samples/plugins/quadrants.md ================================================ # Quadrants ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const data = { datasets: [ { label: 'Dataset 1', data: Utils.points(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.points(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // const quadrants = { id: 'quadrants', beforeDraw(chart, args, options) { const {ctx, chartArea: {left, top, right, bottom}, scales: {x, y}} = chart; const midX = x.getPixelForValue(0); const midY = y.getPixelForValue(0); ctx.save(); ctx.fillStyle = options.topLeft; ctx.fillRect(left, top, midX - left, midY - top); ctx.fillStyle = options.topRight; ctx.fillRect(midX, top, right - midX, midY - top); ctx.fillStyle = options.bottomRight; ctx.fillRect(midX, midY, right - midX, bottom - midY); ctx.fillStyle = options.bottomLeft; ctx.fillRect(left, midY, midX - left, bottom - midY); ctx.restore(); } }; // // const config = { type: 'scatter', data: data, options: { plugins: { quadrants: { topLeft: Utils.CHART_COLORS.red, topRight: Utils.CHART_COLORS.blue, bottomRight: Utils.CHART_COLORS.green, bottomLeft: Utils.CHART_COLORS.yellow, } } }, plugins: [quadrants] }; // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.points(NUMBER_CFG); }); chart.update(); } }, ]; module.exports = { actions, config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Plugins](../../developers/plugins.md) * [Scatter](../../charts/scatter.md) ================================================ FILE: docs/samples/scale-options/center.md ================================================ # Center Positioning This sample show how to place the axis in the center of the chart area, instead of at the edges. ```js chart-editor // const actions = [ { name: 'Default Positions', handler(chart) { chart.options.scales.x.position = 'bottom'; chart.options.scales.y.position = 'left'; chart.update(); } }, { name: 'Position: center', handler(chart) { chart.options.scales.x.position = 'center'; chart.options.scales.y.position = 'center'; chart.update(); } }, { name: 'Position: Vertical: x=-60, Horizontal: y=30', handler(chart) { chart.options.scales.x.position = {y: 30}; chart.options.scales.y.position = {x: -60}; chart.update(); } }, ]; // // const DATA_COUNT = 6; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const data = { datasets: [ { label: 'Dataset 1', data: Utils.points(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.points(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // const config = { type: 'scatter', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Axis Center Positioning' } }, scales: { x: { min: -100, max: 100, }, y: { min: -100, max: 100, } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Scatter](../../charts/scatter.md) * [Cartesian Axes](../../axes/cartesian/) * [Axis Position](../../axes/cartesian/#axis-position) ================================================ FILE: docs/samples/scale-options/grid.md ================================================ # Grid Configuration This sample shows how to use scriptable grid options for an axis to control styling. In this case, the Y axis grid lines are colored based on their value. In addition, booleans are provided to toggle different parts of the X axis grid visibility. ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: -100, max: 100}); }); chart.update(); } }, ]; // // const DATA_COUNT = 7; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Dataset 1', data: [10, 30, 39, 20, 25, 34, -10], fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: [18, 33, 22, 19, 11, -39, 30], fill: false, borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // // Change these settings to change the display for different parts of the X axis // grid configuration const DISPLAY = true; const BORDER = true; const CHART_AREA = true; const TICKS = true; const config = { type: 'line', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Grid Line Settings' } }, scales: { x: { border: { display: BORDER }, grid: { display: DISPLAY, drawOnChartArea: CHART_AREA, drawTicks: TICKS, } }, y: { border: { display: false }, grid: { color: function(context) { if (context.tick.value > 0) { return Utils.CHART_COLORS.green; } else if (context.tick.value < 0) { return Utils.CHART_COLORS.red; } return '#000000'; }, }, } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) * [Tick Context](../../general/options.md#tick) * [Data structures (`labels`)](../../general/data-structures.md) * [Axes Styling](../../axes/styling.md) * [Grid Line Configuration](../../axes/styling.md#grid-line-configuration) ================================================ FILE: docs/samples/scale-options/ticks.md ================================================ # Tick Configuration This sample shows how to use different tick features to control how tick labels are shown on the X axis. These features include: * Multi-line labels * Filtering labels * Changing the tick color * Changing the tick alignment for the X axis ```js chart-editor // const actions = [ { name: 'Alignment: start', handler(chart) { chart.options.scales.x.ticks.align = 'start'; chart.update(); } }, { name: 'Alignment: center (default)', handler(chart) { chart.options.scales.x.ticks.align = 'center'; chart.update(); } }, { name: 'Alignment: end', handler(chart) { chart.options.scales.x.ticks.align = 'end'; chart.update(); } }, ]; // // const DATA_COUNT = 12; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const data = { labels: [['June', '2015'], 'July', 'August', 'September', 'October', 'November', 'December', ['January', '2016'], 'February', 'March', 'April', 'May'], datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Chart with Tick Configuration' } }, scales: { x: { ticks: { // For a category axis, the val is the index so the lookup via getLabelForValue is needed callback: function(val, index) { // Hide every 2nd tick label return index % 2 === 0 ? this.getLabelForValue(val) : ''; }, color: 'red', } } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) * [Tick Context](../../general/options.md#tick) * [Data structures (`labels`)](../../general/data-structures.md) * [Axes Styling](../../axes/styling.md) * [Tick Configuration](../../axes/styling.md#tick-configuration) ================================================ FILE: docs/samples/scale-options/titles.md ================================================ # Title Configuration This sample shows how to configure the title of an axis including alignment, font, and color. ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, scales: { x: { display: true, title: { display: true, text: 'Month', color: '#911', font: { family: 'Comic Sans MS', size: 20, weight: 'bold', lineHeight: 1.2, }, padding: {top: 20, left: 0, right: 0, bottom: 0} } }, y: { display: true, title: { display: true, text: 'Value', color: '#191', font: { family: 'Times', size: 20, style: 'normal', lineHeight: 1.2 }, padding: {top: 30, left: 0, right: 0, bottom: 0} } } } }, }; // module.exports = { actions: [], config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Axes Styling](../../axes/styling.md) * [Cartesian Axes](../../axes/cartesian/) * [Common options to all cartesian axes](../../axes/cartesian/#common-options-to-all-cartesian-axes) * [Labeling Axes](../../axes/labelling.md) * [Scale Title Configuration](../../axes/labelling.md#scale-title-configuration) ================================================ FILE: docs/samples/scales/linear-min-max-suggested.md ================================================ # Linear Scale - Suggested Min-Max ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: [10, 30, 39, 20, 25, 34, -10], borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.CHART_COLORS.red, }, { label: 'Dataset 2', data: [18, 33, 22, 19, 11, 39, 30], borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.CHART_COLORS.blue, } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Suggested Min and Max Settings' } }, scales: { y: { // the data minimum used for determining the ticks is Math.min(dataMin, suggestedMin) suggestedMin: 30, // the data maximum used for determining the ticks is Math.max(dataMax, suggestedMax) suggestedMax: 50, } } }, }; // module.exports = { config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Axes scales](../../axes/) * [Common options to all axes](../../axes/#common-options-to-all-axes) * [Axis Range Settings](../../axes/#axis-range-settings) ================================================ FILE: docs/samples/scales/linear-min-max.md ================================================ # Linear Scale - Min-Max ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: [10, 30, 50, 20, 25, 44, -10], borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.CHART_COLORS.red, }, { label: 'Dataset 2', data: [100, 33, 22, 19, 11, 49, 30], borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.CHART_COLORS.blue, } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Min and Max Settings' } }, scales: { y: { min: 10, max: 50, } } }, }; // module.exports = { config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Axes scales](../../axes/) * [Common options to all axes (`min`,`max`)](../../axes/#common-options-to-all-axes) ================================================ FILE: docs/samples/scales/linear-step-size.md ================================================ # Linear Scale - Step Size ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); }); chart.update(); } }, { name: 'Add Dataset', handler(chart) { const data = chart.data; const dsColor = Utils.namedColor(chart.data.datasets.length); const newDataset = { label: 'Dataset ' + (data.datasets.length + 1), backgroundColor: dsColor, borderColor: dsColor, data: Utils.numbers({count: data.labels.length, min: 0, max: 100}), }; chart.data.datasets.push(newDataset); chart.update(); } }, { name: 'Add Data', handler(chart) { const data = chart.data; if (data.datasets.length > 0) { data.labels = Utils.months({count: data.labels.length + 1}); for (let index = 0; index < data.datasets.length; ++index) { data.datasets[index].data.push(Utils.rand(0, 100)); } chart.update(); } } }, { name: 'Remove Dataset', handler(chart) { chart.data.datasets.pop(); chart.update(); } }, { name: 'Remove Data', handler(chart) { chart.data.labels.splice(-1, 1); // remove the label first chart.data.datasets.forEach(dataset => { dataset.data.pop(); }); chart.update(); } } ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.CHART_COLORS.red, }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.CHART_COLORS.blue, } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { tooltip: { mode: 'index', intersect: false }, title: { display: true, text: 'Chart.js Line Chart' } }, hover: { mode: 'index', intersect: false }, scales: { x: { title: { display: true, text: 'Month' } }, y: { title: { display: true, text: 'Value' }, min: 0, max: 100, ticks: { // forces step size to be 50 units stepSize: 50 } } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Axes scales](../../axes/) * [Common options to all axes (`min`,`max`)](../../axes/#common-options-to-all-axes) * [Linear Axis](../../axes/cartesian/linear.md) * [Linear Axis specific tick options (`stepSize`)](../../axes/cartesian/linear.md#linear-axis-specific-tick-options) * [Step Size](../../axes/cartesian/linear.md#step-size) ================================================ FILE: docs/samples/scales/log.md ================================================ # Log Scale ```js chart-editor // const logNumbers = (num) => { const data = []; for (let i = 0; i < num; ++i) { data.push(Math.ceil(Math.random() * 10.0) * Math.pow(10, Math.ceil(Math.random() * 5))); } return data; }; const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = logNumbers(chart.data.labels.length); }); chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: logNumbers(DATA_COUNT), borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.CHART_COLORS.red, fill: false, }, ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Chart.js Line Chart - Logarithmic' } }, scales: { x: { display: true, }, y: { display: true, type: 'logarithmic', } } }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [Logarithmic Axis](../../axes/cartesian/logarithmic.md) * [Data structures (`labels`)](../../general/data-structures.md) ================================================ FILE: docs/samples/scales/stacked.md ================================================ # Stacked Linear / Category ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = Utils.months({count: 7}); const data = { labels: labels, datasets: [ { label: 'Dataset 1', data: [10, 30, 50, 20, 25, 44, -10], borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.CHART_COLORS.red, }, { label: 'Dataset 2', data: ['ON', 'ON', 'OFF', 'ON', 'OFF', 'OFF', 'ON'], borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.CHART_COLORS.blue, stepped: true, yAxisID: 'y2', } ] }; // // const config = { type: 'line', data: data, options: { responsive: true, plugins: { title: { display: true, text: 'Stacked scales', }, }, scales: { y: { type: 'linear', position: 'left', stack: 'demo', stackWeight: 2, border: { color: Utils.CHART_COLORS.red } }, y2: { type: 'category', labels: ['ON', 'OFF'], offset: true, position: 'left', stack: 'demo', stackWeight: 1, border: { color: Utils.CHART_COLORS.blue } } } }, }; // module.exports = { config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [Axes scales](../../axes/) * [Stacking](../../axes/#stacking) * [Data structures (`labels`)](../../general/data-structures.md) ================================================ FILE: docs/samples/scales/time-combo.md ================================================ # Time Scale - Combo Chart ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.numbers({count: chart.data.labels.length, min: 0, max: 100}); }); chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const labels = []; for (let i = 0; i < DATA_COUNT; ++i) { labels.push(Utils.newDate(i)); } const data = { labels: labels, datasets: [{ type: 'bar', label: 'Dataset 1', backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), borderColor: Utils.CHART_COLORS.red, data: Utils.numbers(NUMBER_CFG), }, { type: 'bar', label: 'Dataset 2', backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), borderColor: Utils.CHART_COLORS.blue, data: Utils.numbers(NUMBER_CFG), }, { type: 'line', label: 'Dataset 3', backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green, 0.5), borderColor: Utils.CHART_COLORS.green, fill: false, data: Utils.numbers(NUMBER_CFG), }] }; // // const config = { type: 'line', data: data, options: { plugins: { title: { text: 'Chart.js Combo Time Scale', display: true } }, scales: { x: { type: 'time', display: true, offset: true, ticks: { source: 'data' }, time: { unit: 'day' }, }, }, }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Bar](../../charts/bar.md) * [Line](../../charts/line.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Time Scale](../../axes/cartesian/time.md) ================================================ FILE: docs/samples/scales/time-line.md ================================================ # Time Scale ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data.forEach(function(dataObj, j) { const newVal = Utils.rand(0, 100); if (typeof dataObj === 'object') { dataObj.y = newVal; } else { dataset.data[j] = newVal; } }); }); chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: 0, max: 100}; const data = { labels: [ // Date Objects Utils.newDate(0), Utils.newDate(1), Utils.newDate(2), Utils.newDate(3), Utils.newDate(4), Utils.newDate(5), Utils.newDate(6) ], datasets: [{ label: 'My First dataset', backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), borderColor: Utils.CHART_COLORS.red, fill: false, data: Utils.numbers(NUMBER_CFG), }, { label: 'My Second dataset', backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), borderColor: Utils.CHART_COLORS.blue, fill: false, data: Utils.numbers(NUMBER_CFG), }, { label: 'Dataset with point data', backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green, 0.5), borderColor: Utils.CHART_COLORS.green, fill: false, data: [{ x: Utils.newDateString(0), y: Utils.rand(0, 100) }, { x: Utils.newDateString(5), y: Utils.rand(0, 100) }, { x: Utils.newDateString(7), y: Utils.rand(0, 100) }, { x: Utils.newDateString(15), y: Utils.rand(0, 100) }], }] }; // // const config = { type: 'line', data: data, options: { plugins: { title: { text: 'Chart.js Time Scale', display: true } }, scales: { x: { type: 'time', time: { // Luxon format string tooltipFormat: 'DD T' }, title: { display: true, text: 'Date' } }, y: { title: { display: true, text: 'value' } } }, }, }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [Time Cartesian Axis](../../axes/cartesian/time.md) ================================================ FILE: docs/samples/scales/time-max-span.md ================================================ # Time Scale - Max Span ```js chart-editor // const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data.forEach(function(dataObj, j) { const newVal = Utils.rand(0, 100); if (typeof dataObj === 'object') { dataObj.y = newVal; } else { dataset.data[j] = newVal; } }); }); chart.update(); } }, ]; // // const data = { datasets: [{ label: 'Dataset with string point data', backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), borderColor: Utils.CHART_COLORS.red, fill: false, data: [{ x: Utils.newDateString(0), y: Utils.rand(0, 100) }, { x: Utils.newDateString(2), y: Utils.rand(0, 100) }, { x: Utils.newDateString(4), y: Utils.rand(0, 100) }, { x: Utils.newDateString(6), y: Utils.rand(0, 100) }], }, { label: 'Dataset with date object point data', backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), borderColor: Utils.CHART_COLORS.blue, fill: false, data: [{ x: Utils.newDate(0), y: Utils.rand(0, 100) }, { x: Utils.newDate(2), y: Utils.rand(0, 100) }, { x: Utils.newDate(5), y: Utils.rand(0, 100) }, { x: Utils.newDate(6), y: Utils.rand(0, 100) }] }] }; // // const config = { type: 'line', data: data, options: { spanGaps: 1000 * 60 * 60 * 24 * 2, // 2 days responsive: true, interaction: { mode: 'nearest', }, plugins: { title: { display: true, text: 'Chart.js Time - spanGaps: 172800000 (2 days in ms)' }, }, scales: { x: { type: 'time', display: true, title: { display: true, text: 'Date' }, ticks: { autoSkip: false, maxRotation: 0, major: { enabled: true }, // color: function(context) { // return context.tick && context.tick.major ? '#FF0000' : 'rgba(0,0,0,0.1)'; // }, font: function(context) { if (context.tick && context.tick.major) { return { weight: 'bold', }; } } } }, y: { display: true, title: { display: true, text: 'value' } } } }, }; // module.exports = { actions: [], config: config, }; ``` ## Docs * [Line](../../charts/line.md) * [`spanGaps`](../../charts/line.md#line-styling) * [Time Scale](../../axes/cartesian/time.md) ================================================ FILE: docs/samples/scriptable/bar.md ================================================ # Bar Chart Demo selecting bar color based on the bar's y value. ```js chart-editor // const DATA_COUNT = 16; Utils.srand(110); const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = generateData(); }); chart.update(); } }, ]; // // function generateData() { return Utils.numbers({ count: DATA_COUNT, min: -100, max: 100 }); } const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [{ data: generateData(), }] }; // // function colorize(opaque) { return (ctx) => { const v = ctx.parsed.y; const c = v < -50 ? '#D60000' : v < 0 ? '#F46300' : v < 50 ? '#0358B6' : '#44DE28'; return opaque ? c : Utils.transparentize(c, 1 - Math.abs(v / 150)); }; } const config = { type: 'bar', data: data, options: { plugins: { legend: false, }, elements: { bar: { backgroundColor: colorize(false), borderColor: colorize(true), borderWidth: 2 } } } }; // module.exports = { actions, config, }; ``` ## Docs * [Bar](../../charts/bar.md) * [Data structures (`labels`)](../../general/data-structures.md) * [Dataset Configuration (`stack`)](../../general/data-structures.md#dataset-configuration) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) ================================================ FILE: docs/samples/scriptable/bubble.md ================================================ # Bubble Chart ```js chart-editor // const DATA_COUNT = 16; const MIN_XY = -150; const MAX_XY = 100; Utils.srand(110); const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = generateData(); }); chart.update(); } }, ]; // // function generateData() { const data = []; let i; for (i = 0; i < DATA_COUNT; ++i) { data.push({ x: Utils.rand(MIN_XY, MAX_XY), y: Utils.rand(MIN_XY, MAX_XY), v: Utils.rand(0, 1000) }); } return data; } const data = { datasets: [{ data: generateData() }, { data: generateData() }] }; // // function channelValue(x, y, values) { return x < 0 && y < 0 ? values[0] : x < 0 ? values[1] : y < 0 ? values[2] : values[3]; } function colorize(opaque, context) { const value = context.raw; const x = value.x / 100; const y = value.y / 100; const r = channelValue(x, y, [250, 150, 50, 0]); const g = channelValue(x, y, [0, 50, 150, 250]); const b = channelValue(x, y, [0, 150, 150, 250]); const a = opaque ? 1 : 0.5 * value.v / 1000; return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')'; } const config = { type: 'bubble', data: data, options: { aspectRatio: 1, plugins: { legend: false, tooltip: false, }, elements: { point: { backgroundColor: colorize.bind(null, false), borderColor: colorize.bind(null, true), borderWidth: function(context) { return Math.min(Math.max(1, context.datasetIndex + 1), 8); }, hoverBackgroundColor: 'transparent', hoverBorderColor: function(context) { return Utils.color(context.datasetIndex); }, hoverBorderWidth: function(context) { return Math.round(8 * context.raw.v / 1000); }, radius: function(context) { const size = context.chart.width; const base = Math.abs(context.raw.v) / 1000; return (size / 24) * base; } } } } }; // module.exports = { actions, config, }; ``` ## Docs * [Bubble](../../charts/bubble.md) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) ================================================ FILE: docs/samples/scriptable/line.md ================================================ # Line Chart ```js chart-editor // const DATA_COUNT = 12; Utils.srand(110); const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = generateData(); }); chart.update(); } }, ]; // // function generateData() { return Utils.numbers({ count: DATA_COUNT, min: 0, max: 100 }); } const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [{ data: generateData() }] }; // // function getLineColor(ctx) { return Utils.color(ctx.datasetIndex); } function alternatePointStyles(ctx) { const index = ctx.dataIndex; return index % 2 === 0 ? 'circle' : 'rect'; } function makeHalfAsOpaque(ctx) { return Utils.transparentize(getLineColor(ctx)); } function adjustRadiusBasedOnData(ctx) { const v = ctx.parsed.y; return v < 10 ? 5 : v < 25 ? 7 : v < 50 ? 9 : v < 75 ? 11 : 15; } const config = { type: 'line', data: data, options: { plugins: { legend: false, tooltip: true, }, elements: { line: { fill: false, backgroundColor: getLineColor, borderColor: getLineColor, }, point: { backgroundColor: getLineColor, hoverBackgroundColor: makeHalfAsOpaque, radius: adjustRadiusBasedOnData, pointStyle: alternatePointStyles, hoverRadius: 15, } } } }; // module.exports = { actions, config, }; ``` ## Docs * [Line](../../charts/line.md) * [Point Styling](../../charts/line.md#point-styling) * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) * [Data structures (`labels`)](../../general/data-structures.md) ================================================ FILE: docs/samples/scriptable/pie.md ================================================ # Pie Chart ```js chart-editor // const DATA_COUNT = 5; Utils.srand(110); const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = generateData(); }); chart.update(); } }, { name: 'Toggle Doughnut View', handler(chart) { if (chart.options.cutout) { chart.options.cutout = 0; } else { chart.options.cutout = '50%'; } chart.update(); } } ]; // // function generateData() { return Utils.numbers({ count: DATA_COUNT, min: -100, max: 100 }); } const data = { datasets: [{ data: generateData() }] }; // // function colorize(opaque, hover, ctx) { const v = ctx.parsed; const c = v < -50 ? '#D60000' : v < 0 ? '#F46300' : v < 50 ? '#0358B6' : '#44DE28'; const opacity = hover ? 1 - Math.abs(v / 150) - 0.2 : 1 - Math.abs(v / 150); return opaque ? c : Utils.transparentize(c, opacity); } function hoverColorize(ctx) { return colorize(false, true, ctx); } const config = { type: 'pie', data: data, options: { plugins: { legend: false, tooltip: false, }, elements: { arc: { backgroundColor: colorize.bind(null, false, false), hoverBackgroundColor: hoverColorize } } } }; // module.exports = { actions, config, }; ``` ## Docs * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) * [Doughnut and Pie Charts](../../charts/doughnut.md) ================================================ FILE: docs/samples/scriptable/polar.md ================================================ # Polar Area Chart ```js chart-editor // const DATA_COUNT = 7; Utils.srand(110); const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = generateData(); }); chart.update(); } }, ]; // // function generateData() { return Utils.numbers({ count: DATA_COUNT, min: 0, max: 100 }); } const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [{ data: generateData() }] }; // // function colorize(opaque, hover, ctx) { const v = ctx.raw; const c = v < 35 ? '#D60000' : v < 55 ? '#F46300' : v < 75 ? '#0358B6' : '#44DE28'; const opacity = hover ? 1 - Math.abs(v / 150) - 0.2 : 1 - Math.abs(v / 150); return opaque ? c : Utils.transparentize(c, opacity); } function hoverColorize(ctx) { return colorize(false, true, ctx); } const config = { type: 'polarArea', data: data, options: { plugins: { legend: false, tooltip: false, }, elements: { arc: { backgroundColor: colorize.bind(null, false, false), hoverBackgroundColor: hoverColorize } } } }; // module.exports = { actions, config, }; ``` ## Docs * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) * [Polar Area Chart](../../charts/polar.md) ================================================ FILE: docs/samples/scriptable/radar.md ================================================ # Radar Chart ```js chart-editor // const DATA_COUNT = 7; Utils.srand(110); const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = generateData(); }); chart.update(); } }, ]; // // function generateData() { return Utils.numbers({ count: DATA_COUNT, min: 0, max: 100 }); } const data = { labels: [['Eating', 'Dinner'], ['Drinking', 'Water'], 'Sleeping', ['Designing', 'Graphics'], 'Coding', 'Cycling', 'Running'], datasets: [{ data: generateData() }] }; // // function getLineColor(ctx) { return Utils.color(ctx.datasetIndex); } function alternatePointStyles(ctx) { const index = ctx.dataIndex; return index % 2 === 0 ? 'circle' : 'rect'; } function makeHalfAsOpaque(ctx) { return Utils.transparentize(getLineColor(ctx)); } function make20PercentOpaque(ctx) { return Utils.transparentize(getLineColor(ctx), 0.8); } function adjustRadiusBasedOnData(ctx) { const v = ctx.parsed.y; return v < 10 ? 5 : v < 25 ? 7 : v < 50 ? 9 : v < 75 ? 11 : 15; } const config = { type: 'radar', data: data, options: { plugins: { legend: false, tooltip: false, }, elements: { line: { backgroundColor: make20PercentOpaque, borderColor: getLineColor, }, point: { backgroundColor: getLineColor, hoverBackgroundColor: makeHalfAsOpaque, radius: adjustRadiusBasedOnData, pointStyle: alternatePointStyles, hoverRadius: 15, } } } }; // module.exports = { actions, config, }; ``` ## Docs * [Options](../../general/options.md) * [Scriptable Options](../../general/options.md#scriptable-options) * [Radar](../../charts/radar.md) ================================================ FILE: docs/samples/subtitle/basic.md ================================================ # Basic This sample shows basic usage of subtitle. ```js chart-editor // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, ] }; // // const config = { type: 'line', data: data, options: { plugins: { title: { display: true, text: 'Chart Title', }, subtitle: { display: true, text: 'Chart Subtitle', color: 'blue', font: { size: 12, family: 'tahoma', weight: 'normal', style: 'italic' }, padding: { bottom: 10 } } } } }; // module.exports = { config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Title](../../configuration/title.md) * [Subtitle](../../configuration/subtitle.md) ================================================ FILE: docs/samples/title/alignment.md ================================================ # Alignment This sample show how to configure the alignment of the chart title ```js chart-editor // const actions = [ { name: 'Title Alignment: start', handler(chart) { chart.options.plugins.title.align = 'start'; chart.update(); } }, { name: 'Title Alignment: center (default)', handler(chart) { chart.options.plugins.title.align = 'center'; chart.update(); } }, { name: 'Title Alignment: end', handler(chart) { chart.options.plugins.title.align = 'end'; chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, ] }; // // const config = { type: 'line', data: data, options: { plugins: { title: { display: true, text: 'Chart Title', } } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Title](../../configuration/title.md) ================================================ FILE: docs/samples/tooltip/content.md ================================================ # Custom Tooltip Content This sample shows how to use the tooltip callbacks to add additional content to the tooltip. ```js chart-editor // const footer = (tooltipItems) => { let sum = 0; tooltipItems.forEach(function(tooltipItem) { sum += tooltipItem.parsed.y; }); return 'Sum: ' + sum; }; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100, decimals: 0}; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), }, ] }; // // const config = { type: 'line', data: data, options: { interaction: { intersect: false, mode: 'index', }, plugins: { tooltip: { callbacks: { footer: footer, } } } } }; // module.exports = { actions: [], config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Tooltip](../../configuration/tooltip.md) * [Tooltip Callbacks](../../configuration/tooltip.md#tooltip-callbacks) ================================================ FILE: docs/samples/tooltip/html.md ================================================ # External HTML Tooltip This sample shows how to use the external tooltip functionality to generate an HTML tooltip. ```js chart-editor // const getOrCreateTooltip = (chart) => { let tooltipEl = chart.canvas.parentNode.querySelector('div'); if (!tooltipEl) { tooltipEl = document.createElement('div'); tooltipEl.style.background = 'rgba(0, 0, 0, 0.7)'; tooltipEl.style.borderRadius = '3px'; tooltipEl.style.color = 'white'; tooltipEl.style.opacity = 1; tooltipEl.style.pointerEvents = 'none'; tooltipEl.style.position = 'absolute'; tooltipEl.style.transform = 'translate(-50%, 0)'; tooltipEl.style.transition = 'all .1s ease'; const table = document.createElement('table'); table.style.margin = '0px'; tooltipEl.appendChild(table); chart.canvas.parentNode.appendChild(tooltipEl); } return tooltipEl; }; const externalTooltipHandler = (context) => { // Tooltip Element const {chart, tooltip} = context; const tooltipEl = getOrCreateTooltip(chart); // Hide if no tooltip if (tooltip.opacity === 0) { tooltipEl.style.opacity = 0; return; } // Set Text if (tooltip.body) { const titleLines = tooltip.title || []; const bodyLines = tooltip.body.map(b => b.lines); const tableHead = document.createElement('thead'); titleLines.forEach(title => { const tr = document.createElement('tr'); tr.style.borderWidth = 0; const th = document.createElement('th'); th.style.borderWidth = 0; const text = document.createTextNode(title); th.appendChild(text); tr.appendChild(th); tableHead.appendChild(tr); }); const tableBody = document.createElement('tbody'); bodyLines.forEach((body, i) => { const colors = tooltip.labelColors[i]; const span = document.createElement('span'); span.style.background = colors.backgroundColor; span.style.borderColor = colors.borderColor; span.style.borderWidth = '2px'; span.style.marginRight = '10px'; span.style.height = '10px'; span.style.width = '10px'; span.style.display = 'inline-block'; const tr = document.createElement('tr'); tr.style.backgroundColor = 'inherit'; tr.style.borderWidth = 0; const td = document.createElement('td'); td.style.borderWidth = 0; const text = document.createTextNode(body); td.appendChild(span); td.appendChild(text); tr.appendChild(td); tableBody.appendChild(tr); }); const tableRoot = tooltipEl.querySelector('table'); // Remove old children while (tableRoot.firstChild) { tableRoot.firstChild.remove(); } // Add new children tableRoot.appendChild(tableHead); tableRoot.appendChild(tableBody); } const {offsetLeft: positionX, offsetTop: positionY} = chart.canvas; // Display, position, and set styles for font tooltipEl.style.opacity = 1; tooltipEl.style.left = positionX + tooltip.caretX + 'px'; tooltipEl.style.top = positionY + tooltip.caretY + 'px'; tooltipEl.style.font = tooltip.options.bodyFont.string; tooltipEl.style.padding = tooltip.options.padding + 'px ' + tooltip.options.padding + 'px'; }; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100, decimals: 0}; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), }, ] }; // // const config = { type: 'line', data: data, options: { interaction: { mode: 'index', intersect: false, }, plugins: { title: { display: true, text: 'Chart.js Line Chart - External Tooltips' }, tooltip: { enabled: false, position: 'nearest', external: externalTooltipHandler } } } }; // module.exports = { actions: [], config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Tooltip](../../configuration/tooltip.md) * [External (Custom) Tooltips](../../configuration/tooltip.md#external-custom-tooltips) ================================================ FILE: docs/samples/tooltip/interactions.md ================================================ # Interaction Modes This sample shows how to use the tooltip position mode setting. ```js chart-editor // const actions = [ { name: 'Mode: index', handler(chart) { chart.options.interaction.axis = 'xy'; chart.options.interaction.mode = 'index'; chart.update(); } }, { name: 'Mode: dataset', handler(chart) { chart.options.interaction.axis = 'xy'; chart.options.interaction.mode = 'dataset'; chart.update(); } }, { name: 'Mode: point', handler(chart) { chart.options.interaction.axis = 'xy'; chart.options.interaction.mode = 'point'; chart.update(); } }, { name: 'Mode: nearest, axis: xy', handler(chart) { chart.options.interaction.axis = 'xy'; chart.options.interaction.mode = 'nearest'; chart.update(); } }, { name: 'Mode: nearest, axis: x', handler(chart) { chart.options.interaction.axis = 'x'; chart.options.interaction.mode = 'nearest'; chart.update(); } }, { name: 'Mode: nearest, axis: y', handler(chart) { chart.options.interaction.axis = 'y'; chart.options.interaction.mode = 'nearest'; chart.update(); } }, { name: 'Mode: x', handler(chart) { chart.options.interaction.mode = 'x'; chart.update(); } }, { name: 'Mode: y', handler(chart) { chart.options.interaction.mode = 'y'; chart.update(); } }, { name: 'Toggle Intersect', handler(chart) { chart.options.interaction.intersect = !chart.options.interaction.intersect; chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), }, ] }; // // const config = { type: 'line', data: data, options: { interaction: { intersect: false, mode: 'index', }, plugins: { title: { display: true, text: (ctx) => { const {axis = 'xy', intersect, mode} = ctx.chart.options.interaction; return 'Mode: ' + mode + ', axis: ' + axis + ', intersect: ' + intersect; } }, } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Tooltip](../../configuration/tooltip.md) * [Interactions](../../configuration/interactions.md) ================================================ FILE: docs/samples/tooltip/point-style.md ================================================ # Point Style This sample shows how to use the dataset point style in the tooltip instead of a rectangle to identify each dataset. ```js chart-editor // const actions = [ { name: 'Toggle Tooltip Point Style', handler(chart) { chart.options.plugins.tooltip.usePointStyle = !chart.options.plugins.tooltip.usePointStyle; chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Triangles', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), pointStyle: 'triangle', pointRadius: 6, }, { label: 'Circles', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), pointStyle: 'circle', pointRadius: 6, }, { label: 'Stars', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.green, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.green, 0.5), pointStyle: 'star', pointRadius: 6, } ] }; // // const config = { type: 'line', data: data, options: { interaction: { mode: 'index', }, plugins: { title: { display: true, text: (ctx) => 'Tooltip point style: ' + ctx.chart.options.plugins.tooltip.usePointStyle, }, tooltip: { usePointStyle: true, } } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Tooltip](../../configuration/tooltip.md) * `usePointStyle` * [Elements](../../configuration/elements.md) * [Point Styles](../../configuration/elements.md#point-styles) ================================================ FILE: docs/samples/tooltip/position.md ================================================ # Position This sample shows how to use the tooltip position mode setting. ```js chart-editor // const actions = [ { name: 'Position: average', handler(chart) { chart.options.plugins.tooltip.position = 'average'; chart.update(); } }, { name: 'Position: nearest', handler(chart) { chart.options.plugins.tooltip.position = 'nearest'; chart.update(); } }, { name: 'Position: bottom (custom)', handler(chart) { chart.options.plugins.tooltip.position = 'bottom'; chart.update(); } }, ]; // // const DATA_COUNT = 7; const NUMBER_CFG = {count: DATA_COUNT, min: -100, max: 100}; const data = { labels: Utils.months({count: DATA_COUNT}), datasets: [ { label: 'Dataset 1', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.red, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.red, 0.5), }, { label: 'Dataset 2', data: Utils.numbers(NUMBER_CFG), fill: false, borderColor: Utils.CHART_COLORS.blue, backgroundColor: Utils.transparentize(Utils.CHART_COLORS.blue, 0.5), }, ] }; // // // Create a custom tooltip positioner to put at the bottom of the chart area components.Tooltip.positioners.bottom = function(items) { const pos = components.Tooltip.positioners.average(items); // Happens when nothing is found if (pos === false) { return false; } const chart = this.chart; return { x: pos.x, y: chart.chartArea.bottom, xAlign: 'center', yAlign: 'bottom', }; }; // // const config = { type: 'line', data: data, options: { interaction: { intersect: false, mode: 'index', }, plugins: { title: { display: true, text: (ctx) => 'Tooltip position mode: ' + ctx.chart.options.plugins.tooltip.position, }, } } }; // module.exports = { actions: actions, config: config, }; ``` ## Docs * [Data structures (`labels`)](../../general/data-structures.md) * [Line](../../charts/line.md) * [Tooltip](../../configuration/tooltip.md) * [Position Modes](../../configuration/tooltip.md#position-modes) * [Custom Position Modes](../../configuration/tooltip.md#custom-position-modes) ================================================ FILE: docs/samples/utils.md ================================================ # Utils ## Disclaimer The Utils file contains multiple helper functions that the chart.js sample pages use to generate charts. These functions are subject to change, including but not limited to breaking changes without prior notice. Because of this please don't rely on this file in production environments. ## Functions <<< @/scripts/utils.js [File on github](https://github.com/chartjs/Chart.js/blob/master/docs/scripts/utils.js) ## Components Some of the samples make reference to a `components` object. This is an artifact of using a module bundler to build the samples. The creation of that components object is shown below. If chart.js is included as a browser script, these items are accessible via the `Chart` object, i.e `Chart.Tooltip`. <<< @/scripts/components.js [File on github](https://github.com/chartjs/Chart.js/blob/master/docs/scripts/components.js) ================================================ FILE: docs/scripts/analyzer.js ================================================ export default { id: 'samples-filler-analyser', beforeInit: function(chart, args, options) { this.element = document.getElementById(options.target); }, afterUpdate: function(chart) { var datasets = chart.data.datasets; var element = this.element; var stats = []; var meta, i, ilen, dataset; if (!element) { return; } for (i = 0, ilen = datasets.length; i < ilen; ++i) { meta = chart.getDatasetMeta(i).$filler; if (meta) { dataset = datasets[i]; stats.push({ fill: dataset.fill, target: meta.fill, visible: meta.visible, index: i }); } } this.element.innerHTML = '' + '' + '' + '' + '' + '' + stats.map(function(stat) { var target = stat.target; var row = '' + ''; if (target === false) { target = 'none'; } else if (isFinite(target)) { target = 'dataset ' + target; } else { target = 'boundary "' + target + '"'; } if (stat.visible) { row += ''; } else { row += ''; } return '' + row + ''; }).join('') + '
DatasetFillTarget (visibility)
' + stat.index + '' + JSON.stringify(stat.fill) + '' + target + '(hidden)
'; } }; ================================================ FILE: docs/scripts/components.js ================================================ // Add Chart components needed in samples here. // Usable through `components[name]`. export {Tooltip} from '../../dist/chart.js'; ================================================ FILE: docs/scripts/derived-bubble.js ================================================ import {Chart, BubbleController} from 'chart.js'; class Custom extends BubbleController { draw() { // Call bubble controller method to draw all the points super.draw(arguments); // Now we can do some custom drawing for this dataset. // Here we'll draw a box around the first point in each dataset, // using `boxStrokeStyle` dataset option for color var meta = this.getMeta(); var pt0 = meta.data[0]; const {x, y} = pt0.getProps(['x', 'y']); const {radius} = pt0.options; var ctx = this.chart.ctx; ctx.save(); ctx.strokeStyle = this.options.boxStrokeStyle; ctx.lineWidth = 1; ctx.strokeRect(x - radius, y - radius, 2 * radius, 2 * radius); ctx.restore(); } } Custom.id = 'derivedBubble'; Custom.defaults = { // Custom defaults. Bubble defaults are inherited. boxStrokeStyle: 'red' }; // Overrides are only inherited, but not merged if defined // Custom.overrides = Chart.overrides.bubble; // Stores the controller so that the chart initialization routine can look it up Chart.register(Custom); ================================================ FILE: docs/scripts/helpers.js ================================================ // Add helpers needed in samples here. // Usable through `helpers[name]`. export {color, getHoverColor, easingEffects} from '../../dist/helpers.js'; ================================================ FILE: docs/scripts/log2.js ================================================ import {Scale, LinearScale} from 'chart.js'; export default class Log2Axis extends Scale { constructor(cfg) { super(cfg); this._startValue = undefined; this._valueRange = 0; } parse(raw, index) { const value = LinearScale.prototype.parse.apply(this, [raw, index]); return isFinite(value) && value > 0 ? value : null; } determineDataLimits() { const {min, max} = this.getMinMax(true); this.min = isFinite(min) ? Math.max(0, min) : null; this.max = isFinite(max) ? Math.max(0, max) : null; } buildTicks() { const ticks = []; let power = Math.floor(Math.log2(this.min || 1)); let maxPower = Math.ceil(Math.log2(this.max || 2)); while (power <= maxPower) { ticks.push({value: Math.pow(2, power)}); power += 1; } this.min = ticks[0].value; this.max = ticks[ticks.length - 1].value; return ticks; } /** * @protected */ configure() { const start = this.min; super.configure(); this._startValue = Math.log2(start); this._valueRange = Math.log2(this.max) - Math.log2(start); } getPixelForValue(value) { if (value === undefined || value === 0) { value = this.min; } return this.getPixelForDecimal(value === this.min ? 0 : (Math.log2(value) - this._startValue) / this._valueRange); } getValueForPixel(pixel) { const decimal = this.getDecimalForPixel(pixel); return Math.pow(2, this._startValue + decimal * this._valueRange); } } Log2Axis.id = 'log2'; Log2Axis.defaults = {}; // The derived axis is registered like this: // Chart.register(Log2Axis); ================================================ FILE: docs/scripts/register.js ================================================ import {Chart, registerables} from '../../dist/chart.js'; import Log2Axis from './log2'; import './derived-bubble'; import analyzer from './analyzer'; Chart.register(...registerables); Chart.register(Log2Axis); Chart.register(analyzer); ================================================ FILE: docs/scripts/utils.js ================================================ import colorLib from '@kurkle/color'; import {DateTime} from 'luxon'; import 'chartjs-adapter-luxon'; import {valueOrDefault} from '../../dist/helpers.js'; // Adapted from http://indiegamr.com/generate-repeatable-random-numbers-in-js/ var _seed = Date.now(); export function srand(seed) { _seed = seed; } export function rand(min, max) { min = valueOrDefault(min, 0); max = valueOrDefault(max, 0); _seed = (_seed * 9301 + 49297) % 233280; return min + (_seed / 233280) * (max - min); } export function numbers(config) { var cfg = config || {}; var min = valueOrDefault(cfg.min, 0); var max = valueOrDefault(cfg.max, 100); var from = valueOrDefault(cfg.from, []); var count = valueOrDefault(cfg.count, 8); var decimals = valueOrDefault(cfg.decimals, 8); var continuity = valueOrDefault(cfg.continuity, 1); var dfactor = Math.pow(10, decimals) || 0; var data = []; var i, value; for (i = 0; i < count; ++i) { value = (from[i] || 0) + this.rand(min, max); if (this.rand() <= continuity) { data.push(Math.round(dfactor * value) / dfactor); } else { data.push(null); } } return data; } export function points(config) { const xs = this.numbers(config); const ys = this.numbers(config); return xs.map((x, i) => ({x, y: ys[i]})); } export function bubbles(config) { return this.points(config).map(pt => { pt.r = this.rand(config.rmin, config.rmax); return pt; }); } export function labels(config) { var cfg = config || {}; var min = cfg.min || 0; var max = cfg.max || 100; var count = cfg.count || 8; var step = (max - min) / count; var decimals = cfg.decimals || 8; var dfactor = Math.pow(10, decimals) || 0; var prefix = cfg.prefix || ''; var values = []; var i; for (i = min; i < max; i += step) { values.push(prefix + Math.round(dfactor * i) / dfactor); } return values; } const MONTHS = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; export function months(config) { var cfg = config || {}; var count = cfg.count || 12; var section = cfg.section; var values = []; var i, value; for (i = 0; i < count; ++i) { value = MONTHS[Math.ceil(i) % 12]; values.push(value.substring(0, section)); } return values; } const COLORS = [ '#4dc9f6', '#f67019', '#f53794', '#537bc4', '#acc236', '#166a8f', '#00a950', '#58595b', '#8549ba' ]; export function color(index) { return COLORS[index % COLORS.length]; } export function transparentize(value, opacity) { var alpha = opacity === undefined ? 0.5 : 1 - opacity; return colorLib(value).alpha(alpha).rgbString(); } export const CHART_COLORS = { red: 'rgb(255, 99, 132)', orange: 'rgb(255, 159, 64)', yellow: 'rgb(255, 205, 86)', green: 'rgb(75, 192, 192)', blue: 'rgb(54, 162, 235)', purple: 'rgb(153, 102, 255)', grey: 'rgb(201, 203, 207)' }; const NAMED_COLORS = [ CHART_COLORS.red, CHART_COLORS.orange, CHART_COLORS.yellow, CHART_COLORS.green, CHART_COLORS.blue, CHART_COLORS.purple, CHART_COLORS.grey, ]; export function namedColor(index) { return NAMED_COLORS[index % NAMED_COLORS.length]; } export function newDate(days) { return DateTime.now().plus({days}).toJSDate(); } export function newDateString(days) { return DateTime.now().plus({days}).toISO(); } export function parseISODate(str) { return DateTime.fromISO(str); } ================================================ FILE: helpers/helpers.cjs ================================================ module.exports = require('../dist/helpers.cjs'); ================================================ FILE: helpers/helpers.d.ts ================================================ export * from '../dist/helpers/index.js'; ================================================ FILE: helpers/helpers.js ================================================ export * from '../dist/helpers.js'; ================================================ FILE: helpers/package.json ================================================ { "name": "chart.js-helpers", "private": true, "description": "Helpers package. Exists to support bundlers without exports support such as webpack 4.", "type": "module", "main": "./helpers.cjs", "module": "./helpers.js", "exports": { "types": "./helpers.d.ts", "import": "./helpers.js", "require": "./helpers.cjs" }, "types": "./helpers.d.ts" } ================================================ FILE: karma.conf.cjs ================================================ /* eslint-disable global-require */ const jasmineSeedReporter = require('./test/seed-reporter.cjs'); const commonjs = require('@rollup/plugin-commonjs'); const istanbul = require('rollup-plugin-istanbul'); const json = require('@rollup/plugin-json'); const resolve = require('@rollup/plugin-node-resolve').default; const yargs = require('yargs'); module.exports = async function(karma) { const builds = (await import('./rollup.config.js')).default; const args = yargs .option('verbose', {default: false}) .argv; const grep = (args.grep === true || args.grep === undefined) ? '' : args.grep; const specPattern = 'test/specs/**/*' + grep + '*.js'; // Use the same rollup config as our dist files: when debugging (npm run dev), // we will prefer the unminified build which is easier to browse and works // better with source mapping. In other cases, pick the minified build to // make sure that the minification process (terser) doesn't break anything. const regex = /chart\.umd(\.min)?\.js$/; const build = builds.filter(v => v.output.file && v.output.file.match(regex))[0]; if (karma.autoWatch) { build.plugins.pop(); } if (args.coverage) { build.plugins.push( istanbul({exclude: ['node_modules/**/*.js', 'package.json']}) ); } // workaround a karma bug where it doesn't resolve dependencies correctly in // the same way that Node does // https://github.com/pnpm/pnpm/issues/720#issuecomment-954120387 const plugins = Object.keys(require('./package').devDependencies).flatMap( (packageName) => { if (!packageName.startsWith('karma-')) { return []; } return [require(packageName)]; } ); plugins.push(jasmineSeedReporter); karma.set({ frameworks: ['jasmine'], plugins, reporters: ['spec', 'kjhtml', 'jasmine-seed'], browsers: (args.browsers || 'chrome,firefox').split(','), logLevel: karma.LOG_INFO, client: { jasmine: { stopOnSpecFailure: !!karma.autoWatch } }, specReporter: { // maxLogLines: 5, // limit number of lines logged per test suppressErrorSummary: true, // do not print error summary suppressFailed: false, // do not print information about failed tests suppressPassed: true, // do not print information about passed tests suppressSkipped: false, // do not print information about skipped tests showSpecTiming: false, // print the time elapsed for each spec failFast: false // test would finish with error when a first fail occurs. }, // Explicitly disable hardware acceleration to make image // diff more stable when ran on Travis and dev machine. // https://github.com/chartjs/Chart.js/pull/5629 // Since FF 110 https://github.com/chartjs/Chart.js/issues/11164 customLaunchers: { chrome: { base: 'Chrome', flags: [ '--disable-accelerated-2d-canvas', '--disable-background-timer-throttling', '--disable-backgrounding-occluded-windows', '--disable-renderer-backgrounding' ] }, firefox: { base: 'Firefox', prefs: { 'layers.acceleration.disabled': true, 'gfx.canvas.accelerated': false } }, safari: { base: 'SafariPrivate' }, edge: { base: 'Edge' } }, files: [ {pattern: 'test/fixtures/**/*.js', included: false}, {pattern: 'test/fixtures/**/*.json', included: false}, {pattern: 'test/fixtures/**/*.png', included: false}, 'node_modules/moment/min/moment.min.js', 'node_modules/moment-timezone/builds/moment-timezone-with-data.min.js', {pattern: 'test/index.js', watched: false}, {pattern: 'test/BasicChartWebWorker.js', included: false}, {pattern: 'src/index.umd.ts', watched: false}, 'node_modules/chartjs-adapter-moment/dist/chartjs-adapter-moment.js', {pattern: specPattern} ], preprocessors: { 'test/index.js': ['rollup'], 'src/index.umd.ts': ['sources'] }, rollupPreprocessor: { plugins: [ json(), resolve(), commonjs({exclude: ['src/**', 'test/**']}), ], output: { name: 'test', format: 'umd', sourcemap: karma.autoWatch ? 'inline' : false } }, customPreprocessors: { sources: { base: 'rollup', options: build } }, // These settings deal with browser disconnects. We had seen test flakiness from Firefox // [Firefox 56.0.0 (Linux 0.0.0)]: Disconnected (1 times), because no message in 10000 ms. // https://github.com/jasmine/jasmine/issues/1327#issuecomment-332939551 browserDisconnectTolerance: 3 }); if (args.coverage) { karma.reporters.push('coverage'); karma.coverageReporter = { dir: 'coverage/', reporters: [ {type: 'html', subdir: 'html'}, {type: 'lcovonly', subdir: (browser) => browser.toLowerCase().split(/[ /-]/)[0]} ] }; } }; ================================================ FILE: package.json ================================================ { "name": "chart.js", "homepage": "https://www.chartjs.org", "description": "Simple HTML5 charts using the canvas element.", "version": "4.5.1", "license": "MIT", "type": "module", "sideEffects": [ "./auto/auto.js", "./auto/auto.cjs", "./dist/chart.umd.min.js", "./dist/chart.umd.js" ], "jsdelivr": "./dist/chart.umd.min.js", "unpkg": "./dist/chart.umd.min.js", "main": "./dist/chart.cjs", "module": "./dist/chart.js", "exports": { ".": { "types": "./dist/types.d.ts", "import": "./dist/chart.js", "require": "./dist/chart.cjs" }, "./auto": { "types": "./auto/auto.d.ts", "import": "./auto/auto.js", "require": "./auto/auto.cjs" }, "./helpers": { "types": "./helpers/helpers.d.ts", "import": "./helpers/helpers.js", "require": "./helpers/helpers.cjs" } }, "types": "./dist/types.d.ts", "keywords": [ "canvas", "charts", "data", "graphs", "html5", "responsive" ], "repository": { "type": "git", "url": "https://github.com/chartjs/Chart.js.git" }, "bugs": { "url": "https://github.com/chartjs/Chart.js/issues" }, "files": [ "auto/**", "dist/**", "!dist/docs/**", "helpers/**" ], "scripts": { "autobuild": "rollup -c -w", "copyDeclarations": "node -e \"fs.cpSync('./src/types/', './dist/types/', {recursive:true})\"", "emitDeclarations": "tsc --emitDeclarationOnly && pnpm copyDeclarations", "build": "rollup -c && pnpm emitDeclarations", "dev": "karma start ./karma.conf.cjs --auto-watch --no-single-run --browsers chrome --grep", "dev:ff": "karma start ./karma.conf.cjs --auto-watch --no-single-run --browsers firefox --grep", "docs": "pnpm run build && pnpm --filter \"./docs/**\" build", "docs:dev": "pnpm run build && pnpm --filter \"./docs/**\" dev", "lint-js": "eslint \"src/**/*.{js,ts}\" \"test/**/*.js\" \"docs/**/*.js\" --cache", "lint-md": "eslint \"**/*.md\" --cache", "lint-types": "pnpm build && node test/types/autogen.js && tsc -p test/types", "lint": "concurrently \"pnpm:lint-*\"", "test": "pnpm lint && pnpm test-ci", "test-ci": "concurrently \"pnpm:test-ci-*\"", "test-ci-karma": "cross-env NODE_ENV=test karma start ./karma.conf.cjs --auto-watch --single-run --coverage --grep", "test-ci-integration": "pnpm --filter \"./test/integration/**\" test" }, "dependencies": { "@kurkle/color": "^0.3.0" }, "devDependencies": { "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-inject": "^5.0.2", "@rollup/plugin-json": "^5.0.1", "@rollup/plugin-node-resolve": "^15.0.1", "@swc/core": "^1.3.18", "@types/estree": "^1.0.0", "@types/offscreencanvas": "^2019.7.0", "@typescript-eslint/eslint-plugin": "^5.32.0", "@typescript-eslint/parser": "^5.32.0", "chartjs-adapter-luxon": "^1.2.0", "chartjs-adapter-moment": "^1.0.0", "chartjs-test-utils": "^0.4.0", "concurrently": "^7.3.0", "coveralls": "^3.1.1", "cross-env": "^7.0.3", "eslint": "^8.21.0", "eslint-config-chartjs": "^0.3.0", "eslint-plugin-es": "^4.1.0", "eslint-plugin-html": "^7.1.0", "eslint-plugin-markdown": "^3.0.0", "esm": "^3.2.25", "glob": "^8.0.3", "jasmine": "^3.7.0", "jasmine-core": "^3.7.1", "karma": "^6.3.2", "karma-chrome-launcher": "^3.1.0", "karma-coverage": "^2.0.3", "karma-edge-launcher": "^0.4.2", "karma-firefox-launcher": "^2.1.0", "karma-jasmine": "^4.0.1", "karma-jasmine-html-reporter": "^1.5.4", "karma-rollup-preprocessor": "7.0.7", "karma-safari-private-launcher": "^1.0.0", "karma-spec-reporter": "0.0.32", "luxon": "^3.0.1", "moment": "^2.29.4", "moment-timezone": "^0.5.34", "pixelmatch": "^5.3.0", "rollup": "^3.3.0", "rollup-plugin-cleanup": "^3.2.1", "rollup-plugin-istanbul": "^4.0.0", "rollup-plugin-swc3": "^0.7.0", "rollup-plugin-terser": "^7.0.2", "typescript": "^4.7.4", "yargs": "^17.5.1" }, "engines": { "pnpm": ">=8" }, "packageManager": "pnpm@8.13.0", "pnpm": { "overrides": { "html-entities": "1.4.0" }, "peerDependencyRules": { "ignoreMissing": [ "chart.js" ] } } } ================================================ FILE: pnpm-workspace.yaml ================================================ packages: - 'docs' - 'test/integration/*' ================================================ FILE: rollup.config.js ================================================ import cleanup from 'rollup-plugin-cleanup'; import json from '@rollup/plugin-json'; import resolve from '@rollup/plugin-node-resolve'; import {swc} from 'rollup-plugin-swc3'; import {terser} from 'rollup-plugin-terser'; import {readFileSync} from 'fs'; const {version, homepage} = JSON.parse(readFileSync('./package.json')); const banner = `/*! * Chart.js v${version} * ${homepage} * (c) ${(new Date(process.env.SOURCE_DATE_EPOCH ? (process.env.SOURCE_DATE_EPOCH * 1000) : new Date().getTime())).getFullYear()} Chart.js Contributors * Released under the MIT License */`; const extensions = ['.js', '.ts']; const plugins = (minify) => [ json(), resolve({ extensions }), swc({ jsc: { parser: { syntax: 'typescript' }, target: 'es2022' }, module: { type: 'es6' }, sourceMaps: true }), minify ? terser({ output: { preamble: banner } }) : cleanup({ comments: ['some', /__PURE__/] }) ]; export default [ // UMD build // dist/chart.umd.min.js { input: 'src/index.umd.ts', plugins: plugins(true), output: { name: 'Chart', file: 'dist/chart.umd.min.js', format: 'umd', indent: false, sourcemap: true, }, }, // UMD build // dist/chart.umd.js (old filename) { input: 'src/index.umd.ts', plugins: plugins(true), output: { name: 'Chart', file: 'dist/chart.umd.js', format: 'umd', indent: false, sourcemap: true, }, }, // ES6 builds // dist/chart.js // helpers/*.js { input: { 'dist/chart': 'src/index.ts', 'dist/helpers': 'src/helpers/index.ts' }, plugins: plugins(), external: _ => (/node_modules/).test(_), output: { dir: './', chunkFileNames: 'dist/chunks/[name].js', entryFileNames: '[name].js', banner, format: 'esm', indent: false, sourcemap: true, }, }, // CommonJS builds // dist/chart.js // helpers/*.js { input: { 'dist/chart': 'src/index.ts', 'dist/helpers': 'src/helpers/index.ts' }, plugins: plugins(), external: _ => (/node_modules/).test(_), output: { dir: './', chunkFileNames: 'dist/chunks/[name].cjs', entryFileNames: '[name].cjs', banner, format: 'commonjs', indent: false, sourcemap: true, }, } ]; ================================================ FILE: scripts/deploy-docs.sh ================================================ #!/bin/bash set -e source ./scripts/utils.sh TARGET_DIR='gh-pages' TARGET_BRANCH='master' TARGET_REPO_URL="https://$GITHUB_TOKEN@github.com/chartjs/chartjs.github.io.git" VERSION=$1 MODE=$2 TAG=$(tag_from_version "$VERSION" "$MODE") function move_sample_redirect { local tag=$1 cp ../scripts/sample-redirect-template.html samples/$tag/index.html sed -i -E "s/TAG/$tag/g" samples/$tag/index.html } function deploy_tagged_files { local tag=$1 rm -rf "docs/$tag" cp -r ../dist/docs docs/$tag rm -rf "samples/$tag" mkdir "samples/$tag" move_sample_redirect $tag deploy_versioned_files $tag } function deploy_versioned_files { local version=$1 local in_files='../dist/chart*.js' local out_path='./dist' rm -rf $out_path/$version mkdir -p $out_path/$version cp -r $in_files $out_path/$version } # Clone the repository and checkout the gh-pages branch git clone $TARGET_REPO_URL $TARGET_DIR cd $TARGET_DIR git checkout $TARGET_BRANCH # https://www.chartjs.org/dist/$VERSION if [["$VERSION" != "$TAG"]]; then deploy_versioned_files $VERSION fi # https://www.chartjs.org/dist/$TAG # https://www.chartjs.org/docs/$TAG # https://www.chartjs.org/samples/$TAG deploy_tagged_files $TAG git add --all git remote add auth-origin $TARGET_REPO_URL git config --global user.email "$GH_AUTH_EMAIL" git config --global user.name "Chart.js" git commit -m "Deploy $VERSION from $GITHUB_REPOSITORY" -m "Commit: $GITHUB_SHA" git push -q auth-origin $TARGET_BRANCH git remote rm auth-origin # Cleanup cd .. rm -rf $TARGET_DIR ================================================ FILE: scripts/docs-config.sh ================================================ #!/bin/bash set -e source ./scripts/utils.sh VERSION=$1 MODE=$2 TAG=$(tag_from_version "$VERSION" "$MODE") sed -i -e "s/VERSION/$TAG/g" "docs/.vuepress/config.ts" ================================================ FILE: scripts/publish.sh ================================================ #!/bin/bash set -e NPM_TAG="next" if [[ "$VERSION" =~ ^[^-]+$ ]]; then echo "Release tag indicates a full release. Releasing as \"latest\"." NPM_TAG="latest" fi npm publish --tag "$NPM_TAG" ================================================ FILE: scripts/sample-redirect-template.html ================================================ Chart.js | Samples ================================================ FILE: scripts/utils.sh ================================================ #!/bin/bash # tag is next|latest|master|x.x.x # https://www.chartjs.org/dist/$tag/ # https://www.chartjs.org/docs/$tag/ # https://www.chartjs.org/samples/$tag/ function tag_from_version { local version=$1 local mode=$2 local tag='' if [ "$version" == "master" ]; then tag=master elif [[ "$version" =~ ^[^-]+$ ]]; then if [[ "$mode" == "release" ]]; then tag=$version else tag=latest fi else tag=next fi echo $tag } ================================================ FILE: src/controllers/controller.bar.js ================================================ import DatasetController from '../core/core.datasetController.js'; import { _arrayUnique, isArray, isNullOrUndef, valueOrDefault, resolveObjectKey, sign, defined } from '../helpers/index.js'; function getAllScaleValues(scale, type) { if (!scale._cache.$bar) { const visibleMetas = scale.getMatchingVisibleMetas(type); let values = []; for (let i = 0, ilen = visibleMetas.length; i < ilen; i++) { values = values.concat(visibleMetas[i].controller.getAllParsedValues(scale)); } scale._cache.$bar = _arrayUnique(values.sort((a, b) => a - b)); } return scale._cache.$bar; } /** * Computes the "optimal" sample size to maintain bars equally sized while preventing overlap. * @private */ function computeMinSampleSize(meta) { const scale = meta.iScale; const values = getAllScaleValues(scale, meta.type); let min = scale._length; let i, ilen, curr, prev; const updateMinAndPrev = () => { if (curr === 32767 || curr === -32768) { // Ignore truncated pixels return; } if (defined(prev)) { // curr - prev === 0 is ignored min = Math.min(min, Math.abs(curr - prev) || min); } prev = curr; }; for (i = 0, ilen = values.length; i < ilen; ++i) { curr = scale.getPixelForValue(values[i]); updateMinAndPrev(); } prev = undefined; for (i = 0, ilen = scale.ticks.length; i < ilen; ++i) { curr = scale.getPixelForTick(i); updateMinAndPrev(); } return min; } /** * Computes an "ideal" category based on the absolute bar thickness or, if undefined or null, * uses the smallest interval (see computeMinSampleSize) that prevents bar overlapping. This * mode currently always generates bars equally sized (until we introduce scriptable options?). * @private */ function computeFitCategoryTraits(index, ruler, options, stackCount) { const thickness = options.barThickness; let size, ratio; if (isNullOrUndef(thickness)) { size = ruler.min * options.categoryPercentage; ratio = options.barPercentage; } else { // When bar thickness is enforced, category and bar percentages are ignored. // Note(SB): we could add support for relative bar thickness (e.g. barThickness: '50%') // and deprecate barPercentage since this value is ignored when thickness is absolute. size = thickness * stackCount; ratio = 1; } return { chunk: size / stackCount, ratio, start: ruler.pixels[index] - (size / 2) }; } /** * Computes an "optimal" category that globally arranges bars side by side (no gap when * percentage options are 1), based on the previous and following categories. This mode * generates bars with different widths when data are not evenly spaced. * @private */ function computeFlexCategoryTraits(index, ruler, options, stackCount) { const pixels = ruler.pixels; const curr = pixels[index]; let prev = index > 0 ? pixels[index - 1] : null; let next = index < pixels.length - 1 ? pixels[index + 1] : null; const percent = options.categoryPercentage; if (prev === null) { // first data: its size is double based on the next point or, // if it's also the last data, we use the scale size. prev = curr - (next === null ? ruler.end - ruler.start : next - curr); } if (next === null) { // last data: its size is also double based on the previous point. next = curr + curr - prev; } const start = curr - (curr - Math.min(prev, next)) / 2 * percent; const size = Math.abs(next - prev) / 2 * percent; return { chunk: size / stackCount, ratio: options.barPercentage, start }; } function parseFloatBar(entry, item, vScale, i) { const startValue = vScale.parse(entry[0], i); const endValue = vScale.parse(entry[1], i); const min = Math.min(startValue, endValue); const max = Math.max(startValue, endValue); let barStart = min; let barEnd = max; if (Math.abs(min) > Math.abs(max)) { barStart = max; barEnd = min; } // Store `barEnd` (furthest away from origin) as parsed value, // to make stacking straight forward item[vScale.axis] = barEnd; item._custom = { barStart, barEnd, start: startValue, end: endValue, min, max }; } function parseValue(entry, item, vScale, i) { if (isArray(entry)) { parseFloatBar(entry, item, vScale, i); } else { item[vScale.axis] = vScale.parse(entry, i); } return item; } function parseArrayOrPrimitive(meta, data, start, count) { const iScale = meta.iScale; const vScale = meta.vScale; const labels = iScale.getLabels(); const singleScale = iScale === vScale; const parsed = []; let i, ilen, item, entry; for (i = start, ilen = start + count; i < ilen; ++i) { entry = data[i]; item = {}; item[iScale.axis] = singleScale || iScale.parse(labels[i], i); parsed.push(parseValue(entry, item, vScale, i)); } return parsed; } function isFloatBar(custom) { return custom && custom.barStart !== undefined && custom.barEnd !== undefined; } function barSign(size, vScale, actualBase) { if (size !== 0) { return sign(size); } return (vScale.isHorizontal() ? 1 : -1) * (vScale.min >= actualBase ? 1 : -1); } function borderProps(properties) { let reverse, start, end, top, bottom; if (properties.horizontal) { reverse = properties.base > properties.x; start = 'left'; end = 'right'; } else { reverse = properties.base < properties.y; start = 'bottom'; end = 'top'; } if (reverse) { top = 'end'; bottom = 'start'; } else { top = 'start'; bottom = 'end'; } return {start, end, reverse, top, bottom}; } function setBorderSkipped(properties, options, stack, index) { let edge = options.borderSkipped; const res = {}; if (!edge) { properties.borderSkipped = res; return; } if (edge === true) { properties.borderSkipped = {top: true, right: true, bottom: true, left: true}; return; } const {start, end, reverse, top, bottom} = borderProps(properties); if (edge === 'middle' && stack) { properties.enableBorderRadius = true; if ((stack._top || 0) === index) { edge = top; } else if ((stack._bottom || 0) === index) { edge = bottom; } else { res[parseEdge(bottom, start, end, reverse)] = true; edge = top; } } res[parseEdge(edge, start, end, reverse)] = true; properties.borderSkipped = res; } function parseEdge(edge, a, b, reverse) { if (reverse) { edge = swap(edge, a, b); edge = startEnd(edge, b, a); } else { edge = startEnd(edge, a, b); } return edge; } function swap(orig, v1, v2) { return orig === v1 ? v2 : orig === v2 ? v1 : orig; } function startEnd(v, start, end) { return v === 'start' ? start : v === 'end' ? end : v; } function setInflateAmount(properties, {inflateAmount}, ratio) { properties.inflateAmount = inflateAmount === 'auto' ? ratio === 1 ? 0.33 : 0 : inflateAmount; } export default class BarController extends DatasetController { static id = 'bar'; /** * @type {any} */ static defaults = { datasetElementType: false, dataElementType: 'bar', categoryPercentage: 0.8, barPercentage: 0.9, grouped: true, animations: { numbers: { type: 'number', properties: ['x', 'y', 'base', 'width', 'height'] } } }; /** * @type {any} */ static overrides = { scales: { _index_: { type: 'category', offset: true, grid: { offset: true } }, _value_: { type: 'linear', beginAtZero: true, } } }; /** * Overriding primitive data parsing since we support mixed primitive/array * data for float bars * @protected */ parsePrimitiveData(meta, data, start, count) { return parseArrayOrPrimitive(meta, data, start, count); } /** * Overriding array data parsing since we support mixed primitive/array * data for float bars * @protected */ parseArrayData(meta, data, start, count) { return parseArrayOrPrimitive(meta, data, start, count); } /** * Overriding object data parsing since we support mixed primitive/array * value-scale data for float bars * @protected */ parseObjectData(meta, data, start, count) { const {iScale, vScale} = meta; const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; const iAxisKey = iScale.axis === 'x' ? xAxisKey : yAxisKey; const vAxisKey = vScale.axis === 'x' ? xAxisKey : yAxisKey; const parsed = []; let i, ilen, item, obj; for (i = start, ilen = start + count; i < ilen; ++i) { obj = data[i]; item = {}; item[iScale.axis] = iScale.parse(resolveObjectKey(obj, iAxisKey), i); parsed.push(parseValue(resolveObjectKey(obj, vAxisKey), item, vScale, i)); } return parsed; } /** * @protected */ updateRangeFromParsed(range, scale, parsed, stack) { super.updateRangeFromParsed(range, scale, parsed, stack); const custom = parsed._custom; if (custom && scale === this._cachedMeta.vScale) { // float bar: only one end of the bar is considered by `super` range.min = Math.min(range.min, custom.min); range.max = Math.max(range.max, custom.max); } } /** * @return {number|boolean} * @protected */ getMaxOverflow() { return 0; } /** * @protected */ getLabelAndValue(index) { const meta = this._cachedMeta; const {iScale, vScale} = meta; const parsed = this.getParsed(index); const custom = parsed._custom; const value = isFloatBar(custom) ? '[' + custom.start + ', ' + custom.end + ']' : '' + vScale.getLabelForValue(parsed[vScale.axis]); return { label: '' + iScale.getLabelForValue(parsed[iScale.axis]), value }; } initialize() { this.enableOptionSharing = true; super.initialize(); const meta = this._cachedMeta; meta.stack = this.getDataset().stack; } update(mode) { const meta = this._cachedMeta; this.updateElements(meta.data, 0, meta.data.length, mode); } updateElements(bars, start, count, mode) { const reset = mode === 'reset'; const {index, _cachedMeta: {vScale}} = this; const base = vScale.getBasePixel(); const horizontal = vScale.isHorizontal(); const ruler = this._getRuler(); const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode); for (let i = start; i < start + count; i++) { const parsed = this.getParsed(i); const vpixels = reset || isNullOrUndef(parsed[vScale.axis]) ? {base, head: base} : this._calculateBarValuePixels(i); const ipixels = this._calculateBarIndexPixels(i, ruler); const stack = (parsed._stacks || {})[vScale.axis]; const properties = { horizontal, base: vpixels.base, enableBorderRadius: !stack || isFloatBar(parsed._custom) || (index === stack._top || index === stack._bottom), x: horizontal ? vpixels.head : ipixels.center, y: horizontal ? ipixels.center : vpixels.head, height: horizontal ? ipixels.size : Math.abs(vpixels.size), width: horizontal ? Math.abs(vpixels.size) : ipixels.size }; if (includeOptions) { properties.options = sharedOptions || this.resolveDataElementOptions(i, bars[i].active ? 'active' : mode); } const options = properties.options || bars[i].options; setBorderSkipped(properties, options, stack, index); setInflateAmount(properties, options, ruler.ratio); this.updateElement(bars[i], i, properties, mode); } } /** * Returns the stacks based on groups and bar visibility. * @param {number} [last] - The dataset index * @param {number} [dataIndex] - The data index of the ruler * @returns {string[]} The list of stack IDs * @private */ _getStacks(last, dataIndex) { const {iScale} = this._cachedMeta; const metasets = iScale.getMatchingVisibleMetas(this._type) .filter(meta => meta.controller.options.grouped); const stacked = iScale.options.stacked; const stacks = []; const currentParsed = this._cachedMeta.controller.getParsed(dataIndex); const iScaleValue = currentParsed && currentParsed[iScale.axis]; const skipNull = (meta) => { const parsed = meta._parsed.find(item => item[iScale.axis] === iScaleValue); const val = parsed && parsed[meta.vScale.axis]; if (isNullOrUndef(val) || isNaN(val)) { return true; } }; for (const meta of metasets) { if (dataIndex !== undefined && skipNull(meta)) { continue; } // stacked | meta.stack // | found | not found | undefined // false | x | x | x // true | | x | // undefined | | x | x if (stacked === false || stacks.indexOf(meta.stack) === -1 || (stacked === undefined && meta.stack === undefined)) { stacks.push(meta.stack); } if (meta.index === last) { break; } } // No stacks? that means there is no visible data. Let's still initialize an `undefined` // stack where possible invisible bars will be located. // https://github.com/chartjs/Chart.js/issues/6368 if (!stacks.length) { stacks.push(undefined); } return stacks; } /** * Returns the effective number of stacks based on groups and bar visibility. * @private */ _getStackCount(index) { return this._getStacks(undefined, index).length; } _getAxisCount() { return this._getAxis().length; } getFirstScaleIdForIndexAxis() { const scales = this.chart.scales; const indexScaleId = this.chart.options.indexAxis; return Object.keys(scales).filter(key => scales[key].axis === indexScaleId).shift(); } _getAxis() { const axis = {}; const firstScaleAxisId = this.getFirstScaleIdForIndexAxis(); for (const dataset of this.chart.data.datasets) { axis[valueOrDefault( this.chart.options.indexAxis === 'x' ? dataset.xAxisID : dataset.yAxisID, firstScaleAxisId )] = true; } return Object.keys(axis); } /** * Returns the stack index for the given dataset based on groups and bar visibility. * @param {number} [datasetIndex] - The dataset index * @param {string} [name] - The stack name to find * @param {number} [dataIndex] * @returns {number} The stack index * @private */ _getStackIndex(datasetIndex, name, dataIndex) { const stacks = this._getStacks(datasetIndex, dataIndex); const index = (name !== undefined) ? stacks.indexOf(name) : -1; // indexOf returns -1 if element is not present return (index === -1) ? stacks.length - 1 : index; } /** * @private */ _getRuler() { const opts = this.options; const meta = this._cachedMeta; const iScale = meta.iScale; const pixels = []; let i, ilen; for (i = 0, ilen = meta.data.length; i < ilen; ++i) { pixels.push(iScale.getPixelForValue(this.getParsed(i)[iScale.axis], i)); } const barThickness = opts.barThickness; const min = barThickness || computeMinSampleSize(meta); return { min, pixels, start: iScale._startPixel, end: iScale._endPixel, stackCount: this._getStackCount(), scale: iScale, grouped: opts.grouped, // bar thickness ratio used for non-grouped bars ratio: barThickness ? 1 : opts.categoryPercentage * opts.barPercentage }; } /** * Note: pixel values are not clamped to the scale area. * @private */ _calculateBarValuePixels(index) { const {_cachedMeta: {vScale, _stacked, index: datasetIndex}, options: {base: baseValue, minBarLength}} = this; const actualBase = baseValue || 0; const parsed = this.getParsed(index); const custom = parsed._custom; const floating = isFloatBar(custom); let value = parsed[vScale.axis]; let start = 0; let length = _stacked ? this.applyStack(vScale, parsed, _stacked) : value; let head, size; if (length !== value) { start = length - value; length = value; } if (floating) { value = custom.barStart; length = custom.barEnd - custom.barStart; // bars crossing origin are not stacked if (value !== 0 && sign(value) !== sign(custom.barEnd)) { start = 0; } start += value; } const startValue = !isNullOrUndef(baseValue) && !floating ? baseValue : start; let base = vScale.getPixelForValue(startValue); if (this.chart.getDataVisibility(index)) { head = vScale.getPixelForValue(start + length); } else { // When not visible, no height head = base; } size = head - base; if (Math.abs(size) < minBarLength) { size = barSign(size, vScale, actualBase) * minBarLength; if (value === actualBase) { base -= size / 2; } const startPixel = vScale.getPixelForDecimal(0); const endPixel = vScale.getPixelForDecimal(1); const min = Math.min(startPixel, endPixel); const max = Math.max(startPixel, endPixel); base = Math.max(Math.min(base, max), min); head = base + size; if (_stacked && !floating) { // visual data coordinates after applying minBarLength parsed._stacks[vScale.axis]._visualValues[datasetIndex] = vScale.getValueForPixel(head) - vScale.getValueForPixel(base); } } if (base === vScale.getPixelForValue(actualBase)) { const halfGrid = sign(size) * vScale.getLineWidthForValue(actualBase) / 2; base += halfGrid; size -= halfGrid; } return { size, base, head, center: head + size / 2 }; } /** * @private */ _calculateBarIndexPixels(index, ruler) { const scale = ruler.scale; const options = this.options; const skipNull = options.skipNull; const maxBarThickness = valueOrDefault(options.maxBarThickness, Infinity); let center, size; const axisCount = this._getAxisCount(); if (ruler.grouped) { const stackCount = skipNull ? this._getStackCount(index) : ruler.stackCount; const range = options.barThickness === 'flex' ? computeFlexCategoryTraits(index, ruler, options, stackCount * axisCount) : computeFitCategoryTraits(index, ruler, options, stackCount * axisCount); const axisID = this.chart.options.indexAxis === 'x' ? this.getDataset().xAxisID : this.getDataset().yAxisID; const axisNumber = this._getAxis().indexOf(valueOrDefault(axisID, this.getFirstScaleIdForIndexAxis())); const stackIndex = this._getStackIndex(this.index, this._cachedMeta.stack, skipNull ? index : undefined) + axisNumber; center = range.start + (range.chunk * stackIndex) + (range.chunk / 2); size = Math.min(maxBarThickness, range.chunk * range.ratio); } else { // For non-grouped bar charts, exact pixel values are used center = scale.getPixelForValue(this.getParsed(index)[scale.axis], index); size = Math.min(maxBarThickness, ruler.min * ruler.ratio); } return { base: center - size / 2, head: center + size / 2, center, size }; } draw() { const meta = this._cachedMeta; const vScale = meta.vScale; const rects = meta.data; const ilen = rects.length; let i = 0; for (; i < ilen; ++i) { if (this.getParsed(i)[vScale.axis] !== null && !rects[i].hidden) { rects[i].draw(this._ctx); } } } } ================================================ FILE: src/controllers/controller.bubble.js ================================================ import DatasetController from '../core/core.datasetController.js'; import {valueOrDefault} from '../helpers/helpers.core.js'; export default class BubbleController extends DatasetController { static id = 'bubble'; /** * @type {any} */ static defaults = { datasetElementType: false, dataElementType: 'point', animations: { numbers: { type: 'number', properties: ['x', 'y', 'borderWidth', 'radius'] } } }; /** * @type {any} */ static overrides = { scales: { x: { type: 'linear' }, y: { type: 'linear' } } }; initialize() { this.enableOptionSharing = true; super.initialize(); } /** * Parse array of primitive values * @protected */ parsePrimitiveData(meta, data, start, count) { const parsed = super.parsePrimitiveData(meta, data, start, count); for (let i = 0; i < parsed.length; i++) { parsed[i]._custom = this.resolveDataElementOptions(i + start).radius; } return parsed; } /** * Parse array of arrays * @protected */ parseArrayData(meta, data, start, count) { const parsed = super.parseArrayData(meta, data, start, count); for (let i = 0; i < parsed.length; i++) { const item = data[start + i]; parsed[i]._custom = valueOrDefault(item[2], this.resolveDataElementOptions(i + start).radius); } return parsed; } /** * Parse array of objects * @protected */ parseObjectData(meta, data, start, count) { const parsed = super.parseObjectData(meta, data, start, count); for (let i = 0; i < parsed.length; i++) { const item = data[start + i]; parsed[i]._custom = valueOrDefault(item && item.r && +item.r, this.resolveDataElementOptions(i + start).radius); } return parsed; } /** * @protected */ getMaxOverflow() { const data = this._cachedMeta.data; let max = 0; for (let i = data.length - 1; i >= 0; --i) { max = Math.max(max, data[i].size(this.resolveDataElementOptions(i)) / 2); } return max > 0 && max; } /** * @protected */ getLabelAndValue(index) { const meta = this._cachedMeta; const labels = this.chart.data.labels || []; const {xScale, yScale} = meta; const parsed = this.getParsed(index); const x = xScale.getLabelForValue(parsed.x); const y = yScale.getLabelForValue(parsed.y); const r = parsed._custom; return { label: labels[index] || '', value: '(' + x + ', ' + y + (r ? ', ' + r : '') + ')' }; } update(mode) { const points = this._cachedMeta.data; // Update Points this.updateElements(points, 0, points.length, mode); } updateElements(points, start, count, mode) { const reset = mode === 'reset'; const {iScale, vScale} = this._cachedMeta; const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode); const iAxis = iScale.axis; const vAxis = vScale.axis; for (let i = start; i < start + count; i++) { const point = points[i]; const parsed = !reset && this.getParsed(i); const properties = {}; const iPixel = properties[iAxis] = reset ? iScale.getPixelForDecimal(0.5) : iScale.getPixelForValue(parsed[iAxis]); const vPixel = properties[vAxis] = reset ? vScale.getBasePixel() : vScale.getPixelForValue(parsed[vAxis]); properties.skip = isNaN(iPixel) || isNaN(vPixel); if (includeOptions) { properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode); if (reset) { properties.options.radius = 0; } } this.updateElement(point, i, properties, mode); } } /** * @param {number} index * @param {string} [mode] * @protected */ resolveDataElementOptions(index, mode) { const parsed = this.getParsed(index); let values = super.resolveDataElementOptions(index, mode); // In case values were cached (and thus frozen), we need to clone the values if (values.$shared) { values = Object.assign({}, values, {$shared: false}); } // Custom radius resolution const radius = values.radius; if (mode !== 'active') { values.radius = 0; } values.radius += valueOrDefault(parsed && parsed._custom, radius); return values; } } ================================================ FILE: src/controllers/controller.doughnut.js ================================================ import DatasetController from '../core/core.datasetController.js'; import {isObject, resolveObjectKey, toPercentage, toDimension, valueOrDefault} from '../helpers/helpers.core.js'; import {formatNumber} from '../helpers/helpers.intl.js'; import {toRadians, PI, TAU, HALF_PI, _angleBetween} from '../helpers/helpers.math.js'; /** * @typedef { import('../core/core.controller.js').default } Chart */ function getRatioAndOffset(rotation, circumference, cutout) { let ratioX = 1; let ratioY = 1; let offsetX = 0; let offsetY = 0; // If the chart's circumference isn't a full circle, calculate size as a ratio of the width/height of the arc if (circumference < TAU) { const startAngle = rotation; const endAngle = startAngle + circumference; const startX = Math.cos(startAngle); const startY = Math.sin(startAngle); const endX = Math.cos(endAngle); const endY = Math.sin(endAngle); const calcMax = (angle, a, b) => _angleBetween(angle, startAngle, endAngle, true) ? 1 : Math.max(a, a * cutout, b, b * cutout); const calcMin = (angle, a, b) => _angleBetween(angle, startAngle, endAngle, true) ? -1 : Math.min(a, a * cutout, b, b * cutout); const maxX = calcMax(0, startX, endX); const maxY = calcMax(HALF_PI, startY, endY); const minX = calcMin(PI, startX, endX); const minY = calcMin(PI + HALF_PI, startY, endY); ratioX = (maxX - minX) / 2; ratioY = (maxY - minY) / 2; offsetX = -(maxX + minX) / 2; offsetY = -(maxY + minY) / 2; } return {ratioX, ratioY, offsetX, offsetY}; } export default class DoughnutController extends DatasetController { static id = 'doughnut'; /** * @type {any} */ static defaults = { datasetElementType: false, dataElementType: 'arc', animation: { // Boolean - Whether we animate the rotation of the Doughnut animateRotate: true, // Boolean - Whether we animate scaling the Doughnut from the centre animateScale: false }, animations: { numbers: { type: 'number', properties: ['circumference', 'endAngle', 'innerRadius', 'outerRadius', 'startAngle', 'x', 'y', 'offset', 'borderWidth', 'spacing'] }, }, // The percentage of the chart that we cut out of the middle. cutout: '50%', // The rotation of the chart, where the first data arc begins. rotation: 0, // The total circumference of the chart. circumference: 360, // The outer radius of the chart radius: '100%', // Spacing between arcs spacing: 0, indexAxis: 'r', }; static descriptors = { _scriptable: (name) => name !== 'spacing', _indexable: (name) => name !== 'spacing' && !name.startsWith('borderDash') && !name.startsWith('hoverBorderDash'), }; /** * @type {any} */ static overrides = { aspectRatio: 1, // Need to override these to give a nice default plugins: { legend: { labels: { generateLabels(chart) { const data = chart.data; const {labels: {pointStyle, textAlign, color, useBorderRadius, borderRadius}} = chart.legend.options; if (data.labels.length && data.datasets.length) { return data.labels.map((label, i) => { const meta = chart.getDatasetMeta(0); const style = meta.controller.getStyle(i); return { text: label, fillStyle: style.backgroundColor, fontColor: color, hidden: !chart.getDataVisibility(i), lineDash: style.borderDash, lineDashOffset: style.borderDashOffset, lineJoin: style.borderJoinStyle, lineWidth: style.borderWidth, strokeStyle: style.borderColor, textAlign: textAlign, pointStyle: pointStyle, borderRadius: useBorderRadius && (borderRadius || style.borderRadius), // Extra data used for toggling the correct item index: i }; }); } return []; } }, onClick(e, legendItem, legend) { legend.chart.toggleDataVisibility(legendItem.index); legend.chart.update(); } } } }; constructor(chart, datasetIndex) { super(chart, datasetIndex); this.enableOptionSharing = true; this.innerRadius = undefined; this.outerRadius = undefined; this.offsetX = undefined; this.offsetY = undefined; } linkScales() {} /** * Override data parsing, since we are not using scales */ parse(start, count) { const data = this.getDataset().data; const meta = this._cachedMeta; if (this._parsing === false) { meta._parsed = data; } else { let getter = (i) => +data[i]; if (isObject(data[start])) { const {key = 'value'} = this._parsing; getter = (i) => +resolveObjectKey(data[i], key); } let i, ilen; for (i = start, ilen = start + count; i < ilen; ++i) { meta._parsed[i] = getter(i); } } } /** * @private */ _getRotation() { return toRadians(this.options.rotation - 90); } /** * @private */ _getCircumference() { return toRadians(this.options.circumference); } /** * Get the maximal rotation & circumference extents * across all visible datasets. */ _getRotationExtents() { let min = TAU; let max = -TAU; for (let i = 0; i < this.chart.data.datasets.length; ++i) { if (this.chart.isDatasetVisible(i) && this.chart.getDatasetMeta(i).type === this._type) { const controller = this.chart.getDatasetMeta(i).controller; const rotation = controller._getRotation(); const circumference = controller._getCircumference(); min = Math.min(min, rotation); max = Math.max(max, rotation + circumference); } } return { rotation: min, circumference: max - min, }; } /** * @param {string} mode */ update(mode) { const chart = this.chart; const {chartArea} = chart; const meta = this._cachedMeta; const arcs = meta.data; const spacing = this.getMaxBorderWidth() + this.getMaxOffset(arcs) + this.options.spacing; const maxSize = Math.max((Math.min(chartArea.width, chartArea.height) - spacing) / 2, 0); const cutout = Math.min(toPercentage(this.options.cutout, maxSize), 1); const chartWeight = this._getRingWeight(this.index); // Compute the maximal rotation & circumference limits. // If we only consider our dataset, this can cause problems when two datasets // are both less than a circle with different rotations (starting angles) const {circumference, rotation} = this._getRotationExtents(); const {ratioX, ratioY, offsetX, offsetY} = getRatioAndOffset(rotation, circumference, cutout); const maxWidth = (chartArea.width - spacing) / ratioX; const maxHeight = (chartArea.height - spacing) / ratioY; const maxRadius = Math.max(Math.min(maxWidth, maxHeight) / 2, 0); const outerRadius = toDimension(this.options.radius, maxRadius); const innerRadius = Math.max(outerRadius * cutout, 0); const radiusLength = (outerRadius - innerRadius) / this._getVisibleDatasetWeightTotal(); this.offsetX = offsetX * outerRadius; this.offsetY = offsetY * outerRadius; meta.total = this.calculateTotal(); this.outerRadius = outerRadius - radiusLength * this._getRingWeightOffset(this.index); this.innerRadius = Math.max(this.outerRadius - radiusLength * chartWeight, 0); this.updateElements(arcs, 0, arcs.length, mode); } /** * @private */ _circumference(i, reset) { const opts = this.options; const meta = this._cachedMeta; const circumference = this._getCircumference(); if ((reset && opts.animation.animateRotate) || !this.chart.getDataVisibility(i) || meta._parsed[i] === null || meta.data[i].hidden) { return 0; } return this.calculateCircumference(meta._parsed[i] * circumference / TAU); } updateElements(arcs, start, count, mode) { const reset = mode === 'reset'; const chart = this.chart; const chartArea = chart.chartArea; const opts = chart.options; const animationOpts = opts.animation; const centerX = (chartArea.left + chartArea.right) / 2; const centerY = (chartArea.top + chartArea.bottom) / 2; const animateScale = reset && animationOpts.animateScale; const innerRadius = animateScale ? 0 : this.innerRadius; const outerRadius = animateScale ? 0 : this.outerRadius; const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode); let startAngle = this._getRotation(); let i; for (i = 0; i < start; ++i) { startAngle += this._circumference(i, reset); } for (i = start; i < start + count; ++i) { const circumference = this._circumference(i, reset); const arc = arcs[i]; const properties = { x: centerX + this.offsetX, y: centerY + this.offsetY, startAngle, endAngle: startAngle + circumference, circumference, outerRadius, innerRadius }; if (includeOptions) { properties.options = sharedOptions || this.resolveDataElementOptions(i, arc.active ? 'active' : mode); } startAngle += circumference; this.updateElement(arc, i, properties, mode); } } calculateTotal() { const meta = this._cachedMeta; const metaData = meta.data; let total = 0; let i; for (i = 0; i < metaData.length; i++) { const value = meta._parsed[i]; if (value !== null && !isNaN(value) && this.chart.getDataVisibility(i) && !metaData[i].hidden) { total += Math.abs(value); } } return total; } calculateCircumference(value) { const total = this._cachedMeta.total; if (total > 0 && !isNaN(value)) { return TAU * (Math.abs(value) / total); } return 0; } getLabelAndValue(index) { const meta = this._cachedMeta; const chart = this.chart; const labels = chart.data.labels || []; const value = formatNumber(meta._parsed[index], chart.options.locale); return { label: labels[index] || '', value, }; } getMaxBorderWidth(arcs) { let max = 0; const chart = this.chart; let i, ilen, meta, controller, options; if (!arcs) { // Find the outmost visible dataset for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { if (chart.isDatasetVisible(i)) { meta = chart.getDatasetMeta(i); arcs = meta.data; controller = meta.controller; break; } } } if (!arcs) { return 0; } for (i = 0, ilen = arcs.length; i < ilen; ++i) { options = controller.resolveDataElementOptions(i); if (options.borderAlign !== 'inner') { max = Math.max(max, options.borderWidth || 0, options.hoverBorderWidth || 0); } } return max; } getMaxOffset(arcs) { let max = 0; for (let i = 0, ilen = arcs.length; i < ilen; ++i) { const options = this.resolveDataElementOptions(i); max = Math.max(max, options.offset || 0, options.hoverOffset || 0); } return max; } /** * Get radius length offset of the dataset in relation to the visible datasets weights. This allows determining the inner and outer radius correctly * @private */ _getRingWeightOffset(datasetIndex) { let ringWeightOffset = 0; for (let i = 0; i < datasetIndex; ++i) { if (this.chart.isDatasetVisible(i)) { ringWeightOffset += this._getRingWeight(i); } } return ringWeightOffset; } /** * @private */ _getRingWeight(datasetIndex) { return Math.max(valueOrDefault(this.chart.data.datasets[datasetIndex].weight, 1), 0); } /** * Returns the sum of all visible data set weights. * @private */ _getVisibleDatasetWeightTotal() { return this._getRingWeightOffset(this.chart.data.datasets.length) || 1; } } ================================================ FILE: src/controllers/controller.line.js ================================================ import DatasetController from '../core/core.datasetController.js'; import {isNullOrUndef} from '../helpers/index.js'; import {isNumber} from '../helpers/helpers.math.js'; import {_getStartAndCountOfVisiblePoints, _scaleRangesChanged} from '../helpers/helpers.extras.js'; export default class LineController extends DatasetController { static id = 'line'; /** * @type {any} */ static defaults = { datasetElementType: 'line', dataElementType: 'point', showLine: true, spanGaps: false, }; /** * @type {any} */ static overrides = { scales: { _index_: { type: 'category', }, _value_: { type: 'linear', }, } }; initialize() { this.enableOptionSharing = true; this.supportsDecimation = true; super.initialize(); } update(mode) { const meta = this._cachedMeta; const {dataset: line, data: points = [], _dataset} = meta; // @ts-ignore const animationsDisabled = this.chart._animationsDisabled; let {start, count} = _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled); this._drawStart = start; this._drawCount = count; if (_scaleRangesChanged(meta)) { start = 0; count = points.length; } // Update Line line._chart = this.chart; line._datasetIndex = this.index; line._decimated = !!_dataset._decimated; line.points = points; const options = this.resolveDatasetElementOptions(mode); if (!this.options.showLine) { options.borderWidth = 0; } options.segment = this.options.segment; this.updateElement(line, undefined, { animated: !animationsDisabled, options }, mode); // Update Points this.updateElements(points, start, count, mode); } updateElements(points, start, count, mode) { const reset = mode === 'reset'; const {iScale, vScale, _stacked, _dataset} = this._cachedMeta; const {sharedOptions, includeOptions} = this._getSharedOptions(start, mode); const iAxis = iScale.axis; const vAxis = vScale.axis; const {spanGaps, segment} = this.options; const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY; const directUpdate = this.chart._animationsDisabled || reset || mode === 'none'; const end = start + count; const pointsCount = points.length; let prevParsed = start > 0 && this.getParsed(start - 1); for (let i = 0; i < pointsCount; ++i) { const point = points[i]; const properties = directUpdate ? point : {}; if (i < start || i >= end) { properties.skip = true; continue; } const parsed = this.getParsed(i); const nullData = isNullOrUndef(parsed[vAxis]); const iPixel = properties[iAxis] = iScale.getPixelForValue(parsed[iAxis], i); const vPixel = properties[vAxis] = reset || nullData ? vScale.getBasePixel() : vScale.getPixelForValue(_stacked ? this.applyStack(vScale, parsed, _stacked) : parsed[vAxis], i); properties.skip = isNaN(iPixel) || isNaN(vPixel) || nullData; properties.stop = i > 0 && (Math.abs(parsed[iAxis] - prevParsed[iAxis])) > maxGapLength; if (segment) { properties.parsed = parsed; properties.raw = _dataset.data[i]; } if (includeOptions) { properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode); } if (!directUpdate) { this.updateElement(point, i, properties, mode); } prevParsed = parsed; } } /** * @protected */ getMaxOverflow() { const meta = this._cachedMeta; const dataset = meta.dataset; const border = dataset.options && dataset.options.borderWidth || 0; const data = meta.data || []; if (!data.length) { return border; } const firstPoint = data[0].size(this.resolveDataElementOptions(0)); const lastPoint = data[data.length - 1].size(this.resolveDataElementOptions(data.length - 1)); return Math.max(border, firstPoint, lastPoint) / 2; } draw() { const meta = this._cachedMeta; meta.dataset.updateControlPoints(this.chart.chartArea, meta.iScale.axis); super.draw(); } } ================================================ FILE: src/controllers/controller.pie.js ================================================ import DoughnutController from './controller.doughnut.js'; // Pie charts are Doughnut chart with different defaults export default class PieController extends DoughnutController { static id = 'pie'; /** * @type {any} */ static defaults = { // The percentage of the chart that we cut out of the middle. cutout: 0, // The rotation of the chart, where the first data arc begins. rotation: 0, // The total circumference of the chart. circumference: 360, // The outer radius of the chart radius: '100%' }; } ================================================ FILE: src/controllers/controller.polarArea.js ================================================ import DatasetController from '../core/core.datasetController.js'; import {toRadians, PI, formatNumber, _parseObjectDataRadialScale} from '../helpers/index.js'; export default class PolarAreaController extends DatasetController { static id = 'polarArea'; /** * @type {any} */ static defaults = { dataElementType: 'arc', animation: { animateRotate: true, animateScale: true }, animations: { numbers: { type: 'number', properties: ['x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius'] }, }, indexAxis: 'r', startAngle: 0, }; /** * @type {any} */ static overrides = { aspectRatio: 1, plugins: { legend: { labels: { generateLabels(chart) { const data = chart.data; if (data.labels.length && data.datasets.length) { const {labels: {pointStyle, color}} = chart.legend.options; return data.labels.map((label, i) => { const meta = chart.getDatasetMeta(0); const style = meta.controller.getStyle(i); return { text: label, fillStyle: style.backgroundColor, strokeStyle: style.borderColor, fontColor: color, lineWidth: style.borderWidth, pointStyle: pointStyle, hidden: !chart.getDataVisibility(i), // Extra data used for toggling the correct item index: i }; }); } return []; } }, onClick(e, legendItem, legend) { legend.chart.toggleDataVisibility(legendItem.index); legend.chart.update(); } } }, scales: { r: { type: 'radialLinear', angleLines: { display: false }, beginAtZero: true, grid: { circular: true }, pointLabels: { display: false }, startAngle: 0 } } }; constructor(chart, datasetIndex) { super(chart, datasetIndex); this.innerRadius = undefined; this.outerRadius = undefined; } getLabelAndValue(index) { const meta = this._cachedMeta; const chart = this.chart; const labels = chart.data.labels || []; const value = formatNumber(meta._parsed[index].r, chart.options.locale); return { label: labels[index] || '', value, }; } parseObjectData(meta, data, start, count) { return _parseObjectDataRadialScale.bind(this)(meta, data, start, count); } update(mode) { const arcs = this._cachedMeta.data; this._updateRadius(); this.updateElements(arcs, 0, arcs.length, mode); } /** * @protected */ getMinMax() { const meta = this._cachedMeta; const range = {min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY}; meta.data.forEach((element, index) => { const parsed = this.getParsed(index).r; if (!isNaN(parsed) && this.chart.getDataVisibility(index)) { if (parsed < range.min) { range.min = parsed; } if (parsed > range.max) { range.max = parsed; } } }); return range; } /** * @private */ _updateRadius() { const chart = this.chart; const chartArea = chart.chartArea; const opts = chart.options; const minSize = Math.min(chartArea.right - chartArea.left, chartArea.bottom - chartArea.top); const outerRadius = Math.max(minSize / 2, 0); const innerRadius = Math.max(opts.cutoutPercentage ? (outerRadius / 100) * (opts.cutoutPercentage) : 1, 0); const radiusLength = (outerRadius - innerRadius) / chart.getVisibleDatasetCount(); this.outerRadius = outerRadius - (radiusLength * this.index); this.innerRadius = this.outerRadius - radiusLength; } updateElements(arcs, start, count, mode) { const reset = mode === 'reset'; const chart = this.chart; const opts = chart.options; const animationOpts = opts.animation; const scale = this._cachedMeta.rScale; const centerX = scale.xCenter; const centerY = scale.yCenter; const datasetStartAngle = scale.getIndexAngle(0) - 0.5 * PI; let angle = datasetStartAngle; let i; const defaultAngle = 360 / this.countVisibleElements(); for (i = 0; i < start; ++i) { angle += this._computeAngle(i, mode, defaultAngle); } for (i = start; i < start + count; i++) { const arc = arcs[i]; let startAngle = angle; let endAngle = angle + this._computeAngle(i, mode, defaultAngle); let outerRadius = chart.getDataVisibility(i) ? scale.getDistanceFromCenterForValue(this.getParsed(i).r) : 0; angle = endAngle; if (reset) { if (animationOpts.animateScale) { outerRadius = 0; } if (animationOpts.animateRotate) { startAngle = endAngle = datasetStartAngle; } } const properties = { x: centerX, y: centerY, innerRadius: 0, outerRadius, startAngle, endAngle, options: this.resolveDataElementOptions(i, arc.active ? 'active' : mode) }; this.updateElement(arc, i, properties, mode); } } countVisibleElements() { const meta = this._cachedMeta; let count = 0; meta.data.forEach((element, index) => { if (!isNaN(this.getParsed(index).r) && this.chart.getDataVisibility(index)) { count++; } }); return count; } /** * @private */ _computeAngle(index, mode, defaultAngle) { return this.chart.getDataVisibility(index) ? toRadians(this.resolveDataElementOptions(index, mode).angle || defaultAngle) : 0; } } ================================================ FILE: src/controllers/controller.radar.js ================================================ import DatasetController from '../core/core.datasetController.js'; import {_parseObjectDataRadialScale} from '../helpers/index.js'; export default class RadarController extends DatasetController { static id = 'radar'; /** * @type {any} */ static defaults = { datasetElementType: 'line', dataElementType: 'point', indexAxis: 'r', showLine: true, elements: { line: { fill: 'start' } }, }; /** * @type {any} */ static overrides = { aspectRatio: 1, scales: { r: { type: 'radialLinear', } } }; /** * @protected */ getLabelAndValue(index) { const vScale = this._cachedMeta.vScale; const parsed = this.getParsed(index); return { label: vScale.getLabels()[index], value: '' + vScale.getLabelForValue(parsed[vScale.axis]) }; } parseObjectData(meta, data, start, count) { return _parseObjectDataRadialScale.bind(this)(meta, data, start, count); } update(mode) { const meta = this._cachedMeta; const line = meta.dataset; const points = meta.data || []; const labels = meta.iScale.getLabels(); // Update Line line.points = points; // In resize mode only point locations change, so no need to set the points or options. if (mode !== 'resize') { const options = this.resolveDatasetElementOptions(mode); if (!this.options.showLine) { options.borderWidth = 0; } const properties = { _loop: true, _fullLoop: labels.length === points.length, options }; this.updateElement(line, undefined, properties, mode); } // Update Points this.updateElements(points, 0, points.length, mode); } updateElements(points, start, count, mode) { const scale = this._cachedMeta.rScale; const reset = mode === 'reset'; for (let i = start; i < start + count; i++) { const point = points[i]; const options = this.resolveDataElementOptions(i, point.active ? 'active' : mode); const pointPosition = scale.getPointPositionForValue(i, this.getParsed(i).r); const x = reset ? scale.xCenter : pointPosition.x; const y = reset ? scale.yCenter : pointPosition.y; const properties = { x, y, angle: pointPosition.angle, skip: isNaN(x) || isNaN(y), options }; this.updateElement(point, i, properties, mode); } } } ================================================ FILE: src/controllers/controller.scatter.js ================================================ import DatasetController from '../core/core.datasetController.js'; import {isNullOrUndef} from '../helpers/index.js'; import {isNumber} from '../helpers/helpers.math.js'; import {_getStartAndCountOfVisiblePoints, _scaleRangesChanged} from '../helpers/helpers.extras.js'; export default class ScatterController extends DatasetController { static id = 'scatter'; /** * @type {any} */ static defaults = { datasetElementType: false, dataElementType: 'point', showLine: false, fill: false }; /** * @type {any} */ static overrides = { interaction: { mode: 'point' }, scales: { x: { type: 'linear' }, y: { type: 'linear' } } }; /** * @protected */ getLabelAndValue(index) { const meta = this._cachedMeta; const labels = this.chart.data.labels || []; const {xScale, yScale} = meta; const parsed = this.getParsed(index); const x = xScale.getLabelForValue(parsed.x); const y = yScale.getLabelForValue(parsed.y); return { label: labels[index] || '', value: '(' + x + ', ' + y + ')' }; } update(mode) { const meta = this._cachedMeta; const {data: points = []} = meta; // @ts-ignore const animationsDisabled = this.chart._animationsDisabled; let {start, count} = _getStartAndCountOfVisiblePoints(meta, points, animationsDisabled); this._drawStart = start; this._drawCount = count; if (_scaleRangesChanged(meta)) { start = 0; count = points.length; } if (this.options.showLine) { // https://github.com/chartjs/Chart.js/issues/11333 if (!this.datasetElementType) { this.addElements(); } const {dataset: line, _dataset} = meta; // Update Line line._chart = this.chart; line._datasetIndex = this.index; line._decimated = !!_dataset._decimated; line.points = points; const options = this.resolveDatasetElementOptions(mode); options.segment = this.options.segment; this.updateElement(line, undefined, { animated: !animationsDisabled, options }, mode); } else if (this.datasetElementType) { // https://github.com/chartjs/Chart.js/issues/11333 delete meta.dataset; this.datasetElementType = false; } // Update Points this.updateElements(points, start, count, mode); } addElements() { const {showLine} = this.options; if (!this.datasetElementType && showLine) { this.datasetElementType = this.chart.registry.getElement('line'); } super.addElements(); } updateElements(points, start, count, mode) { const reset = mode === 'reset'; const {iScale, vScale, _stacked, _dataset} = this._cachedMeta; const firstOpts = this.resolveDataElementOptions(start, mode); const sharedOptions = this.getSharedOptions(firstOpts); const includeOptions = this.includeOptions(mode, sharedOptions); const iAxis = iScale.axis; const vAxis = vScale.axis; const {spanGaps, segment} = this.options; const maxGapLength = isNumber(spanGaps) ? spanGaps : Number.POSITIVE_INFINITY; const directUpdate = this.chart._animationsDisabled || reset || mode === 'none'; let prevParsed = start > 0 && this.getParsed(start - 1); for (let i = start; i < start + count; ++i) { const point = points[i]; const parsed = this.getParsed(i); const properties = directUpdate ? point : {}; const nullData = isNullOrUndef(parsed[vAxis]); const iPixel = properties[iAxis] = iScale.getPixelForValue(parsed[iAxis], i); const vPixel = properties[vAxis] = reset || nullData ? vScale.getBasePixel() : vScale.getPixelForValue(_stacked ? this.applyStack(vScale, parsed, _stacked) : parsed[vAxis], i); properties.skip = isNaN(iPixel) || isNaN(vPixel) || nullData; properties.stop = i > 0 && (Math.abs(parsed[iAxis] - prevParsed[iAxis])) > maxGapLength; if (segment) { properties.parsed = parsed; properties.raw = _dataset.data[i]; } if (includeOptions) { properties.options = sharedOptions || this.resolveDataElementOptions(i, point.active ? 'active' : mode); } if (!directUpdate) { this.updateElement(point, i, properties, mode); } prevParsed = parsed; } this.updateSharedOptions(sharedOptions, mode, firstOpts); } /** * @protected */ getMaxOverflow() { const meta = this._cachedMeta; const data = meta.data || []; if (!this.options.showLine) { let max = 0; for (let i = data.length - 1; i >= 0; --i) { max = Math.max(max, data[i].size(this.resolveDataElementOptions(i)) / 2); } return max > 0 && max; } const dataset = meta.dataset; const border = dataset.options && dataset.options.borderWidth || 0; if (!data.length) { return border; } const firstPoint = data[0].size(this.resolveDataElementOptions(0)); const lastPoint = data[data.length - 1].size(this.resolveDataElementOptions(data.length - 1)); return Math.max(border, firstPoint, lastPoint) / 2; } } ================================================ FILE: src/controllers/index.js ================================================ export {default as BarController} from './controller.bar.js'; export {default as BubbleController} from './controller.bubble.js'; export {default as DoughnutController} from './controller.doughnut.js'; export {default as LineController} from './controller.line.js'; export {default as PolarAreaController} from './controller.polarArea.js'; export {default as PieController} from './controller.pie.js'; export {default as RadarController} from './controller.radar.js'; export {default as ScatterController} from './controller.scatter.js'; ================================================ FILE: src/core/core.adapters.ts ================================================ /** * @namespace Chart._adapters * @since 2.8.0 * @private */ import type {AnyObject} from '../types/basic.js'; import type {ChartOptions} from '../types/index.js'; export type TimeUnit = 'millisecond' | 'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; export interface DateAdapter { readonly options: T; /** * Will called with chart options after adapter creation. */ init(this: DateAdapter, chartOptions: ChartOptions): void; /** * Returns a map of time formats for the supported formatting units defined * in Unit as well as 'datetime' representing a detailed date/time string. */ formats(this: DateAdapter): Record; /** * Parses the given `value` and return the associated timestamp. * @param value - the value to parse (usually comes from the data) * @param [format] - the expected data format */ parse(this: DateAdapter, value: unknown, format?: string): number | null; /** * Returns the formatted date in the specified `format` for a given `timestamp`. * @param timestamp - the timestamp to format * @param format - the date/time token */ format(this: DateAdapter, timestamp: number, format: string): string; /** * Adds the specified `amount` of `unit` to the given `timestamp`. * @param timestamp - the input timestamp * @param amount - the amount to add * @param unit - the unit as string */ add(this: DateAdapter, timestamp: number, amount: number, unit: TimeUnit): number; /** * Returns the number of `unit` between the given timestamps. * @param a - the input timestamp (reference) * @param b - the timestamp to subtract * @param unit - the unit as string */ diff(this: DateAdapter, a: number, b: number, unit: TimeUnit): number; /** * Returns start of `unit` for the given `timestamp`. * @param timestamp - the input timestamp * @param unit - the unit as string * @param [weekday] - the ISO day of the week with 1 being Monday * and 7 being Sunday (only needed if param *unit* is `isoWeek`). */ startOf(this: DateAdapter, timestamp: number, unit: TimeUnit | 'isoWeek', weekday?: number | boolean): number; /** * Returns end of `unit` for the given `timestamp`. * @param timestamp - the input timestamp * @param unit - the unit as string */ endOf(this: DateAdapter, timestamp: number, unit: TimeUnit): number; } function abstract(): T { throw new Error('This method is not implemented: Check that a complete date adapter is provided.'); } /** * Date adapter (current used by the time scale) * @namespace Chart._adapters._date * @memberof Chart._adapters * @private */ class DateAdapterBase implements DateAdapter { /** * Override default date adapter methods. * Accepts type parameter to define options type. * @example * Chart._adapters._date.override<{myAdapterOption: string}>({ * init() { * console.log(this.options.myAdapterOption); * } * }) */ static override( members: Partial, 'options'>> ) { Object.assign(DateAdapterBase.prototype, members); } readonly options: AnyObject; constructor(options?: AnyObject) { this.options = options || {}; } // eslint-disable-next-line @typescript-eslint/no-empty-function init() {} formats(): Record { return abstract(); } parse(): number | null { return abstract(); } format(): string { return abstract(); } add(): number { return abstract(); } diff(): number { return abstract(); } startOf(): number { return abstract(); } endOf(): number { return abstract(); } } export default { _date: DateAdapterBase as { new (options?: AnyObject): DateAdapter; override( members: Partial, 'options'>> ): void; } }; ================================================ FILE: src/core/core.animation.js ================================================ import effects from '../helpers/helpers.easing.js'; import {resolve} from '../helpers/helpers.options.js'; import {color as helpersColor} from '../helpers/helpers.color.js'; const transparent = 'transparent'; const interpolators = { boolean(from, to, factor) { return factor > 0.5 ? to : from; }, /** * @param {string} from * @param {string} to * @param {number} factor */ color(from, to, factor) { const c0 = helpersColor(from || transparent); const c1 = c0.valid && helpersColor(to || transparent); return c1 && c1.valid ? c1.mix(c0, factor).hexString() : to; }, number(from, to, factor) { return from + (to - from) * factor; } }; export default class Animation { constructor(cfg, target, prop, to) { const currentValue = target[prop]; to = resolve([cfg.to, to, currentValue, cfg.from]); const from = resolve([cfg.from, currentValue, to]); this._active = true; this._fn = cfg.fn || interpolators[cfg.type || typeof from]; this._easing = effects[cfg.easing] || effects.linear; this._start = Math.floor(Date.now() + (cfg.delay || 0)); this._duration = this._total = Math.floor(cfg.duration); this._loop = !!cfg.loop; this._target = target; this._prop = prop; this._from = from; this._to = to; this._promises = undefined; } active() { return this._active; } update(cfg, to, date) { if (this._active) { this._notify(false); const currentValue = this._target[this._prop]; const elapsed = date - this._start; const remain = this._duration - elapsed; this._start = date; this._duration = Math.floor(Math.max(remain, cfg.duration)); this._total += elapsed; this._loop = !!cfg.loop; this._to = resolve([cfg.to, to, currentValue, cfg.from]); this._from = resolve([cfg.from, currentValue, to]); } } cancel() { if (this._active) { // update current evaluated value, for smoother animations this.tick(Date.now()); this._active = false; this._notify(false); } } tick(date) { const elapsed = date - this._start; const duration = this._duration; const prop = this._prop; const from = this._from; const loop = this._loop; const to = this._to; let factor; this._active = from !== to && (loop || (elapsed < duration)); if (!this._active) { this._target[prop] = to; this._notify(true); return; } if (elapsed < 0) { this._target[prop] = from; return; } factor = (elapsed / duration) % 2; factor = loop && factor > 1 ? 2 - factor : factor; factor = this._easing(Math.min(1, Math.max(0, factor))); this._target[prop] = this._fn(from, to, factor); } wait() { const promises = this._promises || (this._promises = []); return new Promise((res, rej) => { promises.push({res, rej}); }); } _notify(resolved) { const method = resolved ? 'res' : 'rej'; const promises = this._promises || []; for (let i = 0; i < promises.length; i++) { promises[i][method](); } } } ================================================ FILE: src/core/core.animations.defaults.js ================================================ const numbers = ['x', 'y', 'borderWidth', 'radius', 'tension']; const colors = ['color', 'borderColor', 'backgroundColor']; export function applyAnimationsDefaults(defaults) { defaults.set('animation', { delay: undefined, duration: 1000, easing: 'easeOutQuart', fn: undefined, from: undefined, loop: undefined, to: undefined, type: undefined, }); defaults.describe('animation', { _fallback: false, _indexable: false, _scriptable: (name) => name !== 'onProgress' && name !== 'onComplete' && name !== 'fn', }); defaults.set('animations', { colors: { type: 'color', properties: colors }, numbers: { type: 'number', properties: numbers }, }); defaults.describe('animations', { _fallback: 'animation', }); defaults.set('transitions', { active: { animation: { duration: 400 } }, resize: { animation: { duration: 0 } }, show: { animations: { colors: { from: 'transparent' }, visible: { type: 'boolean', duration: 0 // show immediately }, } }, hide: { animations: { colors: { to: 'transparent' }, visible: { type: 'boolean', easing: 'linear', fn: v => v | 0 // for keeping the dataset visible all the way through the animation }, } } }); } ================================================ FILE: src/core/core.animations.js ================================================ import animator from './core.animator.js'; import Animation from './core.animation.js'; import defaults from './core.defaults.js'; import {isArray, isObject} from '../helpers/helpers.core.js'; export default class Animations { constructor(chart, config) { this._chart = chart; this._properties = new Map(); this.configure(config); } configure(config) { if (!isObject(config)) { return; } const animationOptions = Object.keys(defaults.animation); const animatedProps = this._properties; Object.getOwnPropertyNames(config).forEach(key => { const cfg = config[key]; if (!isObject(cfg)) { return; } const resolved = {}; for (const option of animationOptions) { resolved[option] = cfg[option]; } (isArray(cfg.properties) && cfg.properties || [key]).forEach((prop) => { if (prop === key || !animatedProps.has(prop)) { animatedProps.set(prop, resolved); } }); }); } /** * Utility to handle animation of `options`. * @private */ _animateOptions(target, values) { const newOptions = values.options; const options = resolveTargetOptions(target, newOptions); if (!options) { return []; } const animations = this._createAnimations(options, newOptions); if (newOptions.$shared) { // Going to shared options: // After all animations are done, assign the shared options object to the element // So any new updates to the shared options are observed awaitAll(target.options.$animations, newOptions).then(() => { target.options = newOptions; }, () => { // rejected, noop }); } return animations; } /** * @private */ _createAnimations(target, values) { const animatedProps = this._properties; const animations = []; const running = target.$animations || (target.$animations = {}); const props = Object.keys(values); const date = Date.now(); let i; for (i = props.length - 1; i >= 0; --i) { const prop = props[i]; if (prop.charAt(0) === '$') { continue; } if (prop === 'options') { animations.push(...this._animateOptions(target, values)); continue; } const value = values[prop]; let animation = running[prop]; const cfg = animatedProps.get(prop); if (animation) { if (cfg && animation.active()) { // There is an existing active animation, let's update that animation.update(cfg, value, date); continue; } else { animation.cancel(); } } if (!cfg || !cfg.duration) { // not animated, set directly to new value target[prop] = value; continue; } running[prop] = animation = new Animation(cfg, target, prop, value); animations.push(animation); } return animations; } /** * Update `target` properties to new values, using configured animations * @param {object} target - object to update * @param {object} values - new target properties * @returns {boolean|undefined} - `true` if animations were started **/ update(target, values) { if (this._properties.size === 0) { // Nothing is animated, just apply the new values. Object.assign(target, values); return; } const animations = this._createAnimations(target, values); if (animations.length) { animator.add(this._chart, animations); return true; } } } function awaitAll(animations, properties) { const running = []; const keys = Object.keys(properties); for (let i = 0; i < keys.length; i++) { const anim = animations[keys[i]]; if (anim && anim.active()) { running.push(anim.wait()); } } // @ts-ignore return Promise.all(running); } function resolveTargetOptions(target, newOptions) { if (!newOptions) { return; } let options = target.options; if (!options) { target.options = newOptions; return; } if (options.$shared) { // Going from shared options to distinct one: // Create new options object containing the old shared values and start updating that. target.options = options = Object.assign({}, options, {$shared: false, $animations: {}}); } return options; } ================================================ FILE: src/core/core.animator.js ================================================ import {requestAnimFrame} from '../helpers/helpers.extras.js'; /** * @typedef { import('./core.animation.js').default } Animation * @typedef { import('./core.controller.js').default } Chart */ /** * Please use the module's default export which provides a singleton instance * Note: class is export for typedoc */ export class Animator { constructor() { this._request = null; this._charts = new Map(); this._running = false; this._lastDate = undefined; } /** * @private */ _notify(chart, anims, date, type) { const callbacks = anims.listeners[type]; const numSteps = anims.duration; callbacks.forEach(fn => fn({ chart, initial: anims.initial, numSteps, currentStep: Math.min(date - anims.start, numSteps) })); } /** * @private */ _refresh() { if (this._request) { return; } this._running = true; this._request = requestAnimFrame.call(window, () => { this._update(); this._request = null; if (this._running) { this._refresh(); } }); } /** * @private */ _update(date = Date.now()) { let remaining = 0; this._charts.forEach((anims, chart) => { if (!anims.running || !anims.items.length) { return; } const items = anims.items; let i = items.length - 1; let draw = false; let item; for (; i >= 0; --i) { item = items[i]; if (item._active) { if (item._total > anims.duration) { // if the animation has been updated and its duration prolonged, // update to total duration of current animations run (for progress event) anims.duration = item._total; } item.tick(date); draw = true; } else { // Remove the item by replacing it with last item and removing the last // A lot faster than splice. items[i] = items[items.length - 1]; items.pop(); } } if (draw) { chart.draw(); this._notify(chart, anims, date, 'progress'); } if (!items.length) { anims.running = false; this._notify(chart, anims, date, 'complete'); anims.initial = false; } remaining += items.length; }); this._lastDate = date; if (remaining === 0) { this._running = false; } } /** * @private */ _getAnims(chart) { const charts = this._charts; let anims = charts.get(chart); if (!anims) { anims = { running: false, initial: true, items: [], listeners: { complete: [], progress: [] } }; charts.set(chart, anims); } return anims; } /** * @param {Chart} chart * @param {string} event - event name * @param {Function} cb - callback */ listen(chart, event, cb) { this._getAnims(chart).listeners[event].push(cb); } /** * Add animations * @param {Chart} chart * @param {Animation[]} items - animations */ add(chart, items) { if (!items || !items.length) { return; } this._getAnims(chart).items.push(...items); } /** * Counts number of active animations for the chart * @param {Chart} chart */ has(chart) { return this._getAnims(chart).items.length > 0; } /** * Start animating (all charts) * @param {Chart} chart */ start(chart) { const anims = this._charts.get(chart); if (!anims) { return; } anims.running = true; anims.start = Date.now(); anims.duration = anims.items.reduce((acc, cur) => Math.max(acc, cur._duration), 0); this._refresh(); } running(chart) { if (!this._running) { return false; } const anims = this._charts.get(chart); if (!anims || !anims.running || !anims.items.length) { return false; } return true; } /** * Stop all animations for the chart * @param {Chart} chart */ stop(chart) { const anims = this._charts.get(chart); if (!anims || !anims.items.length) { return; } const items = anims.items; let i = items.length - 1; for (; i >= 0; --i) { items[i].cancel(); } anims.items = []; this._notify(chart, anims, Date.now(), 'complete'); } /** * Remove chart from Animator * @param {Chart} chart */ remove(chart) { return this._charts.delete(chart); } } // singleton instance export default /* #__PURE__ */ new Animator(); ================================================ FILE: src/core/core.config.js ================================================ import defaults, {overrides, descriptors} from './core.defaults.js'; import {mergeIf, resolveObjectKey, isArray, isFunction, valueOrDefault, isObject} from '../helpers/helpers.core.js'; import {_attachContext, _createResolver, _descriptors} from '../helpers/helpers.config.js'; export function getIndexAxis(type, options) { const datasetDefaults = defaults.datasets[type] || {}; const datasetOptions = (options.datasets || {})[type] || {}; return datasetOptions.indexAxis || options.indexAxis || datasetDefaults.indexAxis || 'x'; } function getAxisFromDefaultScaleID(id, indexAxis) { let axis = id; if (id === '_index_') { axis = indexAxis; } else if (id === '_value_') { axis = indexAxis === 'x' ? 'y' : 'x'; } return axis; } function getDefaultScaleIDFromAxis(axis, indexAxis) { return axis === indexAxis ? '_index_' : '_value_'; } function idMatchesAxis(id) { if (id === 'x' || id === 'y' || id === 'r') { return id; } } function axisFromPosition(position) { if (position === 'top' || position === 'bottom') { return 'x'; } if (position === 'left' || position === 'right') { return 'y'; } } export function determineAxis(id, ...scaleOptions) { if (idMatchesAxis(id)) { return id; } for (const opts of scaleOptions) { const axis = opts.axis || axisFromPosition(opts.position) || id.length > 1 && idMatchesAxis(id[0].toLowerCase()); if (axis) { return axis; } } throw new Error(`Cannot determine type of '${id}' axis. Please provide 'axis' or 'position' option.`); } function getAxisFromDataset(id, axis, dataset) { if (dataset[axis + 'AxisID'] === id) { return {axis}; } } function retrieveAxisFromDatasets(id, config) { if (config.data && config.data.datasets) { const boundDs = config.data.datasets.filter((d) => d.xAxisID === id || d.yAxisID === id); if (boundDs.length) { return getAxisFromDataset(id, 'x', boundDs[0]) || getAxisFromDataset(id, 'y', boundDs[0]); } } return {}; } function mergeScaleConfig(config, options) { const chartDefaults = overrides[config.type] || {scales: {}}; const configScales = options.scales || {}; const chartIndexAxis = getIndexAxis(config.type, options); const scales = Object.create(null); // First figure out first scale id's per axis. Object.keys(configScales).forEach(id => { const scaleConf = configScales[id]; if (!isObject(scaleConf)) { return console.error(`Invalid scale configuration for scale: ${id}`); } if (scaleConf._proxy) { return console.warn(`Ignoring resolver passed as options for scale: ${id}`); } const axis = determineAxis(id, scaleConf, retrieveAxisFromDatasets(id, config), defaults.scales[scaleConf.type]); const defaultId = getDefaultScaleIDFromAxis(axis, chartIndexAxis); const defaultScaleOptions = chartDefaults.scales || {}; scales[id] = mergeIf(Object.create(null), [{axis}, scaleConf, defaultScaleOptions[axis], defaultScaleOptions[defaultId]]); }); // Then merge dataset defaults to scale configs config.data.datasets.forEach(dataset => { const type = dataset.type || config.type; const indexAxis = dataset.indexAxis || getIndexAxis(type, options); const datasetDefaults = overrides[type] || {}; const defaultScaleOptions = datasetDefaults.scales || {}; Object.keys(defaultScaleOptions).forEach(defaultID => { const axis = getAxisFromDefaultScaleID(defaultID, indexAxis); const id = dataset[axis + 'AxisID'] || axis; scales[id] = scales[id] || Object.create(null); mergeIf(scales[id], [{axis}, configScales[id], defaultScaleOptions[defaultID]]); }); }); // apply scale defaults, if not overridden by dataset defaults Object.keys(scales).forEach(key => { const scale = scales[key]; mergeIf(scale, [defaults.scales[scale.type], defaults.scale]); }); return scales; } function initOptions(config) { const options = config.options || (config.options = {}); options.plugins = valueOrDefault(options.plugins, {}); options.scales = mergeScaleConfig(config, options); } function initData(data) { data = data || {}; data.datasets = data.datasets || []; data.labels = data.labels || []; return data; } function initConfig(config) { config = config || {}; config.data = initData(config.data); initOptions(config); return config; } const keyCache = new Map(); const keysCached = new Set(); function cachedKeys(cacheKey, generate) { let keys = keyCache.get(cacheKey); if (!keys) { keys = generate(); keyCache.set(cacheKey, keys); keysCached.add(keys); } return keys; } const addIfFound = (set, obj, key) => { const opts = resolveObjectKey(obj, key); if (opts !== undefined) { set.add(opts); } }; export default class Config { constructor(config) { this._config = initConfig(config); this._scopeCache = new Map(); this._resolverCache = new Map(); } get platform() { return this._config.platform; } get type() { return this._config.type; } set type(type) { this._config.type = type; } get data() { return this._config.data; } set data(data) { this._config.data = initData(data); } get options() { return this._config.options; } set options(options) { this._config.options = options; } get plugins() { return this._config.plugins; } update() { const config = this._config; this.clearCache(); initOptions(config); } clearCache() { this._scopeCache.clear(); this._resolverCache.clear(); } /** * Returns the option scope keys for resolving dataset options. * These keys do not include the dataset itself, because it is not under options. * @param {string} datasetType * @return {string[][]} */ datasetScopeKeys(datasetType) { return cachedKeys(datasetType, () => [[ `datasets.${datasetType}`, '' ]]); } /** * Returns the option scope keys for resolving dataset animation options. * These keys do not include the dataset itself, because it is not under options. * @param {string} datasetType * @param {string} transition * @return {string[][]} */ datasetAnimationScopeKeys(datasetType, transition) { return cachedKeys(`${datasetType}.transition.${transition}`, () => [ [ `datasets.${datasetType}.transitions.${transition}`, `transitions.${transition}`, ], // The following are used for looking up the `animations` and `animation` keys [ `datasets.${datasetType}`, '' ] ]); } /** * Returns the options scope keys for resolving element options that belong * to an dataset. These keys do not include the dataset itself, because it * is not under options. * @param {string} datasetType * @param {string} elementType * @return {string[][]} */ datasetElementScopeKeys(datasetType, elementType) { return cachedKeys(`${datasetType}-${elementType}`, () => [[ `datasets.${datasetType}.elements.${elementType}`, `datasets.${datasetType}`, `elements.${elementType}`, '' ]]); } /** * Returns the options scope keys for resolving plugin options. * @param {{id: string, additionalOptionScopes?: string[]}} plugin * @return {string[][]} */ pluginScopeKeys(plugin) { const id = plugin.id; const type = this.type; return cachedKeys(`${type}-plugin-${id}`, () => [[ `plugins.${id}`, ...plugin.additionalOptionScopes || [], ]]); } /** * @private */ _cachedScopes(mainScope, resetCache) { const _scopeCache = this._scopeCache; let cache = _scopeCache.get(mainScope); if (!cache || resetCache) { cache = new Map(); _scopeCache.set(mainScope, cache); } return cache; } /** * Resolves the objects from options and defaults for option value resolution. * @param {object} mainScope - The main scope object for options * @param {string[][]} keyLists - The arrays of keys in resolution order * @param {boolean} [resetCache] - reset the cache for this mainScope */ getOptionScopes(mainScope, keyLists, resetCache) { const {options, type} = this; const cache = this._cachedScopes(mainScope, resetCache); const cached = cache.get(keyLists); if (cached) { return cached; } const scopes = new Set(); keyLists.forEach(keys => { if (mainScope) { scopes.add(mainScope); keys.forEach(key => addIfFound(scopes, mainScope, key)); } keys.forEach(key => addIfFound(scopes, options, key)); keys.forEach(key => addIfFound(scopes, overrides[type] || {}, key)); keys.forEach(key => addIfFound(scopes, defaults, key)); keys.forEach(key => addIfFound(scopes, descriptors, key)); }); const array = Array.from(scopes); if (array.length === 0) { array.push(Object.create(null)); } if (keysCached.has(keyLists)) { cache.set(keyLists, array); } return array; } /** * Returns the option scopes for resolving chart options * @return {object[]} */ chartOptionScopes() { const {options, type} = this; return [ options, overrides[type] || {}, defaults.datasets[type] || {}, // https://github.com/chartjs/Chart.js/issues/8531 {type}, defaults, descriptors ]; } /** * @param {object[]} scopes * @param {string[]} names * @param {function|object} context * @param {string[]} [prefixes] * @return {object} */ resolveNamedOptions(scopes, names, context, prefixes = ['']) { const result = {$shared: true}; const {resolver, subPrefixes} = getResolver(this._resolverCache, scopes, prefixes); let options = resolver; if (needContext(resolver, names)) { result.$shared = false; context = isFunction(context) ? context() : context; // subResolver is passed to scriptable options. It should not resolve to hover options. const subResolver = this.createResolver(scopes, context, subPrefixes); options = _attachContext(resolver, context, subResolver); } for (const prop of names) { result[prop] = options[prop]; } return result; } /** * @param {object[]} scopes * @param {object} [context] * @param {string[]} [prefixes] * @param {{scriptable: boolean, indexable: boolean, allKeys?: boolean}} [descriptorDefaults] */ createResolver(scopes, context, prefixes = [''], descriptorDefaults) { const {resolver} = getResolver(this._resolverCache, scopes, prefixes); return isObject(context) ? _attachContext(resolver, context, undefined, descriptorDefaults) : resolver; } } function getResolver(resolverCache, scopes, prefixes) { let cache = resolverCache.get(scopes); if (!cache) { cache = new Map(); resolverCache.set(scopes, cache); } const cacheKey = prefixes.join(); let cached = cache.get(cacheKey); if (!cached) { const resolver = _createResolver(scopes, prefixes); cached = { resolver, subPrefixes: prefixes.filter(p => !p.toLowerCase().includes('hover')) }; cache.set(cacheKey, cached); } return cached; } const hasFunction = value => isObject(value) && Object.getOwnPropertyNames(value).some((key) => isFunction(value[key])); function needContext(proxy, names) { const {isScriptable, isIndexable} = _descriptors(proxy); for (const prop of names) { const scriptable = isScriptable(prop); const indexable = isIndexable(prop); const value = (indexable || scriptable) && proxy[prop]; if ((scriptable && (isFunction(value) || hasFunction(value))) || (indexable && isArray(value))) { return true; } } return false; } ================================================ FILE: src/core/core.controller.js ================================================ import animator from './core.animator.js'; import defaults, {overrides} from './core.defaults.js'; import Interaction from './core.interaction.js'; import layouts from './core.layouts.js'; import {_detectPlatform} from '../platform/index.js'; import PluginService from './core.plugins.js'; import registry from './core.registry.js'; import Config, {determineAxis, getIndexAxis} from './core.config.js'; import {each, callback as callCallback, uid, valueOrDefault, _elementsEqual, isNullOrUndef, setsEqual, defined, isFunction, _isClickEvent} from '../helpers/helpers.core.js'; import {clearCanvas, clipArea, createContext, unclipArea, _isPointInArea, _isDomSupported, retinaScale, getDatasetClipArea} from '../helpers/index.js'; // @ts-ignore import {version} from '../../package.json'; import {debounce} from '../helpers/helpers.extras.js'; /** * @typedef { import('../types/index.js').ChartEvent } ChartEvent * @typedef { import('../types/index.js').Point } Point */ const KNOWN_POSITIONS = ['top', 'bottom', 'left', 'right', 'chartArea']; function positionIsHorizontal(position, axis) { return position === 'top' || position === 'bottom' || (KNOWN_POSITIONS.indexOf(position) === -1 && axis === 'x'); } function compare2Level(l1, l2) { return function(a, b) { return a[l1] === b[l1] ? a[l2] - b[l2] : a[l1] - b[l1]; }; } function onAnimationsComplete(context) { const chart = context.chart; const animationOptions = chart.options.animation; chart.notifyPlugins('afterRender'); callCallback(animationOptions && animationOptions.onComplete, [context], chart); } function onAnimationProgress(context) { const chart = context.chart; const animationOptions = chart.options.animation; callCallback(animationOptions && animationOptions.onProgress, [context], chart); } /** * Chart.js can take a string id of a canvas element, a 2d context, or a canvas element itself. * Attempt to unwrap the item passed into the chart constructor so that it is a canvas element (if possible). */ function getCanvas(item) { if (_isDomSupported() && typeof item === 'string') { item = document.getElementById(item); } else if (item && item.length) { // Support for array based queries (such as jQuery) item = item[0]; } if (item && item.canvas) { // Support for any object associated to a canvas (including a context2d) item = item.canvas; } return item; } const instances = {}; const getChart = (key) => { const canvas = getCanvas(key); return Object.values(instances).filter((c) => c.canvas === canvas).pop(); }; function moveNumericKeys(obj, start, move) { const keys = Object.keys(obj); for (const key of keys) { const intKey = +key; if (intKey >= start) { const value = obj[key]; delete obj[key]; if (move > 0 || intKey > start) { obj[intKey + move] = value; } } } } /** * @param {ChartEvent} e * @param {ChartEvent|null} lastEvent * @param {boolean} inChartArea * @param {boolean} isClick * @returns {ChartEvent|null} */ function determineLastEvent(e, lastEvent, inChartArea, isClick) { if (!inChartArea || e.type === 'mouseout') { return null; } if (isClick) { return lastEvent; } return e; } class Chart { static defaults = defaults; static instances = instances; static overrides = overrides; static registry = registry; static version = version; static getChart = getChart; static register(...items) { registry.add(...items); invalidatePlugins(); } static unregister(...items) { registry.remove(...items); invalidatePlugins(); } // eslint-disable-next-line max-statements constructor(item, userConfig) { const config = this.config = new Config(userConfig); const initialCanvas = getCanvas(item); const existingChart = getChart(initialCanvas); if (existingChart) { throw new Error( 'Canvas is already in use. Chart with ID \'' + existingChart.id + '\'' + ' must be destroyed before the canvas with ID \'' + existingChart.canvas.id + '\' can be reused.' ); } const options = config.createResolver(config.chartOptionScopes(), this.getContext()); this.platform = new (config.platform || _detectPlatform(initialCanvas))(); this.platform.updateConfig(config); const context = this.platform.acquireContext(initialCanvas, options.aspectRatio); const canvas = context && context.canvas; const height = canvas && canvas.height; const width = canvas && canvas.width; this.id = uid(); this.ctx = context; this.canvas = canvas; this.width = width; this.height = height; this._options = options; // Store the previously used aspect ratio to determine if a resize // is needed during updates. Do this after _options is set since // aspectRatio uses a getter this._aspectRatio = this.aspectRatio; this._layers = []; this._metasets = []; this._stacks = undefined; this.boxes = []; this.currentDevicePixelRatio = undefined; this.chartArea = undefined; this._active = []; this._lastEvent = undefined; this._listeners = {}; /** @type {?{attach?: function, detach?: function, resize?: function}} */ this._responsiveListeners = undefined; this._sortedMetasets = []; this.scales = {}; this._plugins = new PluginService(); this.$proxies = {}; this._hiddenIndices = {}; this.attached = false; this._animationsDisabled = undefined; this.$context = undefined; this._doResize = debounce(mode => this.update(mode), options.resizeDelay || 0); this._dataChanges = []; // Add the chart instance to the global namespace instances[this.id] = this; if (!context || !canvas) { // The given item is not a compatible context2d element, let's return before finalizing // the chart initialization but after setting basic chart / controller properties that // can help to figure out that the chart is not valid (e.g chart.canvas !== null); // https://github.com/chartjs/Chart.js/issues/2807 console.error("Failed to create chart: can't acquire context from the given item"); return; } animator.listen(this, 'complete', onAnimationsComplete); animator.listen(this, 'progress', onAnimationProgress); this._initialize(); if (this.attached) { this.update(); } } get aspectRatio() { const {options: {aspectRatio, maintainAspectRatio}, width, height, _aspectRatio} = this; if (!isNullOrUndef(aspectRatio)) { // If aspectRatio is defined in options, use that. return aspectRatio; } if (maintainAspectRatio && _aspectRatio) { // If maintainAspectRatio is truthly and we had previously determined _aspectRatio, use that return _aspectRatio; } // Calculate return height ? width / height : null; } get data() { return this.config.data; } set data(data) { this.config.data = data; } get options() { return this._options; } set options(options) { this.config.options = options; } get registry() { return registry; } /** * @private */ _initialize() { // Before init plugin notification this.notifyPlugins('beforeInit'); if (this.options.responsive) { this.resize(); } else { retinaScale(this, this.options.devicePixelRatio); } this.bindEvents(); // After init plugin notification this.notifyPlugins('afterInit'); return this; } clear() { clearCanvas(this.canvas, this.ctx); return this; } stop() { animator.stop(this); return this; } /** * Resize the chart to its container or to explicit dimensions. * @param {number} [width] * @param {number} [height] */ resize(width, height) { if (!animator.running(this)) { this._resize(width, height); } else { this._resizeBeforeDraw = {width, height}; } } _resize(width, height) { const options = this.options; const canvas = this.canvas; const aspectRatio = options.maintainAspectRatio && this.aspectRatio; const newSize = this.platform.getMaximumSize(canvas, width, height, aspectRatio); const newRatio = options.devicePixelRatio || this.platform.getDevicePixelRatio(); const mode = this.width ? 'resize' : 'attach'; this.width = newSize.width; this.height = newSize.height; this._aspectRatio = this.aspectRatio; if (!retinaScale(this, newRatio, true)) { return; } this.notifyPlugins('resize', {size: newSize}); callCallback(options.onResize, [this, newSize], this); if (this.attached) { if (this._doResize(mode)) { // The resize update is delayed, only draw without updating. this.render(); } } } ensureScalesHaveIDs() { const options = this.options; const scalesOptions = options.scales || {}; each(scalesOptions, (axisOptions, axisID) => { axisOptions.id = axisID; }); } /** * Builds a map of scale ID to scale object for future lookup. */ buildOrUpdateScales() { const options = this.options; const scaleOpts = options.scales; const scales = this.scales; const updated = Object.keys(scales).reduce((obj, id) => { obj[id] = false; return obj; }, {}); let items = []; if (scaleOpts) { items = items.concat( Object.keys(scaleOpts).map((id) => { const scaleOptions = scaleOpts[id]; const axis = determineAxis(id, scaleOptions); const isRadial = axis === 'r'; const isHorizontal = axis === 'x'; return { options: scaleOptions, dposition: isRadial ? 'chartArea' : isHorizontal ? 'bottom' : 'left', dtype: isRadial ? 'radialLinear' : isHorizontal ? 'category' : 'linear' }; }) ); } each(items, (item) => { const scaleOptions = item.options; const id = scaleOptions.id; const axis = determineAxis(id, scaleOptions); const scaleType = valueOrDefault(scaleOptions.type, item.dtype); if (scaleOptions.position === undefined || positionIsHorizontal(scaleOptions.position, axis) !== positionIsHorizontal(item.dposition)) { scaleOptions.position = item.dposition; } updated[id] = true; let scale = null; if (id in scales && scales[id].type === scaleType) { scale = scales[id]; } else { const scaleClass = registry.getScale(scaleType); scale = new scaleClass({ id, type: scaleType, ctx: this.ctx, chart: this }); scales[scale.id] = scale; } scale.init(scaleOptions, options); }); // clear up discarded scales each(updated, (hasUpdated, id) => { if (!hasUpdated) { delete scales[id]; } }); each(scales, (scale) => { layouts.configure(this, scale, scale.options); layouts.addBox(this, scale); }); } /** * @private */ _updateMetasets() { const metasets = this._metasets; const numData = this.data.datasets.length; const numMeta = metasets.length; metasets.sort((a, b) => a.index - b.index); if (numMeta > numData) { for (let i = numData; i < numMeta; ++i) { this._destroyDatasetMeta(i); } metasets.splice(numData, numMeta - numData); } this._sortedMetasets = metasets.slice(0).sort(compare2Level('order', 'index')); } /** * @private */ _removeUnreferencedMetasets() { const {_metasets: metasets, data: {datasets}} = this; if (metasets.length > datasets.length) { delete this._stacks; } metasets.forEach((meta, index) => { if (datasets.filter(x => x === meta._dataset).length === 0) { this._destroyDatasetMeta(index); } }); } buildOrUpdateControllers() { const newControllers = []; const datasets = this.data.datasets; let i, ilen; this._removeUnreferencedMetasets(); for (i = 0, ilen = datasets.length; i < ilen; i++) { const dataset = datasets[i]; let meta = this.getDatasetMeta(i); const type = dataset.type || this.config.type; if (meta.type && meta.type !== type) { this._destroyDatasetMeta(i); meta = this.getDatasetMeta(i); } meta.type = type; meta.indexAxis = dataset.indexAxis || getIndexAxis(type, this.options); meta.order = dataset.order || 0; meta.index = i; meta.label = '' + dataset.label; meta.visible = this.isDatasetVisible(i); if (meta.controller) { meta.controller.updateIndex(i); meta.controller.linkScales(); } else { const ControllerClass = registry.getController(type); const {datasetElementType, dataElementType} = defaults.datasets[type]; Object.assign(ControllerClass, { dataElementType: registry.getElement(dataElementType), datasetElementType: datasetElementType && registry.getElement(datasetElementType) }); meta.controller = new ControllerClass(this, i); newControllers.push(meta.controller); } } this._updateMetasets(); return newControllers; } /** * Reset the elements of all datasets * @private */ _resetElements() { each(this.data.datasets, (dataset, datasetIndex) => { this.getDatasetMeta(datasetIndex).controller.reset(); }, this); } /** * Resets the chart back to its state before the initial animation */ reset() { this._resetElements(); this.notifyPlugins('reset'); } update(mode) { const config = this.config; config.update(); const options = this._options = config.createResolver(config.chartOptionScopes(), this.getContext()); const animsDisabled = this._animationsDisabled = !options.animation; this._updateScales(); this._checkEventBindings(); this._updateHiddenIndices(); // plugins options references might have change, let's invalidate the cache // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 this._plugins.invalidate(); if (this.notifyPlugins('beforeUpdate', {mode, cancelable: true}) === false) { return; } // Make sure dataset controllers are updated and new controllers are reset const newControllers = this.buildOrUpdateControllers(); this.notifyPlugins('beforeElementsUpdate'); // Make sure all dataset controllers have correct meta data counts let minPadding = 0; for (let i = 0, ilen = this.data.datasets.length; i < ilen; i++) { const {controller} = this.getDatasetMeta(i); const reset = !animsDisabled && newControllers.indexOf(controller) === -1; // New controllers will be reset after the layout pass, so we only want to modify // elements added to new datasets controller.buildOrUpdateElements(reset); minPadding = Math.max(+controller.getMaxOverflow(), minPadding); } minPadding = this._minPadding = options.layout.autoPadding ? minPadding : 0; this._updateLayout(minPadding); // Only reset the controllers if we have animations if (!animsDisabled) { // Can only reset the new controllers after the scales have been updated // Reset is done to get the starting point for the initial animation each(newControllers, (controller) => { controller.reset(); }); } this._updateDatasets(mode); // Do this before render so that any plugins that need final scale updates can use it this.notifyPlugins('afterUpdate', {mode}); this._layers.sort(compare2Level('z', '_idx')); // Replay last event from before update, or set hover styles on active elements const {_active, _lastEvent} = this; if (_lastEvent) { this._eventHandler(_lastEvent, true); } else if (_active.length) { this._updateHoverStyles(_active, _active, true); } this.render(); } /** * @private */ _updateScales() { each(this.scales, (scale) => { layouts.removeBox(this, scale); }); this.ensureScalesHaveIDs(); this.buildOrUpdateScales(); } /** * @private */ _checkEventBindings() { const options = this.options; const existingEvents = new Set(Object.keys(this._listeners)); const newEvents = new Set(options.events); if (!setsEqual(existingEvents, newEvents) || !!this._responsiveListeners !== options.responsive) { // The configured events have changed. Rebind. this.unbindEvents(); this.bindEvents(); } } /** * @private */ _updateHiddenIndices() { const {_hiddenIndices} = this; const changes = this._getUniformDataChanges() || []; for (const {method, start, count} of changes) { const move = method === '_removeElements' ? -count : count; moveNumericKeys(_hiddenIndices, start, move); } } /** * @private */ _getUniformDataChanges() { const _dataChanges = this._dataChanges; if (!_dataChanges || !_dataChanges.length) { return; } this._dataChanges = []; const datasetCount = this.data.datasets.length; const makeSet = (idx) => new Set( _dataChanges .filter(c => c[0] === idx) .map((c, i) => i + ',' + c.splice(1).join(',')) ); const changeSet = makeSet(0); for (let i = 1; i < datasetCount; i++) { if (!setsEqual(changeSet, makeSet(i))) { return; } } return Array.from(changeSet) .map(c => c.split(',')) .map(a => ({method: a[1], start: +a[2], count: +a[3]})); } /** * Updates the chart layout unless a plugin returns `false` to the `beforeLayout` * hook, in which case, plugins will not be called on `afterLayout`. * @private */ _updateLayout(minPadding) { if (this.notifyPlugins('beforeLayout', {cancelable: true}) === false) { return; } layouts.update(this, this.width, this.height, minPadding); const area = this.chartArea; const noArea = area.width <= 0 || area.height <= 0; this._layers = []; each(this.boxes, (box) => { if (noArea && box.position === 'chartArea') { // Skip drawing and configuring chartArea boxes when chartArea is zero or negative return; } // configure is called twice, once in core.scale.update and once here. // Here the boxes are fully updated and at their final positions. if (box.configure) { box.configure(); } this._layers.push(...box._layers()); }, this); this._layers.forEach((item, index) => { item._idx = index; }); this.notifyPlugins('afterLayout'); } /** * Updates all datasets unless a plugin returns `false` to the `beforeDatasetsUpdate` * hook, in which case, plugins will not be called on `afterDatasetsUpdate`. * @private */ _updateDatasets(mode) { if (this.notifyPlugins('beforeDatasetsUpdate', {mode, cancelable: true}) === false) { return; } for (let i = 0, ilen = this.data.datasets.length; i < ilen; ++i) { this.getDatasetMeta(i).controller.configure(); } for (let i = 0, ilen = this.data.datasets.length; i < ilen; ++i) { this._updateDataset(i, isFunction(mode) ? mode({datasetIndex: i}) : mode); } this.notifyPlugins('afterDatasetsUpdate', {mode}); } /** * Updates dataset at index unless a plugin returns `false` to the `beforeDatasetUpdate` * hook, in which case, plugins will not be called on `afterDatasetUpdate`. * @private */ _updateDataset(index, mode) { const meta = this.getDatasetMeta(index); const args = {meta, index, mode, cancelable: true}; if (this.notifyPlugins('beforeDatasetUpdate', args) === false) { return; } meta.controller._update(mode); args.cancelable = false; this.notifyPlugins('afterDatasetUpdate', args); } render() { if (this.notifyPlugins('beforeRender', {cancelable: true}) === false) { return; } if (animator.has(this)) { if (this.attached && !animator.running(this)) { animator.start(this); } } else { this.draw(); onAnimationsComplete({chart: this}); } } draw() { let i; if (this._resizeBeforeDraw) { const {width, height} = this._resizeBeforeDraw; // Unset pending resize request now to avoid possible recursion within _resize this._resizeBeforeDraw = null; this._resize(width, height); } this.clear(); if (this.width <= 0 || this.height <= 0) { return; } if (this.notifyPlugins('beforeDraw', {cancelable: true}) === false) { return; } // Because of plugin hooks (before/afterDatasetsDraw), datasets can't // currently be part of layers. Instead, we draw // layers <= 0 before(default, backward compat), and the rest after const layers = this._layers; for (i = 0; i < layers.length && layers[i].z <= 0; ++i) { layers[i].draw(this.chartArea); } this._drawDatasets(); // Rest of layers for (; i < layers.length; ++i) { layers[i].draw(this.chartArea); } this.notifyPlugins('afterDraw'); } /** * @private */ _getSortedDatasetMetas(filterVisible) { const metasets = this._sortedMetasets; const result = []; let i, ilen; for (i = 0, ilen = metasets.length; i < ilen; ++i) { const meta = metasets[i]; if (!filterVisible || meta.visible) { result.push(meta); } } return result; } /** * Gets the visible dataset metas in drawing order * @return {object[]} */ getSortedVisibleDatasetMetas() { return this._getSortedDatasetMetas(true); } /** * Draws all datasets unless a plugin returns `false` to the `beforeDatasetsDraw` * hook, in which case, plugins will not be called on `afterDatasetsDraw`. * @private */ _drawDatasets() { if (this.notifyPlugins('beforeDatasetsDraw', {cancelable: true}) === false) { return; } const metasets = this.getSortedVisibleDatasetMetas(); for (let i = metasets.length - 1; i >= 0; --i) { this._drawDataset(metasets[i]); } this.notifyPlugins('afterDatasetsDraw'); } /** * Draws dataset at index unless a plugin returns `false` to the `beforeDatasetDraw` * hook, in which case, plugins will not be called on `afterDatasetDraw`. * @private */ _drawDataset(meta) { const ctx = this.ctx; const args = { meta, index: meta.index, cancelable: true }; // @ts-expect-error const clip = getDatasetClipArea(this, meta); if (this.notifyPlugins('beforeDatasetDraw', args) === false) { return; } if (clip) { clipArea(ctx, clip); } meta.controller.draw(); if (clip) { unclipArea(ctx); } args.cancelable = false; this.notifyPlugins('afterDatasetDraw', args); } /** * Checks whether the given point is in the chart area. * @param {Point} point - in relative coordinates (see, e.g., getRelativePosition) * @returns {boolean} */ isPointInArea(point) { return _isPointInArea(point, this.chartArea, this._minPadding); } getElementsAtEventForMode(e, mode, options, useFinalPosition) { const method = Interaction.modes[mode]; if (typeof method === 'function') { return method(this, e, options, useFinalPosition); } return []; } getDatasetMeta(datasetIndex) { const dataset = this.data.datasets[datasetIndex]; const metasets = this._metasets; let meta = metasets.filter(x => x && x._dataset === dataset).pop(); if (!meta) { meta = { type: null, data: [], dataset: null, controller: null, hidden: null, // See isDatasetVisible() comment xAxisID: null, yAxisID: null, order: dataset && dataset.order || 0, index: datasetIndex, _dataset: dataset, _parsed: [], _sorted: false }; metasets.push(meta); } return meta; } getContext() { return this.$context || (this.$context = createContext(null, {chart: this, type: 'chart'})); } getVisibleDatasetCount() { return this.getSortedVisibleDatasetMetas().length; } isDatasetVisible(datasetIndex) { const dataset = this.data.datasets[datasetIndex]; if (!dataset) { return false; } const meta = this.getDatasetMeta(datasetIndex); // meta.hidden is a per chart dataset hidden flag override with 3 states: if true or false, // the dataset.hidden value is ignored, else if null, the dataset hidden state is returned. return typeof meta.hidden === 'boolean' ? !meta.hidden : !dataset.hidden; } setDatasetVisibility(datasetIndex, visible) { const meta = this.getDatasetMeta(datasetIndex); meta.hidden = !visible; } toggleDataVisibility(index) { this._hiddenIndices[index] = !this._hiddenIndices[index]; } getDataVisibility(index) { return !this._hiddenIndices[index]; } /** * @private */ _updateVisibility(datasetIndex, dataIndex, visible) { const mode = visible ? 'show' : 'hide'; const meta = this.getDatasetMeta(datasetIndex); const anims = meta.controller._resolveAnimations(undefined, mode); if (defined(dataIndex)) { meta.data[dataIndex].hidden = !visible; this.update(); } else { this.setDatasetVisibility(datasetIndex, visible); // Animate visible state, so hide animation can be seen. This could be handled better if update / updateDataset returned a Promise. anims.update(meta, {visible}); this.update((ctx) => ctx.datasetIndex === datasetIndex ? mode : undefined); } } hide(datasetIndex, dataIndex) { this._updateVisibility(datasetIndex, dataIndex, false); } show(datasetIndex, dataIndex) { this._updateVisibility(datasetIndex, dataIndex, true); } /** * @private */ _destroyDatasetMeta(datasetIndex) { const meta = this._metasets[datasetIndex]; if (meta && meta.controller) { meta.controller._destroy(); } delete this._metasets[datasetIndex]; } _stop() { let i, ilen; this.stop(); animator.remove(this); for (i = 0, ilen = this.data.datasets.length; i < ilen; ++i) { this._destroyDatasetMeta(i); } } destroy() { this.notifyPlugins('beforeDestroy'); const {canvas, ctx} = this; this._stop(); this.config.clearCache(); if (canvas) { this.unbindEvents(); clearCanvas(canvas, ctx); this.platform.releaseContext(ctx); this.canvas = null; this.ctx = null; } delete instances[this.id]; this.notifyPlugins('afterDestroy'); } toBase64Image(...args) { return this.canvas.toDataURL(...args); } /** * @private */ bindEvents() { this.bindUserEvents(); if (this.options.responsive) { this.bindResponsiveEvents(); } else { this.attached = true; } } /** * @private */ bindUserEvents() { const listeners = this._listeners; const platform = this.platform; const _add = (type, listener) => { platform.addEventListener(this, type, listener); listeners[type] = listener; }; const listener = (e, x, y) => { e.offsetX = x; e.offsetY = y; this._eventHandler(e); }; each(this.options.events, (type) => _add(type, listener)); } /** * @private */ bindResponsiveEvents() { if (!this._responsiveListeners) { this._responsiveListeners = {}; } const listeners = this._responsiveListeners; const platform = this.platform; const _add = (type, listener) => { platform.addEventListener(this, type, listener); listeners[type] = listener; }; const _remove = (type, listener) => { if (listeners[type]) { platform.removeEventListener(this, type, listener); delete listeners[type]; } }; const listener = (width, height) => { if (this.canvas) { this.resize(width, height); } }; let detached; // eslint-disable-line prefer-const const attached = () => { _remove('attach', attached); this.attached = true; this.resize(); _add('resize', listener); _add('detach', detached); }; detached = () => { this.attached = false; _remove('resize', listener); // Stop animating and remove metasets, so when re-attached, the animations start from beginning. this._stop(); this._resize(0, 0); _add('attach', attached); }; if (platform.isAttached(this.canvas)) { attached(); } else { detached(); } } /** * @private */ unbindEvents() { each(this._listeners, (listener, type) => { this.platform.removeEventListener(this, type, listener); }); this._listeners = {}; each(this._responsiveListeners, (listener, type) => { this.platform.removeEventListener(this, type, listener); }); this._responsiveListeners = undefined; } updateHoverStyle(items, mode, enabled) { const prefix = enabled ? 'set' : 'remove'; let meta, item, i, ilen; if (mode === 'dataset') { meta = this.getDatasetMeta(items[0].datasetIndex); meta.controller['_' + prefix + 'DatasetHoverStyle'](); } for (i = 0, ilen = items.length; i < ilen; ++i) { item = items[i]; const controller = item && this.getDatasetMeta(item.datasetIndex).controller; if (controller) { controller[prefix + 'HoverStyle'](item.element, item.datasetIndex, item.index); } } } /** * Get active (hovered) elements * @returns array */ getActiveElements() { return this._active || []; } /** * Set active (hovered) elements * @param {array} activeElements New active data points */ setActiveElements(activeElements) { const lastActive = this._active || []; const active = activeElements.map(({datasetIndex, index}) => { const meta = this.getDatasetMeta(datasetIndex); if (!meta) { throw new Error('No dataset found at index ' + datasetIndex); } return { datasetIndex, element: meta.data[index], index, }; }); const changed = !_elementsEqual(active, lastActive); if (changed) { this._active = active; // Make sure we don't use the previous mouse event to override the active elements in update. this._lastEvent = null; this._updateHoverStyles(active, lastActive); } } /** * Calls enabled plugins on the specified hook and with the given args. * This method immediately returns as soon as a plugin explicitly returns false. The * returned value can be used, for instance, to interrupt the current action. * @param {string} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). * @param {Object} [args] - Extra arguments to apply to the hook call. * @param {import('./core.plugins.js').filterCallback} [filter] - Filtering function for limiting which plugins are notified * @returns {boolean} false if any of the plugins return false, else returns true. */ notifyPlugins(hook, args, filter) { return this._plugins.notify(this, hook, args, filter); } /** * Check if a plugin with the specific ID is registered and enabled * @param {string} pluginId - The ID of the plugin of which to check if it is enabled * @returns {boolean} */ isPluginEnabled(pluginId) { return this._plugins._cache.filter(p => p.plugin.id === pluginId).length === 1; } /** * @private */ _updateHoverStyles(active, lastActive, replay) { const hoverOptions = this.options.hover; const diff = (a, b) => a.filter(x => !b.some(y => x.datasetIndex === y.datasetIndex && x.index === y.index)); const deactivated = diff(lastActive, active); const activated = replay ? active : diff(active, lastActive); if (deactivated.length) { this.updateHoverStyle(deactivated, hoverOptions.mode, false); } if (activated.length && hoverOptions.mode) { this.updateHoverStyle(activated, hoverOptions.mode, true); } } /** * @private */ _eventHandler(e, replay) { const args = { event: e, replay, cancelable: true, inChartArea: this.isPointInArea(e) }; const eventFilter = (plugin) => (plugin.options.events || this.options.events).includes(e.native.type); if (this.notifyPlugins('beforeEvent', args, eventFilter) === false) { return; } const changed = this._handleEvent(e, replay, args.inChartArea); args.cancelable = false; this.notifyPlugins('afterEvent', args, eventFilter); if (changed || args.changed) { this.render(); } return this; } /** * Handle an event * @param {ChartEvent} e the event to handle * @param {boolean} [replay] - true if the event was replayed by `update` * @param {boolean} [inChartArea] - true if the event is inside chartArea * @return {boolean} true if the chart needs to re-render * @private */ _handleEvent(e, replay, inChartArea) { const {_active: lastActive = [], options} = this; // If the event is replayed from `update`, we should evaluate with the final positions. // // The `replay`: // It's the last event (excluding click) that has occurred before `update`. // So mouse has not moved. It's also over the chart, because there is a `replay`. // // The why: // If animations are active, the elements haven't moved yet compared to state before update. // But if they will, we are activating the elements that would be active, if this check // was done after the animations have completed. => "final positions". // If there is no animations, the "final" and "current" positions are equal. // This is done so we do not have to evaluate the active elements each animation frame // - it would be expensive. const useFinalPosition = replay; const active = this._getActiveElements(e, lastActive, inChartArea, useFinalPosition); const isClick = _isClickEvent(e); const lastEvent = determineLastEvent(e, this._lastEvent, inChartArea, isClick); if (inChartArea) { // Set _lastEvent to null while we are processing the event handlers. // This prevents recursion if the handler calls chart.update() this._lastEvent = null; // Invoke onHover hook callCallback(options.onHover, [e, active, this], this); if (isClick) { callCallback(options.onClick, [e, active, this], this); } } const changed = !_elementsEqual(active, lastActive); if (changed || replay) { this._active = active; this._updateHoverStyles(active, lastActive, replay); } this._lastEvent = lastEvent; return changed; } /** * @param {ChartEvent} e - The event * @param {import('../types/index.js').ActiveElement[]} lastActive - Previously active elements * @param {boolean} inChartArea - Is the event inside chartArea * @param {boolean} useFinalPosition - Should the evaluation be done with current or final (after animation) element positions * @returns {import('../types/index.js').ActiveElement[]} - The active elements * @pravate */ _getActiveElements(e, lastActive, inChartArea, useFinalPosition) { if (e.type === 'mouseout') { return []; } if (!inChartArea) { // Let user control the active elements outside chartArea. Eg. using Legend. return lastActive; } const hoverOptions = this.options.hover; return this.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions, useFinalPosition); } } // @ts-ignore function invalidatePlugins() { return each(Chart.instances, (chart) => chart._plugins.invalidate()); } export default Chart; ================================================ FILE: src/core/core.datasetController.js ================================================ import Animations from './core.animations.js'; import defaults from './core.defaults.js'; import {isArray, isFinite, isObject, valueOrDefault, resolveObjectKey, defined} from '../helpers/helpers.core.js'; import {listenArrayEvents, unlistenArrayEvents} from '../helpers/helpers.collection.js'; import {createContext, sign} from '../helpers/index.js'; /** * @typedef { import('./core.controller.js').default } Chart * @typedef { import('./core.scale.js').default } Scale */ function scaleClip(scale, allowedOverflow) { const opts = scale && scale.options || {}; const reverse = opts.reverse; const min = opts.min === undefined ? allowedOverflow : 0; const max = opts.max === undefined ? allowedOverflow : 0; return { start: reverse ? max : min, end: reverse ? min : max }; } function defaultClip(xScale, yScale, allowedOverflow) { if (allowedOverflow === false) { return false; } const x = scaleClip(xScale, allowedOverflow); const y = scaleClip(yScale, allowedOverflow); return { top: y.end, right: x.end, bottom: y.start, left: x.start }; } function toClip(value) { let t, r, b, l; if (isObject(value)) { t = value.top; r = value.right; b = value.bottom; l = value.left; } else { t = r = b = l = value; } return { top: t, right: r, bottom: b, left: l, disabled: value === false }; } function getSortedDatasetIndices(chart, filterVisible) { const keys = []; const metasets = chart._getSortedDatasetMetas(filterVisible); let i, ilen; for (i = 0, ilen = metasets.length; i < ilen; ++i) { keys.push(metasets[i].index); } return keys; } function applyStack(stack, value, dsIndex, options = {}) { const keys = stack.keys; const singleMode = options.mode === 'single'; let i, ilen, datasetIndex, otherValue; if (value === null) { return; } let found = false; for (i = 0, ilen = keys.length; i < ilen; ++i) { datasetIndex = +keys[i]; if (datasetIndex === dsIndex) { found = true; if (options.all) { continue; } break; } otherValue = stack.values[datasetIndex]; if (isFinite(otherValue) && (singleMode || (value === 0 || sign(value) === sign(otherValue)))) { value += otherValue; } } if (!found && !options.all) { return 0; } return value; } function convertObjectDataToArray(data, meta) { const {iScale, vScale} = meta; const iAxisKey = iScale.axis === 'x' ? 'x' : 'y'; const vAxisKey = vScale.axis === 'x' ? 'x' : 'y'; const keys = Object.keys(data); const adata = new Array(keys.length); let i, ilen, key; for (i = 0, ilen = keys.length; i < ilen; ++i) { key = keys[i]; adata[i] = { [iAxisKey]: key, [vAxisKey]: data[key] }; } return adata; } function isStacked(scale, meta) { const stacked = scale && scale.options.stacked; return stacked || (stacked === undefined && meta.stack !== undefined); } function getStackKey(indexScale, valueScale, meta) { return `${indexScale.id}.${valueScale.id}.${meta.stack || meta.type}`; } function getUserBounds(scale) { const {min, max, minDefined, maxDefined} = scale.getUserBounds(); return { min: minDefined ? min : Number.NEGATIVE_INFINITY, max: maxDefined ? max : Number.POSITIVE_INFINITY }; } function getOrCreateStack(stacks, stackKey, indexValue) { const subStack = stacks[stackKey] || (stacks[stackKey] = {}); return subStack[indexValue] || (subStack[indexValue] = {}); } function getLastIndexInStack(stack, vScale, positive, type) { for (const meta of vScale.getMatchingVisibleMetas(type).reverse()) { const value = stack[meta.index]; if ((positive && value > 0) || (!positive && value < 0)) { return meta.index; } } return null; } function updateStacks(controller, parsed) { const {chart, _cachedMeta: meta} = controller; const stacks = chart._stacks || (chart._stacks = {}); // map structure is {stackKey: {datasetIndex: value}} const {iScale, vScale, index: datasetIndex} = meta; const iAxis = iScale.axis; const vAxis = vScale.axis; const key = getStackKey(iScale, vScale, meta); const ilen = parsed.length; let stack; for (let i = 0; i < ilen; ++i) { const item = parsed[i]; const {[iAxis]: index, [vAxis]: value} = item; const itemStacks = item._stacks || (item._stacks = {}); stack = itemStacks[vAxis] = getOrCreateStack(stacks, key, index); stack[datasetIndex] = value; stack._top = getLastIndexInStack(stack, vScale, true, meta.type); stack._bottom = getLastIndexInStack(stack, vScale, false, meta.type); const visualValues = stack._visualValues || (stack._visualValues = {}); visualValues[datasetIndex] = value; } } function getFirstScaleId(chart, axis) { const scales = chart.scales; return Object.keys(scales).filter(key => scales[key].axis === axis).shift(); } function createDatasetContext(parent, index) { return createContext(parent, { active: false, dataset: undefined, datasetIndex: index, index, mode: 'default', type: 'dataset' } ); } function createDataContext(parent, index, element) { return createContext(parent, { active: false, dataIndex: index, parsed: undefined, raw: undefined, element, index, mode: 'default', type: 'data' }); } function clearStacks(meta, items) { // Not using meta.index here, because it might be already updated if the dataset changed location const datasetIndex = meta.controller.index; const axis = meta.vScale && meta.vScale.axis; if (!axis) { return; } items = items || meta._parsed; for (const parsed of items) { const stacks = parsed._stacks; if (!stacks || stacks[axis] === undefined || stacks[axis][datasetIndex] === undefined) { return; } delete stacks[axis][datasetIndex]; if (stacks[axis]._visualValues !== undefined && stacks[axis]._visualValues[datasetIndex] !== undefined) { delete stacks[axis]._visualValues[datasetIndex]; } } } const isDirectUpdateMode = (mode) => mode === 'reset' || mode === 'none'; const cloneIfNotShared = (cached, shared) => shared ? cached : Object.assign({}, cached); const createStack = (canStack, meta, chart) => canStack && !meta.hidden && meta._stacked && {keys: getSortedDatasetIndices(chart, true), values: null}; export default class DatasetController { /** * @type {any} */ static defaults = {}; /** * Element type used to generate a meta dataset (e.g. Chart.element.LineElement). */ static datasetElementType = null; /** * Element type used to generate a meta data (e.g. Chart.element.PointElement). */ static dataElementType = null; /** * @param {Chart} chart * @param {number} datasetIndex */ constructor(chart, datasetIndex) { this.chart = chart; this._ctx = chart.ctx; this.index = datasetIndex; this._cachedDataOpts = {}; this._cachedMeta = this.getMeta(); this._type = this._cachedMeta.type; this.options = undefined; /** @type {boolean | object} */ this._parsing = false; this._data = undefined; this._objectData = undefined; this._sharedOptions = undefined; this._drawStart = undefined; this._drawCount = undefined; this.enableOptionSharing = false; this.supportsDecimation = false; this.$context = undefined; this._syncList = []; this.datasetElementType = new.target.datasetElementType; this.dataElementType = new.target.dataElementType; this.initialize(); } initialize() { const meta = this._cachedMeta; this.configure(); this.linkScales(); meta._stacked = isStacked(meta.vScale, meta); this.addElements(); if (this.options.fill && !this.chart.isPluginEnabled('filler')) { console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options"); } } updateIndex(datasetIndex) { if (this.index !== datasetIndex) { clearStacks(this._cachedMeta); } this.index = datasetIndex; } linkScales() { const chart = this.chart; const meta = this._cachedMeta; const dataset = this.getDataset(); const chooseId = (axis, x, y, r) => axis === 'x' ? x : axis === 'r' ? r : y; const xid = meta.xAxisID = valueOrDefault(dataset.xAxisID, getFirstScaleId(chart, 'x')); const yid = meta.yAxisID = valueOrDefault(dataset.yAxisID, getFirstScaleId(chart, 'y')); const rid = meta.rAxisID = valueOrDefault(dataset.rAxisID, getFirstScaleId(chart, 'r')); const indexAxis = meta.indexAxis; const iid = meta.iAxisID = chooseId(indexAxis, xid, yid, rid); const vid = meta.vAxisID = chooseId(indexAxis, yid, xid, rid); meta.xScale = this.getScaleForId(xid); meta.yScale = this.getScaleForId(yid); meta.rScale = this.getScaleForId(rid); meta.iScale = this.getScaleForId(iid); meta.vScale = this.getScaleForId(vid); } getDataset() { return this.chart.data.datasets[this.index]; } getMeta() { return this.chart.getDatasetMeta(this.index); } /** * @param {string} scaleID * @return {Scale} */ getScaleForId(scaleID) { return this.chart.scales[scaleID]; } /** * @private */ _getOtherScale(scale) { const meta = this._cachedMeta; return scale === meta.iScale ? meta.vScale : meta.iScale; } reset() { this._update('reset'); } /** * @private */ _destroy() { const meta = this._cachedMeta; if (this._data) { unlistenArrayEvents(this._data, this); } if (meta._stacked) { clearStacks(meta); } } /** * @private */ _dataCheck() { const dataset = this.getDataset(); const data = dataset.data || (dataset.data = []); const _data = this._data; // In order to correctly handle data addition/deletion animation (and thus simulate // real-time charts), we need to monitor these data modifications and synchronize // the internal metadata accordingly. if (isObject(data)) { const meta = this._cachedMeta; this._data = convertObjectDataToArray(data, meta); } else if (_data !== data) { if (_data) { // This case happens when the user replaced the data array instance. unlistenArrayEvents(_data, this); // Discard old parsed data and stacks const meta = this._cachedMeta; clearStacks(meta); meta._parsed = []; } if (data && Object.isExtensible(data)) { listenArrayEvents(data, this); } this._syncList = []; this._data = data; } } addElements() { const meta = this._cachedMeta; this._dataCheck(); if (this.datasetElementType) { meta.dataset = new this.datasetElementType(); } } buildOrUpdateElements(resetNewElements) { const meta = this._cachedMeta; const dataset = this.getDataset(); let stackChanged = false; this._dataCheck(); // make sure cached _stacked status is current const oldStacked = meta._stacked; meta._stacked = isStacked(meta.vScale, meta); // detect change in stack option if (meta.stack !== dataset.stack) { stackChanged = true; // remove values from old stack clearStacks(meta); meta.stack = dataset.stack; } // Re-sync meta data in case the user replaced the data array or if we missed // any updates and so make sure that we handle number of datapoints changing. this._resyncElements(resetNewElements); // if stack changed, update stack values for the whole dataset if (stackChanged || oldStacked !== meta._stacked) { updateStacks(this, meta._parsed); meta._stacked = isStacked(meta.vScale, meta); } } /** * Merges user-supplied and default dataset-level options * @private */ configure() { const config = this.chart.config; const scopeKeys = config.datasetScopeKeys(this._type); const scopes = config.getOptionScopes(this.getDataset(), scopeKeys, true); this.options = config.createResolver(scopes, this.getContext()); this._parsing = this.options.parsing; this._cachedDataOpts = {}; } /** * @param {number} start * @param {number} count */ parse(start, count) { const {_cachedMeta: meta, _data: data} = this; const {iScale, _stacked} = meta; const iAxis = iScale.axis; let sorted = start === 0 && count === data.length ? true : meta._sorted; let prev = start > 0 && meta._parsed[start - 1]; let i, cur, parsed; if (this._parsing === false) { meta._parsed = data; meta._sorted = true; parsed = data; } else { if (isArray(data[start])) { parsed = this.parseArrayData(meta, data, start, count); } else if (isObject(data[start])) { parsed = this.parseObjectData(meta, data, start, count); } else { parsed = this.parsePrimitiveData(meta, data, start, count); } const isNotInOrderComparedToPrev = () => cur[iAxis] === null || (prev && cur[iAxis] < prev[iAxis]); for (i = 0; i < count; ++i) { meta._parsed[i + start] = cur = parsed[i]; if (sorted) { if (isNotInOrderComparedToPrev()) { sorted = false; } prev = cur; } } meta._sorted = sorted; } if (_stacked) { updateStacks(this, parsed); } } /** * Parse array of primitive values * @param {object} meta - dataset meta * @param {array} data - data array. Example [1,3,4] * @param {number} start - start index * @param {number} count - number of items to parse * @returns {object} parsed item - item containing index and a parsed value * for each scale id. * Example: {xScale0: 0, yScale0: 1} * @protected */ parsePrimitiveData(meta, data, start, count) { const {iScale, vScale} = meta; const iAxis = iScale.axis; const vAxis = vScale.axis; const labels = iScale.getLabels(); const singleScale = iScale === vScale; const parsed = new Array(count); let i, ilen, index; for (i = 0, ilen = count; i < ilen; ++i) { index = i + start; parsed[i] = { [iAxis]: singleScale || iScale.parse(labels[index], index), [vAxis]: vScale.parse(data[index], index) }; } return parsed; } /** * Parse array of arrays * @param {object} meta - dataset meta * @param {array} data - data array. Example [[1,2],[3,4]] * @param {number} start - start index * @param {number} count - number of items to parse * @returns {object} parsed item - item containing index and a parsed value * for each scale id. * Example: {x: 0, y: 1} * @protected */ parseArrayData(meta, data, start, count) { const {xScale, yScale} = meta; const parsed = new Array(count); let i, ilen, index, item; for (i = 0, ilen = count; i < ilen; ++i) { index = i + start; item = data[index]; parsed[i] = { x: xScale.parse(item[0], index), y: yScale.parse(item[1], index) }; } return parsed; } /** * Parse array of objects * @param {object} meta - dataset meta * @param {array} data - data array. Example [{x:1, y:5}, {x:2, y:10}] * @param {number} start - start index * @param {number} count - number of items to parse * @returns {object} parsed item - item containing index and a parsed value * for each scale id. _custom is optional * Example: {xScale0: 0, yScale0: 1, _custom: {r: 10, foo: 'bar'}} * @protected */ parseObjectData(meta, data, start, count) { const {xScale, yScale} = meta; const {xAxisKey = 'x', yAxisKey = 'y'} = this._parsing; const parsed = new Array(count); let i, ilen, index, item; for (i = 0, ilen = count; i < ilen; ++i) { index = i + start; item = data[index]; parsed[i] = { x: xScale.parse(resolveObjectKey(item, xAxisKey), index), y: yScale.parse(resolveObjectKey(item, yAxisKey), index) }; } return parsed; } /** * @protected */ getParsed(index) { return this._cachedMeta._parsed[index]; } /** * @protected */ getDataElement(index) { return this._cachedMeta.data[index]; } /** * @protected */ applyStack(scale, parsed, mode) { const chart = this.chart; const meta = this._cachedMeta; const value = parsed[scale.axis]; const stack = { keys: getSortedDatasetIndices(chart, true), values: parsed._stacks[scale.axis]._visualValues }; return applyStack(stack, value, meta.index, {mode}); } /** * @protected */ updateRangeFromParsed(range, scale, parsed, stack) { const parsedValue = parsed[scale.axis]; let value = parsedValue === null ? NaN : parsedValue; const values = stack && parsed._stacks[scale.axis]; if (stack && values) { stack.values = values; value = applyStack(stack, parsedValue, this._cachedMeta.index); } range.min = Math.min(range.min, value); range.max = Math.max(range.max, value); } /** * @protected */ getMinMax(scale, canStack) { const meta = this._cachedMeta; const _parsed = meta._parsed; const sorted = meta._sorted && scale === meta.iScale; const ilen = _parsed.length; const otherScale = this._getOtherScale(scale); const stack = createStack(canStack, meta, this.chart); const range = {min: Number.POSITIVE_INFINITY, max: Number.NEGATIVE_INFINITY}; const {min: otherMin, max: otherMax} = getUserBounds(otherScale); let i, parsed; function _skip() { parsed = _parsed[i]; const otherValue = parsed[otherScale.axis]; return !isFinite(parsed[scale.axis]) || otherMin > otherValue || otherMax < otherValue; } for (i = 0; i < ilen; ++i) { if (_skip()) { continue; } this.updateRangeFromParsed(range, scale, parsed, stack); if (sorted) { // if the data is sorted, we don't need to check further from this end of array break; } } if (sorted) { // in the sorted case, find first non-skipped value from other end of array for (i = ilen - 1; i >= 0; --i) { if (_skip()) { continue; } this.updateRangeFromParsed(range, scale, parsed, stack); break; } } return range; } getAllParsedValues(scale) { const parsed = this._cachedMeta._parsed; const values = []; let i, ilen, value; for (i = 0, ilen = parsed.length; i < ilen; ++i) { value = parsed[i][scale.axis]; if (isFinite(value)) { values.push(value); } } return values; } /** * @return {number|boolean} * @protected */ getMaxOverflow() { return false; } /** * @protected */ getLabelAndValue(index) { const meta = this._cachedMeta; const iScale = meta.iScale; const vScale = meta.vScale; const parsed = this.getParsed(index); return { label: iScale ? '' + iScale.getLabelForValue(parsed[iScale.axis]) : '', value: vScale ? '' + vScale.getLabelForValue(parsed[vScale.axis]) : '' }; } /** * @private */ _update(mode) { const meta = this._cachedMeta; this.update(mode || 'default'); meta._clip = toClip(valueOrDefault(this.options.clip, defaultClip(meta.xScale, meta.yScale, this.getMaxOverflow()))); } /** * @param {string} mode */ update(mode) {} // eslint-disable-line no-unused-vars draw() { const ctx = this._ctx; const chart = this.chart; const meta = this._cachedMeta; const elements = meta.data || []; const area = chart.chartArea; const active = []; const start = this._drawStart || 0; const count = this._drawCount || (elements.length - start); const drawActiveElementsOnTop = this.options.drawActiveElementsOnTop; let i; if (meta.dataset) { meta.dataset.draw(ctx, area, start, count); } for (i = start; i < start + count; ++i) { const element = elements[i]; if (element.hidden) { continue; } if (element.active && drawActiveElementsOnTop) { active.push(element); } else { element.draw(ctx, area); } } for (i = 0; i < active.length; ++i) { active[i].draw(ctx, area); } } /** * Returns a set of predefined style properties that should be used to represent the dataset * or the data if the index is specified * @param {number} index - data index * @param {boolean} [active] - true if hover * @return {object} style object */ getStyle(index, active) { const mode = active ? 'active' : 'default'; return index === undefined && this._cachedMeta.dataset ? this.resolveDatasetElementOptions(mode) : this.resolveDataElementOptions(index || 0, mode); } /** * @protected */ getContext(index, active, mode) { const dataset = this.getDataset(); let context; if (index >= 0 && index < this._cachedMeta.data.length) { const element = this._cachedMeta.data[index]; context = element.$context || (element.$context = createDataContext(this.getContext(), index, element)); context.parsed = this.getParsed(index); context.raw = dataset.data[index]; context.index = context.dataIndex = index; } else { context = this.$context || (this.$context = createDatasetContext(this.chart.getContext(), this.index)); context.dataset = dataset; context.index = context.datasetIndex = this.index; } context.active = !!active; context.mode = mode; return context; } /** * @param {string} [mode] * @protected */ resolveDatasetElementOptions(mode) { return this._resolveElementOptions(this.datasetElementType.id, mode); } /** * @param {number} index * @param {string} [mode] * @protected */ resolveDataElementOptions(index, mode) { return this._resolveElementOptions(this.dataElementType.id, mode, index); } /** * @private */ _resolveElementOptions(elementType, mode = 'default', index) { const active = mode === 'active'; const cache = this._cachedDataOpts; const cacheKey = elementType + '-' + mode; const cached = cache[cacheKey]; const sharing = this.enableOptionSharing && defined(index); if (cached) { return cloneIfNotShared(cached, sharing); } const config = this.chart.config; const scopeKeys = config.datasetElementScopeKeys(this._type, elementType); const prefixes = active ? [`${elementType}Hover`, 'hover', elementType, ''] : [elementType, '']; const scopes = config.getOptionScopes(this.getDataset(), scopeKeys); const names = Object.keys(defaults.elements[elementType]); // context is provided as a function, and is called only if needed, // so we don't create a context for each element if not needed. const context = () => this.getContext(index, active, mode); const values = config.resolveNamedOptions(scopes, names, context, prefixes); if (values.$shared) { // `$shared` indicates this set of options can be shared between multiple elements. // Sharing is used to reduce number of properties to change during animation. values.$shared = sharing; // We cache options by `mode`, which can be 'active' for example. This enables us // to have the 'active' element options and 'default' options to switch between // when interacting. cache[cacheKey] = Object.freeze(cloneIfNotShared(values, sharing)); } return values; } /** * @private */ _resolveAnimations(index, transition, active) { const chart = this.chart; const cache = this._cachedDataOpts; const cacheKey = `animation-${transition}`; const cached = cache[cacheKey]; if (cached) { return cached; } let options; if (chart.options.animation !== false) { const config = this.chart.config; const scopeKeys = config.datasetAnimationScopeKeys(this._type, transition); const scopes = config.getOptionScopes(this.getDataset(), scopeKeys); options = config.createResolver(scopes, this.getContext(index, active, transition)); } const animations = new Animations(chart, options && options.animations); if (options && options._cacheable) { cache[cacheKey] = Object.freeze(animations); } return animations; } /** * Utility for getting the options object shared between elements * @protected */ getSharedOptions(options) { if (!options.$shared) { return; } return this._sharedOptions || (this._sharedOptions = Object.assign({}, options)); } /** * Utility for determining if `options` should be included in the updated properties * @protected */ includeOptions(mode, sharedOptions) { return !sharedOptions || isDirectUpdateMode(mode) || this.chart._animationsDisabled; } /** * @todo v4, rename to getSharedOptions and remove excess functions */ _getSharedOptions(start, mode) { const firstOpts = this.resolveDataElementOptions(start, mode); const previouslySharedOptions = this._sharedOptions; const sharedOptions = this.getSharedOptions(firstOpts); const includeOptions = this.includeOptions(mode, sharedOptions) || (sharedOptions !== previouslySharedOptions); this.updateSharedOptions(sharedOptions, mode, firstOpts); return {sharedOptions, includeOptions}; } /** * Utility for updating an element with new properties, using animations when appropriate. * @protected */ updateElement(element, index, properties, mode) { if (isDirectUpdateMode(mode)) { Object.assign(element, properties); } else { this._resolveAnimations(index, mode).update(element, properties); } } /** * Utility to animate the shared options, that are potentially affecting multiple elements. * @protected */ updateSharedOptions(sharedOptions, mode, newOptions) { if (sharedOptions && !isDirectUpdateMode(mode)) { this._resolveAnimations(undefined, mode).update(sharedOptions, newOptions); } } /** * @private */ _setStyle(element, index, mode, active) { element.active = active; const options = this.getStyle(index, active); this._resolveAnimations(index, mode, active).update(element, { // When going from active to inactive, we need to update to the shared options. // This way the once hovered element will end up with the same original shared options instance, after animation. options: (!active && this.getSharedOptions(options)) || options }); } removeHoverStyle(element, datasetIndex, index) { this._setStyle(element, index, 'active', false); } setHoverStyle(element, datasetIndex, index) { this._setStyle(element, index, 'active', true); } /** * @private */ _removeDatasetHoverStyle() { const element = this._cachedMeta.dataset; if (element) { this._setStyle(element, undefined, 'active', false); } } /** * @private */ _setDatasetHoverStyle() { const element = this._cachedMeta.dataset; if (element) { this._setStyle(element, undefined, 'active', true); } } /** * @private */ _resyncElements(resetNewElements) { const data = this._data; const elements = this._cachedMeta.data; // Apply changes detected through array listeners for (const [method, arg1, arg2] of this._syncList) { this[method](arg1, arg2); } this._syncList = []; const numMeta = elements.length; const numData = data.length; const count = Math.min(numData, numMeta); if (count) { // TODO: It is not optimal to always parse the old data // This is done because we are not detecting direct assignments: // chart.data.datasets[0].data[5] = 10; // chart.data.datasets[0].data[5].y = 10; this.parse(0, count); } if (numData > numMeta) { this._insertElements(numMeta, numData - numMeta, resetNewElements); } else if (numData < numMeta) { this._removeElements(numData, numMeta - numData); } } /** * @private */ _insertElements(start, count, resetNewElements = true) { const meta = this._cachedMeta; const data = meta.data; const end = start + count; let i; const move = (arr) => { arr.length += count; for (i = arr.length - 1; i >= end; i--) { arr[i] = arr[i - count]; } }; move(data); for (i = start; i < end; ++i) { data[i] = new this.dataElementType(); } if (this._parsing) { move(meta._parsed); } this.parse(start, count); if (resetNewElements) { this.updateElements(data, start, count, 'reset'); } } updateElements(element, start, count, mode) {} // eslint-disable-line no-unused-vars /** * @private */ _removeElements(start, count) { const meta = this._cachedMeta; if (this._parsing) { const removed = meta._parsed.splice(start, count); if (meta._stacked) { clearStacks(meta, removed); } } meta.data.splice(start, count); } /** * @private */ _sync(args) { if (this._parsing) { this._syncList.push(args); } else { const [method, arg1, arg2] = args; this[method](arg1, arg2); } this.chart._dataChanges.push([this.index, ...args]); } _onDataPush() { const count = arguments.length; this._sync(['_insertElements', this.getDataset().data.length - count, count]); } _onDataPop() { this._sync(['_removeElements', this._cachedMeta.data.length - 1, 1]); } _onDataShift() { this._sync(['_removeElements', 0, 1]); } _onDataSplice(start, count) { if (count) { this._sync(['_removeElements', start, count]); } const newCount = arguments.length - 2; if (newCount) { this._sync(['_insertElements', start, newCount]); } } _onDataUnshift() { this._sync(['_insertElements', 0, arguments.length]); } } ================================================ FILE: src/core/core.defaults.js ================================================ import {getHoverColor} from '../helpers/helpers.color.js'; import {isObject, merge, valueOrDefault} from '../helpers/helpers.core.js'; import {applyAnimationsDefaults} from './core.animations.defaults.js'; import {applyLayoutsDefaults} from './core.layouts.defaults.js'; import {applyScaleDefaults} from './core.scale.defaults.js'; export const overrides = Object.create(null); export const descriptors = Object.create(null); /** * @param {object} node * @param {string} key * @return {object} */ function getScope(node, key) { if (!key) { return node; } const keys = key.split('.'); for (let i = 0, n = keys.length; i < n; ++i) { const k = keys[i]; node = node[k] || (node[k] = Object.create(null)); } return node; } function set(root, scope, values) { if (typeof scope === 'string') { return merge(getScope(root, scope), values); } return merge(getScope(root, ''), scope); } /** * Please use the module's default export which provides a singleton instance * Note: class is exported for typedoc */ export class Defaults { constructor(_descriptors, _appliers) { this.animation = undefined; this.backgroundColor = 'rgba(0,0,0,0.1)'; this.borderColor = 'rgba(0,0,0,0.1)'; this.color = '#666'; this.datasets = {}; this.devicePixelRatio = (context) => context.chart.platform.getDevicePixelRatio(); this.elements = {}; this.events = [ 'mousemove', 'mouseout', 'click', 'touchstart', 'touchmove' ]; this.font = { family: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", size: 12, style: 'normal', lineHeight: 1.2, weight: null }; this.hover = {}; this.hoverBackgroundColor = (ctx, options) => getHoverColor(options.backgroundColor); this.hoverBorderColor = (ctx, options) => getHoverColor(options.borderColor); this.hoverColor = (ctx, options) => getHoverColor(options.color); this.indexAxis = 'x'; this.interaction = { mode: 'nearest', intersect: true, includeInvisible: false }; this.maintainAspectRatio = true; this.onHover = null; this.onClick = null; this.parsing = true; this.plugins = {}; this.responsive = true; this.scale = undefined; this.scales = {}; this.showLine = true; this.drawActiveElementsOnTop = true; this.describe(_descriptors); this.apply(_appliers); } /** * @param {string|object} scope * @param {object} [values] */ set(scope, values) { return set(this, scope, values); } /** * @param {string} scope */ get(scope) { return getScope(this, scope); } /** * @param {string|object} scope * @param {object} [values] */ describe(scope, values) { return set(descriptors, scope, values); } override(scope, values) { return set(overrides, scope, values); } /** * Routes the named defaults to fallback to another scope/name. * This routing is useful when those target values, like defaults.color, are changed runtime. * If the values would be copied, the runtime change would not take effect. By routing, the * fallback is evaluated at each access, so its always up to date. * * Example: * * defaults.route('elements.arc', 'backgroundColor', '', 'color') * - reads the backgroundColor from defaults.color when undefined locally * * @param {string} scope Scope this route applies to. * @param {string} name Property name that should be routed to different namespace when not defined here. * @param {string} targetScope The namespace where those properties should be routed to. * Empty string ('') is the root of defaults. * @param {string} targetName The target name in the target scope the property should be routed to. */ route(scope, name, targetScope, targetName) { const scopeObject = getScope(this, scope); const targetScopeObject = getScope(this, targetScope); const privateName = '_' + name; Object.defineProperties(scopeObject, { // A private property is defined to hold the actual value, when this property is set in its scope (set in the setter) [privateName]: { value: scopeObject[name], writable: true }, // The actual property is defined as getter/setter so we can do the routing when value is not locally set. [name]: { enumerable: true, get() { const local = this[privateName]; const target = targetScopeObject[targetName]; if (isObject(local)) { return Object.assign({}, target, local); } return valueOrDefault(local, target); }, set(value) { this[privateName] = value; } } }); } apply(appliers) { appliers.forEach((apply) => apply(this)); } } // singleton instance export default /* #__PURE__ */ new Defaults({ _scriptable: (name) => !name.startsWith('on'), _indexable: (name) => name !== 'events', hover: { _fallback: 'interaction' }, interaction: { _scriptable: false, _indexable: false, } }, [applyAnimationsDefaults, applyLayoutsDefaults, applyScaleDefaults]); ================================================ FILE: src/core/core.element.ts ================================================ import type {AnyObject} from '../types/basic.js'; import type {Point} from '../types/geometric.js'; import type {Animation} from '../types/animation.js'; import {isNumber} from '../helpers/helpers.math.js'; export default class Element { static defaults = {}; static defaultRoutes = undefined; x: number; y: number; active = false; options: O; $animations: Record; tooltipPosition(useFinalPosition: boolean): Point { const {x, y} = this.getProps(['x', 'y'], useFinalPosition); return {x, y} as Point; } hasValue() { return isNumber(this.x) && isNumber(this.y); } /** * Gets the current or final value of each prop. Can return extra properties (whole object). * @param props - properties to get * @param [final] - get the final value (animation target) */ getProps

(props: P, final?: boolean): Pick; getProps

(props: P[], final?: boolean): Partial>; getProps(props: string[], final?: boolean): Partial> { const anims = this.$animations; if (!final || !anims) { // let's not create an object, if not needed return this as Record; } const ret: Record = {}; props.forEach((prop) => { ret[prop] = anims[prop] && anims[prop].active() ? anims[prop]._to : this[prop as string]; }); return ret; } } ================================================ FILE: src/core/core.interaction.js ================================================ import {_lookupByKey, _rlookupByKey} from '../helpers/helpers.collection.js'; import {getRelativePosition} from '../helpers/helpers.dom.js'; import {_angleBetween, getAngleFromPoint} from '../helpers/helpers.math.js'; import {_isPointInArea, isNullOrUndef} from '../helpers/index.js'; /** * @typedef { import('./core.controller.js').default } Chart * @typedef { import('../types/index.js').ChartEvent } ChartEvent * @typedef {{axis?: string, intersect?: boolean, includeInvisible?: boolean}} InteractionOptions * @typedef {{datasetIndex: number, index: number, element: import('./core.element.js').default}} InteractionItem * @typedef { import('../types/index.js').Point } Point */ /** * Helper function to do binary search when possible * @param {object} metaset - the dataset meta * @param {string} axis - the axis mode. x|y|xy|r * @param {number} value - the value to find * @param {boolean} [intersect] - should the element intersect * @returns {{lo:number, hi:number}} indices to search data array between */ function binarySearch(metaset, axis, value, intersect) { const {controller, data, _sorted} = metaset; const iScale = controller._cachedMeta.iScale; const spanGaps = metaset.dataset ? metaset.dataset.options ? metaset.dataset.options.spanGaps : null : null; if (iScale && axis === iScale.axis && axis !== 'r' && _sorted && data.length) { const lookupMethod = iScale._reversePixels ? _rlookupByKey : _lookupByKey; if (!intersect) { const result = lookupMethod(data, axis, value); if (spanGaps) { const {vScale} = controller._cachedMeta; const {_parsed} = metaset; const distanceToDefinedLo = (_parsed .slice(0, result.lo + 1) .reverse() .findIndex( point => !isNullOrUndef(point[vScale.axis]))); result.lo -= Math.max(0, distanceToDefinedLo); const distanceToDefinedHi = (_parsed .slice(result.hi) .findIndex( point => !isNullOrUndef(point[vScale.axis]))); result.hi += Math.max(0, distanceToDefinedHi); } return result; } else if (controller._sharedOptions) { // _sharedOptions indicates that each element has equal options -> equal proportions // So we can do a ranged binary search based on the range of first element and // be confident to get the full range of indices that can intersect with the value. const el = data[0]; const range = typeof el.getRange === 'function' && el.getRange(axis); if (range) { const start = lookupMethod(data, axis, value - range); const end = lookupMethod(data, axis, value + range); return {lo: start.lo, hi: end.hi}; } } } // Default to all elements, when binary search can not be used. return {lo: 0, hi: data.length - 1}; } /** * Helper function to select candidate elements for interaction * @param {Chart} chart - the chart * @param {string} axis - the axis mode. x|y|xy|r * @param {Point} position - the point to be nearest to, in relative coordinates * @param {function} handler - the callback to execute for each visible item * @param {boolean} [intersect] - consider intersecting items */ function evaluateInteractionItems(chart, axis, position, handler, intersect) { const metasets = chart.getSortedVisibleDatasetMetas(); const value = position[axis]; for (let i = 0, ilen = metasets.length; i < ilen; ++i) { const {index, data} = metasets[i]; const {lo, hi} = binarySearch(metasets[i], axis, value, intersect); for (let j = lo; j <= hi; ++j) { const element = data[j]; if (!element.skip) { handler(element, index, j); } } } } /** * Get a distance metric function for two points based on the * axis mode setting * @param {string} axis - the axis mode. x|y|xy|r */ function getDistanceMetricForAxis(axis) { const useX = axis.indexOf('x') !== -1; const useY = axis.indexOf('y') !== -1; return function(pt1, pt2) { const deltaX = useX ? Math.abs(pt1.x - pt2.x) : 0; const deltaY = useY ? Math.abs(pt1.y - pt2.y) : 0; return Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)); }; } /** * Helper function to get the items that intersect the event position * @param {Chart} chart - the chart * @param {Point} position - the point to be nearest to, in relative coordinates * @param {string} axis - the axis mode. x|y|xy|r * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position * @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area * @return {InteractionItem[]} the nearest items */ function getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) { const items = []; if (!includeInvisible && !chart.isPointInArea(position)) { return items; } const evaluationFunc = function(element, datasetIndex, index) { if (!includeInvisible && !_isPointInArea(element, chart.chartArea, 0)) { return; } if (element.inRange(position.x, position.y, useFinalPosition)) { items.push({element, datasetIndex, index}); } }; evaluateInteractionItems(chart, axis, position, evaluationFunc, true); return items; } /** * Helper function to get the items nearest to the event position for a radial chart * @param {Chart} chart - the chart to look at elements from * @param {Point} position - the point to be nearest to, in relative coordinates * @param {string} axis - the axes along which to measure distance * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position * @return {InteractionItem[]} the nearest items */ function getNearestRadialItems(chart, position, axis, useFinalPosition) { let items = []; function evaluationFunc(element, datasetIndex, index) { const {startAngle, endAngle} = element.getProps(['startAngle', 'endAngle'], useFinalPosition); const {angle} = getAngleFromPoint(element, {x: position.x, y: position.y}); if (_angleBetween(angle, startAngle, endAngle)) { items.push({element, datasetIndex, index}); } } evaluateInteractionItems(chart, axis, position, evaluationFunc); return items; } /** * Helper function to get the items nearest to the event position for a cartesian chart * @param {Chart} chart - the chart to look at elements from * @param {Point} position - the point to be nearest to, in relative coordinates * @param {string} axis - the axes along which to measure distance * @param {boolean} [intersect] - if true, only consider items that intersect the position * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position * @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area * @return {InteractionItem[]} the nearest items */ function getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition, includeInvisible) { let items = []; const distanceMetric = getDistanceMetricForAxis(axis); let minDistance = Number.POSITIVE_INFINITY; function evaluationFunc(element, datasetIndex, index) { const inRange = element.inRange(position.x, position.y, useFinalPosition); if (intersect && !inRange) { return; } const center = element.getCenterPoint(useFinalPosition); const pointInArea = !!includeInvisible || chart.isPointInArea(center); if (!pointInArea && !inRange) { return; } const distance = distanceMetric(position, center); if (distance < minDistance) { items = [{element, datasetIndex, index}]; minDistance = distance; } else if (distance === minDistance) { // Can have multiple items at the same distance in which case we sort by size items.push({element, datasetIndex, index}); } } evaluateInteractionItems(chart, axis, position, evaluationFunc); return items; } /** * Helper function to get the items nearest to the event position considering all visible items in the chart * @param {Chart} chart - the chart to look at elements from * @param {Point} position - the point to be nearest to, in relative coordinates * @param {string} axis - the axes along which to measure distance * @param {boolean} [intersect] - if true, only consider items that intersect the position * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position * @param {boolean} [includeInvisible] - include invisible points that are outside of the chart area * @return {InteractionItem[]} the nearest items */ function getNearestItems(chart, position, axis, intersect, useFinalPosition, includeInvisible) { if (!includeInvisible && !chart.isPointInArea(position)) { return []; } return axis === 'r' && !intersect ? getNearestRadialItems(chart, position, axis, useFinalPosition) : getNearestCartesianItems(chart, position, axis, intersect, useFinalPosition, includeInvisible); } /** * Helper function to get the items matching along the given X or Y axis * @param {Chart} chart - the chart to look at elements from * @param {Point} position - the point to be nearest to, in relative coordinates * @param {string} axis - the axis to match * @param {boolean} [intersect] - if true, only consider items that intersect the position * @param {boolean} [useFinalPosition] - use the element's animation target instead of current position * @return {InteractionItem[]} the nearest items */ function getAxisItems(chart, position, axis, intersect, useFinalPosition) { const items = []; const rangeMethod = axis === 'x' ? 'inXRange' : 'inYRange'; let intersectsItem = false; evaluateInteractionItems(chart, axis, position, (element, datasetIndex, index) => { if (element[rangeMethod] && element[rangeMethod](position[axis], useFinalPosition)) { items.push({element, datasetIndex, index}); intersectsItem = intersectsItem || element.inRange(position.x, position.y, useFinalPosition); } }); // If we want to trigger on an intersect and we don't have any items // that intersect the position, return nothing if (intersect && !intersectsItem) { return []; } return items; } /** * Contains interaction related functions * @namespace Chart.Interaction */ export default { // Part of the public API to facilitate developers creating their own modes evaluateInteractionItems, // Helper function for different modes modes: { /** * Returns items at the same index. If the options.intersect parameter is true, we only return items if we intersect something * If the options.intersect mode is false, we find the nearest item and return the items at the same index as that item * @function Chart.Interaction.modes.index * @since v2.4.0 * @param {Chart} chart - the chart we are returning items from * @param {Event} e - the event we are find things at * @param {InteractionOptions} options - options to use * @param {boolean} [useFinalPosition] - use final element position (animation target) * @return {InteractionItem[]} - items that are found */ index(chart, e, options, useFinalPosition) { const position = getRelativePosition(e, chart); // Default axis for index mode is 'x' to match old behaviour const axis = options.axis || 'x'; const includeInvisible = options.includeInvisible || false; const items = options.intersect ? getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) : getNearestItems(chart, position, axis, false, useFinalPosition, includeInvisible); const elements = []; if (!items.length) { return []; } chart.getSortedVisibleDatasetMetas().forEach((meta) => { const index = items[0].index; const element = meta.data[index]; // don't count items that are skipped (null data) if (element && !element.skip) { elements.push({element, datasetIndex: meta.index, index}); } }); return elements; }, /** * Returns items in the same dataset. If the options.intersect parameter is true, we only return items if we intersect something * If the options.intersect is false, we find the nearest item and return the items in that dataset * @function Chart.Interaction.modes.dataset * @param {Chart} chart - the chart we are returning items from * @param {Event} e - the event we are find things at * @param {InteractionOptions} options - options to use * @param {boolean} [useFinalPosition] - use final element position (animation target) * @return {InteractionItem[]} - items that are found */ dataset(chart, e, options, useFinalPosition) { const position = getRelativePosition(e, chart); const axis = options.axis || 'xy'; const includeInvisible = options.includeInvisible || false; let items = options.intersect ? getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible) : getNearestItems(chart, position, axis, false, useFinalPosition, includeInvisible); if (items.length > 0) { const datasetIndex = items[0].datasetIndex; const data = chart.getDatasetMeta(datasetIndex).data; items = []; for (let i = 0; i < data.length; ++i) { items.push({element: data[i], datasetIndex, index: i}); } } return items; }, /** * Point mode returns all elements that hit test based on the event position * of the event * @function Chart.Interaction.modes.intersect * @param {Chart} chart - the chart we are returning items from * @param {Event} e - the event we are find things at * @param {InteractionOptions} options - options to use * @param {boolean} [useFinalPosition] - use final element position (animation target) * @return {InteractionItem[]} - items that are found */ point(chart, e, options, useFinalPosition) { const position = getRelativePosition(e, chart); const axis = options.axis || 'xy'; const includeInvisible = options.includeInvisible || false; return getIntersectItems(chart, position, axis, useFinalPosition, includeInvisible); }, /** * nearest mode returns the element closest to the point * @function Chart.Interaction.modes.intersect * @param {Chart} chart - the chart we are returning items from * @param {Event} e - the event we are find things at * @param {InteractionOptions} options - options to use * @param {boolean} [useFinalPosition] - use final element position (animation target) * @return {InteractionItem[]} - items that are found */ nearest(chart, e, options, useFinalPosition) { const position = getRelativePosition(e, chart); const axis = options.axis || 'xy'; const includeInvisible = options.includeInvisible || false; return getNearestItems(chart, position, axis, options.intersect, useFinalPosition, includeInvisible); }, /** * x mode returns the elements that hit-test at the current x coordinate * @function Chart.Interaction.modes.x * @param {Chart} chart - the chart we are returning items from * @param {Event} e - the event we are find things at * @param {InteractionOptions} options - options to use * @param {boolean} [useFinalPosition] - use final element position (animation target) * @return {InteractionItem[]} - items that are found */ x(chart, e, options, useFinalPosition) { const position = getRelativePosition(e, chart); return getAxisItems(chart, position, 'x', options.intersect, useFinalPosition); }, /** * y mode returns the elements that hit-test at the current y coordinate * @function Chart.Interaction.modes.y * @param {Chart} chart - the chart we are returning items from * @param {Event} e - the event we are find things at * @param {InteractionOptions} options - options to use * @param {boolean} [useFinalPosition] - use final element position (animation target) * @return {InteractionItem[]} - items that are found */ y(chart, e, options, useFinalPosition) { const position = getRelativePosition(e, chart); return getAxisItems(chart, position, 'y', options.intersect, useFinalPosition); } } }; ================================================ FILE: src/core/core.layouts.defaults.js ================================================ export function applyLayoutsDefaults(defaults) { defaults.set('layout', { autoPadding: true, padding: { top: 0, right: 0, bottom: 0, left: 0 } }); } ================================================ FILE: src/core/core.layouts.js ================================================ import {defined, each, isObject} from '../helpers/helpers.core.js'; import {toPadding} from '../helpers/helpers.options.js'; /** * @typedef { import('./core.controller.js').default } Chart */ const STATIC_POSITIONS = ['left', 'top', 'right', 'bottom']; function filterByPosition(array, position) { return array.filter(v => v.pos === position); } function filterDynamicPositionByAxis(array, axis) { return array.filter(v => STATIC_POSITIONS.indexOf(v.pos) === -1 && v.box.axis === axis); } function sortByWeight(array, reverse) { return array.sort((a, b) => { const v0 = reverse ? b : a; const v1 = reverse ? a : b; return v0.weight === v1.weight ? v0.index - v1.index : v0.weight - v1.weight; }); } function wrapBoxes(boxes) { const layoutBoxes = []; let i, ilen, box, pos, stack, stackWeight; for (i = 0, ilen = (boxes || []).length; i < ilen; ++i) { box = boxes[i]; ({position: pos, options: {stack, stackWeight = 1}} = box); layoutBoxes.push({ index: i, box, pos, horizontal: box.isHorizontal(), weight: box.weight, stack: stack && (pos + stack), stackWeight }); } return layoutBoxes; } function buildStacks(layouts) { const stacks = {}; for (const wrap of layouts) { const {stack, pos, stackWeight} = wrap; if (!stack || !STATIC_POSITIONS.includes(pos)) { continue; } const _stack = stacks[stack] || (stacks[stack] = {count: 0, placed: 0, weight: 0, size: 0}); _stack.count++; _stack.weight += stackWeight; } return stacks; } /** * store dimensions used instead of available chartArea in fitBoxes **/ function setLayoutDims(layouts, params) { const stacks = buildStacks(layouts); const {vBoxMaxWidth, hBoxMaxHeight} = params; let i, ilen, layout; for (i = 0, ilen = layouts.length; i < ilen; ++i) { layout = layouts[i]; const {fullSize} = layout.box; const stack = stacks[layout.stack]; const factor = stack && layout.stackWeight / stack.weight; if (layout.horizontal) { layout.width = factor ? factor * vBoxMaxWidth : fullSize && params.availableWidth; layout.height = hBoxMaxHeight; } else { layout.width = vBoxMaxWidth; layout.height = factor ? factor * hBoxMaxHeight : fullSize && params.availableHeight; } } return stacks; } function buildLayoutBoxes(boxes) { const layoutBoxes = wrapBoxes(boxes); const fullSize = sortByWeight(layoutBoxes.filter(wrap => wrap.box.fullSize), true); const left = sortByWeight(filterByPosition(layoutBoxes, 'left'), true); const right = sortByWeight(filterByPosition(layoutBoxes, 'right')); const top = sortByWeight(filterByPosition(layoutBoxes, 'top'), true); const bottom = sortByWeight(filterByPosition(layoutBoxes, 'bottom')); const centerHorizontal = filterDynamicPositionByAxis(layoutBoxes, 'x'); const centerVertical = filterDynamicPositionByAxis(layoutBoxes, 'y'); return { fullSize, leftAndTop: left.concat(top), rightAndBottom: right.concat(centerVertical).concat(bottom).concat(centerHorizontal), chartArea: filterByPosition(layoutBoxes, 'chartArea'), vertical: left.concat(right).concat(centerVertical), horizontal: top.concat(bottom).concat(centerHorizontal) }; } function getCombinedMax(maxPadding, chartArea, a, b) { return Math.max(maxPadding[a], chartArea[a]) + Math.max(maxPadding[b], chartArea[b]); } function updateMaxPadding(maxPadding, boxPadding) { maxPadding.top = Math.max(maxPadding.top, boxPadding.top); maxPadding.left = Math.max(maxPadding.left, boxPadding.left); maxPadding.bottom = Math.max(maxPadding.bottom, boxPadding.bottom); maxPadding.right = Math.max(maxPadding.right, boxPadding.right); } function updateDims(chartArea, params, layout, stacks) { const {pos, box} = layout; const maxPadding = chartArea.maxPadding; // dynamically placed boxes size is not considered if (!isObject(pos)) { if (layout.size) { // this layout was already counted for, lets first reduce old size chartArea[pos] -= layout.size; } const stack = stacks[layout.stack] || {size: 0, count: 1}; stack.size = Math.max(stack.size, layout.horizontal ? box.height : box.width); layout.size = stack.size / stack.count; chartArea[pos] += layout.size; } if (box.getPadding) { updateMaxPadding(maxPadding, box.getPadding()); } const newWidth = Math.max(0, params.outerWidth - getCombinedMax(maxPadding, chartArea, 'left', 'right')); const newHeight = Math.max(0, params.outerHeight - getCombinedMax(maxPadding, chartArea, 'top', 'bottom')); const widthChanged = newWidth !== chartArea.w; const heightChanged = newHeight !== chartArea.h; chartArea.w = newWidth; chartArea.h = newHeight; // return booleans on the changes per direction return layout.horizontal ? {same: widthChanged, other: heightChanged} : {same: heightChanged, other: widthChanged}; } function handleMaxPadding(chartArea) { const maxPadding = chartArea.maxPadding; function updatePos(pos) { const change = Math.max(maxPadding[pos] - chartArea[pos], 0); chartArea[pos] += change; return change; } chartArea.y += updatePos('top'); chartArea.x += updatePos('left'); updatePos('right'); updatePos('bottom'); } function getMargins(horizontal, chartArea) { const maxPadding = chartArea.maxPadding; function marginForPositions(positions) { const margin = {left: 0, top: 0, right: 0, bottom: 0}; positions.forEach((pos) => { margin[pos] = Math.max(chartArea[pos], maxPadding[pos]); }); return margin; } return horizontal ? marginForPositions(['left', 'right']) : marginForPositions(['top', 'bottom']); } function fitBoxes(boxes, chartArea, params, stacks) { const refitBoxes = []; let i, ilen, layout, box, refit, changed; for (i = 0, ilen = boxes.length, refit = 0; i < ilen; ++i) { layout = boxes[i]; box = layout.box; box.update( layout.width || chartArea.w, layout.height || chartArea.h, getMargins(layout.horizontal, chartArea) ); const {same, other} = updateDims(chartArea, params, layout, stacks); // Dimensions changed and there were non full width boxes before this // -> we have to refit those refit |= same && refitBoxes.length; // Chart area changed in the opposite direction changed = changed || other; if (!box.fullSize) { // fullSize boxes don't need to be re-fitted in any case refitBoxes.push(layout); } } return refit && fitBoxes(refitBoxes, chartArea, params, stacks) || changed; } function setBoxDims(box, left, top, width, height) { box.top = top; box.left = left; box.right = left + width; box.bottom = top + height; box.width = width; box.height = height; } function placeBoxes(boxes, chartArea, params, stacks) { const userPadding = params.padding; let {x, y} = chartArea; for (const layout of boxes) { const box = layout.box; const stack = stacks[layout.stack] || {count: 1, placed: 0, weight: 1}; const weight = (layout.stackWeight / stack.weight) || 1; if (layout.horizontal) { const width = chartArea.w * weight; const height = stack.size || box.height; if (defined(stack.start)) { y = stack.start; } if (box.fullSize) { setBoxDims(box, userPadding.left, y, params.outerWidth - userPadding.right - userPadding.left, height); } else { setBoxDims(box, chartArea.left + stack.placed, y, width, height); } stack.start = y; stack.placed += width; y = box.bottom; } else { const height = chartArea.h * weight; const width = stack.size || box.width; if (defined(stack.start)) { x = stack.start; } if (box.fullSize) { setBoxDims(box, x, userPadding.top, width, params.outerHeight - userPadding.bottom - userPadding.top); } else { setBoxDims(box, x, chartArea.top + stack.placed, width, height); } stack.start = x; stack.placed += height; x = box.right; } } chartArea.x = x; chartArea.y = y; } /** * @interface LayoutItem * @typedef {object} LayoutItem * @prop {string} position - The position of the item in the chart layout. Possible values are * 'left', 'top', 'right', 'bottom', and 'chartArea' * @prop {number} weight - The weight used to sort the item. Higher weights are further away from the chart area * @prop {boolean} fullSize - if true, and the item is horizontal, then push vertical boxes down * @prop {function} isHorizontal - returns true if the layout item is horizontal (ie. top or bottom) * @prop {function} update - Takes two parameters: width and height. Returns size of item * @prop {function} draw - Draws the element * @prop {function} [getPadding] - Returns an object with padding on the edges * @prop {number} width - Width of item. Must be valid after update() * @prop {number} height - Height of item. Must be valid after update() * @prop {number} left - Left edge of the item. Set by layout system and cannot be used in update * @prop {number} top - Top edge of the item. Set by layout system and cannot be used in update * @prop {number} right - Right edge of the item. Set by layout system and cannot be used in update * @prop {number} bottom - Bottom edge of the item. Set by layout system and cannot be used in update */ // The layout service is very self explanatory. It's responsible for the layout within a chart. // Scales, Legends and Plugins all rely on the layout service and can easily register to be placed anywhere they need // It is this service's responsibility of carrying out that layout. export default { /** * Register a box to a chart. * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. * @param {Chart} chart - the chart to use * @param {LayoutItem} item - the item to add to be laid out */ addBox(chart, item) { if (!chart.boxes) { chart.boxes = []; } // initialize item with default values item.fullSize = item.fullSize || false; item.position = item.position || 'top'; item.weight = item.weight || 0; // @ts-ignore item._layers = item._layers || function() { return [{ z: 0, draw(chartArea) { item.draw(chartArea); } }]; }; chart.boxes.push(item); }, /** * Remove a layoutItem from a chart * @param {Chart} chart - the chart to remove the box from * @param {LayoutItem} layoutItem - the item to remove from the layout */ removeBox(chart, layoutItem) { const index = chart.boxes ? chart.boxes.indexOf(layoutItem) : -1; if (index !== -1) { chart.boxes.splice(index, 1); } }, /** * Sets (or updates) options on the given `item`. * @param {Chart} chart - the chart in which the item lives (or will be added to) * @param {LayoutItem} item - the item to configure with the given options * @param {object} options - the new item options. */ configure(chart, item, options) { item.fullSize = options.fullSize; item.position = options.position; item.weight = options.weight; }, /** * Fits boxes of the given chart into the given size by having each box measure itself * then running a fitting algorithm * @param {Chart} chart - the chart * @param {number} width - the width to fit into * @param {number} height - the height to fit into * @param {number} minPadding - minimum padding required for each side of chart area */ update(chart, width, height, minPadding) { if (!chart) { return; } const padding = toPadding(chart.options.layout.padding); const availableWidth = Math.max(width - padding.width, 0); const availableHeight = Math.max(height - padding.height, 0); const boxes = buildLayoutBoxes(chart.boxes); const verticalBoxes = boxes.vertical; const horizontalBoxes = boxes.horizontal; // Before any changes are made, notify boxes that an update is about to being // This is used to clear any cached data (e.g. scale limits) each(chart.boxes, box => { if (typeof box.beforeLayout === 'function') { box.beforeLayout(); } }); // Essentially we now have any number of boxes on each of the 4 sides. // Our canvas looks like the following. // The areas L1 and L2 are the left axes. R1 is the right axis, T1 is the top axis and // B1 is the bottom axis // There are also 4 quadrant-like locations (left to right instead of clockwise) reserved for chart overlays // These locations are single-box locations only, when trying to register a chartArea location that is already taken, // an error will be thrown. // // |----------------------------------------------------| // | T1 (Full Width) | // |----------------------------------------------------| // | | | T2 | | // | |----|-------------------------------------|----| // | | | C1 | | C2 | | // | | |----| |----| | // | | | | | // | L1 | L2 | ChartArea (C0) | R1 | // | | | | | // | | |----| |----| | // | | | C3 | | C4 | | // | |----|-------------------------------------|----| // | | | B1 | | // |----------------------------------------------------| // | B2 (Full Width) | // |----------------------------------------------------| // const visibleVerticalBoxCount = verticalBoxes.reduce((total, wrap) => wrap.box.options && wrap.box.options.display === false ? total : total + 1, 0) || 1; const params = Object.freeze({ outerWidth: width, outerHeight: height, padding, availableWidth, availableHeight, vBoxMaxWidth: availableWidth / 2 / visibleVerticalBoxCount, hBoxMaxHeight: availableHeight / 2 }); const maxPadding = Object.assign({}, padding); updateMaxPadding(maxPadding, toPadding(minPadding)); const chartArea = Object.assign({ maxPadding, w: availableWidth, h: availableHeight, x: padding.left, y: padding.top }, padding); const stacks = setLayoutDims(verticalBoxes.concat(horizontalBoxes), params); // First fit the fullSize boxes, to reduce probability of re-fitting. fitBoxes(boxes.fullSize, chartArea, params, stacks); // Then fit vertical boxes fitBoxes(verticalBoxes, chartArea, params, stacks); // Then fit horizontal boxes if (fitBoxes(horizontalBoxes, chartArea, params, stacks)) { // if the area changed, re-fit vertical boxes fitBoxes(verticalBoxes, chartArea, params, stacks); } handleMaxPadding(chartArea); // Finally place the boxes to correct coordinates placeBoxes(boxes.leftAndTop, chartArea, params, stacks); // Move to opposite side of chart chartArea.x += chartArea.w; chartArea.y += chartArea.h; placeBoxes(boxes.rightAndBottom, chartArea, params, stacks); chart.chartArea = { left: chartArea.left, top: chartArea.top, right: chartArea.left + chartArea.w, bottom: chartArea.top + chartArea.h, height: chartArea.h, width: chartArea.w, }; // Finally update boxes in chartArea (radial scale for example) each(boxes.chartArea, (layout) => { const box = layout.box; Object.assign(box, chart.chartArea); box.update(chartArea.w, chartArea.h, {left: 0, top: 0, right: 0, bottom: 0}); }); } }; ================================================ FILE: src/core/core.plugins.js ================================================ import registry from './core.registry.js'; import {callback as callCallback, isNullOrUndef, valueOrDefault} from '../helpers/helpers.core.js'; /** * @typedef { import('./core.controller.js').default } Chart * @typedef { import('../types/index.js').ChartEvent } ChartEvent * @typedef { import('../plugins/plugin.tooltip.js').default } Tooltip */ /** * @callback filterCallback * @param {{plugin: object, options: object}} value * @param {number} [index] * @param {array} [array] * @param {object} [thisArg] * @return {boolean} */ export default class PluginService { constructor() { this._init = undefined; } /** * Calls enabled plugins for `chart` on the specified hook and with the given args. * This method immediately returns as soon as a plugin explicitly returns false. The * returned value can be used, for instance, to interrupt the current action. * @param {Chart} chart - The chart instance for which plugins should be called. * @param {string} hook - The name of the plugin method to call (e.g. 'beforeUpdate'). * @param {object} [args] - Extra arguments to apply to the hook call. * @param {filterCallback} [filter] - Filtering function for limiting which plugins are notified * @returns {boolean} false if any of the plugins return false, else returns true. */ notify(chart, hook, args, filter) { if (hook === 'beforeInit') { this._init = this._createDescriptors(chart, true); this._notify(this._init, chart, 'install'); } if (this._init === undefined) { // Do not trigger events before install return; } const descriptors = filter ? this._descriptors(chart).filter(filter) : this._descriptors(chart); const result = this._notify(descriptors, chart, hook, args); if (hook === 'afterDestroy') { this._notify(descriptors, chart, 'stop'); this._notify(this._init, chart, 'uninstall'); this._init = undefined; // Do not trigger events after uninstall } return result; } /** * @private */ _notify(descriptors, chart, hook, args) { args = args || {}; for (const descriptor of descriptors) { const plugin = descriptor.plugin; const method = plugin[hook]; const params = [chart, args, descriptor.options]; if (callCallback(method, params, plugin) === false && args.cancelable) { return false; } } return true; } invalidate() { // When plugins are registered, there is the possibility of a double // invalidate situation. In this case, we only want to invalidate once. // If we invalidate multiple times, the `_oldCache` is lost and all of the // plugins are restarted without being correctly stopped. // See https://github.com/chartjs/Chart.js/issues/8147 if (!isNullOrUndef(this._cache)) { this._oldCache = this._cache; this._cache = undefined; } } /** * @param {Chart} chart * @private */ _descriptors(chart) { if (this._cache) { return this._cache; } const descriptors = this._cache = this._createDescriptors(chart); this._notifyStateChanges(chart); return descriptors; } _createDescriptors(chart, all) { const config = chart && chart.config; const options = valueOrDefault(config.options && config.options.plugins, {}); const plugins = allPlugins(config); // options === false => all plugins are disabled return options === false && !all ? [] : createDescriptors(chart, plugins, options, all); } /** * @param {Chart} chart * @private */ _notifyStateChanges(chart) { const previousDescriptors = this._oldCache || []; const descriptors = this._cache; const diff = (a, b) => a.filter(x => !b.some(y => x.plugin.id === y.plugin.id)); this._notify(diff(previousDescriptors, descriptors), chart, 'stop'); this._notify(diff(descriptors, previousDescriptors), chart, 'start'); } } /** * @param {import('./core.config.js').default} config */ function allPlugins(config) { const localIds = {}; const plugins = []; const keys = Object.keys(registry.plugins.items); for (let i = 0; i < keys.length; i++) { plugins.push(registry.getPlugin(keys[i])); } const local = config.plugins || []; for (let i = 0; i < local.length; i++) { const plugin = local[i]; if (plugins.indexOf(plugin) === -1) { plugins.push(plugin); localIds[plugin.id] = true; } } return {plugins, localIds}; } function getOpts(options, all) { if (!all && options === false) { return null; } if (options === true) { return {}; } return options; } function createDescriptors(chart, {plugins, localIds}, options, all) { const result = []; const context = chart.getContext(); for (const plugin of plugins) { const id = plugin.id; const opts = getOpts(options[id], all); if (opts === null) { continue; } result.push({ plugin, options: pluginOpts(chart.config, {plugin, local: localIds[id]}, opts, context) }); } return result; } function pluginOpts(config, {plugin, local}, opts, context) { const keys = config.pluginScopeKeys(plugin); const scopes = config.getOptionScopes(opts, keys); if (local && plugin.defaults) { // make sure plugin defaults are in scopes for local (not registered) plugins scopes.push(plugin.defaults); } return config.createResolver(scopes, context, [''], { // These are just defaults that plugins can override scriptable: false, indexable: false, allKeys: true }); } ================================================ FILE: src/core/core.registry.js ================================================ import DatasetController from './core.datasetController.js'; import Element from './core.element.js'; import Scale from './core.scale.js'; import TypedRegistry from './core.typedRegistry.js'; import {each, callback as call, _capitalize} from '../helpers/helpers.core.js'; /** * Please use the module's default export which provides a singleton instance * Note: class is exported for typedoc */ export class Registry { constructor() { this.controllers = new TypedRegistry(DatasetController, 'datasets', true); this.elements = new TypedRegistry(Element, 'elements'); this.plugins = new TypedRegistry(Object, 'plugins'); this.scales = new TypedRegistry(Scale, 'scales'); // Order is important, Scale has Element in prototype chain, // so Scales must be before Elements. Plugins are a fallback, so not listed here. this._typedRegistries = [this.controllers, this.scales, this.elements]; } /** * @param {...any} args */ add(...args) { this._each('register', args); } remove(...args) { this._each('unregister', args); } /** * @param {...typeof DatasetController} args */ addControllers(...args) { this._each('register', args, this.controllers); } /** * @param {...typeof Element} args */ addElements(...args) { this._each('register', args, this.elements); } /** * @param {...any} args */ addPlugins(...args) { this._each('register', args, this.plugins); } /** * @param {...typeof Scale} args */ addScales(...args) { this._each('register', args, this.scales); } /** * @param {string} id * @returns {typeof DatasetController} */ getController(id) { return this._get(id, this.controllers, 'controller'); } /** * @param {string} id * @returns {typeof Element} */ getElement(id) { return this._get(id, this.elements, 'element'); } /** * @param {string} id * @returns {object} */ getPlugin(id) { return this._get(id, this.plugins, 'plugin'); } /** * @param {string} id * @returns {typeof Scale} */ getScale(id) { return this._get(id, this.scales, 'scale'); } /** * @param {...typeof DatasetController} args */ removeControllers(...args) { this._each('unregister', args, this.controllers); } /** * @param {...typeof Element} args */ removeElements(...args) { this._each('unregister', args, this.elements); } /** * @param {...any} args */ removePlugins(...args) { this._each('unregister', args, this.plugins); } /** * @param {...typeof Scale} args */ removeScales(...args) { this._each('unregister', args, this.scales); } /** * @private */ _each(method, args, typedRegistry) { [...args].forEach(arg => { const reg = typedRegistry || this._getRegistryForType(arg); if (typedRegistry || reg.isForType(arg) || (reg === this.plugins && arg.id)) { this._exec(method, reg, arg); } else { // Handle loopable args // Use case: // import * as plugins from './plugins.js'; // Chart.register(plugins); each(arg, item => { // If there are mixed types in the loopable, make sure those are // registered in correct registry // Use case: (treemap exporting controller, elements etc) // import * as treemap from 'chartjs-chart-treemap.js'; // Chart.register(treemap); const itemReg = typedRegistry || this._getRegistryForType(item); this._exec(method, itemReg, item); }); } }); } /** * @private */ _exec(method, registry, component) { const camelMethod = _capitalize(method); call(component['before' + camelMethod], [], component); // beforeRegister / beforeUnregister registry[method](component); call(component['after' + camelMethod], [], component); // afterRegister / afterUnregister } /** * @private */ _getRegistryForType(type) { for (let i = 0; i < this._typedRegistries.length; i++) { const reg = this._typedRegistries[i]; if (reg.isForType(type)) { return reg; } } // plugins is the fallback registry return this.plugins; } /** * @private */ _get(id, typedRegistry, type) { const item = typedRegistry.get(id); if (item === undefined) { throw new Error('"' + id + '" is not a registered ' + type + '.'); } return item; } } // singleton instance export default /* #__PURE__ */ new Registry(); ================================================ FILE: src/core/core.scale.autoskip.js ================================================ import {isNullOrUndef, valueOrDefault} from '../helpers/helpers.core.js'; import {_factorize} from '../helpers/helpers.math.js'; /** * @typedef { import('./core.controller.js').default } Chart * @typedef {{value:number | string, label?:string, major?:boolean, $context?:any}} Tick */ /** * Returns a subset of ticks to be plotted to avoid overlapping labels. * @param {import('./core.scale.js').default} scale * @param {Tick[]} ticks * @return {Tick[]} * @private */ export function autoSkip(scale, ticks) { const tickOpts = scale.options.ticks; const determinedMaxTicks = determineMaxTicks(scale); const ticksLimit = Math.min(tickOpts.maxTicksLimit || determinedMaxTicks, determinedMaxTicks); const majorIndices = tickOpts.major.enabled ? getMajorIndices(ticks) : []; const numMajorIndices = majorIndices.length; const first = majorIndices[0]; const last = majorIndices[numMajorIndices - 1]; const newTicks = []; // If there are too many major ticks to display them all if (numMajorIndices > ticksLimit) { skipMajors(ticks, newTicks, majorIndices, numMajorIndices / ticksLimit); return newTicks; } const spacing = calculateSpacing(majorIndices, ticks, ticksLimit); if (numMajorIndices > 0) { let i, ilen; const avgMajorSpacing = numMajorIndices > 1 ? Math.round((last - first) / (numMajorIndices - 1)) : null; skip(ticks, newTicks, spacing, isNullOrUndef(avgMajorSpacing) ? 0 : first - avgMajorSpacing, first); for (i = 0, ilen = numMajorIndices - 1; i < ilen; i++) { skip(ticks, newTicks, spacing, majorIndices[i], majorIndices[i + 1]); } skip(ticks, newTicks, spacing, last, isNullOrUndef(avgMajorSpacing) ? ticks.length : last + avgMajorSpacing); return newTicks; } skip(ticks, newTicks, spacing); return newTicks; } function determineMaxTicks(scale) { const offset = scale.options.offset; const tickLength = scale._tickSize(); const maxScale = scale._length / tickLength + (offset ? 0 : 1); const maxChart = scale._maxLength / tickLength; return Math.floor(Math.min(maxScale, maxChart)); } /** * @param {number[]} majorIndices * @param {Tick[]} ticks * @param {number} ticksLimit */ function calculateSpacing(majorIndices, ticks, ticksLimit) { const evenMajorSpacing = getEvenSpacing(majorIndices); const spacing = ticks.length / ticksLimit; // If the major ticks are evenly spaced apart, place the minor ticks // so that they divide the major ticks into even chunks if (!evenMajorSpacing) { return Math.max(spacing, 1); } const factors = _factorize(evenMajorSpacing); for (let i = 0, ilen = factors.length - 1; i < ilen; i++) { const factor = factors[i]; if (factor > spacing) { return factor; } } return Math.max(spacing, 1); } /** * @param {Tick[]} ticks */ function getMajorIndices(ticks) { const result = []; let i, ilen; for (i = 0, ilen = ticks.length; i < ilen; i++) { if (ticks[i].major) { result.push(i); } } return result; } /** * @param {Tick[]} ticks * @param {Tick[]} newTicks * @param {number[]} majorIndices * @param {number} spacing */ function skipMajors(ticks, newTicks, majorIndices, spacing) { let count = 0; let next = majorIndices[0]; let i; spacing = Math.ceil(spacing); for (i = 0; i < ticks.length; i++) { if (i === next) { newTicks.push(ticks[i]); count++; next = majorIndices[count * spacing]; } } } /** * @param {Tick[]} ticks * @param {Tick[]} newTicks * @param {number} spacing * @param {number} [majorStart] * @param {number} [majorEnd] */ function skip(ticks, newTicks, spacing, majorStart, majorEnd) { const start = valueOrDefault(majorStart, 0); const end = Math.min(valueOrDefault(majorEnd, ticks.length), ticks.length); let count = 0; let length, i, next; spacing = Math.ceil(spacing); if (majorEnd) { length = majorEnd - majorStart; spacing = length / Math.floor(length / spacing); } next = start; while (next < 0) { count++; next = Math.round(start + count * spacing); } for (i = Math.max(start, 0); i < end; i++) { if (i === next) { newTicks.push(ticks[i]); count++; next = Math.round(start + count * spacing); } } } /** * @param {number[]} arr */ function getEvenSpacing(arr) { const len = arr.length; let i, diff; if (len < 2) { return false; } for (diff = arr[0], i = 1; i < len; ++i) { if (arr[i] - arr[i - 1] !== diff) { return false; } } return diff; } ================================================ FILE: src/core/core.scale.defaults.js ================================================ import Ticks from './core.ticks.js'; export function applyScaleDefaults(defaults) { defaults.set('scale', { display: true, offset: false, reverse: false, beginAtZero: false, /** * Scale boundary strategy (bypassed by min/max time options) * - `data`: make sure data are fully visible, ticks outside are removed * - `ticks`: make sure ticks are fully visible, data outside are truncated * @see https://github.com/chartjs/Chart.js/pull/4556 * @since 3.0.0 */ bounds: 'ticks', clip: true, /** * Addition grace added to max and reduced from min data value. * @since 3.0.0 */ grace: 0, // grid line settings grid: { display: true, lineWidth: 1, drawOnChartArea: true, drawTicks: true, tickLength: 8, tickWidth: (_ctx, options) => options.lineWidth, tickColor: (_ctx, options) => options.color, offset: false, }, border: { display: true, dash: [], dashOffset: 0.0, width: 1 }, // scale title title: { // display property display: false, // actual label text: '', // top/bottom padding padding: { top: 4, bottom: 4 } }, // label settings ticks: { minRotation: 0, maxRotation: 50, mirror: false, textStrokeWidth: 0, textStrokeColor: '', padding: 3, display: true, autoSkip: true, autoSkipPadding: 3, labelOffset: 0, // We pass through arrays to be rendered as multiline labels, we convert Others to strings here. callback: Ticks.formatters.values, minor: {}, major: {}, align: 'center', crossAlign: 'near', showLabelBackdrop: false, backdropColor: 'rgba(255, 255, 255, 0.75)', backdropPadding: 2, } }); defaults.route('scale.ticks', 'color', '', 'color'); defaults.route('scale.grid', 'color', '', 'borderColor'); defaults.route('scale.border', 'color', '', 'borderColor'); defaults.route('scale.title', 'color', '', 'color'); defaults.describe('scale', { _fallback: false, _scriptable: (name) => !name.startsWith('before') && !name.startsWith('after') && name !== 'callback' && name !== 'parser', _indexable: (name) => name !== 'borderDash' && name !== 'tickBorderDash' && name !== 'dash', }); defaults.describe('scales', { _fallback: 'scale', }); defaults.describe('scale.ticks', { _scriptable: (name) => name !== 'backdropPadding' && name !== 'callback', _indexable: (name) => name !== 'backdropPadding', }); } ================================================ FILE: src/core/core.scale.js ================================================ import Element from './core.element.js'; import {_alignPixel, _measureText, renderText, clipArea, unclipArea} from '../helpers/helpers.canvas.js'; import {callback as call, each, finiteOrDefault, isArray, isFinite, isNullOrUndef, isObject, valueOrDefault} from '../helpers/helpers.core.js'; import {toDegrees, toRadians, _int16Range, _limitValue, HALF_PI} from '../helpers/helpers.math.js'; import {_alignStartEnd, _toLeftRightCenter} from '../helpers/helpers.extras.js'; import {createContext, toFont, toPadding, _addGrace} from '../helpers/helpers.options.js'; import {autoSkip} from './core.scale.autoskip.js'; const reverseAlign = (align) => align === 'left' ? 'right' : align === 'right' ? 'left' : align; const offsetFromEdge = (scale, edge, offset) => edge === 'top' || edge === 'left' ? scale[edge] + offset : scale[edge] - offset; const getTicksLimit = (ticksLength, maxTicksLimit) => Math.min(maxTicksLimit || ticksLength, ticksLength); /** * @typedef { import('../types/index.js').Chart } Chart * @typedef {{value:number | string, label?:string, major?:boolean, $context?:any}} Tick */ /** * Returns a new array containing numItems from arr * @param {any[]} arr * @param {number} numItems */ function sample(arr, numItems) { const result = []; const increment = arr.length / numItems; const len = arr.length; let i = 0; for (; i < len; i += increment) { result.push(arr[Math.floor(i)]); } return result; } /** * @param {Scale} scale * @param {number} index * @param {boolean} offsetGridLines */ function getPixelForGridLine(scale, index, offsetGridLines) { const length = scale.ticks.length; const validIndex = Math.min(index, length - 1); const start = scale._startPixel; const end = scale._endPixel; const epsilon = 1e-6; // 1e-6 is margin in pixels for accumulated error. let lineValue = scale.getPixelForTick(validIndex); let offset; if (offsetGridLines) { if (length === 1) { offset = Math.max(lineValue - start, end - lineValue); } else if (index === 0) { offset = (scale.getPixelForTick(1) - lineValue) / 2; } else { offset = (lineValue - scale.getPixelForTick(validIndex - 1)) / 2; } lineValue += validIndex < index ? offset : -offset; // Return undefined if the pixel is out of the range if (lineValue < start - epsilon || lineValue > end + epsilon) { return; } } return lineValue; } /** * @param {object} caches * @param {number} length */ function garbageCollect(caches, length) { each(caches, (cache) => { const gc = cache.gc; const gcLen = gc.length / 2; let i; if (gcLen > length) { for (i = 0; i < gcLen; ++i) { delete cache.data[gc[i]]; } gc.splice(0, gcLen); } }); } /** * @param {object} options */ function getTickMarkLength(options) { return options.drawTicks ? options.tickLength : 0; } /** * @param {object} options */ function getTitleHeight(options, fallback) { if (!options.display) { return 0; } const font = toFont(options.font, fallback); const padding = toPadding(options.padding); const lines = isArray(options.text) ? options.text.length : 1; return (lines * font.lineHeight) + padding.height; } function createScaleContext(parent, scale) { return createContext(parent, { scale, type: 'scale' }); } function createTickContext(parent, index, tick) { return createContext(parent, { tick, index, type: 'tick' }); } function titleAlign(align, position, reverse) { /** @type {CanvasTextAlign} */ let ret = _toLeftRightCenter(align); if ((reverse && position !== 'right') || (!reverse && position === 'right')) { ret = reverseAlign(ret); } return ret; } function titleArgs(scale, offset, position, align) { const {top, left, bottom, right, chart} = scale; const {chartArea, scales} = chart; let rotation = 0; let maxWidth, titleX, titleY; const height = bottom - top; const width = right - left; if (scale.isHorizontal()) { titleX = _alignStartEnd(align, left, right); if (isObject(position)) { const positionAxisID = Object.keys(position)[0]; const value = position[positionAxisID]; titleY = scales[positionAxisID].getPixelForValue(value) + height - offset; } else if (position === 'center') { titleY = (chartArea.bottom + chartArea.top) / 2 + height - offset; } else { titleY = offsetFromEdge(scale, position, offset); } maxWidth = right - left; } else { if (isObject(position)) { const positionAxisID = Object.keys(position)[0]; const value = position[positionAxisID]; titleX = scales[positionAxisID].getPixelForValue(value) - width + offset; } else if (position === 'center') { titleX = (chartArea.left + chartArea.right) / 2 - width + offset; } else { titleX = offsetFromEdge(scale, position, offset); } titleY = _alignStartEnd(align, bottom, top); rotation = position === 'left' ? -HALF_PI : HALF_PI; } return {titleX, titleY, maxWidth, rotation}; } export default class Scale extends Element { // eslint-disable-next-line max-statements constructor(cfg) { super(); /** @type {string} */ this.id = cfg.id; /** @type {string} */ this.type = cfg.type; /** @type {any} */ this.options = undefined; /** @type {CanvasRenderingContext2D} */ this.ctx = cfg.ctx; /** @type {Chart} */ this.chart = cfg.chart; // implements box /** @type {number} */ this.top = undefined; /** @type {number} */ this.bottom = undefined; /** @type {number} */ this.left = undefined; /** @type {number} */ this.right = undefined; /** @type {number} */ this.width = undefined; /** @type {number} */ this.height = undefined; this._margins = { left: 0, right: 0, top: 0, bottom: 0 }; /** @type {number} */ this.maxWidth = undefined; /** @type {number} */ this.maxHeight = undefined; /** @type {number} */ this.paddingTop = undefined; /** @type {number} */ this.paddingBottom = undefined; /** @type {number} */ this.paddingLeft = undefined; /** @type {number} */ this.paddingRight = undefined; // scale-specific properties /** @type {string=} */ this.axis = undefined; /** @type {number=} */ this.labelRotation = undefined; this.min = undefined; this.max = undefined; this._range = undefined; /** @type {Tick[]} */ this.ticks = []; /** @type {object[]|null} */ this._gridLineItems = null; /** @type {object[]|null} */ this._labelItems = null; /** @type {object|null} */ this._labelSizes = null; this._length = 0; this._maxLength = 0; this._longestTextCache = {}; /** @type {number} */ this._startPixel = undefined; /** @type {number} */ this._endPixel = undefined; this._reversePixels = false; this._userMax = undefined; this._userMin = undefined; this._suggestedMax = undefined; this._suggestedMin = undefined; this._ticksLength = 0; this._borderValue = 0; this._cache = {}; this._dataLimitsCached = false; this.$context = undefined; } /** * @param {any} options * @since 3.0 */ init(options) { this.options = options.setContext(this.getContext()); this.axis = options.axis; // parse min/max value, so we can properly determine min/max for other scales this._userMin = this.parse(options.min); this._userMax = this.parse(options.max); this._suggestedMin = this.parse(options.suggestedMin); this._suggestedMax = this.parse(options.suggestedMax); } /** * Parse a supported input value to internal representation. * @param {*} raw * @param {number} [index] * @since 3.0 */ parse(raw, index) { // eslint-disable-line no-unused-vars return raw; } /** * @return {{min: number, max: number, minDefined: boolean, maxDefined: boolean}} * @protected * @since 3.0 */ getUserBounds() { let {_userMin, _userMax, _suggestedMin, _suggestedMax} = this; _userMin = finiteOrDefault(_userMin, Number.POSITIVE_INFINITY); _userMax = finiteOrDefault(_userMax, Number.NEGATIVE_INFINITY); _suggestedMin = finiteOrDefault(_suggestedMin, Number.POSITIVE_INFINITY); _suggestedMax = finiteOrDefault(_suggestedMax, Number.NEGATIVE_INFINITY); return { min: finiteOrDefault(_userMin, _suggestedMin), max: finiteOrDefault(_userMax, _suggestedMax), minDefined: isFinite(_userMin), maxDefined: isFinite(_userMax) }; } /** * @param {boolean} canStack * @return {{min: number, max: number}} * @protected * @since 3.0 */ getMinMax(canStack) { let {min, max, minDefined, maxDefined} = this.getUserBounds(); let range; if (minDefined && maxDefined) { return {min, max}; } const metas = this.getMatchingVisibleMetas(); for (let i = 0, ilen = metas.length; i < ilen; ++i) { range = metas[i].controller.getMinMax(this, canStack); if (!minDefined) { min = Math.min(min, range.min); } if (!maxDefined) { max = Math.max(max, range.max); } } // Make sure min <= max when only min or max is defined by user and the data is outside that range min = maxDefined && min > max ? max : min; max = minDefined && min > max ? min : max; return { min: finiteOrDefault(min, finiteOrDefault(max, min)), max: finiteOrDefault(max, finiteOrDefault(min, max)) }; } /** * Get the padding needed for the scale * @return {{top: number, left: number, bottom: number, right: number}} the necessary padding * @private */ getPadding() { return { left: this.paddingLeft || 0, top: this.paddingTop || 0, right: this.paddingRight || 0, bottom: this.paddingBottom || 0 }; } /** * Returns the scale tick objects * @return {Tick[]} * @since 2.7 */ getTicks() { return this.ticks; } /** * @return {string[]} */ getLabels() { const data = this.chart.data; return this.options.labels || (this.isHorizontal() ? data.xLabels : data.yLabels) || data.labels || []; } /** * @return {import('../types.js').LabelItem[]} */ getLabelItems(chartArea = this.chart.chartArea) { const items = this._labelItems || (this._labelItems = this._computeLabelItems(chartArea)); return items; } // When a new layout is created, reset the data limits cache beforeLayout() { this._cache = {}; this._dataLimitsCached = false; } // These methods are ordered by lifecycle. Utilities then follow. // Any function defined here is inherited by all scale types. // Any function can be extended by the scale type beforeUpdate() { call(this.options.beforeUpdate, [this]); } /** * @param {number} maxWidth - the max width in pixels * @param {number} maxHeight - the max height in pixels * @param {{top: number, left: number, bottom: number, right: number}} margins - the space between the edge of the other scales and edge of the chart * This space comes from two sources: * - padding - space that's required to show the labels at the edges of the scale * - thickness of scales or legends in another orientation */ update(maxWidth, maxHeight, margins) { const {beginAtZero, grace, ticks: tickOpts} = this.options; const sampleSize = tickOpts.sampleSize; // Update Lifecycle - Probably don't want to ever extend or overwrite this function ;) this.beforeUpdate(); // Absorb the master measurements this.maxWidth = maxWidth; this.maxHeight = maxHeight; this._margins = margins = Object.assign({ left: 0, right: 0, top: 0, bottom: 0 }, margins); this.ticks = null; this._labelSizes = null; this._gridLineItems = null; this._labelItems = null; // Dimensions this.beforeSetDimensions(); this.setDimensions(); this.afterSetDimensions(); this._maxLength = this.isHorizontal() ? this.width + margins.left + margins.right : this.height + margins.top + margins.bottom; // Data min/max if (!this._dataLimitsCached) { this.beforeDataLimits(); this.determineDataLimits(); this.afterDataLimits(); this._range = _addGrace(this, grace, beginAtZero); this._dataLimitsCached = true; } this.beforeBuildTicks(); this.ticks = this.buildTicks() || []; // Allow modification of ticks in callback. this.afterBuildTicks(); // Compute tick rotation and fit using a sampled subset of labels // We generally don't need to compute the size of every single label for determining scale size const samplingEnabled = sampleSize < this.ticks.length; this._convertTicksToLabels(samplingEnabled ? sample(this.ticks, sampleSize) : this.ticks); // configure is called twice, once here, once from core.controller.updateLayout. // Here we haven't been positioned yet, but dimensions are correct. // Variables set in configure are needed for calculateLabelRotation, and // it's ok that coordinates are not correct there, only dimensions matter. this.configure(); // Tick Rotation this.beforeCalculateLabelRotation(); this.calculateLabelRotation(); // Preconditions: number of ticks and sizes of largest labels must be calculated beforehand this.afterCalculateLabelRotation(); // Auto-skip if (tickOpts.display && (tickOpts.autoSkip || tickOpts.source === 'auto')) { this.ticks = autoSkip(this, this.ticks); this._labelSizes = null; this.afterAutoSkip(); } if (samplingEnabled) { // Generate labels using all non-skipped ticks this._convertTicksToLabels(this.ticks); } this.beforeFit(); this.fit(); // Preconditions: label rotation and label sizes must be calculated beforehand this.afterFit(); // IMPORTANT: after this point, we consider that `this.ticks` will NEVER change! this.afterUpdate(); } /** * @protected */ configure() { let reversePixels = this.options.reverse; let startPixel, endPixel; if (this.isHorizontal()) { startPixel = this.left; endPixel = this.right; } else { startPixel = this.top; endPixel = this.bottom; // by default vertical scales are from bottom to top, so pixels are reversed reversePixels = !reversePixels; } this._startPixel = startPixel; this._endPixel = endPixel; this._reversePixels = reversePixels; this._length = endPixel - startPixel; this._alignToPixels = this.options.alignToPixels; } afterUpdate() { call(this.options.afterUpdate, [this]); } // beforeSetDimensions() { call(this.options.beforeSetDimensions, [this]); } setDimensions() { // Set the unconstrained dimension before label rotation if (this.isHorizontal()) { // Reset position before calculating rotation this.width = this.maxWidth; this.left = 0; this.right = this.width; } else { this.height = this.maxHeight; // Reset position before calculating rotation this.top = 0; this.bottom = this.height; } // Reset padding this.paddingLeft = 0; this.paddingTop = 0; this.paddingRight = 0; this.paddingBottom = 0; } afterSetDimensions() { call(this.options.afterSetDimensions, [this]); } _callHooks(name) { this.chart.notifyPlugins(name, this.getContext()); call(this.options[name], [this]); } // Data limits beforeDataLimits() { this._callHooks('beforeDataLimits'); } determineDataLimits() {} afterDataLimits() { this._callHooks('afterDataLimits'); } // beforeBuildTicks() { this._callHooks('beforeBuildTicks'); } /** * @return {object[]} the ticks */ buildTicks() { return []; } afterBuildTicks() { this._callHooks('afterBuildTicks'); } beforeTickToLabelConversion() { call(this.options.beforeTickToLabelConversion, [this]); } /** * Convert ticks to label strings * @param {Tick[]} ticks */ generateTickLabels(ticks) { const tickOpts = this.options.ticks; let i, ilen, tick; for (i = 0, ilen = ticks.length; i < ilen; i++) { tick = ticks[i]; tick.label = call(tickOpts.callback, [tick.value, i, ticks], this); } } afterTickToLabelConversion() { call(this.options.afterTickToLabelConversion, [this]); } // beforeCalculateLabelRotation() { call(this.options.beforeCalculateLabelRotation, [this]); } calculateLabelRotation() { const options = this.options; const tickOpts = options.ticks; const numTicks = getTicksLimit(this.ticks.length, options.ticks.maxTicksLimit); const minRotation = tickOpts.minRotation || 0; const maxRotation = tickOpts.maxRotation; let labelRotation = minRotation; let tickWidth, maxHeight, maxLabelDiagonal; if (!this._isVisible() || !tickOpts.display || minRotation >= maxRotation || numTicks <= 1 || !this.isHorizontal()) { this.labelRotation = minRotation; return; } const labelSizes = this._getLabelSizes(); const maxLabelWidth = labelSizes.widest.width; const maxLabelHeight = labelSizes.highest.height; // Estimate the width of each grid based on the canvas width, the maximum // label width and the number of tick intervals const maxWidth = _limitValue(this.chart.width - maxLabelWidth, 0, this.maxWidth); tickWidth = options.offset ? this.maxWidth / numTicks : maxWidth / (numTicks - 1); // Allow 3 pixels x2 padding either side for label readability if (maxLabelWidth + 6 > tickWidth) { tickWidth = maxWidth / (numTicks - (options.offset ? 0.5 : 1)); maxHeight = this.maxHeight - getTickMarkLength(options.grid) - tickOpts.padding - getTitleHeight(options.title, this.chart.options.font); maxLabelDiagonal = Math.sqrt(maxLabelWidth * maxLabelWidth + maxLabelHeight * maxLabelHeight); labelRotation = toDegrees(Math.min( Math.asin(_limitValue((labelSizes.highest.height + 6) / tickWidth, -1, 1)), Math.asin(_limitValue(maxHeight / maxLabelDiagonal, -1, 1)) - Math.asin(_limitValue(maxLabelHeight / maxLabelDiagonal, -1, 1)) )); labelRotation = Math.max(minRotation, Math.min(maxRotation, labelRotation)); } this.labelRotation = labelRotation; } afterCalculateLabelRotation() { call(this.options.afterCalculateLabelRotation, [this]); } afterAutoSkip() {} // beforeFit() { call(this.options.beforeFit, [this]); } fit() { // Reset const minSize = { width: 0, height: 0 }; const {chart, options: {ticks: tickOpts, title: titleOpts, grid: gridOpts}} = this; const display = this._isVisible(); const isHorizontal = this.isHorizontal(); if (display) { const titleHeight = getTitleHeight(titleOpts, chart.options.font); if (isHorizontal) { minSize.width = this.maxWidth; minSize.height = getTickMarkLength(gridOpts) + titleHeight; } else { minSize.height = this.maxHeight; // fill all the height minSize.width = getTickMarkLength(gridOpts) + titleHeight; } // Don't bother fitting the ticks if we are not showing the labels if (tickOpts.display && this.ticks.length) { const {first, last, widest, highest} = this._getLabelSizes(); const tickPadding = tickOpts.padding * 2; const angleRadians = toRadians(this.labelRotation); const cos = Math.cos(angleRadians); const sin = Math.sin(angleRadians); if (isHorizontal) { // A horizontal axis is more constrained by the height. const labelHeight = tickOpts.mirror ? 0 : sin * widest.width + cos * highest.height; minSize.height = Math.min(this.maxHeight, minSize.height + labelHeight + tickPadding); } else { // A vertical axis is more constrained by the width. Labels are the // dominant factor here, so get that length first and account for padding const labelWidth = tickOpts.mirror ? 0 : cos * widest.width + sin * highest.height; minSize.width = Math.min(this.maxWidth, minSize.width + labelWidth + tickPadding); } this._calculatePadding(first, last, sin, cos); } } this._handleMargins(); if (isHorizontal) { this.width = this._length = chart.width - this._margins.left - this._margins.right; this.height = minSize.height; } else { this.width = minSize.width; this.height = this._length = chart.height - this._margins.top - this._margins.bottom; } } _calculatePadding(first, last, sin, cos) { const {ticks: {align, padding}, position} = this.options; const isRotated = this.labelRotation !== 0; const labelsBelowTicks = position !== 'top' && this.axis === 'x'; if (this.isHorizontal()) { const offsetLeft = this.getPixelForTick(0) - this.left; const offsetRight = this.right - this.getPixelForTick(this.ticks.length - 1); let paddingLeft = 0; let paddingRight = 0; // Ensure that our ticks are always inside the canvas. When rotated, ticks are right aligned // which means that the right padding is dominated by the font height if (isRotated) { if (labelsBelowTicks) { paddingLeft = cos * first.width; paddingRight = sin * last.height; } else { paddingLeft = sin * first.height; paddingRight = cos * last.width; } } else if (align === 'start') { paddingRight = last.width; } else if (align === 'end') { paddingLeft = first.width; } else if (align !== 'inner') { paddingLeft = first.width / 2; paddingRight = last.width / 2; } // Adjust padding taking into account changes in offsets this.paddingLeft = Math.max((paddingLeft - offsetLeft + padding) * this.width / (this.width - offsetLeft), 0); this.paddingRight = Math.max((paddingRight - offsetRight + padding) * this.width / (this.width - offsetRight), 0); } else { let paddingTop = last.height / 2; let paddingBottom = first.height / 2; if (align === 'start') { paddingTop = 0; paddingBottom = first.height; } else if (align === 'end') { paddingTop = last.height; paddingBottom = 0; } this.paddingTop = paddingTop + padding; this.paddingBottom = paddingBottom + padding; } } /** * Handle margins and padding interactions * @private */ _handleMargins() { if (this._margins) { this._margins.left = Math.max(this.paddingLeft, this._margins.left); this._margins.top = Math.max(this.paddingTop, this._margins.top); this._margins.right = Math.max(this.paddingRight, this._margins.right); this._margins.bottom = Math.max(this.paddingBottom, this._margins.bottom); } } afterFit() { call(this.options.afterFit, [this]); } // Shared Methods /** * @return {boolean} */ isHorizontal() { const {axis, position} = this.options; return position === 'top' || position === 'bottom' || axis === 'x'; } /** * @return {boolean} */ isFullSize() { return this.options.fullSize; } /** * @param {Tick[]} ticks * @private */ _convertTicksToLabels(ticks) { this.beforeTickToLabelConversion(); this.generateTickLabels(ticks); // Ticks should be skipped when callback returns null or undef, so lets remove those. let i, ilen; for (i = 0, ilen = ticks.length; i < ilen; i++) { if (isNullOrUndef(ticks[i].label)) { ticks.splice(i, 1); ilen--; i--; } } this.afterTickToLabelConversion(); } /** * @return {{ first: object, last: object, widest: object, highest: object, widths: Array, heights: array }} * @private */ _getLabelSizes() { let labelSizes = this._labelSizes; if (!labelSizes) { const sampleSize = this.options.ticks.sampleSize; let ticks = this.ticks; if (sampleSize < ticks.length) { ticks = sample(ticks, sampleSize); } this._labelSizes = labelSizes = this._computeLabelSizes(ticks, ticks.length, this.options.ticks.maxTicksLimit); } return labelSizes; } /** * Returns {width, height, offset} objects for the first, last, widest, highest tick * labels where offset indicates the anchor point offset from the top in pixels. * @return {{ first: object, last: object, widest: object, highest: object, widths: Array, heights: array }} * @private */ _computeLabelSizes(ticks, length, maxTicksLimit) { const {ctx, _longestTextCache: caches} = this; const widths = []; const heights = []; const increment = Math.floor(length / getTicksLimit(length, maxTicksLimit)); let widestLabelSize = 0; let highestLabelSize = 0; let i, j, jlen, label, tickFont, fontString, cache, lineHeight, width, height, nestedLabel; for (i = 0; i < length; i += increment) { label = ticks[i].label; tickFont = this._resolveTickFontOptions(i); ctx.font = fontString = tickFont.string; cache = caches[fontString] = caches[fontString] || {data: {}, gc: []}; lineHeight = tickFont.lineHeight; width = height = 0; // Undefined labels and arrays should not be measured if (!isNullOrUndef(label) && !isArray(label)) { width = _measureText(ctx, cache.data, cache.gc, width, label); height = lineHeight; } else if (isArray(label)) { // if it is an array let's measure each element for (j = 0, jlen = label.length; j < jlen; ++j) { nestedLabel = /** @type {string} */ (label[j]); // Undefined labels and arrays should not be measured if (!isNullOrUndef(nestedLabel) && !isArray(nestedLabel)) { width = _measureText(ctx, cache.data, cache.gc, width, nestedLabel); height += lineHeight; } } } widths.push(width); heights.push(height); widestLabelSize = Math.max(width, widestLabelSize); highestLabelSize = Math.max(height, highestLabelSize); } garbageCollect(caches, length); const widest = widths.indexOf(widestLabelSize); const highest = heights.indexOf(highestLabelSize); const valueAt = (idx) => ({width: widths[idx] || 0, height: heights[idx] || 0}); return { first: valueAt(0), last: valueAt(length - 1), widest: valueAt(widest), highest: valueAt(highest), widths, heights, }; } /** * Used to get the label to display in the tooltip for the given value * @param {*} value * @return {string} */ getLabelForValue(value) { return value; } /** * Returns the location of the given data point. Value can either be an index or a numerical value * The coordinate (0, 0) is at the upper-left corner of the canvas * @param {*} value * @param {number} [index] * @return {number} */ getPixelForValue(value, index) { // eslint-disable-line no-unused-vars return NaN; } /** * Used to get the data value from a given pixel. This is the inverse of getPixelForValue * The coordinate (0, 0) is at the upper-left corner of the canvas * @param {number} pixel * @return {*} */ getValueForPixel(pixel) {} // eslint-disable-line no-unused-vars /** * Returns the location of the tick at the given index * The coordinate (0, 0) is at the upper-left corner of the canvas * @param {number} index * @return {number} */ getPixelForTick(index) { const ticks = this.ticks; if (index < 0 || index > ticks.length - 1) { return null; } return this.getPixelForValue(ticks[index].value); } /** * Utility for getting the pixel location of a percentage of scale * The coordinate (0, 0) is at the upper-left corner of the canvas * @param {number} decimal * @return {number} */ getPixelForDecimal(decimal) { if (this._reversePixels) { decimal = 1 - decimal; } const pixel = this._startPixel + decimal * this._length; return _int16Range(this._alignToPixels ? _alignPixel(this.chart, pixel, 0) : pixel); } /** * @param {number} pixel * @return {number} */ getDecimalForPixel(pixel) { const decimal = (pixel - this._startPixel) / this._length; return this._reversePixels ? 1 - decimal : decimal; } /** * Returns the pixel for the minimum chart value * The coordinate (0, 0) is at the upper-left corner of the canvas * @return {number} */ getBasePixel() { return this.getPixelForValue(this.getBaseValue()); } /** * @return {number} */ getBaseValue() { const {min, max} = this; return min < 0 && max < 0 ? max : min > 0 && max > 0 ? min : 0; } /** * @protected */ getContext(index) { const ticks = this.ticks || []; if (index >= 0 && index < ticks.length) { const tick = ticks[index]; return tick.$context || (tick.$context = createTickContext(this.getContext(), index, tick)); } return this.$context || (this.$context = createScaleContext(this.chart.getContext(), this)); } /** * @return {number} * @private */ _tickSize() { const optionTicks = this.options.ticks; // Calculate space needed by label in axis direction. const rot = toRadians(this.labelRotation); const cos = Math.abs(Math.cos(rot)); const sin = Math.abs(Math.sin(rot)); const labelSizes = this._getLabelSizes(); const padding = optionTicks.autoSkipPadding || 0; const w = labelSizes ? labelSizes.widest.width + padding : 0; const h = labelSizes ? labelSizes.highest.height + padding : 0; // Calculate space needed for 1 tick in axis direction. return this.isHorizontal() ? h * cos > w * sin ? w / cos : h / sin : h * sin < w * cos ? h / cos : w / sin; } /** * @return {boolean} * @private */ _isVisible() { const display = this.options.display; if (display !== 'auto') { return !!display; } return this.getMatchingVisibleMetas().length > 0; } /** * @private */ _computeGridLineItems(chartArea) { const axis = this.axis; const chart = this.chart; const options = this.options; const {grid, position, border} = options; const offset = grid.offset; const isHorizontal = this.isHorizontal(); const ticks = this.ticks; const ticksLength = ticks.length + (offset ? 1 : 0); const tl = getTickMarkLength(grid); const items = []; const borderOpts = border.setContext(this.getContext()); const axisWidth = borderOpts.display ? borderOpts.width : 0; const axisHalfWidth = axisWidth / 2; const alignBorderValue = function(pixel) { return _alignPixel(chart, pixel, axisWidth); }; let borderValue, i, lineValue, alignedLineValue; let tx1, ty1, tx2, ty2, x1, y1, x2, y2; if (position === 'top') { borderValue = alignBorderValue(this.bottom); ty1 = this.bottom - tl; ty2 = borderValue - axisHalfWidth; y1 = alignBorderValue(chartArea.top) + axisHalfWidth; y2 = chartArea.bottom; } else if (position === 'bottom') { borderValue = alignBorderValue(this.top); y1 = chartArea.top; y2 = alignBorderValue(chartArea.bottom) - axisHalfWidth; ty1 = borderValue + axisHalfWidth; ty2 = this.top + tl; } else if (position === 'left') { borderValue = alignBorderValue(this.right); tx1 = this.right - tl; tx2 = borderValue - axisHalfWidth; x1 = alignBorderValue(chartArea.left) + axisHalfWidth; x2 = chartArea.right; } else if (position === 'right') { borderValue = alignBorderValue(this.left); x1 = chartArea.left; x2 = alignBorderValue(chartArea.right) - axisHalfWidth; tx1 = borderValue + axisHalfWidth; tx2 = this.left + tl; } else if (axis === 'x') { if (position === 'center') { borderValue = alignBorderValue((chartArea.top + chartArea.bottom) / 2 + 0.5); } else if (isObject(position)) { const positionAxisID = Object.keys(position)[0]; const value = position[positionAxisID]; borderValue = alignBorderValue(this.chart.scales[positionAxisID].getPixelForValue(value)); } y1 = chartArea.top; y2 = chartArea.bottom; ty1 = borderValue + axisHalfWidth; ty2 = ty1 + tl; } else if (axis === 'y') { if (position === 'center') { borderValue = alignBorderValue((chartArea.left + chartArea.right) / 2); } else if (isObject(position)) { const positionAxisID = Object.keys(position)[0]; const value = position[positionAxisID]; borderValue = alignBorderValue(this.chart.scales[positionAxisID].getPixelForValue(value)); } tx1 = borderValue - axisHalfWidth; tx2 = tx1 - tl; x1 = chartArea.left; x2 = chartArea.right; } const limit = valueOrDefault(options.ticks.maxTicksLimit, ticksLength); const step = Math.max(1, Math.ceil(ticksLength / limit)); for (i = 0; i < ticksLength; i += step) { const context = this.getContext(i); const optsAtIndex = grid.setContext(context); const optsAtIndexBorder = border.setContext(context); const lineWidth = optsAtIndex.lineWidth; const lineColor = optsAtIndex.color; const borderDash = optsAtIndexBorder.dash || []; const borderDashOffset = optsAtIndexBorder.dashOffset; const tickWidth = optsAtIndex.tickWidth; const tickColor = optsAtIndex.tickColor; const tickBorderDash = optsAtIndex.tickBorderDash || []; const tickBorderDashOffset = optsAtIndex.tickBorderDashOffset; lineValue = getPixelForGridLine(this, i, offset); // Skip if the pixel is out of the range if (lineValue === undefined) { continue; } alignedLineValue = _alignPixel(chart, lineValue, lineWidth); if (isHorizontal) { tx1 = tx2 = x1 = x2 = alignedLineValue; } else { ty1 = ty2 = y1 = y2 = alignedLineValue; } items.push({ tx1, ty1, tx2, ty2, x1, y1, x2, y2, width: lineWidth, color: lineColor, borderDash, borderDashOffset, tickWidth, tickColor, tickBorderDash, tickBorderDashOffset, }); } this._ticksLength = ticksLength; this._borderValue = borderValue; return items; } /** * @private */ _computeLabelItems(chartArea) { const axis = this.axis; const options = this.options; const {position, ticks: optionTicks} = options; const isHorizontal = this.isHorizontal(); const ticks = this.ticks; const {align, crossAlign, padding, mirror} = optionTicks; const tl = getTickMarkLength(options.grid); const tickAndPadding = tl + padding; const hTickAndPadding = mirror ? -padding : tickAndPadding; const rotation = -toRadians(this.labelRotation); const items = []; let i, ilen, tick, label, x, y, textAlign, pixel, font, lineHeight, lineCount, textOffset; let textBaseline = 'middle'; if (position === 'top') { y = this.bottom - hTickAndPadding; textAlign = this._getXAxisLabelAlignment(); } else if (position === 'bottom') { y = this.top + hTickAndPadding; textAlign = this._getXAxisLabelAlignment(); } else if (position === 'left') { const ret = this._getYAxisLabelAlignment(tl); textAlign = ret.textAlign; x = ret.x; } else if (position === 'right') { const ret = this._getYAxisLabelAlignment(tl); textAlign = ret.textAlign; x = ret.x; } else if (axis === 'x') { if (position === 'center') { y = ((chartArea.top + chartArea.bottom) / 2) + tickAndPadding; } else if (isObject(position)) { const positionAxisID = Object.keys(position)[0]; const value = position[positionAxisID]; y = this.chart.scales[positionAxisID].getPixelForValue(value) + tickAndPadding; } textAlign = this._getXAxisLabelAlignment(); } else if (axis === 'y') { if (position === 'center') { x = ((chartArea.left + chartArea.right) / 2) - tickAndPadding; } else if (isObject(position)) { const positionAxisID = Object.keys(position)[0]; const value = position[positionAxisID]; x = this.chart.scales[positionAxisID].getPixelForValue(value); } textAlign = this._getYAxisLabelAlignment(tl).textAlign; } if (axis === 'y') { if (align === 'start') { textBaseline = 'top'; } else if (align === 'end') { textBaseline = 'bottom'; } } const labelSizes = this._getLabelSizes(); for (i = 0, ilen = ticks.length; i < ilen; ++i) { tick = ticks[i]; label = tick.label; const optsAtIndex = optionTicks.setContext(this.getContext(i)); pixel = this.getPixelForTick(i) + optionTicks.labelOffset; font = this._resolveTickFontOptions(i); lineHeight = font.lineHeight; lineCount = isArray(label) ? label.length : 1; const halfCount = lineCount / 2; const color = optsAtIndex.color; const strokeColor = optsAtIndex.textStrokeColor; const strokeWidth = optsAtIndex.textStrokeWidth; let tickTextAlign = textAlign; if (isHorizontal) { x = pixel; if (textAlign === 'inner') { if (i === ilen - 1) { tickTextAlign = !this.options.reverse ? 'right' : 'left'; } else if (i === 0) { tickTextAlign = !this.options.reverse ? 'left' : 'right'; } else { tickTextAlign = 'center'; } } if (position === 'top') { if (crossAlign === 'near' || rotation !== 0) { textOffset = -lineCount * lineHeight + lineHeight / 2; } else if (crossAlign === 'center') { textOffset = -labelSizes.highest.height / 2 - halfCount * lineHeight + lineHeight; } else { textOffset = -labelSizes.highest.height + lineHeight / 2; } } else { // eslint-disable-next-line no-lonely-if if (crossAlign === 'near' || rotation !== 0) { textOffset = lineHeight / 2; } else if (crossAlign === 'center') { textOffset = labelSizes.highest.height / 2 - halfCount * lineHeight; } else { textOffset = labelSizes.highest.height - lineCount * lineHeight; } } if (mirror) { textOffset *= -1; } if (rotation !== 0 && !optsAtIndex.showLabelBackdrop) { x += (lineHeight / 2) * Math.sin(rotation); } } else { y = pixel; textOffset = (1 - lineCount) * lineHeight / 2; } let backdrop; if (optsAtIndex.showLabelBackdrop) { const labelPadding = toPadding(optsAtIndex.backdropPadding); const height = labelSizes.heights[i]; const width = labelSizes.widths[i]; let top = textOffset - labelPadding.top; let left = 0 - labelPadding.left; switch (textBaseline) { case 'middle': top -= height / 2; break; case 'bottom': top -= height; break; default: break; } switch (textAlign) { case 'center': left -= width / 2; break; case 'right': left -= width; break; case 'inner': if (i === ilen - 1) { left -= width; } else if (i > 0) { left -= width / 2; } break; default: break; } backdrop = { left, top, width: width + labelPadding.width, height: height + labelPadding.height, color: optsAtIndex.backdropColor, }; } items.push({ label, font, textOffset, options: { rotation, color, strokeColor, strokeWidth, textAlign: tickTextAlign, textBaseline, translation: [x, y], backdrop, } }); } return items; } _getXAxisLabelAlignment() { const {position, ticks} = this.options; const rotation = -toRadians(this.labelRotation); if (rotation) { return position === 'top' ? 'left' : 'right'; } let align = 'center'; if (ticks.align === 'start') { align = 'left'; } else if (ticks.align === 'end') { align = 'right'; } else if (ticks.align === 'inner') { align = 'inner'; } return align; } _getYAxisLabelAlignment(tl) { const {position, ticks: {crossAlign, mirror, padding}} = this.options; const labelSizes = this._getLabelSizes(); const tickAndPadding = tl + padding; const widest = labelSizes.widest.width; let textAlign; let x; if (position === 'left') { if (mirror) { x = this.right + padding; if (crossAlign === 'near') { textAlign = 'left'; } else if (crossAlign === 'center') { textAlign = 'center'; x += (widest / 2); } else { textAlign = 'right'; x += widest; } } else { x = this.right - tickAndPadding; if (crossAlign === 'near') { textAlign = 'right'; } else if (crossAlign === 'center') { textAlign = 'center'; x -= (widest / 2); } else { textAlign = 'left'; x = this.left; } } } else if (position === 'right') { if (mirror) { x = this.left + padding; if (crossAlign === 'near') { textAlign = 'right'; } else if (crossAlign === 'center') { textAlign = 'center'; x -= (widest / 2); } else { textAlign = 'left'; x -= widest; } } else { x = this.left + tickAndPadding; if (crossAlign === 'near') { textAlign = 'left'; } else if (crossAlign === 'center') { textAlign = 'center'; x += widest / 2; } else { textAlign = 'right'; x = this.right; } } } else { textAlign = 'right'; } return {textAlign, x}; } /** * @private */ _computeLabelArea() { if (this.options.ticks.mirror) { return; } const chart = this.chart; const position = this.options.position; if (position === 'left' || position === 'right') { return {top: 0, left: this.left, bottom: chart.height, right: this.right}; } if (position === 'top' || position === 'bottom') { return {top: this.top, left: 0, bottom: this.bottom, right: chart.width}; } } /** * @protected */ drawBackground() { const {ctx, options: {backgroundColor}, left, top, width, height} = this; if (backgroundColor) { ctx.save(); ctx.fillStyle = backgroundColor; ctx.fillRect(left, top, width, height); ctx.restore(); } } getLineWidthForValue(value) { const grid = this.options.grid; if (!this._isVisible() || !grid.display) { return 0; } const ticks = this.ticks; const index = ticks.findIndex(t => t.value === value); if (index >= 0) { const opts = grid.setContext(this.getContext(index)); return opts.lineWidth; } return 0; } /** * @protected */ drawGrid(chartArea) { const grid = this.options.grid; const ctx = this.ctx; const items = this._gridLineItems || (this._gridLineItems = this._computeGridLineItems(chartArea)); let i, ilen; const drawLine = (p1, p2, style) => { if (!style.width || !style.color) { return; } ctx.save(); ctx.lineWidth = style.width; ctx.strokeStyle = style.color; ctx.setLineDash(style.borderDash || []); ctx.lineDashOffset = style.borderDashOffset; ctx.beginPath(); ctx.moveTo(p1.x, p1.y); ctx.lineTo(p2.x, p2.y); ctx.stroke(); ctx.restore(); }; if (grid.display) { for (i = 0, ilen = items.length; i < ilen; ++i) { const item = items[i]; if (grid.drawOnChartArea) { drawLine( {x: item.x1, y: item.y1}, {x: item.x2, y: item.y2}, item ); } if (grid.drawTicks) { drawLine( {x: item.tx1, y: item.ty1}, {x: item.tx2, y: item.ty2}, { color: item.tickColor, width: item.tickWidth, borderDash: item.tickBorderDash, borderDashOffset: item.tickBorderDashOffset } ); } } } } /** * @protected */ drawBorder() { const {chart, ctx, options: {border, grid}} = this; const borderOpts = border.setContext(this.getContext()); const axisWidth = border.display ? borderOpts.width : 0; if (!axisWidth) { return; } const lastLineWidth = grid.setContext(this.getContext(0)).lineWidth; const borderValue = this._borderValue; let x1, x2, y1, y2; if (this.isHorizontal()) { x1 = _alignPixel(chart, this.left, axisWidth) - axisWidth / 2; x2 = _alignPixel(chart, this.right, lastLineWidth) + lastLineWidth / 2; y1 = y2 = borderValue; } else { y1 = _alignPixel(chart, this.top, axisWidth) - axisWidth / 2; y2 = _alignPixel(chart, this.bottom, lastLineWidth) + lastLineWidth / 2; x1 = x2 = borderValue; } ctx.save(); ctx.lineWidth = borderOpts.width; ctx.strokeStyle = borderOpts.color; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.restore(); } /** * @protected */ drawLabels(chartArea) { const optionTicks = this.options.ticks; if (!optionTicks.display) { return; } const ctx = this.ctx; const area = this._computeLabelArea(); if (area) { clipArea(ctx, area); } const items = this.getLabelItems(chartArea); for (const item of items) { const renderTextOptions = item.options; const tickFont = item.font; const label = item.label; const y = item.textOffset; renderText(ctx, label, 0, y, tickFont, renderTextOptions); } if (area) { unclipArea(ctx); } } /** * @protected */ drawTitle() { const {ctx, options: {position, title, reverse}} = this; if (!title.display) { return; } const font = toFont(title.font); const padding = toPadding(title.padding); const align = title.align; let offset = font.lineHeight / 2; if (position === 'bottom' || position === 'center' || isObject(position)) { offset += padding.bottom; if (isArray(title.text)) { offset += font.lineHeight * (title.text.length - 1); } } else { offset += padding.top; } const {titleX, titleY, maxWidth, rotation} = titleArgs(this, offset, position, align); renderText(ctx, title.text, 0, 0, font, { color: title.color, maxWidth, rotation, textAlign: titleAlign(align, position, reverse), textBaseline: 'middle', translation: [titleX, titleY], strokeColor: title.strokeColor, strokeWidth: title.strokeWidth }); } draw(chartArea) { if (!this._isVisible()) { return; } this.drawBackground(); this.drawGrid(chartArea); this.drawBorder(); this.drawTitle(); this.drawLabels(chartArea); } /** * @return {object[]} * @private */ _layers() { const opts = this.options; const tz = opts.ticks && opts.ticks.z || 0; const gz = valueOrDefault(opts.grid && opts.grid.z, -1); const bz = valueOrDefault(opts.border && opts.border.z, 0); if (!this._isVisible() || this.draw !== Scale.prototype.draw) { // backward compatibility: draw has been overridden by custom scale return [{ z: tz, draw: (chartArea) => { this.draw(chartArea); } }]; } return [{ z: gz, draw: (chartArea) => { this.drawBackground(); this.drawGrid(chartArea); this.drawTitle(); } }, { z: bz, draw: () => { this.drawBorder(); } }, { z: tz, draw: (chartArea) => { this.drawLabels(chartArea); } }]; } /** * Returns visible dataset metas that are attached to this scale * @param {string} [type] - if specified, also filter by dataset type * @return {object[]} */ getMatchingVisibleMetas(type) { const metas = this.chart.getSortedVisibleDatasetMetas(); const axisID = this.axis + 'AxisID'; const result = []; let i, ilen; for (i = 0, ilen = metas.length; i < ilen; ++i) { const meta = metas[i]; if (meta[axisID] === this.id && (!type || meta.type === type)) { result.push(meta); } } return result; } /** * @param {number} index * @return {object} * @protected */ _resolveTickFontOptions(index) { const opts = this.options.ticks.setContext(this.getContext(index)); return toFont(opts.font); } /** * @protected */ _maxDigits() { const fontSize = this._resolveTickFontOptions(0).lineHeight; return (this.isHorizontal() ? this.width : this.height) / fontSize; } } ================================================ FILE: src/core/core.ticks.js ================================================ import {isArray} from '../helpers/helpers.core.js'; import {formatNumber} from '../helpers/helpers.intl.js'; import {log10} from '../helpers/helpers.math.js'; /** * Namespace to hold formatters for different types of ticks * @namespace Chart.Ticks.formatters */ const formatters = { /** * Formatter for value labels * @method Chart.Ticks.formatters.values * @param value the value to display * @return {string|string[]} the label to display */ values(value) { return isArray(value) ? /** @type {string[]} */ (value) : '' + value; }, /** * Formatter for numeric ticks * @method Chart.Ticks.formatters.numeric * @param tickValue {number} the value to be formatted * @param index {number} the position of the tickValue parameter in the ticks array * @param ticks {object[]} the list of ticks being converted * @return {string} string representation of the tickValue parameter */ numeric(tickValue, index, ticks) { if (tickValue === 0) { return '0'; // never show decimal places for 0 } const locale = this.chart.options.locale; let notation; let delta = tickValue; // This is used when there are less than 2 ticks as the tick interval. if (ticks.length > 1) { // all ticks are small or there huge numbers; use scientific notation const maxTick = Math.max(Math.abs(ticks[0].value), Math.abs(ticks[ticks.length - 1].value)); if (maxTick < 1e-4 || maxTick > 1e+15) { notation = 'scientific'; } delta = calculateDelta(tickValue, ticks); } const logDelta = log10(Math.abs(delta)); // When datasets have values approaching Number.MAX_VALUE, the tick calculations might result in // infinity and eventually NaN. Passing NaN for minimumFractionDigits or maximumFractionDigits // will make the number formatter throw. So instead we check for isNaN and use a fallback value. // // toFixed has a max of 20 decimal places const numDecimal = isNaN(logDelta) ? 1 : Math.max(Math.min(-1 * Math.floor(logDelta), 20), 0); const options = {notation, minimumFractionDigits: numDecimal, maximumFractionDigits: numDecimal}; Object.assign(options, this.options.ticks.format); return formatNumber(tickValue, locale, options); }, /** * Formatter for logarithmic ticks * @method Chart.Ticks.formatters.logarithmic * @param tickValue {number} the value to be formatted * @param index {number} the position of the tickValue parameter in the ticks array * @param ticks {object[]} the list of ticks being converted * @return {string} string representation of the tickValue parameter */ logarithmic(tickValue, index, ticks) { if (tickValue === 0) { return '0'; } const remain = ticks[index].significand || (tickValue / (Math.pow(10, Math.floor(log10(tickValue))))); if ([1, 2, 3, 5, 10, 15].includes(remain) || index > 0.8 * ticks.length) { return formatters.numeric.call(this, tickValue, index, ticks); } return ''; } }; function calculateDelta(tickValue, ticks) { // Figure out how many digits to show // The space between the first two ticks might be smaller than normal spacing let delta = ticks.length > 3 ? ticks[2].value - ticks[1].value : ticks[1].value - ticks[0].value; // If we have a number like 2.5 as the delta, figure out how many decimal places we need if (Math.abs(delta) >= 1 && tickValue !== Math.floor(tickValue)) { // not an integer delta = tickValue - Math.floor(tickValue); } return delta; } /** * Namespace to hold static tick generation functions * @namespace Chart.Ticks */ export default {formatters}; ================================================ FILE: src/core/core.typedRegistry.js ================================================ import {merge} from '../helpers/index.js'; import defaults, {overrides} from './core.defaults.js'; /** * @typedef {{id: string, defaults: any, overrides?: any, defaultRoutes: any}} IChartComponent */ export default class TypedRegistry { constructor(type, scope, override) { this.type = type; this.scope = scope; this.override = override; this.items = Object.create(null); } isForType(type) { return Object.prototype.isPrototypeOf.call(this.type.prototype, type.prototype); } /** * @param {IChartComponent} item * @returns {string} The scope where items defaults were registered to. */ register(item) { const proto = Object.getPrototypeOf(item); let parentScope; if (isIChartComponent(proto)) { // Make sure the parent is registered and note the scope where its defaults are. parentScope = this.register(proto); } const items = this.items; const id = item.id; const scope = this.scope + '.' + id; if (!id) { throw new Error('class does not have id: ' + item); } if (id in items) { // already registered return scope; } items[id] = item; registerDefaults(item, scope, parentScope); if (this.override) { defaults.override(item.id, item.overrides); } return scope; } /** * @param {string} id * @returns {object?} */ get(id) { return this.items[id]; } /** * @param {IChartComponent} item */ unregister(item) { const items = this.items; const id = item.id; const scope = this.scope; if (id in items) { delete items[id]; } if (scope && id in defaults[scope]) { delete defaults[scope][id]; if (this.override) { delete overrides[id]; } } } } function registerDefaults(item, scope, parentScope) { // Inherit the parent's defaults and keep existing defaults const itemDefaults = merge(Object.create(null), [ parentScope ? defaults.get(parentScope) : {}, defaults.get(scope), item.defaults ]); defaults.set(scope, itemDefaults); if (item.defaultRoutes) { routeDefaults(scope, item.defaultRoutes); } if (item.descriptors) { defaults.describe(scope, item.descriptors); } } function routeDefaults(scope, routes) { Object.keys(routes).forEach(property => { const propertyParts = property.split('.'); const sourceName = propertyParts.pop(); const sourceScope = [scope].concat(propertyParts).join('.'); const parts = routes[property].split('.'); const targetName = parts.pop(); const targetScope = parts.join('.'); defaults.route(sourceScope, sourceName, targetScope, targetName); }); } function isIChartComponent(proto) { return 'id' in proto && 'defaults' in proto; } ================================================ FILE: src/core/index.ts ================================================ export type {DateAdapter, TimeUnit} from './core.adapters.js'; export {default as _adapters} from './core.adapters.js'; export {default as Animation} from './core.animation.js'; export {default as Animations} from './core.animations.js'; export {default as animator} from './core.animator.js'; export {default as Chart} from './core.controller.js'; export {default as DatasetController} from './core.datasetController.js'; export {default as defaults} from './core.defaults.js'; export {default as Element} from './core.element.js'; export {default as Interaction} from './core.interaction.js'; export {default as layouts} from './core.layouts.js'; export {default as plugins} from './core.plugins.js'; export {default as registry} from './core.registry.js'; export {default as Scale} from './core.scale.js'; export {default as Ticks} from './core.ticks.js'; ================================================ FILE: src/elements/element.arc.ts ================================================ import Element from '../core/core.element.js'; import {_angleBetween, getAngleFromPoint, TAU, HALF_PI, valueOrDefault} from '../helpers/index.js'; import {PI, _angleDiff, _normalizeAngle, _isBetween, _limitValue} from '../helpers/helpers.math.js'; import {_readValueToProps} from '../helpers/helpers.options.js'; import type {ArcOptions, Point} from '../types/index.js'; function clipSelf(ctx: CanvasRenderingContext2D, element: ArcElement, endAngle: number) { const {startAngle, x, y, outerRadius, innerRadius, options} = element; const {borderWidth, borderJoinStyle} = options; const outerAngleClip = Math.min(borderWidth / outerRadius, _normalizeAngle(startAngle - endAngle)); ctx.beginPath(); ctx.arc(x, y, outerRadius - borderWidth / 2, startAngle + outerAngleClip / 2, endAngle - outerAngleClip / 2); if (innerRadius > 0) { const innerAngleClip = Math.min(borderWidth / innerRadius, _normalizeAngle(startAngle - endAngle)); ctx.arc(x, y, innerRadius + borderWidth / 2, endAngle - innerAngleClip / 2, startAngle + innerAngleClip / 2, true); } else { const clipWidth = Math.min(borderWidth / 2, outerRadius * _normalizeAngle(startAngle - endAngle)); if (borderJoinStyle === 'round') { ctx.arc(x, y, clipWidth, endAngle - PI / 2, startAngle + PI / 2, true); } else if (borderJoinStyle === 'bevel') { const r = 2 * clipWidth * clipWidth; const endX = -r * Math.cos(endAngle + PI / 2) + x; const endY = -r * Math.sin(endAngle + PI / 2) + y; const startX = r * Math.cos(startAngle + PI / 2) + x; const startY = r * Math.sin(startAngle + PI / 2) + y; ctx.lineTo(endX, endY); ctx.lineTo(startX, startY); } } ctx.closePath(); ctx.moveTo(0, 0); ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height); ctx.clip('evenodd'); } function clipArc(ctx: CanvasRenderingContext2D, element: ArcElement, endAngle: number) { const {startAngle, pixelMargin, x, y, outerRadius, innerRadius} = element; let angleMargin = pixelMargin / outerRadius; // Draw an inner border by clipping the arc and drawing a double-width border // Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders ctx.beginPath(); ctx.arc(x, y, outerRadius, startAngle - angleMargin, endAngle + angleMargin); if (innerRadius > pixelMargin) { angleMargin = pixelMargin / innerRadius; ctx.arc(x, y, innerRadius, endAngle + angleMargin, startAngle - angleMargin, true); } else { ctx.arc(x, y, pixelMargin, endAngle + HALF_PI, startAngle - HALF_PI); } ctx.closePath(); ctx.clip(); } function toRadiusCorners(value) { return _readValueToProps(value, ['outerStart', 'outerEnd', 'innerStart', 'innerEnd']); } /** * Parse border radius from the provided options */ function parseBorderRadius(arc: ArcElement, innerRadius: number, outerRadius: number, angleDelta: number) { const o = toRadiusCorners(arc.options.borderRadius); const halfThickness = (outerRadius - innerRadius) / 2; const innerLimit = Math.min(halfThickness, angleDelta * innerRadius / 2); // Outer limits are complicated. We want to compute the available angular distance at // a radius of outerRadius - borderRadius because for small angular distances, this term limits. // We compute at r = outerRadius - borderRadius because this circle defines the center of the border corners. // // If the borderRadius is large, that value can become negative. // This causes the outer borders to lose their radius entirely, which is rather unexpected. To solve that, if borderRadius > outerRadius // we know that the thickness term will dominate and compute the limits at that point const computeOuterLimit = (val) => { const outerArcLimit = (outerRadius - Math.min(halfThickness, val)) * angleDelta / 2; return _limitValue(val, 0, Math.min(halfThickness, outerArcLimit)); }; return { outerStart: computeOuterLimit(o.outerStart), outerEnd: computeOuterLimit(o.outerEnd), innerStart: _limitValue(o.innerStart, 0, innerLimit), innerEnd: _limitValue(o.innerEnd, 0, innerLimit), }; } /** * Convert (r, 𝜃) to (x, y) */ function rThetaToXY(r: number, theta: number, x: number, y: number) { return { x: x + r * Math.cos(theta), y: y + r * Math.sin(theta), }; } /** * Path the arc, respecting border radius by separating into left and right halves. * * Start End * * 1--->a--->2 Outer * / \ * 8 3 * | | * | | * 7 4 * \ / * 6<---b<---5 Inner */ function pathArc( ctx: CanvasRenderingContext2D, element: ArcElement, offset: number, spacing: number, end: number, circular: boolean, ) { const {x, y, startAngle: start, pixelMargin, innerRadius: innerR} = element; const outerRadius = Math.max(element.outerRadius + spacing + offset - pixelMargin, 0); const innerRadius = innerR > 0 ? innerR + spacing + offset + pixelMargin : 0; let spacingOffset = 0; const alpha = end - start; if (spacing) { // When spacing is present, it is the same for all items // So we adjust the start and end angle of the arc such that // the distance is the same as it would be without the spacing const noSpacingInnerRadius = innerR > 0 ? innerR - spacing : 0; const noSpacingOuterRadius = outerRadius > 0 ? outerRadius - spacing : 0; const avNogSpacingRadius = (noSpacingInnerRadius + noSpacingOuterRadius) / 2; const adjustedAngle = avNogSpacingRadius !== 0 ? (alpha * avNogSpacingRadius) / (avNogSpacingRadius + spacing) : alpha; spacingOffset = (alpha - adjustedAngle) / 2; } const beta = Math.max(0.001, alpha * outerRadius - offset / PI) / outerRadius; const angleOffset = (alpha - beta) / 2; const startAngle = start + angleOffset + spacingOffset; const endAngle = end - angleOffset - spacingOffset; const {outerStart, outerEnd, innerStart, innerEnd} = parseBorderRadius(element, innerRadius, outerRadius, endAngle - startAngle); const outerStartAdjustedRadius = outerRadius - outerStart; const outerEndAdjustedRadius = outerRadius - outerEnd; const outerStartAdjustedAngle = startAngle + outerStart / outerStartAdjustedRadius; const outerEndAdjustedAngle = endAngle - outerEnd / outerEndAdjustedRadius; const innerStartAdjustedRadius = innerRadius + innerStart; const innerEndAdjustedRadius = innerRadius + innerEnd; const innerStartAdjustedAngle = startAngle + innerStart / innerStartAdjustedRadius; const innerEndAdjustedAngle = endAngle - innerEnd / innerEndAdjustedRadius; ctx.beginPath(); if (circular) { // The first arc segments from point 1 to point a to point 2 const outerMidAdjustedAngle = (outerStartAdjustedAngle + outerEndAdjustedAngle) / 2; ctx.arc(x, y, outerRadius, outerStartAdjustedAngle, outerMidAdjustedAngle); ctx.arc(x, y, outerRadius, outerMidAdjustedAngle, outerEndAdjustedAngle); // The corner segment from point 2 to point 3 if (outerEnd > 0) { const pCenter = rThetaToXY(outerEndAdjustedRadius, outerEndAdjustedAngle, x, y); ctx.arc(pCenter.x, pCenter.y, outerEnd, outerEndAdjustedAngle, endAngle + HALF_PI); } // The line from point 3 to point 4 const p4 = rThetaToXY(innerEndAdjustedRadius, endAngle, x, y); ctx.lineTo(p4.x, p4.y); // The corner segment from point 4 to point 5 if (innerEnd > 0) { const pCenter = rThetaToXY(innerEndAdjustedRadius, innerEndAdjustedAngle, x, y); ctx.arc(pCenter.x, pCenter.y, innerEnd, endAngle + HALF_PI, innerEndAdjustedAngle + Math.PI); } // The inner arc from point 5 to point b to point 6 const innerMidAdjustedAngle = ((endAngle - (innerEnd / innerRadius)) + (startAngle + (innerStart / innerRadius))) / 2; ctx.arc(x, y, innerRadius, endAngle - (innerEnd / innerRadius), innerMidAdjustedAngle, true); ctx.arc(x, y, innerRadius, innerMidAdjustedAngle, startAngle + (innerStart / innerRadius), true); // The corner segment from point 6 to point 7 if (innerStart > 0) { const pCenter = rThetaToXY(innerStartAdjustedRadius, innerStartAdjustedAngle, x, y); ctx.arc(pCenter.x, pCenter.y, innerStart, innerStartAdjustedAngle + Math.PI, startAngle - HALF_PI); } // The line from point 7 to point 8 const p8 = rThetaToXY(outerStartAdjustedRadius, startAngle, x, y); ctx.lineTo(p8.x, p8.y); // The corner segment from point 8 to point 1 if (outerStart > 0) { const pCenter = rThetaToXY(outerStartAdjustedRadius, outerStartAdjustedAngle, x, y); ctx.arc(pCenter.x, pCenter.y, outerStart, startAngle - HALF_PI, outerStartAdjustedAngle); } } else { ctx.moveTo(x, y); const outerStartX = Math.cos(outerStartAdjustedAngle) * outerRadius + x; const outerStartY = Math.sin(outerStartAdjustedAngle) * outerRadius + y; ctx.lineTo(outerStartX, outerStartY); const outerEndX = Math.cos(outerEndAdjustedAngle) * outerRadius + x; const outerEndY = Math.sin(outerEndAdjustedAngle) * outerRadius + y; ctx.lineTo(outerEndX, outerEndY); } ctx.closePath(); } function drawArc( ctx: CanvasRenderingContext2D, element: ArcElement, offset: number, spacing: number, circular: boolean, ) { const {fullCircles, startAngle, circumference} = element; let endAngle = element.endAngle; if (fullCircles) { pathArc(ctx, element, offset, spacing, endAngle, circular); for (let i = 0; i < fullCircles; ++i) { ctx.fill(); } if (!isNaN(circumference)) { endAngle = startAngle + (circumference % TAU || TAU); } } pathArc(ctx, element, offset, spacing, endAngle, circular); ctx.fill(); return endAngle; } function drawBorder( ctx: CanvasRenderingContext2D, element: ArcElement, offset: number, spacing: number, circular: boolean, ) { const {fullCircles, startAngle, circumference, options} = element; const {borderWidth, borderJoinStyle, borderDash, borderDashOffset, borderRadius} = options; const inner = options.borderAlign === 'inner'; if (!borderWidth) { return; } ctx.setLineDash(borderDash || []); ctx.lineDashOffset = borderDashOffset; if (inner) { ctx.lineWidth = borderWidth * 2; ctx.lineJoin = borderJoinStyle || 'round'; } else { ctx.lineWidth = borderWidth; ctx.lineJoin = borderJoinStyle || 'bevel'; } let endAngle = element.endAngle; if (fullCircles) { pathArc(ctx, element, offset, spacing, endAngle, circular); for (let i = 0; i < fullCircles; ++i) { ctx.stroke(); } if (!isNaN(circumference)) { endAngle = startAngle + (circumference % TAU || TAU); } } if (inner) { clipArc(ctx, element, endAngle); } if (options.selfJoin && endAngle - startAngle >= PI && borderRadius === 0 && borderJoinStyle !== 'miter') { clipSelf(ctx, element, endAngle); } if (!fullCircles) { pathArc(ctx, element, offset, spacing, endAngle, circular); ctx.stroke(); } } export interface ArcProps extends Point { startAngle: number; endAngle: number; innerRadius: number; outerRadius: number; circumference: number; } export default class ArcElement extends Element { static id = 'arc'; static defaults = { borderAlign: 'center', borderColor: '#fff', borderDash: [], borderDashOffset: 0, borderJoinStyle: undefined, borderRadius: 0, borderWidth: 2, offset: 0, spacing: 0, angle: undefined, circular: true, selfJoin: false, }; static defaultRoutes = { backgroundColor: 'backgroundColor' }; static descriptors = { _scriptable: true, _indexable: (name) => name !== 'borderDash' }; circumference: number; endAngle: number; fullCircles: number; innerRadius: number; outerRadius: number; pixelMargin: number; startAngle: number; constructor(cfg) { super(); this.options = undefined; this.circumference = undefined; this.startAngle = undefined; this.endAngle = undefined; this.innerRadius = undefined; this.outerRadius = undefined; this.pixelMargin = 0; this.fullCircles = 0; if (cfg) { Object.assign(this, cfg); } } inRange(chartX: number, chartY: number, useFinalPosition: boolean) { const point = this.getProps(['x', 'y'], useFinalPosition); const {angle, distance} = getAngleFromPoint(point, {x: chartX, y: chartY}); const {startAngle, endAngle, innerRadius, outerRadius, circumference} = this.getProps([ 'startAngle', 'endAngle', 'innerRadius', 'outerRadius', 'circumference' ], useFinalPosition); const rAdjust = (this.options.spacing + this.options.borderWidth) / 2; const _circumference = valueOrDefault(circumference, endAngle - startAngle); const nonZeroBetween = _angleBetween(angle, startAngle, endAngle) && startAngle !== endAngle; const betweenAngles = _circumference >= TAU || nonZeroBetween; const withinRadius = _isBetween(distance, innerRadius + rAdjust, outerRadius + rAdjust); return (betweenAngles && withinRadius); } getCenterPoint(useFinalPosition: boolean) { const {x, y, startAngle, endAngle, innerRadius, outerRadius} = this.getProps([ 'x', 'y', 'startAngle', 'endAngle', 'innerRadius', 'outerRadius' ], useFinalPosition); const {offset, spacing} = this.options; const halfAngle = (startAngle + endAngle) / 2; const halfRadius = (innerRadius + outerRadius + spacing + offset) / 2; return { x: x + Math.cos(halfAngle) * halfRadius, y: y + Math.sin(halfAngle) * halfRadius }; } tooltipPosition(useFinalPosition: boolean) { return this.getCenterPoint(useFinalPosition); } draw(ctx: CanvasRenderingContext2D) { const {options, circumference} = this; const offset = (options.offset || 0) / 4; const spacing = (options.spacing || 0) / 2; const circular = options.circular; this.pixelMargin = (options.borderAlign === 'inner') ? 0.33 : 0; this.fullCircles = circumference > TAU ? Math.floor(circumference / TAU) : 0; if (circumference === 0 || this.innerRadius < 0 || this.outerRadius < 0) { return; } ctx.save(); const halfAngle = (this.startAngle + this.endAngle) / 2; ctx.translate(Math.cos(halfAngle) * offset, Math.sin(halfAngle) * offset); const fix = 1 - Math.sin(Math.min(PI, circumference || 0)); const radiusOffset = offset * fix; ctx.fillStyle = options.backgroundColor; ctx.strokeStyle = options.borderColor; drawArc(ctx, this, radiusOffset, spacing, circular); drawBorder(ctx, this, radiusOffset, spacing, circular); ctx.restore(); } } ================================================ FILE: src/elements/element.bar.js ================================================ import Element from '../core/core.element.js'; import {isObject, _isBetween, _limitValue} from '../helpers/index.js'; import {addRoundedRectPath} from '../helpers/helpers.canvas.js'; import {toTRBL, toTRBLCorners} from '../helpers/helpers.options.js'; /** @typedef {{ x: number, y: number, base: number, horizontal: boolean, width: number, height: number }} BarProps */ /** * Helper function to get the bounds of the bar regardless of the orientation * @param {BarElement} bar the bar * @param {boolean} [useFinalPosition] * @return {object} bounds of the bar * @private */ function getBarBounds(bar, useFinalPosition) { const {x, y, base, width, height} = /** @type {BarProps} */ (bar.getProps(['x', 'y', 'base', 'width', 'height'], useFinalPosition)); let left, right, top, bottom, half; if (bar.horizontal) { half = height / 2; left = Math.min(x, base); right = Math.max(x, base); top = y - half; bottom = y + half; } else { half = width / 2; left = x - half; right = x + half; top = Math.min(y, base); bottom = Math.max(y, base); } return {left, top, right, bottom}; } function skipOrLimit(skip, value, min, max) { return skip ? 0 : _limitValue(value, min, max); } function parseBorderWidth(bar, maxW, maxH) { const value = bar.options.borderWidth; const skip = bar.borderSkipped; const o = toTRBL(value); return { t: skipOrLimit(skip.top, o.top, 0, maxH), r: skipOrLimit(skip.right, o.right, 0, maxW), b: skipOrLimit(skip.bottom, o.bottom, 0, maxH), l: skipOrLimit(skip.left, o.left, 0, maxW) }; } function parseBorderRadius(bar, maxW, maxH) { const {enableBorderRadius} = bar.getProps(['enableBorderRadius']); const value = bar.options.borderRadius; const o = toTRBLCorners(value); const maxR = Math.min(maxW, maxH); const skip = bar.borderSkipped; // If the value is an object, assume the user knows what they are doing // and apply as directed. const enableBorder = enableBorderRadius || isObject(value); return { topLeft: skipOrLimit(!enableBorder || skip.top || skip.left, o.topLeft, 0, maxR), topRight: skipOrLimit(!enableBorder || skip.top || skip.right, o.topRight, 0, maxR), bottomLeft: skipOrLimit(!enableBorder || skip.bottom || skip.left, o.bottomLeft, 0, maxR), bottomRight: skipOrLimit(!enableBorder || skip.bottom || skip.right, o.bottomRight, 0, maxR) }; } function boundingRects(bar) { const bounds = getBarBounds(bar); const width = bounds.right - bounds.left; const height = bounds.bottom - bounds.top; const border = parseBorderWidth(bar, width / 2, height / 2); const radius = parseBorderRadius(bar, width / 2, height / 2); return { outer: { x: bounds.left, y: bounds.top, w: width, h: height, radius }, inner: { x: bounds.left + border.l, y: bounds.top + border.t, w: width - border.l - border.r, h: height - border.t - border.b, radius: { topLeft: Math.max(0, radius.topLeft - Math.max(border.t, border.l)), topRight: Math.max(0, radius.topRight - Math.max(border.t, border.r)), bottomLeft: Math.max(0, radius.bottomLeft - Math.max(border.b, border.l)), bottomRight: Math.max(0, radius.bottomRight - Math.max(border.b, border.r)), } } }; } function inRange(bar, x, y, useFinalPosition) { const skipX = x === null; const skipY = y === null; const skipBoth = skipX && skipY; const bounds = bar && !skipBoth && getBarBounds(bar, useFinalPosition); return bounds && (skipX || _isBetween(x, bounds.left, bounds.right)) && (skipY || _isBetween(y, bounds.top, bounds.bottom)); } function hasRadius(radius) { return radius.topLeft || radius.topRight || radius.bottomLeft || radius.bottomRight; } /** * Add a path of a rectangle to the current sub-path * @param {CanvasRenderingContext2D} ctx Context * @param {*} rect Bounding rect */ function addNormalRectPath(ctx, rect) { ctx.rect(rect.x, rect.y, rect.w, rect.h); } function inflateRect(rect, amount, refRect = {}) { const x = rect.x !== refRect.x ? -amount : 0; const y = rect.y !== refRect.y ? -amount : 0; const w = (rect.x + rect.w !== refRect.x + refRect.w ? amount : 0) - x; const h = (rect.y + rect.h !== refRect.y + refRect.h ? amount : 0) - y; return { x: rect.x + x, y: rect.y + y, w: rect.w + w, h: rect.h + h, radius: rect.radius }; } export default class BarElement extends Element { static id = 'bar'; /** * @type {any} */ static defaults = { borderSkipped: 'start', borderWidth: 0, borderRadius: 0, inflateAmount: 'auto', pointStyle: undefined }; /** * @type {any} */ static defaultRoutes = { backgroundColor: 'backgroundColor', borderColor: 'borderColor' }; constructor(cfg) { super(); this.options = undefined; this.horizontal = undefined; this.base = undefined; this.width = undefined; this.height = undefined; this.inflateAmount = undefined; if (cfg) { Object.assign(this, cfg); } } draw(ctx) { const {inflateAmount, options: {borderColor, backgroundColor}} = this; const {inner, outer} = boundingRects(this); const addRectPath = hasRadius(outer.radius) ? addRoundedRectPath : addNormalRectPath; ctx.save(); if (outer.w !== inner.w || outer.h !== inner.h) { ctx.beginPath(); addRectPath(ctx, inflateRect(outer, inflateAmount, inner)); ctx.clip(); addRectPath(ctx, inflateRect(inner, -inflateAmount, outer)); ctx.fillStyle = borderColor; ctx.fill('evenodd'); } ctx.beginPath(); addRectPath(ctx, inflateRect(inner, inflateAmount)); ctx.fillStyle = backgroundColor; ctx.fill(); ctx.restore(); } inRange(mouseX, mouseY, useFinalPosition) { return inRange(this, mouseX, mouseY, useFinalPosition); } inXRange(mouseX, useFinalPosition) { return inRange(this, mouseX, null, useFinalPosition); } inYRange(mouseY, useFinalPosition) { return inRange(this, null, mouseY, useFinalPosition); } getCenterPoint(useFinalPosition) { const {x, y, base, horizontal} = /** @type {BarProps} */ (this.getProps(['x', 'y', 'base', 'horizontal'], useFinalPosition)); return { x: horizontal ? (x + base) / 2 : x, y: horizontal ? y : (y + base) / 2 }; } getRange(axis) { return axis === 'x' ? this.width / 2 : this.height / 2; } } ================================================ FILE: src/elements/element.line.js ================================================ import Element from '../core/core.element.js'; import {_bezierInterpolation, _pointInLine, _steppedInterpolation} from '../helpers/helpers.interpolation.js'; import {_computeSegments, _boundSegments} from '../helpers/helpers.segment.js'; import {_steppedLineTo, _bezierCurveTo} from '../helpers/helpers.canvas.js'; import {_updateBezierControlPoints} from '../helpers/helpers.curve.js'; import {valueOrDefault} from '../helpers/index.js'; /** * @typedef { import('./element.point.js').default } PointElement */ function setStyle(ctx, options, style = options) { ctx.lineCap = valueOrDefault(style.borderCapStyle, options.borderCapStyle); ctx.setLineDash(valueOrDefault(style.borderDash, options.borderDash)); ctx.lineDashOffset = valueOrDefault(style.borderDashOffset, options.borderDashOffset); ctx.lineJoin = valueOrDefault(style.borderJoinStyle, options.borderJoinStyle); ctx.lineWidth = valueOrDefault(style.borderWidth, options.borderWidth); ctx.strokeStyle = valueOrDefault(style.borderColor, options.borderColor); } function lineTo(ctx, previous, target) { ctx.lineTo(target.x, target.y); } /** * @returns {any} */ function getLineMethod(options) { if (options.stepped) { return _steppedLineTo; } if (options.tension || options.cubicInterpolationMode === 'monotone') { return _bezierCurveTo; } return lineTo; } function pathVars(points, segment, params = {}) { const count = points.length; const {start: paramsStart = 0, end: paramsEnd = count - 1} = params; const {start: segmentStart, end: segmentEnd} = segment; const start = Math.max(paramsStart, segmentStart); const end = Math.min(paramsEnd, segmentEnd); const outside = paramsStart < segmentStart && paramsEnd < segmentStart || paramsStart > segmentEnd && paramsEnd > segmentEnd; return { count, start, loop: segment.loop, ilen: end < start && !outside ? count + end - start : end - start }; } /** * Create path from points, grouping by truncated x-coordinate * Points need to be in order by x-coordinate for this to work efficiently * @param {CanvasRenderingContext2D|Path2D} ctx - Context * @param {LineElement} line * @param {object} segment * @param {number} segment.start - start index of the segment, referring the points array * @param {number} segment.end - end index of the segment, referring the points array * @param {boolean} segment.loop - indicates that the segment is a loop * @param {object} params * @param {boolean} params.move - move to starting point (vs line to it) * @param {boolean} params.reverse - path the segment from end to start * @param {number} params.start - limit segment to points starting from `start` index * @param {number} params.end - limit segment to points ending at `start` + `count` index */ function pathSegment(ctx, line, segment, params) { const {points, options} = line; const {count, start, loop, ilen} = pathVars(points, segment, params); const lineMethod = getLineMethod(options); // eslint-disable-next-line prefer-const let {move = true, reverse} = params || {}; let i, point, prev; for (i = 0; i <= ilen; ++i) { point = points[(start + (reverse ? ilen - i : i)) % count]; if (point.skip) { // If there is a skipped point inside a segment, spanGaps must be true continue; } else if (move) { ctx.moveTo(point.x, point.y); move = false; } else { lineMethod(ctx, prev, point, reverse, options.stepped); } prev = point; } if (loop) { point = points[(start + (reverse ? ilen : 0)) % count]; lineMethod(ctx, prev, point, reverse, options.stepped); } return !!loop; } /** * Create path from points, grouping by truncated x-coordinate * Points need to be in order by x-coordinate for this to work efficiently * @param {CanvasRenderingContext2D|Path2D} ctx - Context * @param {LineElement} line * @param {object} segment * @param {number} segment.start - start index of the segment, referring the points array * @param {number} segment.end - end index of the segment, referring the points array * @param {boolean} segment.loop - indicates that the segment is a loop * @param {object} params * @param {boolean} params.move - move to starting point (vs line to it) * @param {boolean} params.reverse - path the segment from end to start * @param {number} params.start - limit segment to points starting from `start` index * @param {number} params.end - limit segment to points ending at `start` + `count` index */ function fastPathSegment(ctx, line, segment, params) { const points = line.points; const {count, start, ilen} = pathVars(points, segment, params); const {move = true, reverse} = params || {}; let avgX = 0; let countX = 0; let i, point, prevX, minY, maxY, lastY; const pointIndex = (index) => (start + (reverse ? ilen - index : index)) % count; const drawX = () => { if (minY !== maxY) { // Draw line to maxY and minY, using the average x-coordinate ctx.lineTo(avgX, maxY); ctx.lineTo(avgX, minY); // Line to y-value of last point in group. So the line continues // from correct position. Not using move, to have solid path. ctx.lineTo(avgX, lastY); } }; if (move) { point = points[pointIndex(0)]; ctx.moveTo(point.x, point.y); } for (i = 0; i <= ilen; ++i) { point = points[pointIndex(i)]; if (point.skip) { // If there is a skipped point inside a segment, spanGaps must be true continue; } const x = point.x; const y = point.y; const truncX = x | 0; // truncated x-coordinate if (truncX === prevX) { // Determine `minY` / `maxY` and `avgX` while we stay within same x-position if (y < minY) { minY = y; } else if (y > maxY) { maxY = y; } // For first point in group, countX is `0`, so average will be `x` / 1. avgX = (countX * avgX + x) / ++countX; } else { drawX(); // Draw line to next x-position, using the first (or only) // y-value in that group ctx.lineTo(x, y); prevX = truncX; countX = 0; minY = maxY = y; } // Keep track of the last y-value in group lastY = y; } drawX(); } /** * @param {LineElement} line - the line * @returns {function} * @private */ function _getSegmentMethod(line) { const opts = line.options; const borderDash = opts.borderDash && opts.borderDash.length; const useFastPath = !line._decimated && !line._loop && !opts.tension && opts.cubicInterpolationMode !== 'monotone' && !opts.stepped && !borderDash; return useFastPath ? fastPathSegment : pathSegment; } /** * @private */ function _getInterpolationMethod(options) { if (options.stepped) { return _steppedInterpolation; } if (options.tension || options.cubicInterpolationMode === 'monotone') { return _bezierInterpolation; } return _pointInLine; } function strokePathWithCache(ctx, line, start, count) { let path = line._path; if (!path) { path = line._path = new Path2D(); if (line.path(path, start, count)) { path.closePath(); } } setStyle(ctx, line.options); ctx.stroke(path); } function strokePathDirect(ctx, line, start, count) { const {segments, options} = line; const segmentMethod = _getSegmentMethod(line); for (const segment of segments) { setStyle(ctx, options, segment.style); ctx.beginPath(); if (segmentMethod(ctx, line, segment, {start, end: start + count - 1})) { ctx.closePath(); } ctx.stroke(); } } const usePath2D = typeof Path2D === 'function'; function draw(ctx, line, start, count) { if (usePath2D && !line.options.segment) { strokePathWithCache(ctx, line, start, count); } else { strokePathDirect(ctx, line, start, count); } } export default class LineElement extends Element { static id = 'line'; /** * @type {any} */ static defaults = { borderCapStyle: 'butt', borderDash: [], borderDashOffset: 0, borderJoinStyle: 'miter', borderWidth: 3, capBezierPoints: true, cubicInterpolationMode: 'default', fill: false, spanGaps: false, stepped: false, tension: 0, }; /** * @type {any} */ static defaultRoutes = { backgroundColor: 'backgroundColor', borderColor: 'borderColor' }; static descriptors = { _scriptable: true, _indexable: (name) => name !== 'borderDash' && name !== 'fill', }; constructor(cfg) { super(); this.animated = true; this.options = undefined; this._chart = undefined; this._loop = undefined; this._fullLoop = undefined; this._path = undefined; this._points = undefined; this._segments = undefined; this._decimated = false; this._pointsUpdated = false; this._datasetIndex = undefined; if (cfg) { Object.assign(this, cfg); } } updateControlPoints(chartArea, indexAxis) { const options = this.options; if ((options.tension || options.cubicInterpolationMode === 'monotone') && !options.stepped && !this._pointsUpdated) { const loop = options.spanGaps ? this._loop : this._fullLoop; _updateBezierControlPoints(this._points, options, chartArea, loop, indexAxis); this._pointsUpdated = true; } } set points(points) { this._points = points; delete this._segments; delete this._path; this._pointsUpdated = false; } get points() { return this._points; } get segments() { return this._segments || (this._segments = _computeSegments(this, this.options.segment)); } /** * First non-skipped point on this line * @returns {PointElement|undefined} */ first() { const segments = this.segments; const points = this.points; return segments.length && points[segments[0].start]; } /** * Last non-skipped point on this line * @returns {PointElement|undefined} */ last() { const segments = this.segments; const points = this.points; const count = segments.length; return count && points[segments[count - 1].end]; } /** * Interpolate a point in this line at the same value on `property` as * the reference `point` provided * @param {PointElement} point - the reference point * @param {string} property - the property to match on * @returns {PointElement|undefined} */ interpolate(point, property) { const options = this.options; const value = point[property]; const points = this.points; const segments = _boundSegments(this, {property, start: value, end: value}); if (!segments.length) { return; } const result = []; const _interpolate = _getInterpolationMethod(options); let i, ilen; for (i = 0, ilen = segments.length; i < ilen; ++i) { const {start, end} = segments[i]; const p1 = points[start]; const p2 = points[end]; if (p1 === p2) { result.push(p1); continue; } const t = Math.abs((value - p1[property]) / (p2[property] - p1[property])); const interpolated = _interpolate(p1, p2, t, options.stepped); interpolated[property] = point[property]; result.push(interpolated); } return result.length === 1 ? result[0] : result; } /** * Append a segment of this line to current path. * @param {CanvasRenderingContext2D} ctx * @param {object} segment * @param {number} segment.start - start index of the segment, referring the points array * @param {number} segment.end - end index of the segment, referring the points array * @param {boolean} segment.loop - indicates that the segment is a loop * @param {object} params * @param {boolean} params.move - move to starting point (vs line to it) * @param {boolean} params.reverse - path the segment from end to start * @param {number} params.start - limit segment to points starting from `start` index * @param {number} params.end - limit segment to points ending at `start` + `count` index * @returns {undefined|boolean} - true if the segment is a full loop (path should be closed) */ pathSegment(ctx, segment, params) { const segmentMethod = _getSegmentMethod(this); return segmentMethod(ctx, this, segment, params); } /** * Append all segments of this line to current path. * @param {CanvasRenderingContext2D|Path2D} ctx * @param {number} [start] * @param {number} [count] * @returns {undefined|boolean} - true if line is a full loop (path should be closed) */ path(ctx, start, count) { const segments = this.segments; const segmentMethod = _getSegmentMethod(this); let loop = this._loop; start = start || 0; count = count || (this.points.length - start); for (const segment of segments) { loop &= segmentMethod(ctx, this, segment, {start, end: start + count - 1}); } return !!loop; } /** * Draw * @param {CanvasRenderingContext2D} ctx * @param {object} chartArea * @param {number} [start] * @param {number} [count] */ draw(ctx, chartArea, start, count) { const options = this.options || {}; const points = this.points || []; if (points.length && options.borderWidth) { ctx.save(); draw(ctx, this, start, count); ctx.restore(); } if (this.animated) { // When line is animated, the control points and path are not cached. this._pointsUpdated = false; this._path = undefined; } } } ================================================ FILE: src/elements/element.point.ts ================================================ import Element from '../core/core.element.js'; import {drawPoint, _isPointInArea} from '../helpers/helpers.canvas.js'; import type { CartesianParsedData, ChartArea, Point, PointHoverOptions, PointOptions, } from '../types/index.js'; function inRange(el: PointElement, pos: number, axis: 'x' | 'y', useFinalPosition?: boolean) { const options = el.options; const {[axis]: value} = el.getProps([axis], useFinalPosition); return (Math.abs(pos - value) < options.radius + options.hitRadius); } export type PointProps = Point export default class PointElement extends Element { static id = 'point'; parsed: CartesianParsedData; skip?: boolean; stop?: boolean; /** * @type {any} */ static defaults = { borderWidth: 1, hitRadius: 1, hoverBorderWidth: 1, hoverRadius: 4, pointStyle: 'circle', radius: 3, rotation: 0 }; /** * @type {any} */ static defaultRoutes = { backgroundColor: 'backgroundColor', borderColor: 'borderColor' }; constructor(cfg) { super(); this.options = undefined; this.parsed = undefined; this.skip = undefined; this.stop = undefined; if (cfg) { Object.assign(this, cfg); } } inRange(mouseX: number, mouseY: number, useFinalPosition?: boolean) { const options = this.options; const {x, y} = this.getProps(['x', 'y'], useFinalPosition); return ((Math.pow(mouseX - x, 2) + Math.pow(mouseY - y, 2)) < Math.pow(options.hitRadius + options.radius, 2)); } inXRange(mouseX: number, useFinalPosition?: boolean) { return inRange(this, mouseX, 'x', useFinalPosition); } inYRange(mouseY: number, useFinalPosition?: boolean) { return inRange(this, mouseY, 'y', useFinalPosition); } getCenterPoint(useFinalPosition?: boolean) { const {x, y} = this.getProps(['x', 'y'], useFinalPosition); return {x, y}; } size(options?: Partial) { options = options || this.options || {}; let radius = options.radius || 0; radius = Math.max(radius, radius && options.hoverRadius || 0); const borderWidth = radius && options.borderWidth || 0; return (radius + borderWidth) * 2; } draw(ctx: CanvasRenderingContext2D, area: ChartArea) { const options = this.options; if (this.skip || options.radius < 0.1 || !_isPointInArea(this, area, this.size(options) / 2)) { return; } ctx.strokeStyle = options.borderColor; ctx.lineWidth = options.borderWidth; ctx.fillStyle = options.backgroundColor; drawPoint(ctx, options, this.x, this.y); } getRange() { const options = this.options || {}; // @ts-expect-error Fallbacks should never be hit in practice return options.radius + options.hitRadius; } } ================================================ FILE: src/elements/index.js ================================================ export {default as ArcElement} from './element.arc.js'; export {default as LineElement} from './element.line.js'; export {default as PointElement} from './element.point.js'; export {default as BarElement} from './element.bar.js'; ================================================ FILE: src/helpers/helpers.canvas.ts ================================================ import type { Chart, Point, FontSpec, CanvasFontSpec, PointStyle, RenderTextOpts, BackdropOptions } from '../types/index.js'; import type { TRBL, SplinePoint, RoundedRect, TRBLCorners } from '../types/geometric.js'; import {isArray, isNullOrUndef} from './helpers.core.js'; import {PI, TAU, HALF_PI, QUARTER_PI, TWO_THIRDS_PI, RAD_PER_DEG} from './helpers.math.js'; /** * Converts the given font object into a CSS font string. * @param font - A font object. * @return The CSS font string. See https://developer.mozilla.org/en-US/docs/Web/CSS/font * @private */ export function toFontString(font: FontSpec) { if (!font || isNullOrUndef(font.size) || isNullOrUndef(font.family)) { return null; } return (font.style ? font.style + ' ' : '') + (font.weight ? font.weight + ' ' : '') + font.size + 'px ' + font.family; } /** * @private */ export function _measureText( ctx: CanvasRenderingContext2D, data: Record, gc: string[], longest: number, string: string ) { let textWidth = data[string]; if (!textWidth) { textWidth = data[string] = ctx.measureText(string).width; gc.push(string); } if (textWidth > longest) { longest = textWidth; } return longest; } type Thing = string | undefined | null type Things = (Thing | Thing[])[] /** * @private */ // eslint-disable-next-line complexity export function _longestText( ctx: CanvasRenderingContext2D, font: string, arrayOfThings: Things, cache?: {data?: Record, garbageCollect?: string[], font?: string} ) { cache = cache || {}; let data = cache.data = cache.data || {}; let gc = cache.garbageCollect = cache.garbageCollect || []; if (cache.font !== font) { data = cache.data = {}; gc = cache.garbageCollect = []; cache.font = font; } ctx.save(); ctx.font = font; let longest = 0; const ilen = arrayOfThings.length; let i: number, j: number, jlen: number, thing: Thing | Thing[], nestedThing: Thing | Thing[]; for (i = 0; i < ilen; i++) { thing = arrayOfThings[i]; // Undefined strings and arrays should not be measured if (thing !== undefined && thing !== null && !isArray(thing)) { longest = _measureText(ctx, data, gc, longest, thing); } else if (isArray(thing)) { // if it is an array lets measure each element // to do maybe simplify this function a bit so we can do this more recursively? for (j = 0, jlen = thing.length; j < jlen; j++) { nestedThing = thing[j]; // Undefined strings and arrays should not be measured if (nestedThing !== undefined && nestedThing !== null && !isArray(nestedThing)) { longest = _measureText(ctx, data, gc, longest, nestedThing); } } } } ctx.restore(); const gcLen = gc.length / 2; if (gcLen > arrayOfThings.length) { for (i = 0; i < gcLen; i++) { delete data[gc[i]]; } gc.splice(0, gcLen); } return longest; } /** * Returns the aligned pixel value to avoid anti-aliasing blur * @param chart - The chart instance. * @param pixel - A pixel value. * @param width - The width of the element. * @returns The aligned pixel value. * @private */ export function _alignPixel(chart: Chart, pixel: number, width: number) { const devicePixelRatio = chart.currentDevicePixelRatio; const halfWidth = width !== 0 ? Math.max(width / 2, 0.5) : 0; return Math.round((pixel - halfWidth) * devicePixelRatio) / devicePixelRatio + halfWidth; } /** * Clears the entire canvas. */ export function clearCanvas(canvas?: HTMLCanvasElement, ctx?: CanvasRenderingContext2D) { if (!ctx && !canvas) { return; } ctx = ctx || canvas.getContext('2d'); ctx.save(); // canvas.width and canvas.height do not consider the canvas transform, // while clearRect does ctx.resetTransform(); ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.restore(); } export interface DrawPointOptions { pointStyle: PointStyle; rotation?: number; radius: number; borderWidth: number; } export function drawPoint( ctx: CanvasRenderingContext2D, options: DrawPointOptions, x: number, y: number ) { // eslint-disable-next-line @typescript-eslint/no-use-before-define drawPointLegend(ctx, options, x, y, null); } // eslint-disable-next-line complexity export function drawPointLegend( ctx: CanvasRenderingContext2D, options: DrawPointOptions, x: number, y: number, w: number ) { let type: string, xOffset: number, yOffset: number, size: number, cornerRadius: number, width: number, xOffsetW: number, yOffsetW: number; const style = options.pointStyle; const rotation = options.rotation; const radius = options.radius; let rad = (rotation || 0) * RAD_PER_DEG; if (style && typeof style === 'object') { type = style.toString(); if (type === '[object HTMLImageElement]' || type === '[object HTMLCanvasElement]') { ctx.save(); ctx.translate(x, y); ctx.rotate(rad); ctx.drawImage(style, -style.width / 2, -style.height / 2, style.width, style.height); ctx.restore(); return; } } if (isNaN(radius) || radius <= 0) { return; } ctx.beginPath(); switch (style) { // Default includes circle default: if (w) { ctx.ellipse(x, y, w / 2, radius, 0, 0, TAU); } else { ctx.arc(x, y, radius, 0, TAU); } ctx.closePath(); break; case 'triangle': width = w ? w / 2 : radius; ctx.moveTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius); rad += TWO_THIRDS_PI; ctx.lineTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius); rad += TWO_THIRDS_PI; ctx.lineTo(x + Math.sin(rad) * width, y - Math.cos(rad) * radius); ctx.closePath(); break; case 'rectRounded': // NOTE: the rounded rect implementation changed to use `arc` instead of // `quadraticCurveTo` since it generates better results when rect is // almost a circle. 0.516 (instead of 0.5) produces results with visually // closer proportion to the previous impl and it is inscribed in the // circle with `radius`. For more details, see the following PRs: // https://github.com/chartjs/Chart.js/issues/5597 // https://github.com/chartjs/Chart.js/issues/5858 cornerRadius = radius * 0.516; size = radius - cornerRadius; xOffset = Math.cos(rad + QUARTER_PI) * size; xOffsetW = Math.cos(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size); yOffset = Math.sin(rad + QUARTER_PI) * size; yOffsetW = Math.sin(rad + QUARTER_PI) * (w ? w / 2 - cornerRadius : size); ctx.arc(x - xOffsetW, y - yOffset, cornerRadius, rad - PI, rad - HALF_PI); ctx.arc(x + yOffsetW, y - xOffset, cornerRadius, rad - HALF_PI, rad); ctx.arc(x + xOffsetW, y + yOffset, cornerRadius, rad, rad + HALF_PI); ctx.arc(x - yOffsetW, y + xOffset, cornerRadius, rad + HALF_PI, rad + PI); ctx.closePath(); break; case 'rect': if (!rotation) { size = Math.SQRT1_2 * radius; width = w ? w / 2 : size; ctx.rect(x - width, y - size, 2 * width, 2 * size); break; } rad += QUARTER_PI; /* falls through */ case 'rectRot': xOffsetW = Math.cos(rad) * (w ? w / 2 : radius); xOffset = Math.cos(rad) * radius; yOffset = Math.sin(rad) * radius; yOffsetW = Math.sin(rad) * (w ? w / 2 : radius); ctx.moveTo(x - xOffsetW, y - yOffset); ctx.lineTo(x + yOffsetW, y - xOffset); ctx.lineTo(x + xOffsetW, y + yOffset); ctx.lineTo(x - yOffsetW, y + xOffset); ctx.closePath(); break; case 'crossRot': rad += QUARTER_PI; /* falls through */ case 'cross': xOffsetW = Math.cos(rad) * (w ? w / 2 : radius); xOffset = Math.cos(rad) * radius; yOffset = Math.sin(rad) * radius; yOffsetW = Math.sin(rad) * (w ? w / 2 : radius); ctx.moveTo(x - xOffsetW, y - yOffset); ctx.lineTo(x + xOffsetW, y + yOffset); ctx.moveTo(x + yOffsetW, y - xOffset); ctx.lineTo(x - yOffsetW, y + xOffset); break; case 'star': xOffsetW = Math.cos(rad) * (w ? w / 2 : radius); xOffset = Math.cos(rad) * radius; yOffset = Math.sin(rad) * radius; yOffsetW = Math.sin(rad) * (w ? w / 2 : radius); ctx.moveTo(x - xOffsetW, y - yOffset); ctx.lineTo(x + xOffsetW, y + yOffset); ctx.moveTo(x + yOffsetW, y - xOffset); ctx.lineTo(x - yOffsetW, y + xOffset); rad += QUARTER_PI; xOffsetW = Math.cos(rad) * (w ? w / 2 : radius); xOffset = Math.cos(rad) * radius; yOffset = Math.sin(rad) * radius; yOffsetW = Math.sin(rad) * (w ? w / 2 : radius); ctx.moveTo(x - xOffsetW, y - yOffset); ctx.lineTo(x + xOffsetW, y + yOffset); ctx.moveTo(x + yOffsetW, y - xOffset); ctx.lineTo(x - yOffsetW, y + xOffset); break; case 'line': xOffset = w ? w / 2 : Math.cos(rad) * radius; yOffset = Math.sin(rad) * radius; ctx.moveTo(x - xOffset, y - yOffset); ctx.lineTo(x + xOffset, y + yOffset); break; case 'dash': ctx.moveTo(x, y); ctx.lineTo(x + Math.cos(rad) * (w ? w / 2 : radius), y + Math.sin(rad) * radius); break; case false: ctx.closePath(); break; } ctx.fill(); if (options.borderWidth > 0) { ctx.stroke(); } } /** * Returns true if the point is inside the rectangle * @param point - The point to test * @param area - The rectangle * @param margin - allowed margin * @private */ export function _isPointInArea( point: Point, area: TRBL, margin?: number ) { margin = margin || 0.5; // margin - default is to match rounded decimals return !area || (point && point.x > area.left - margin && point.x < area.right + margin && point.y > area.top - margin && point.y < area.bottom + margin); } export function clipArea(ctx: CanvasRenderingContext2D, area: TRBL) { ctx.save(); ctx.beginPath(); ctx.rect(area.left, area.top, area.right - area.left, area.bottom - area.top); ctx.clip(); } export function unclipArea(ctx: CanvasRenderingContext2D) { ctx.restore(); } /** * @private */ export function _steppedLineTo( ctx: CanvasRenderingContext2D, previous: Point, target: Point, flip?: boolean, mode?: string ) { if (!previous) { return ctx.lineTo(target.x, target.y); } if (mode === 'middle') { const midpoint = (previous.x + target.x) / 2.0; ctx.lineTo(midpoint, previous.y); ctx.lineTo(midpoint, target.y); } else if (mode === 'after' !== !!flip) { ctx.lineTo(previous.x, target.y); } else { ctx.lineTo(target.x, previous.y); } ctx.lineTo(target.x, target.y); } /** * @private */ export function _bezierCurveTo( ctx: CanvasRenderingContext2D, previous: SplinePoint, target: SplinePoint, flip?: boolean ) { if (!previous) { return ctx.lineTo(target.x, target.y); } ctx.bezierCurveTo( flip ? previous.cp1x : previous.cp2x, flip ? previous.cp1y : previous.cp2y, flip ? target.cp2x : target.cp1x, flip ? target.cp2y : target.cp1y, target.x, target.y); } function setRenderOpts(ctx: CanvasRenderingContext2D, opts: RenderTextOpts) { if (opts.translation) { ctx.translate(opts.translation[0], opts.translation[1]); } if (!isNullOrUndef(opts.rotation)) { ctx.rotate(opts.rotation); } if (opts.color) { ctx.fillStyle = opts.color; } if (opts.textAlign) { ctx.textAlign = opts.textAlign; } if (opts.textBaseline) { ctx.textBaseline = opts.textBaseline; } } function decorateText( ctx: CanvasRenderingContext2D, x: number, y: number, line: string, opts: RenderTextOpts ) { if (opts.strikethrough || opts.underline) { /** * Now that IE11 support has been dropped, we can use more * of the TextMetrics object. The actual bounding boxes * are unflagged in Chrome, Firefox, Edge, and Safari so they * can be safely used. * See https://developer.mozilla.org/en-US/docs/Web/API/TextMetrics#Browser_compatibility */ const metrics = ctx.measureText(line); const left = x - metrics.actualBoundingBoxLeft; const right = x + metrics.actualBoundingBoxRight; const top = y - metrics.actualBoundingBoxAscent; const bottom = y + metrics.actualBoundingBoxDescent; const yDecoration = opts.strikethrough ? (top + bottom) / 2 : bottom; ctx.strokeStyle = ctx.fillStyle; ctx.beginPath(); ctx.lineWidth = opts.decorationWidth || 2; ctx.moveTo(left, yDecoration); ctx.lineTo(right, yDecoration); ctx.stroke(); } } function drawBackdrop(ctx: CanvasRenderingContext2D, opts: BackdropOptions) { const oldColor = ctx.fillStyle; ctx.fillStyle = opts.color as string; ctx.fillRect(opts.left, opts.top, opts.width, opts.height); ctx.fillStyle = oldColor; } /** * Render text onto the canvas */ export function renderText( ctx: CanvasRenderingContext2D, text: string | string[], x: number, y: number, font: CanvasFontSpec, opts: RenderTextOpts = {} ) { const lines = isArray(text) ? text : [text]; const stroke = opts.strokeWidth > 0 && opts.strokeColor !== ''; let i: number, line: string; ctx.save(); ctx.font = font.string; setRenderOpts(ctx, opts); for (i = 0; i < lines.length; ++i) { line = lines[i]; if (opts.backdrop) { drawBackdrop(ctx, opts.backdrop); } if (stroke) { if (opts.strokeColor) { ctx.strokeStyle = opts.strokeColor; } if (!isNullOrUndef(opts.strokeWidth)) { ctx.lineWidth = opts.strokeWidth; } ctx.strokeText(line, x, y, opts.maxWidth); } ctx.fillText(line, x, y, opts.maxWidth); decorateText(ctx, x, y, line, opts); y += Number(font.lineHeight); } ctx.restore(); } /** * Add a path of a rectangle with rounded corners to the current sub-path * @param ctx - Context * @param rect - Bounding rect */ export function addRoundedRectPath( ctx: CanvasRenderingContext2D, rect: RoundedRect & { radius: TRBLCorners } ) { const {x, y, w, h, radius} = rect; // top left arc ctx.arc(x + radius.topLeft, y + radius.topLeft, radius.topLeft, 1.5 * PI, PI, true); // line from top left to bottom left ctx.lineTo(x, y + h - radius.bottomLeft); // bottom left arc ctx.arc(x + radius.bottomLeft, y + h - radius.bottomLeft, radius.bottomLeft, PI, HALF_PI, true); // line from bottom left to bottom right ctx.lineTo(x + w - radius.bottomRight, y + h); // bottom right arc ctx.arc(x + w - radius.bottomRight, y + h - radius.bottomRight, radius.bottomRight, HALF_PI, 0, true); // line from bottom right to top right ctx.lineTo(x + w, y + radius.topRight); // top right arc ctx.arc(x + w - radius.topRight, y + radius.topRight, radius.topRight, 0, -HALF_PI, true); // line from top right to top left ctx.lineTo(x + radius.topLeft, y); } ================================================ FILE: src/helpers/helpers.collection.ts ================================================ import {_capitalize} from './helpers.core.js'; /** * Binary search * @param table - the table search. must be sorted! * @param value - value to find * @param cmp * @private */ export function _lookup( table: number[], value: number, cmp?: (value: number) => boolean ): {lo: number, hi: number}; export function _lookup( table: T[], value: number, cmp: (value: number) => boolean ): {lo: number, hi: number}; export function _lookup( table: unknown[], value: number, cmp?: (value: number) => boolean ) { cmp = cmp || ((index) => table[index] < value); let hi = table.length - 1; let lo = 0; let mid: number; while (hi - lo > 1) { mid = (lo + hi) >> 1; if (cmp(mid)) { lo = mid; } else { hi = mid; } } return {lo, hi}; } /** * Binary search * @param table - the table search. must be sorted! * @param key - property name for the value in each entry * @param value - value to find * @param last - lookup last index * @private */ export const _lookupByKey = ( table: Record[], key: string, value: number, last?: boolean ) => _lookup(table, value, last ? index => { const ti = table[index][key]; return ti < value || ti === value && table[index + 1][key] === value; } : index => table[index][key] < value); /** * Reverse binary search * @param table - the table search. must be sorted! * @param key - property name for the value in each entry * @param value - value to find * @private */ export const _rlookupByKey = ( table: Record[], key: string, value: number ) => _lookup(table, value, index => table[index][key] >= value); /** * Return subset of `values` between `min` and `max` inclusive. * Values are assumed to be in sorted order. * @param values - sorted array of values * @param min - min value * @param max - max value */ export function _filterBetween(values: number[], min: number, max: number) { let start = 0; let end = values.length; while (start < end && values[start] < min) { start++; } while (end > start && values[end - 1] > max) { end--; } return start > 0 || end < values.length ? values.slice(start, end) : values; } const arrayEvents = ['push', 'pop', 'shift', 'splice', 'unshift'] as const; export interface ArrayListener { _onDataPush?(...item: T[]): void; _onDataPop?(): void; _onDataShift?(): void; _onDataSplice?(index: number, deleteCount: number, ...items: T[]): void; _onDataUnshift?(...item: T[]): void; } /** * Hooks the array methods that add or remove values ('push', pop', 'shift', 'splice', * 'unshift') and notify the listener AFTER the array has been altered. Listeners are * called on the '_onData*' callbacks (e.g. _onDataPush, etc.) with same arguments. */ export function listenArrayEvents(array: T[], listener: ArrayListener): void; export function listenArrayEvents(array, listener) { if (array._chartjs) { array._chartjs.listeners.push(listener); return; } Object.defineProperty(array, '_chartjs', { configurable: true, enumerable: false, value: { listeners: [listener] } }); arrayEvents.forEach((key) => { const method = '_onData' + _capitalize(key); const base = array[key]; Object.defineProperty(array, key, { configurable: true, enumerable: false, value(...args) { const res = base.apply(this, args); array._chartjs.listeners.forEach((object) => { if (typeof object[method] === 'function') { object[method](...args); } }); return res; } }); }); } /** * Removes the given array event listener and cleanup extra attached properties (such as * the _chartjs stub and overridden methods) if array doesn't have any more listeners. */ export function unlistenArrayEvents(array: T[], listener: ArrayListener): void; export function unlistenArrayEvents(array, listener) { const stub = array._chartjs; if (!stub) { return; } const listeners = stub.listeners; const index = listeners.indexOf(listener); if (index !== -1) { listeners.splice(index, 1); } if (listeners.length > 0) { return; } arrayEvents.forEach((key) => { delete array[key]; }); delete array._chartjs; } /** * @param items */ export function _arrayUnique(items: T[]) { const set = new Set(items); if (set.size === items.length) { return items; } return Array.from(set); } ================================================ FILE: src/helpers/helpers.color.ts ================================================ import {Color} from '@kurkle/color'; export function isPatternOrGradient(value: unknown): value is CanvasPattern | CanvasGradient { if (value && typeof value === 'object') { const type = value.toString(); return type === '[object CanvasPattern]' || type === '[object CanvasGradient]'; } return false; } export function color(value: CanvasGradient): CanvasGradient; export function color(value: CanvasPattern): CanvasPattern; export function color( value: | string | { r: number; g: number; b: number; a: number } | [number, number, number] | [number, number, number, number] ): Color; export function color(value) { return isPatternOrGradient(value) ? value : new Color(value); } export function getHoverColor(value: CanvasGradient): CanvasGradient; export function getHoverColor(value: CanvasPattern): CanvasPattern; export function getHoverColor(value: string): string; export function getHoverColor(value) { return isPatternOrGradient(value) ? value : new Color(value).saturate(0.5).darken(0.1).hexString(); } ================================================ FILE: src/helpers/helpers.config.ts ================================================ /* eslint-disable @typescript-eslint/no-use-before-define */ import type {AnyObject} from '../types/basic.js'; import type {ChartMeta} from '../types/index.js'; import type { ResolverObjectKey, ResolverCache, ResolverProxy, DescriptorDefaults, Descriptor, ContextCache, ContextProxy } from './helpers.config.types.js'; import {isArray, isFunction, isObject, resolveObjectKey, _capitalize} from './helpers.core.js'; export * from './helpers.config.types.js'; /** * Creates a Proxy for resolving raw values for options. * @param scopes - The option scopes to look for values, in resolution order * @param prefixes - The prefixes for values, in resolution order. * @param rootScopes - The root option scopes * @param fallback - Parent scopes fallback * @param getTarget - callback for getting the target for changed values * @returns Proxy * @private */ export function _createResolver< T extends AnyObject[] = AnyObject[], R extends AnyObject[] = T >( scopes: T, prefixes = [''], rootScopes?: R, fallback?: ResolverObjectKey, getTarget = () => scopes[0] ) { const finalRootScopes = rootScopes || scopes; if (typeof fallback === 'undefined') { fallback = _resolve('_fallback', scopes); } const cache: ResolverCache = { [Symbol.toStringTag]: 'Object', _cacheable: true, _scopes: scopes, _rootScopes: finalRootScopes, _fallback: fallback, _getTarget: getTarget, override: (scope: AnyObject) => _createResolver([scope, ...scopes], prefixes, finalRootScopes, fallback), }; return new Proxy(cache, { /** * A trap for the delete operator. */ deleteProperty(target, prop: string) { delete target[prop]; // remove from cache delete target._keys; // remove cached keys delete scopes[0][prop]; // remove from top level scope return true; }, /** * A trap for getting property values. */ get(target, prop: string) { return _cached(target, prop, () => _resolveWithPrefixes(prop, prefixes, scopes, target)); }, /** * A trap for Object.getOwnPropertyDescriptor. * Also used by Object.hasOwnProperty. */ getOwnPropertyDescriptor(target, prop) { return Reflect.getOwnPropertyDescriptor(target._scopes[0], prop); }, /** * A trap for Object.getPrototypeOf. */ getPrototypeOf() { return Reflect.getPrototypeOf(scopes[0]); }, /** * A trap for the in operator. */ has(target, prop: string) { return getKeysFromAllScopes(target).includes(prop); }, /** * A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols. */ ownKeys(target) { return getKeysFromAllScopes(target); }, /** * A trap for setting property values. */ set(target, prop: string, value) { const storage = target._storage || (target._storage = getTarget()); target[prop] = storage[prop] = value; // set to top level scope + cache delete target._keys; // remove cached keys return true; } }) as ResolverProxy; } /** * Returns an Proxy for resolving option values with context. * @param proxy - The Proxy returned by `_createResolver` * @param context - Context object for scriptable/indexable options * @param subProxy - The proxy provided for scriptable options * @param descriptorDefaults - Defaults for descriptors * @private */ export function _attachContext< T extends AnyObject[] = AnyObject[], R extends AnyObject[] = T >( proxy: ResolverProxy, context: AnyObject, subProxy?: ResolverProxy, descriptorDefaults?: DescriptorDefaults ) { const cache: ContextCache = { _cacheable: false, _proxy: proxy, _context: context, _subProxy: subProxy, _stack: new Set(), _descriptors: _descriptors(proxy, descriptorDefaults), setContext: (ctx: AnyObject) => _attachContext(proxy, ctx, subProxy, descriptorDefaults), override: (scope: AnyObject) => _attachContext(proxy.override(scope), context, subProxy, descriptorDefaults) }; return new Proxy(cache, { /** * A trap for the delete operator. */ deleteProperty(target, prop) { delete target[prop]; // remove from cache delete proxy[prop]; // remove from proxy return true; }, /** * A trap for getting property values. */ get(target, prop: string, receiver) { return _cached(target, prop, () => _resolveWithContext(target, prop, receiver)); }, /** * A trap for Object.getOwnPropertyDescriptor. * Also used by Object.hasOwnProperty. */ getOwnPropertyDescriptor(target, prop) { return target._descriptors.allKeys ? Reflect.has(proxy, prop) ? {enumerable: true, configurable: true} : undefined : Reflect.getOwnPropertyDescriptor(proxy, prop); }, /** * A trap for Object.getPrototypeOf. */ getPrototypeOf() { return Reflect.getPrototypeOf(proxy); }, /** * A trap for the in operator. */ has(target, prop) { return Reflect.has(proxy, prop); }, /** * A trap for Object.getOwnPropertyNames and Object.getOwnPropertySymbols. */ ownKeys() { return Reflect.ownKeys(proxy); }, /** * A trap for setting property values. */ set(target, prop, value) { proxy[prop] = value; // set to proxy delete target[prop]; // remove from cache return true; } }) as ContextProxy; } /** * @private */ export function _descriptors( proxy: ResolverCache, defaults: DescriptorDefaults = {scriptable: true, indexable: true} ): Descriptor { const {_scriptable = defaults.scriptable, _indexable = defaults.indexable, _allKeys = defaults.allKeys} = proxy; return { allKeys: _allKeys, scriptable: _scriptable, indexable: _indexable, isScriptable: isFunction(_scriptable) ? _scriptable : () => _scriptable, isIndexable: isFunction(_indexable) ? _indexable : () => _indexable }; } const readKey = (prefix: string, name: string) => prefix ? prefix + _capitalize(name) : name; const needsSubResolver = (prop: string, value: unknown) => isObject(value) && prop !== 'adapters' && (Object.getPrototypeOf(value) === null || value.constructor === Object); function _cached( target: AnyObject, prop: string, resolve: () => unknown ) { if (Object.prototype.hasOwnProperty.call(target, prop) || prop === 'constructor') { return target[prop]; } const value = resolve(); // cache the resolved value target[prop] = value; return value; } function _resolveWithContext( target: ContextCache, prop: string, receiver: AnyObject ) { const {_proxy, _context, _subProxy, _descriptors: descriptors} = target; let value = _proxy[prop]; // resolve from proxy // resolve with context if (isFunction(value) && descriptors.isScriptable(prop)) { value = _resolveScriptable(prop, value, target, receiver); } if (isArray(value) && value.length) { value = _resolveArray(prop, value, target, descriptors.isIndexable); } if (needsSubResolver(prop, value)) { // if the resolved value is an object, create a sub resolver for it value = _attachContext(value, _context, _subProxy && _subProxy[prop], descriptors); } return value; } function _resolveScriptable( prop: string, getValue: (ctx: AnyObject, sub: AnyObject) => unknown, target: ContextCache, receiver: AnyObject ) { const {_proxy, _context, _subProxy, _stack} = target; if (_stack.has(prop)) { throw new Error('Recursion detected: ' + Array.from(_stack).join('->') + '->' + prop); } _stack.add(prop); let value = getValue(_context, _subProxy || receiver); _stack.delete(prop); if (needsSubResolver(prop, value)) { // When scriptable option returns an object, create a resolver on that. value = createSubResolver(_proxy._scopes, _proxy, prop, value); } return value; } function _resolveArray( prop: string, value: unknown[], target: ContextCache, isIndexable: (key: string) => boolean ) { const {_proxy, _context, _subProxy, _descriptors: descriptors} = target; if (typeof _context.index !== 'undefined' && isIndexable(prop)) { return value[_context.index % value.length]; } else if (isObject(value[0])) { // Array of objects, return array or resolvers const arr = value; const scopes = _proxy._scopes.filter(s => s !== arr); value = []; for (const item of arr) { const resolver = createSubResolver(scopes, _proxy, prop, item); value.push(_attachContext(resolver, _context, _subProxy && _subProxy[prop], descriptors)); } } return value; } function resolveFallback( fallback: ResolverObjectKey | ((prop: ResolverObjectKey, value: unknown) => ResolverObjectKey), prop: ResolverObjectKey, value: unknown ) { return isFunction(fallback) ? fallback(prop, value) : fallback; } const getScope = (key: ResolverObjectKey, parent: AnyObject) => key === true ? parent : typeof key === 'string' ? resolveObjectKey(parent, key) : undefined; function addScopes( set: Set, parentScopes: AnyObject[], key: ResolverObjectKey, parentFallback: ResolverObjectKey, value: unknown ) { for (const parent of parentScopes) { const scope = getScope(key, parent); if (scope) { set.add(scope); const fallback = resolveFallback(scope._fallback, key, value); if (typeof fallback !== 'undefined' && fallback !== key && fallback !== parentFallback) { // When we reach the descriptor that defines a new _fallback, return that. // The fallback will resume to that new scope. return fallback; } } else if (scope === false && typeof parentFallback !== 'undefined' && key !== parentFallback) { // Fallback to `false` results to `false`, when falling back to different key. // For example `interaction` from `hover` or `plugins.tooltip` and `animation` from `animations` return null; } } return false; } function createSubResolver( parentScopes: AnyObject[], resolver: ResolverCache, prop: ResolverObjectKey, value: unknown ) { const rootScopes = resolver._rootScopes; const fallback = resolveFallback(resolver._fallback, prop, value); const allScopes = [...parentScopes, ...rootScopes]; const set = new Set(); set.add(value); let key = addScopesFromKey(set, allScopes, prop, fallback || prop, value); if (key === null) { return false; } if (typeof fallback !== 'undefined' && fallback !== prop) { key = addScopesFromKey(set, allScopes, fallback, key, value); if (key === null) { return false; } } return _createResolver(Array.from(set), [''], rootScopes, fallback, () => subGetTarget(resolver, prop as string, value)); } function addScopesFromKey( set: Set, allScopes: AnyObject[], key: ResolverObjectKey, fallback: ResolverObjectKey, item: unknown ) { while (key) { key = addScopes(set, allScopes, key, fallback, item); } return key; } function subGetTarget( resolver: ResolverCache, prop: string, value: unknown ) { const parent = resolver._getTarget(); if (!(prop in parent)) { parent[prop] = {}; } const target = parent[prop]; if (isArray(target) && isObject(value)) { // For array of objects, the object is used to store updated values return value; } return target || {}; } function _resolveWithPrefixes( prop: string, prefixes: string[], scopes: AnyObject[], proxy: ResolverProxy ) { let value: unknown; for (const prefix of prefixes) { value = _resolve(readKey(prefix, prop), scopes); if (typeof value !== 'undefined') { return needsSubResolver(prop, value) ? createSubResolver(scopes, proxy, prop, value) : value; } } } function _resolve(key: string, scopes: AnyObject[]) { for (const scope of scopes) { if (!scope) { continue; } const value = scope[key]; if (typeof value !== 'undefined') { return value; } } } function getKeysFromAllScopes(target: ResolverCache) { let keys = target._keys; if (!keys) { keys = target._keys = resolveKeysFromAllScopes(target._scopes); } return keys; } function resolveKeysFromAllScopes(scopes: AnyObject[]) { const set = new Set(); for (const scope of scopes) { for (const key of Object.keys(scope).filter(k => !k.startsWith('_'))) { set.add(key); } } return Array.from(set); } export function _parseObjectDataRadialScale( meta: ChartMeta<'line' | 'scatter'>, data: AnyObject[], start: number, count: number ) { const {iScale} = meta; const {key = 'r'} = this._parsing; const parsed = new Array<{r: unknown}>(count); let i: number, ilen: number, index: number, item: AnyObject; for (i = 0, ilen = count; i < ilen; ++i) { index = i + start; item = data[index]; parsed[i] = { r: iScale.parse(resolveObjectKey(item, key), index) }; } return parsed; } ================================================ FILE: src/helpers/helpers.config.types.ts ================================================ import type {AnyObject} from '../types/basic.js'; import type {Merge} from '../types/utils.js'; export type ResolverObjectKey = string | boolean; export interface ResolverCache< T extends AnyObject[] = AnyObject[], R extends AnyObject[] = T > { [Symbol.toStringTag]: 'Object'; _cacheable: boolean; _scopes: T; _rootScopes: T | R; _fallback: ResolverObjectKey; _keys?: string[]; _scriptable?: boolean; _indexable?: boolean; _allKeys?: boolean; _storage?: T[number]; _getTarget(): T[number]; override(scope: S): ResolverProxy<(T[number] | S)[], T | R> } export type ResolverProxy< T extends AnyObject[] = AnyObject[], R extends AnyObject[] = T > = Merge & ResolverCache export interface DescriptorDefaults { scriptable: boolean; indexable: boolean; allKeys?: boolean } export interface Descriptor { allKeys: boolean; scriptable: boolean; indexable: boolean; isScriptable(key: string): boolean; isIndexable(key: string): boolean; } export interface ContextCache< T extends AnyObject[] = AnyObject[], R extends AnyObject[] = T > { _cacheable: boolean; _proxy: ResolverProxy; _context: AnyObject; _subProxy: ResolverProxy; _stack: Set; _descriptors: Descriptor setContext(ctx: AnyObject): ContextProxy override(scope: S): ContextProxy<(T[number] | S)[], T | R> } export type ContextProxy< T extends AnyObject[] = AnyObject[], R extends AnyObject[] = T > = Merge & ContextCache; ================================================ FILE: src/helpers/helpers.core.ts ================================================ /** * @namespace Chart.helpers */ import type {AnyObject} from '../types/basic.js'; import type {ActiveDataPoint, ChartEvent} from '../types/index.js'; /** * An empty function that can be used, for example, for optional callback. */ export function noop() { /* noop */ } /** * Returns a unique id, sequentially generated from a global variable. */ export const uid = (() => { let id = 0; return () => id++; })(); /** * Returns true if `value` is neither null nor undefined, else returns false. * @param value - The value to test. * @since 2.7.0 */ export function isNullOrUndef(value: unknown): value is null | undefined { return value === null || value === undefined; } /** * Returns true if `value` is an array (including typed arrays), else returns false. * @param value - The value to test. * @function */ export function isArray(value: unknown): value is T[] { if (Array.isArray && Array.isArray(value)) { return true; } const type = Object.prototype.toString.call(value); if (type.slice(0, 7) === '[object' && type.slice(-6) === 'Array]') { return true; } return false; } /** * Returns true if `value` is an object (excluding null), else returns false. * @param value - The value to test. * @since 2.7.0 */ export function isObject(value: unknown): value is AnyObject { return value !== null && Object.prototype.toString.call(value) === '[object Object]'; } /** * Returns true if `value` is a finite number, else returns false * @param value - The value to test. */ function isNumberFinite(value: unknown): value is number { return (typeof value === 'number' || value instanceof Number) && isFinite(+value); } export { isNumberFinite as isFinite, }; /** * Returns `value` if finite, else returns `defaultValue`. * @param value - The value to return if defined. * @param defaultValue - The value to return if `value` is not finite. */ export function finiteOrDefault(value: unknown, defaultValue: number) { return isNumberFinite(value) ? value : defaultValue; } /** * Returns `value` if defined, else returns `defaultValue`. * @param value - The value to return if defined. * @param defaultValue - The value to return if `value` is undefined. */ export function valueOrDefault(value: T | undefined, defaultValue: T) { return typeof value === 'undefined' ? defaultValue : value; } export const toPercentage = (value: number | string, dimension: number) => typeof value === 'string' && value.endsWith('%') ? parseFloat(value) / 100 : +value / dimension; export const toDimension = (value: number | string, dimension: number) => typeof value === 'string' && value.endsWith('%') ? parseFloat(value) / 100 * dimension : +value; /** * Calls `fn` with the given `args` in the scope defined by `thisArg` and returns the * value returned by `fn`. If `fn` is not a function, this method returns undefined. * @param fn - The function to call. * @param args - The arguments with which `fn` should be called. * @param [thisArg] - The value of `this` provided for the call to `fn`. */ export function callback R, TA, R>( fn: T | undefined, args: unknown[], thisArg?: TA ): R | undefined { if (fn && typeof fn.call === 'function') { return fn.apply(thisArg, args); } } /** * Note(SB) for performance sake, this method should only be used when loopable type * is unknown or in none intensive code (not called often and small loopable). Else * it's preferable to use a regular for() loop and save extra function calls. * @param loopable - The object or array to be iterated. * @param fn - The function to call for each item. * @param [thisArg] - The value of `this` provided for the call to `fn`. * @param [reverse] - If true, iterates backward on the loopable. */ export function each( loopable: Record, fn: (this: TA, v: T, i: string) => void, thisArg?: TA, reverse?: boolean ): void; export function each( loopable: T[], fn: (this: TA, v: T, i: number) => void, thisArg?: TA, reverse?: boolean ): void; export function each( loopable: T[] | Record, fn: (this: TA, v: T, i: any) => void, thisArg?: TA, reverse?: boolean ) { let i: number, len: number, keys: string[]; if (isArray(loopable)) { len = loopable.length; if (reverse) { for (i = len - 1; i >= 0; i--) { fn.call(thisArg, loopable[i], i); } } else { for (i = 0; i < len; i++) { fn.call(thisArg, loopable[i], i); } } } else if (isObject(loopable)) { keys = Object.keys(loopable); len = keys.length; for (i = 0; i < len; i++) { fn.call(thisArg, loopable[keys[i]], keys[i]); } } } /** * Returns true if the `a0` and `a1` arrays have the same content, else returns false. * @param a0 - The array to compare * @param a1 - The array to compare * @private */ export function _elementsEqual(a0: ActiveDataPoint[], a1: ActiveDataPoint[]) { let i: number, ilen: number, v0: ActiveDataPoint, v1: ActiveDataPoint; if (!a0 || !a1 || a0.length !== a1.length) { return false; } for (i = 0, ilen = a0.length; i < ilen; ++i) { v0 = a0[i]; v1 = a1[i]; if (v0.datasetIndex !== v1.datasetIndex || v0.index !== v1.index) { return false; } } return true; } /** * Returns a deep copy of `source` without keeping references on objects and arrays. * @param source - The value to clone. */ export function clone(source: T): T { if (isArray(source)) { return source.map(clone) as unknown as T; } if (isObject(source)) { const target = Object.create(null); const keys = Object.keys(source); const klen = keys.length; let k = 0; for (; k < klen; ++k) { target[keys[k]] = clone(source[keys[k]]); } return target; } return source; } function isValidKey(key: string) { return ['__proto__', 'prototype', 'constructor'].indexOf(key) === -1; } /** * The default merger when Chart.helpers.merge is called without merger option. * Note(SB): also used by mergeConfig and mergeScaleConfig as fallback. * @private */ export function _merger(key: string, target: AnyObject, source: AnyObject, options: AnyObject) { if (!isValidKey(key)) { return; } const tval = target[key]; const sval = source[key]; if (isObject(tval) && isObject(sval)) { // eslint-disable-next-line @typescript-eslint/no-use-before-define merge(tval, sval, options); } else { target[key] = clone(sval); } } export interface MergeOptions { merger?: (key: string, target: AnyObject, source: AnyObject, options?: AnyObject) => void; } /** * Recursively deep copies `source` properties into `target` with the given `options`. * IMPORTANT: `target` is not cloned and will be updated with `source` properties. * @param target - The target object in which all sources are merged into. * @param source - Object(s) to merge into `target`. * @param [options] - Merging options: * @param [options.merger] - The merge method (key, target, source, options) * @returns The `target` object. */ export function merge(target: T, source: [], options?: MergeOptions): T; export function merge(target: T, source: S1, options?: MergeOptions): T & S1; export function merge(target: T, source: [S1], options?: MergeOptions): T & S1; export function merge(target: T, source: [S1, S2], options?: MergeOptions): T & S1 & S2; export function merge(target: T, source: [S1, S2, S3], options?: MergeOptions): T & S1 & S2 & S3; export function merge( target: T, source: [S1, S2, S3, S4], options?: MergeOptions ): T & S1 & S2 & S3 & S4; export function merge(target: T, source: AnyObject[], options?: MergeOptions): AnyObject; export function merge(target: T, source: AnyObject[], options?: MergeOptions): AnyObject { const sources = isArray(source) ? source : [source]; const ilen = sources.length; if (!isObject(target)) { return target as AnyObject; } options = options || {}; const merger = options.merger || _merger; let current: AnyObject; for (let i = 0; i < ilen; ++i) { current = sources[i]; if (!isObject(current)) { continue; } const keys = Object.keys(current); for (let k = 0, klen = keys.length; k < klen; ++k) { merger(keys[k], target, current, options as AnyObject); } } return target; } /** * Recursively deep copies `source` properties into `target` *only* if not defined in target. * IMPORTANT: `target` is not cloned and will be updated with `source` properties. * @param target - The target object in which all sources are merged into. * @param source - Object(s) to merge into `target`. * @returns The `target` object. */ export function mergeIf(target: T, source: []): T; export function mergeIf(target: T, source: S1): T & S1; export function mergeIf(target: T, source: [S1]): T & S1; export function mergeIf(target: T, source: [S1, S2]): T & S1 & S2; export function mergeIf(target: T, source: [S1, S2, S3]): T & S1 & S2 & S3; export function mergeIf(target: T, source: [S1, S2, S3, S4]): T & S1 & S2 & S3 & S4; export function mergeIf(target: T, source: AnyObject[]): AnyObject; export function mergeIf(target: T, source: AnyObject[]): AnyObject { // eslint-disable-next-line @typescript-eslint/no-use-before-define return merge(target, source, {merger: _mergerIf}); } /** * Merges source[key] in target[key] only if target[key] is undefined. * @private */ export function _mergerIf(key: string, target: AnyObject, source: AnyObject) { if (!isValidKey(key)) { return; } const tval = target[key]; const sval = source[key]; if (isObject(tval) && isObject(sval)) { mergeIf(tval, sval); } else if (!Object.prototype.hasOwnProperty.call(target, key)) { target[key] = clone(sval); } } /** * @private */ export function _deprecated(scope: string, value: unknown, previous: string, current: string) { if (value !== undefined) { console.warn(scope + ': "' + previous + '" is deprecated. Please use "' + current + '" instead'); } } // resolveObjectKey resolver cache const keyResolvers = { // Chart.helpers.core resolveObjectKey should resolve empty key to root object '': v => v, // default resolvers x: o => o.x, y: o => o.y }; /** * @private */ export function _splitKey(key: string) { const parts = key.split('.'); const keys: string[] = []; let tmp = ''; for (const part of parts) { tmp += part; if (tmp.endsWith('\\')) { tmp = tmp.slice(0, -1) + '.'; } else { keys.push(tmp); tmp = ''; } } return keys; } function _getKeyResolver(key: string) { const keys = _splitKey(key); return obj => { for (const k of keys) { if (k === '') { // For backward compatibility: // Chart.helpers.core resolveObjectKey should break at empty key break; } obj = obj && obj[k]; } return obj; }; } export function resolveObjectKey(obj: AnyObject, key: string): any { const resolver = keyResolvers[key] || (keyResolvers[key] = _getKeyResolver(key)); return resolver(obj); } /** * @private */ export function _capitalize(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); } export const defined = (value: unknown) => typeof value !== 'undefined'; export const isFunction = (value: unknown): value is (...args: any[]) => any => typeof value === 'function'; // Adapted from https://stackoverflow.com/questions/31128855/comparing-ecma6-sets-for-equality#31129384 export const setsEqual = (a: Set, b: Set) => { if (a.size !== b.size) { return false; } for (const item of a) { if (!b.has(item)) { return false; } } return true; }; /** * @param e - The event * @private */ export function _isClickEvent(e: ChartEvent) { return e.type === 'mouseup' || e.type === 'click' || e.type === 'contextmenu'; } ================================================ FILE: src/helpers/helpers.curve.ts ================================================ import {almostEquals, distanceBetweenPoints, sign} from './helpers.math.js'; import {_isPointInArea} from './helpers.canvas.js'; import type {ChartArea} from '../types/index.js'; import type {SplinePoint} from '../types/geometric.js'; const EPSILON = Number.EPSILON || 1e-14; type OptionalSplinePoint = SplinePoint | false const getPoint = (points: SplinePoint[], i: number): OptionalSplinePoint => i < points.length && !points[i].skip && points[i]; const getValueAxis = (indexAxis: 'x' | 'y') => indexAxis === 'x' ? 'y' : 'x'; export function splineCurve( firstPoint: SplinePoint, middlePoint: SplinePoint, afterPoint: SplinePoint, t: number ): { previous: SplinePoint next: SplinePoint } { // Props to Rob Spencer at scaled innovation for his post on splining between points // http://scaledinnovation.com/analytics/splines/aboutSplines.html // This function must also respect "skipped" points const previous = firstPoint.skip ? middlePoint : firstPoint; const current = middlePoint; const next = afterPoint.skip ? middlePoint : afterPoint; const d01 = distanceBetweenPoints(current, previous); const d12 = distanceBetweenPoints(next, current); let s01 = d01 / (d01 + d12); let s12 = d12 / (d01 + d12); // If all points are the same, s01 & s02 will be inf s01 = isNaN(s01) ? 0 : s01; s12 = isNaN(s12) ? 0 : s12; const fa = t * s01; // scaling factor for triangle Ta const fb = t * s12; return { previous: { x: current.x - fa * (next.x - previous.x), y: current.y - fa * (next.y - previous.y) }, next: { x: current.x + fb * (next.x - previous.x), y: current.y + fb * (next.y - previous.y) } }; } /** * Adjust tangents to ensure monotonic properties */ function monotoneAdjust(points: SplinePoint[], deltaK: number[], mK: number[]) { const pointsLen = points.length; let alphaK: number, betaK: number, tauK: number, squaredMagnitude: number, pointCurrent: OptionalSplinePoint; let pointAfter = getPoint(points, 0); for (let i = 0; i < pointsLen - 1; ++i) { pointCurrent = pointAfter; pointAfter = getPoint(points, i + 1); if (!pointCurrent || !pointAfter) { continue; } if (almostEquals(deltaK[i], 0, EPSILON)) { mK[i] = mK[i + 1] = 0; continue; } alphaK = mK[i] / deltaK[i]; betaK = mK[i + 1] / deltaK[i]; squaredMagnitude = Math.pow(alphaK, 2) + Math.pow(betaK, 2); if (squaredMagnitude <= 9) { continue; } tauK = 3 / Math.sqrt(squaredMagnitude); mK[i] = alphaK * tauK * deltaK[i]; mK[i + 1] = betaK * tauK * deltaK[i]; } } function monotoneCompute(points: SplinePoint[], mK: number[], indexAxis: 'x' | 'y' = 'x') { const valueAxis = getValueAxis(indexAxis); const pointsLen = points.length; let delta: number, pointBefore: OptionalSplinePoint, pointCurrent: OptionalSplinePoint; let pointAfter = getPoint(points, 0); for (let i = 0; i < pointsLen; ++i) { pointBefore = pointCurrent; pointCurrent = pointAfter; pointAfter = getPoint(points, i + 1); if (!pointCurrent) { continue; } const iPixel = pointCurrent[indexAxis]; const vPixel = pointCurrent[valueAxis]; if (pointBefore) { delta = (iPixel - pointBefore[indexAxis]) / 3; pointCurrent[`cp1${indexAxis}`] = iPixel - delta; pointCurrent[`cp1${valueAxis}`] = vPixel - delta * mK[i]; } if (pointAfter) { delta = (pointAfter[indexAxis] - iPixel) / 3; pointCurrent[`cp2${indexAxis}`] = iPixel + delta; pointCurrent[`cp2${valueAxis}`] = vPixel + delta * mK[i]; } } } /** * This function calculates Bézier control points in a similar way than |splineCurve|, * but preserves monotonicity of the provided data and ensures no local extremums are added * between the dataset discrete points due to the interpolation. * See : https://en.wikipedia.org/wiki/Monotone_cubic_interpolation */ export function splineCurveMonotone(points: SplinePoint[], indexAxis: 'x' | 'y' = 'x') { const valueAxis = getValueAxis(indexAxis); const pointsLen = points.length; const deltaK: number[] = Array(pointsLen).fill(0); const mK: number[] = Array(pointsLen); // Calculate slopes (deltaK) and initialize tangents (mK) let i, pointBefore: OptionalSplinePoint, pointCurrent: OptionalSplinePoint; let pointAfter = getPoint(points, 0); for (i = 0; i < pointsLen; ++i) { pointBefore = pointCurrent; pointCurrent = pointAfter; pointAfter = getPoint(points, i + 1); if (!pointCurrent) { continue; } if (pointAfter) { const slopeDelta = pointAfter[indexAxis] - pointCurrent[indexAxis]; // In the case of two points that appear at the same x pixel, slopeDeltaX is 0 deltaK[i] = slopeDelta !== 0 ? (pointAfter[valueAxis] - pointCurrent[valueAxis]) / slopeDelta : 0; } mK[i] = !pointBefore ? deltaK[i] : !pointAfter ? deltaK[i - 1] : (sign(deltaK[i - 1]) !== sign(deltaK[i])) ? 0 : (deltaK[i - 1] + deltaK[i]) / 2; } monotoneAdjust(points, deltaK, mK); monotoneCompute(points, mK, indexAxis); } function capControlPoint(pt: number, min: number, max: number) { return Math.max(Math.min(pt, max), min); } function capBezierPoints(points: SplinePoint[], area: ChartArea) { let i, ilen, point, inArea, inAreaPrev; let inAreaNext = _isPointInArea(points[0], area); for (i = 0, ilen = points.length; i < ilen; ++i) { inAreaPrev = inArea; inArea = inAreaNext; inAreaNext = i < ilen - 1 && _isPointInArea(points[i + 1], area); if (!inArea) { continue; } point = points[i]; if (inAreaPrev) { point.cp1x = capControlPoint(point.cp1x, area.left, area.right); point.cp1y = capControlPoint(point.cp1y, area.top, area.bottom); } if (inAreaNext) { point.cp2x = capControlPoint(point.cp2x, area.left, area.right); point.cp2y = capControlPoint(point.cp2y, area.top, area.bottom); } } } /** * @private */ export function _updateBezierControlPoints( points: SplinePoint[], options, area: ChartArea, loop: boolean, indexAxis: 'x' | 'y' ) { let i: number, ilen: number, point: SplinePoint, controlPoints: ReturnType; // Only consider points that are drawn in case the spanGaps option is used if (options.spanGaps) { points = points.filter((pt) => !pt.skip); } if (options.cubicInterpolationMode === 'monotone') { splineCurveMonotone(points, indexAxis); } else { let prev = loop ? points[points.length - 1] : points[0]; for (i = 0, ilen = points.length; i < ilen; ++i) { point = points[i]; controlPoints = splineCurve( prev, point, points[Math.min(i + 1, ilen - (loop ? 0 : 1)) % ilen], options.tension ); point.cp1x = controlPoints.previous.x; point.cp1y = controlPoints.previous.y; point.cp2x = controlPoints.next.x; point.cp2y = controlPoints.next.y; prev = point; } } if (options.capBezierPoints) { capBezierPoints(points, area); } } ================================================ FILE: src/helpers/helpers.dataset.ts ================================================ import type {Chart, ChartArea, ChartMeta, Scale, TRBL} from '../types/index.js'; function getSizeForArea(scale: Scale, chartArea: ChartArea, field: keyof ChartArea) { return scale.options.clip ? scale[field] : chartArea[field]; } function getDatasetArea(meta: ChartMeta, chartArea: ChartArea): TRBL { const {xScale, yScale} = meta; if (xScale && yScale) { return { left: getSizeForArea(xScale, chartArea, 'left'), right: getSizeForArea(xScale, chartArea, 'right'), top: getSizeForArea(yScale, chartArea, 'top'), bottom: getSizeForArea(yScale, chartArea, 'bottom') }; } return chartArea; } export function getDatasetClipArea(chart: Chart, meta: ChartMeta): TRBL | false { const clip = meta._clip; if (clip.disabled) { return false; } const area = getDatasetArea(meta, chart.chartArea); return { left: clip.left === false ? 0 : area.left - (clip.left === true ? 0 : clip.left), right: clip.right === false ? chart.width : area.right + (clip.right === true ? 0 : clip.right), top: clip.top === false ? 0 : area.top - (clip.top === true ? 0 : clip.top), bottom: clip.bottom === false ? chart.height : area.bottom + (clip.bottom === true ? 0 : clip.bottom) }; } ================================================ FILE: src/helpers/helpers.dom.ts ================================================ import type {ChartArea, Scale} from '../types/index.js'; import type PrivateChart from '../core/core.controller.js'; import type {Chart, ChartEvent} from '../types.js'; import {INFINITY} from './helpers.math.js'; /** * @private */ export function _isDomSupported(): boolean { return typeof window !== 'undefined' && typeof document !== 'undefined'; } /** * @private */ export function _getParentNode(domNode: HTMLCanvasElement): HTMLCanvasElement { let parent = domNode.parentNode; if (parent && parent.toString() === '[object ShadowRoot]') { parent = (parent as ShadowRoot).host; } return parent as HTMLCanvasElement; } /** * convert max-width/max-height values that may be percentages into a number * @private */ function parseMaxStyle(styleValue: string | number, node: HTMLElement, parentProperty: string) { let valueInPixels: number; if (typeof styleValue === 'string') { valueInPixels = parseInt(styleValue, 10); if (styleValue.indexOf('%') !== -1) { // percentage * size in dimension valueInPixels = (valueInPixels / 100) * node.parentNode[parentProperty]; } } else { valueInPixels = styleValue; } return valueInPixels; } const getComputedStyle = (element: HTMLElement): CSSStyleDeclaration => element.ownerDocument.defaultView.getComputedStyle(element, null); export function getStyle(el: HTMLElement, property: string): string { return getComputedStyle(el).getPropertyValue(property); } const positions = ['top', 'right', 'bottom', 'left']; function getPositionedStyle(styles: CSSStyleDeclaration, style: string, suffix?: string): ChartArea { const result = {} as ChartArea; suffix = suffix ? '-' + suffix : ''; for (let i = 0; i < 4; i++) { const pos = positions[i]; result[pos] = parseFloat(styles[style + '-' + pos + suffix]) || 0; } result.width = result.left + result.right; result.height = result.top + result.bottom; return result; } const useOffsetPos = (x: number, y: number, target: HTMLElement | EventTarget) => (x > 0 || y > 0) && (!target || !(target as HTMLElement).shadowRoot); /** * @param e * @param canvas * @returns Canvas position */ function getCanvasPosition( e: Event | TouchEvent | MouseEvent, canvas: HTMLCanvasElement ): { x: number; y: number; box: boolean; } { const touches = (e as TouchEvent).touches; const source = (touches && touches.length ? touches[0] : e) as MouseEvent; const {offsetX, offsetY} = source as MouseEvent; let box = false; let x, y; if (useOffsetPos(offsetX, offsetY, e.target)) { x = offsetX; y = offsetY; } else { const rect = canvas.getBoundingClientRect(); x = source.clientX - rect.left; y = source.clientY - rect.top; box = true; } return {x, y, box}; } /** * Gets an event's x, y coordinates, relative to the chart area * @param event * @param chart * @returns x and y coordinates of the event */ export function getRelativePosition( event: Event | ChartEvent | TouchEvent | MouseEvent, chart: Chart | PrivateChart ): { x: number; y: number } { if ('native' in event) { return event; } const {canvas, currentDevicePixelRatio} = chart; const style = getComputedStyle(canvas); const borderBox = style.boxSizing === 'border-box'; const paddings = getPositionedStyle(style, 'padding'); const borders = getPositionedStyle(style, 'border', 'width'); const {x, y, box} = getCanvasPosition(event, canvas); const xOffset = paddings.left + (box && borders.left); const yOffset = paddings.top + (box && borders.top); let {width, height} = chart; if (borderBox) { width -= paddings.width + borders.width; height -= paddings.height + borders.height; } return { x: Math.round((x - xOffset) / width * canvas.width / currentDevicePixelRatio), y: Math.round((y - yOffset) / height * canvas.height / currentDevicePixelRatio) }; } function getContainerSize(canvas: HTMLCanvasElement, width: number, height: number): Partial { let maxWidth: number, maxHeight: number; if (width === undefined || height === undefined) { const container = canvas && _getParentNode(canvas); if (!container) { width = canvas.clientWidth; height = canvas.clientHeight; } else { const rect = container.getBoundingClientRect(); // this is the border box of the container const containerStyle = getComputedStyle(container); const containerBorder = getPositionedStyle(containerStyle, 'border', 'width'); const containerPadding = getPositionedStyle(containerStyle, 'padding'); width = rect.width - containerPadding.width - containerBorder.width; height = rect.height - containerPadding.height - containerBorder.height; maxWidth = parseMaxStyle(containerStyle.maxWidth, container, 'clientWidth'); maxHeight = parseMaxStyle(containerStyle.maxHeight, container, 'clientHeight'); } } return { width, height, maxWidth: maxWidth || INFINITY, maxHeight: maxHeight || INFINITY }; } const round1 = (v: number) => Math.round(v * 10) / 10; // eslint-disable-next-line complexity export function getMaximumSize( canvas: HTMLCanvasElement, bbWidth?: number, bbHeight?: number, aspectRatio?: number ): { width: number; height: number } { const style = getComputedStyle(canvas); const margins = getPositionedStyle(style, 'margin'); const maxWidth = parseMaxStyle(style.maxWidth, canvas, 'clientWidth') || INFINITY; const maxHeight = parseMaxStyle(style.maxHeight, canvas, 'clientHeight') || INFINITY; const containerSize = getContainerSize(canvas, bbWidth, bbHeight); let {width, height} = containerSize; if (style.boxSizing === 'content-box') { const borders = getPositionedStyle(style, 'border', 'width'); const paddings = getPositionedStyle(style, 'padding'); width -= paddings.width + borders.width; height -= paddings.height + borders.height; } width = Math.max(0, width - margins.width); height = Math.max(0, aspectRatio ? width / aspectRatio : height - margins.height); width = round1(Math.min(width, maxWidth, containerSize.maxWidth)); height = round1(Math.min(height, maxHeight, containerSize.maxHeight)); if (width && !height) { // https://github.com/chartjs/Chart.js/issues/4659 // If the canvas has width, but no height, default to aspectRatio of 2 (canvas default) height = round1(width / 2); } const maintainHeight = bbWidth !== undefined || bbHeight !== undefined; if (maintainHeight && aspectRatio && containerSize.height && height > containerSize.height) { height = containerSize.height; width = round1(Math.floor(height * aspectRatio)); } return {width, height}; } /** * @param chart * @param forceRatio * @param forceStyle * @returns True if the canvas context size or transformation has changed. */ export function retinaScale( chart: Chart | PrivateChart, forceRatio: number, forceStyle?: boolean ): boolean | void { const pixelRatio = forceRatio || 1; const deviceHeight = round1(chart.height * pixelRatio); const deviceWidth = round1(chart.width * pixelRatio); (chart as PrivateChart).height = round1(chart.height); (chart as PrivateChart).width = round1(chart.width); const canvas = chart.canvas; // If no style has been set on the canvas, the render size is used as display size, // making the chart visually bigger, so let's enforce it to the "correct" values. // See https://github.com/chartjs/Chart.js/issues/3575 if (canvas.style && (forceStyle || (!canvas.style.height && !canvas.style.width))) { canvas.style.height = `${chart.height}px`; canvas.style.width = `${chart.width}px`; } const canvasHeight = Math.floor(deviceHeight); const canvasWidth = Math.floor(deviceWidth); if (chart.currentDevicePixelRatio !== pixelRatio || canvas.height !== canvasHeight || canvas.width !== canvasWidth) { (chart as PrivateChart).currentDevicePixelRatio = pixelRatio; canvas.height = canvasHeight; canvas.width = canvasWidth; chart.ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); return true; } return false; } /** * Detects support for options object argument in addEventListener. * https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Safely_detecting_option_support * @private */ export const supportsEventListenerOptions = (function() { let passiveSupported = false; try { const options = { get passive() { // This function will be called when the browser attempts to access the passive property. passiveSupported = true; return false; } } as EventListenerOptions; if (_isDomSupported()) { window.addEventListener('test', null, options); window.removeEventListener('test', null, options); } } catch (e) { // continue regardless of error } return passiveSupported; }()); /** * The "used" size is the final value of a dimension property after all calculations have * been performed. This method uses the computed style of `element` but returns undefined * if the computed style is not expressed in pixels. That can happen in some cases where * `element` has a size relative to its parent and this last one is not yet displayed, * for example because of `display: none` on a parent node. * @see https://developer.mozilla.org/en-US/docs/Web/CSS/used_value * @returns Size in pixels or undefined if unknown. */ export function readUsedSize( element: HTMLElement, property: 'width' | 'height' ): number | undefined { const value = getStyle(element, property); const matches = value && value.match(/^(\d+)(\.\d+)?px$/); return matches ? +matches[1] : undefined; } ================================================ FILE: src/helpers/helpers.easing.ts ================================================ import {PI, TAU, HALF_PI} from './helpers.math.js'; const atEdge = (t: number) => t === 0 || t === 1; const elasticIn = (t: number, s: number, p: number) => -(Math.pow(2, 10 * (t -= 1)) * Math.sin((t - s) * TAU / p)); const elasticOut = (t: number, s: number, p: number) => Math.pow(2, -10 * t) * Math.sin((t - s) * TAU / p) + 1; /** * Easing functions adapted from Robert Penner's easing equations. * @namespace Chart.helpers.easing.effects * @see http://www.robertpenner.com/easing/ */ const effects = { linear: (t: number) => t, easeInQuad: (t: number) => t * t, easeOutQuad: (t: number) => -t * (t - 2), easeInOutQuad: (t: number) => ((t /= 0.5) < 1) ? 0.5 * t * t : -0.5 * ((--t) * (t - 2) - 1), easeInCubic: (t: number) => t * t * t, easeOutCubic: (t: number) => (t -= 1) * t * t + 1, easeInOutCubic: (t: number) => ((t /= 0.5) < 1) ? 0.5 * t * t * t : 0.5 * ((t -= 2) * t * t + 2), easeInQuart: (t: number) => t * t * t * t, easeOutQuart: (t: number) => -((t -= 1) * t * t * t - 1), easeInOutQuart: (t: number) => ((t /= 0.5) < 1) ? 0.5 * t * t * t * t : -0.5 * ((t -= 2) * t * t * t - 2), easeInQuint: (t: number) => t * t * t * t * t, easeOutQuint: (t: number) => (t -= 1) * t * t * t * t + 1, easeInOutQuint: (t: number) => ((t /= 0.5) < 1) ? 0.5 * t * t * t * t * t : 0.5 * ((t -= 2) * t * t * t * t + 2), easeInSine: (t: number) => -Math.cos(t * HALF_PI) + 1, easeOutSine: (t: number) => Math.sin(t * HALF_PI), easeInOutSine: (t: number) => -0.5 * (Math.cos(PI * t) - 1), easeInExpo: (t: number) => (t === 0) ? 0 : Math.pow(2, 10 * (t - 1)), easeOutExpo: (t: number) => (t === 1) ? 1 : -Math.pow(2, -10 * t) + 1, easeInOutExpo: (t: number) => atEdge(t) ? t : t < 0.5 ? 0.5 * Math.pow(2, 10 * (t * 2 - 1)) : 0.5 * (-Math.pow(2, -10 * (t * 2 - 1)) + 2), easeInCirc: (t: number) => (t >= 1) ? t : -(Math.sqrt(1 - t * t) - 1), easeOutCirc: (t: number) => Math.sqrt(1 - (t -= 1) * t), easeInOutCirc: (t: number) => ((t /= 0.5) < 1) ? -0.5 * (Math.sqrt(1 - t * t) - 1) : 0.5 * (Math.sqrt(1 - (t -= 2) * t) + 1), easeInElastic: (t: number) => atEdge(t) ? t : elasticIn(t, 0.075, 0.3), easeOutElastic: (t: number) => atEdge(t) ? t : elasticOut(t, 0.075, 0.3), easeInOutElastic(t: number) { const s = 0.1125; const p = 0.45; return atEdge(t) ? t : t < 0.5 ? 0.5 * elasticIn(t * 2, s, p) : 0.5 + 0.5 * elasticOut(t * 2 - 1, s, p); }, easeInBack(t: number) { const s = 1.70158; return t * t * ((s + 1) * t - s); }, easeOutBack(t: number) { const s = 1.70158; return (t -= 1) * t * ((s + 1) * t + s) + 1; }, easeInOutBack(t: number) { let s = 1.70158; if ((t /= 0.5) < 1) { return 0.5 * (t * t * (((s *= (1.525)) + 1) * t - s)); } return 0.5 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2); }, easeInBounce: (t: number) => 1 - effects.easeOutBounce(1 - t), easeOutBounce(t: number) { const m = 7.5625; const d = 2.75; if (t < (1 / d)) { return m * t * t; } if (t < (2 / d)) { return m * (t -= (1.5 / d)) * t + 0.75; } if (t < (2.5 / d)) { return m * (t -= (2.25 / d)) * t + 0.9375; } return m * (t -= (2.625 / d)) * t + 0.984375; }, easeInOutBounce: (t: number) => (t < 0.5) ? effects.easeInBounce(t * 2) * 0.5 : effects.easeOutBounce(t * 2 - 1) * 0.5 + 0.5, } as const; export type EasingFunction = keyof typeof effects export default effects; ================================================ FILE: src/helpers/helpers.extras.ts ================================================ import type {ChartMeta, PointElement} from '../types/index.js'; import {_limitValue} from './helpers.math.js'; import {_lookupByKey} from './helpers.collection.js'; import {isNullOrUndef} from './helpers.core.js'; export function fontString(pixelSize: number, fontStyle: string, fontFamily: string) { return fontStyle + ' ' + pixelSize + 'px ' + fontFamily; } /** * Request animation polyfill */ export const requestAnimFrame = (function() { if (typeof window === 'undefined') { return function(callback) { return callback(); }; } return window.requestAnimationFrame; }()); /** * Throttles calling `fn` once per animation frame * Latest arguments are used on the actual call */ export function throttled>( fn: (...args: TArgs) => void, thisArg: any, ) { let argsToUse = [] as TArgs; let ticking = false; return function(...args: TArgs) { // Save the args for use later argsToUse = args; if (!ticking) { ticking = true; requestAnimFrame.call(window, () => { ticking = false; fn.apply(thisArg, argsToUse); }); } }; } /** * Debounces calling `fn` for `delay` ms */ export function debounce>(fn: (...args: TArgs) => void, delay: number) { let timeout; return function(...args: TArgs) { if (delay) { clearTimeout(timeout); timeout = setTimeout(fn, delay, args); } else { fn.apply(this, args); } return delay; }; } /** * Converts 'start' to 'left', 'end' to 'right' and others to 'center' * @private */ export const _toLeftRightCenter = (align: 'start' | 'end' | 'center') => align === 'start' ? 'left' : align === 'end' ? 'right' : 'center'; /** * Returns `start`, `end` or `(start + end) / 2` depending on `align`. Defaults to `center` * @private */ export const _alignStartEnd = (align: 'start' | 'end' | 'center', start: number, end: number) => align === 'start' ? start : align === 'end' ? end : (start + end) / 2; /** * Returns `left`, `right` or `(left + right) / 2` depending on `align`. Defaults to `left` * @private */ export const _textX = (align: 'left' | 'right' | 'center', left: number, right: number, rtl: boolean) => { const check = rtl ? 'left' : 'right'; return align === check ? right : align === 'center' ? (left + right) / 2 : left; }; /** * Return start and count of visible points. * @private */ export function _getStartAndCountOfVisiblePoints(meta: ChartMeta<'line' | 'scatter'>, points: PointElement[], animationsDisabled: boolean) { const pointCount = points.length; let start = 0; let count = pointCount; if (meta._sorted) { const {iScale, vScale, _parsed} = meta; const spanGaps = meta.dataset ? meta.dataset.options ? meta.dataset.options.spanGaps : null : null; const axis = iScale.axis; const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); if (minDefined) { start = Math.min( // @ts-expect-error Need to type _parsed _lookupByKey(_parsed, axis, min).lo, // @ts-expect-error Need to fix types on _lookupByKey animationsDisabled ? pointCount : _lookupByKey(points, axis, iScale.getPixelForValue(min)).lo); if (spanGaps) { const distanceToDefinedLo = (_parsed .slice(0, start + 1) .reverse() .findIndex( point => !isNullOrUndef(point[vScale.axis]))); start -= Math.max(0, distanceToDefinedLo); } start = _limitValue(start, 0, pointCount - 1); } if (maxDefined) { let end = Math.max( // @ts-expect-error Need to type _parsed _lookupByKey(_parsed, iScale.axis, max, true).hi + 1, // @ts-expect-error Need to fix types on _lookupByKey animationsDisabled ? 0 : _lookupByKey(points, axis, iScale.getPixelForValue(max), true).hi + 1); if (spanGaps) { const distanceToDefinedHi = (_parsed .slice(end - 1) .findIndex( point => !isNullOrUndef(point[vScale.axis]))); end += Math.max(0, distanceToDefinedHi); } count = _limitValue(end, start, pointCount) - start; } else { count = pointCount - start; } } return {start, count}; } /** * Checks if the scale ranges have changed. * @param {object} meta - dataset meta. * @returns {boolean} * @private */ export function _scaleRangesChanged(meta) { const {xScale, yScale, _scaleRanges} = meta; const newRanges = { xmin: xScale.min, xmax: xScale.max, ymin: yScale.min, ymax: yScale.max }; if (!_scaleRanges) { meta._scaleRanges = newRanges; return true; } const changed = _scaleRanges.xmin !== xScale.min || _scaleRanges.xmax !== xScale.max || _scaleRanges.ymin !== yScale.min || _scaleRanges.ymax !== yScale.max; Object.assign(_scaleRanges, newRanges); return changed; } ================================================ FILE: src/helpers/helpers.interpolation.ts ================================================ import type {Point, SplinePoint} from '../types/geometric.js'; /** * @private */ export function _pointInLine(p1: Point, p2: Point, t: number, mode?) { // eslint-disable-line @typescript-eslint/no-unused-vars return { x: p1.x + t * (p2.x - p1.x), y: p1.y + t * (p2.y - p1.y) }; } /** * @private */ export function _steppedInterpolation( p1: Point, p2: Point, t: number, mode: 'middle' | 'after' | unknown ) { return { x: p1.x + t * (p2.x - p1.x), y: mode === 'middle' ? t < 0.5 ? p1.y : p2.y : mode === 'after' ? t < 1 ? p1.y : p2.y : t > 0 ? p2.y : p1.y }; } /** * @private */ export function _bezierInterpolation(p1: SplinePoint, p2: SplinePoint, t: number, mode?) { // eslint-disable-line @typescript-eslint/no-unused-vars const cp1 = {x: p1.cp2x, y: p1.cp2y}; const cp2 = {x: p2.cp1x, y: p2.cp1y}; const a = _pointInLine(p1, cp1, t); const b = _pointInLine(cp1, cp2, t); const c = _pointInLine(cp2, p2, t); const d = _pointInLine(a, b, t); const e = _pointInLine(b, c, t); return _pointInLine(d, e, t); } ================================================ FILE: src/helpers/helpers.intl.ts ================================================ const intlCache = new Map(); function getNumberFormat(locale: string, options?: Intl.NumberFormatOptions) { options = options || {}; const cacheKey = locale + JSON.stringify(options); let formatter = intlCache.get(cacheKey); if (!formatter) { formatter = new Intl.NumberFormat(locale, options); intlCache.set(cacheKey, formatter); } return formatter; } export function formatNumber(num: number, locale: string, options?: Intl.NumberFormatOptions) { return getNumberFormat(locale, options).format(num); } ================================================ FILE: src/helpers/helpers.math.ts ================================================ import type {Point} from '../types/geometric.js'; import {isFinite as isFiniteNumber} from './helpers.core.js'; /** * @alias Chart.helpers.math * @namespace */ export const PI = Math.PI; export const TAU = 2 * PI; export const PITAU = TAU + PI; export const INFINITY = Number.POSITIVE_INFINITY; export const RAD_PER_DEG = PI / 180; export const HALF_PI = PI / 2; export const QUARTER_PI = PI / 4; export const TWO_THIRDS_PI = PI * 2 / 3; export const log10 = Math.log10; export const sign = Math.sign; export function almostEquals(x: number, y: number, epsilon: number) { return Math.abs(x - y) < epsilon; } /** * Implementation of the nice number algorithm used in determining where axis labels will go */ export function niceNum(range: number) { const roundedRange = Math.round(range); range = almostEquals(range, roundedRange, range / 1000) ? roundedRange : range; const niceRange = Math.pow(10, Math.floor(log10(range))); const fraction = range / niceRange; const niceFraction = fraction <= 1 ? 1 : fraction <= 2 ? 2 : fraction <= 5 ? 5 : 10; return niceFraction * niceRange; } /** * Returns an array of factors sorted from 1 to sqrt(value) * @private */ export function _factorize(value: number) { const result: number[] = []; const sqrt = Math.sqrt(value); let i: number; for (i = 1; i < sqrt; i++) { if (value % i === 0) { result.push(i); result.push(value / i); } } if (sqrt === (sqrt | 0)) { // if value is a square number result.push(sqrt); } result.sort((a, b) => a - b).pop(); return result; } /** * Verifies that attempting to coerce n to string or number won't throw a TypeError. */ function isNonPrimitive(n: unknown) { return typeof n === 'symbol' || (typeof n === 'object' && n !== null && !(Symbol.toPrimitive in n || 'toString' in n || 'valueOf' in n)); } export function isNumber(n: unknown): n is number { return !isNonPrimitive(n) && !isNaN(parseFloat(n as string)) && isFinite(n as number); } export function almostWhole(x: number, epsilon: number) { const rounded = Math.round(x); return ((rounded - epsilon) <= x) && ((rounded + epsilon) >= x); } /** * @private */ export function _setMinAndMaxByKey( array: Record[], target: { min: number, max: number }, property: string ) { let i: number, ilen: number, value: number; for (i = 0, ilen = array.length; i < ilen; i++) { value = array[i][property]; if (!isNaN(value)) { target.min = Math.min(target.min, value); target.max = Math.max(target.max, value); } } } export function toRadians(degrees: number) { return degrees * (PI / 180); } export function toDegrees(radians: number) { return radians * (180 / PI); } /** * Returns the number of decimal places * i.e. the number of digits after the decimal point, of the value of this Number. * @param x - A number. * @returns The number of decimal places. * @private */ export function _decimalPlaces(x: number) { if (!isFiniteNumber(x)) { return; } let e = 1; let p = 0; while (Math.round(x * e) / e !== x) { e *= 10; p++; } return p; } // Gets the angle from vertical upright to the point about a centre. export function getAngleFromPoint( centrePoint: Point, anglePoint: Point ) { const distanceFromXCenter = anglePoint.x - centrePoint.x; const distanceFromYCenter = anglePoint.y - centrePoint.y; const radialDistanceFromCenter = Math.sqrt(distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter); let angle = Math.atan2(distanceFromYCenter, distanceFromXCenter); if (angle < (-0.5 * PI)) { angle += TAU; // make sure the returned angle is in the range of (-PI/2, 3PI/2] } return { angle, distance: radialDistanceFromCenter }; } export function distanceBetweenPoints(pt1: Point, pt2: Point) { return Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2)); } /** * Shortest distance between angles, in either direction. * @private */ export function _angleDiff(a: number, b: number) { return (a - b + PITAU) % TAU - PI; } /** * Normalize angle to be between 0 and 2*PI * @private */ export function _normalizeAngle(a: number) { return (a % TAU + TAU) % TAU; } /** * @private */ export function _angleBetween(angle: number, start: number, end: number, sameAngleIsFullCircle?: boolean) { const a = _normalizeAngle(angle); const s = _normalizeAngle(start); const e = _normalizeAngle(end); const angleToStart = _normalizeAngle(s - a); const angleToEnd = _normalizeAngle(e - a); const startToAngle = _normalizeAngle(a - s); const endToAngle = _normalizeAngle(a - e); return a === s || a === e || (sameAngleIsFullCircle && s === e) || (angleToStart > angleToEnd && startToAngle < endToAngle); } /** * Limit `value` between `min` and `max` * @param value * @param min * @param max * @private */ export function _limitValue(value: number, min: number, max: number) { return Math.max(min, Math.min(max, value)); } /** * @param {number} value * @private */ export function _int16Range(value: number) { return _limitValue(value, -32768, 32767); } /** * @param value * @param start * @param end * @param [epsilon] * @private */ export function _isBetween(value: number, start: number, end: number, epsilon = 1e-6) { return value >= Math.min(start, end) - epsilon && value <= Math.max(start, end) + epsilon; } ================================================ FILE: src/helpers/helpers.options.ts ================================================ import defaults from '../core/core.defaults.js'; import {isArray, isObject, toDimension, valueOrDefault} from './helpers.core.js'; import {toFontString} from './helpers.canvas.js'; import type {ChartArea, FontSpec, Point} from '../types/index.js'; import type {TRBL, TRBLCorners} from '../types/geometric.js'; const LINE_HEIGHT = /^(normal|(\d+(?:\.\d+)?)(px|em|%)?)$/; const FONT_STYLE = /^(normal|italic|initial|inherit|unset|(oblique( -?[0-9]?[0-9]deg)?))$/; /** * @alias Chart.helpers.options * @namespace */ /** * Converts the given line height `value` in pixels for a specific font `size`. * @param value - The lineHeight to parse (eg. 1.6, '14px', '75%', '1.6em'). * @param size - The font size (in pixels) used to resolve relative `value`. * @returns The effective line height in pixels (size * 1.2 if value is invalid). * @see https://developer.mozilla.org/en-US/docs/Web/CSS/line-height * @since 2.7.0 */ export function toLineHeight(value: number | string, size: number): number { const matches = ('' + value).match(LINE_HEIGHT); if (!matches || matches[1] === 'normal') { return size * 1.2; } value = +matches[2]; switch (matches[3]) { case 'px': return value; case '%': value /= 100; break; default: break; } return size * value; } const numberOrZero = (v: unknown) => +v || 0; /** * @param value * @param props */ export function _readValueToProps(value: number | Record, props: K[]): Record; export function _readValueToProps(value: number | Record, props: Record): Record; export function _readValueToProps(value: number | Record, props: string[] | Record) { const ret = {}; const objProps = isObject(props); const keys = objProps ? Object.keys(props) : props; const read = isObject(value) ? objProps ? prop => valueOrDefault(value[prop], value[props[prop]]) : prop => value[prop] : () => value; for (const prop of keys) { ret[prop] = numberOrZero(read(prop)); } return ret; } /** * Converts the given value into a TRBL object. * @param value - If a number, set the value to all TRBL component, * else, if an object, use defined properties and sets undefined ones to 0. * x / y are shorthands for same value for left/right and top/bottom. * @returns The padding values (top, right, bottom, left) * @since 3.0.0 */ export function toTRBL(value: number | TRBL | Point) { return _readValueToProps(value, {top: 'y', right: 'x', bottom: 'y', left: 'x'}); } /** * Converts the given value into a TRBL corners object (similar with css border-radius). * @param value - If a number, set the value to all TRBL corner components, * else, if an object, use defined properties and sets undefined ones to 0. * @returns The TRBL corner values (topLeft, topRight, bottomLeft, bottomRight) * @since 3.0.0 */ export function toTRBLCorners(value: number | TRBLCorners) { return _readValueToProps(value, ['topLeft', 'topRight', 'bottomLeft', 'bottomRight']); } /** * Converts the given value into a padding object with pre-computed width/height. * @param value - If a number, set the value to all TRBL component, * else, if an object, use defined properties and sets undefined ones to 0. * x / y are shorthands for same value for left/right and top/bottom. * @returns The padding values (top, right, bottom, left, width, height) * @since 2.7.0 */ export function toPadding(value?: number | TRBL): ChartArea { const obj = toTRBL(value) as ChartArea; obj.width = obj.left + obj.right; obj.height = obj.top + obj.bottom; return obj; } /** * Parses font options and returns the font object. * @param options - A object that contains font options to be parsed. * @param fallback - A object that contains fallback font options. * @return The font object. * @private */ export function toFont(options: Partial, fallback?: Partial) { options = options || {}; fallback = fallback || defaults.font as FontSpec; let size = valueOrDefault(options.size, fallback.size); if (typeof size === 'string') { size = parseInt(size, 10); } let style = valueOrDefault(options.style, fallback.style); if (style && !('' + style).match(FONT_STYLE)) { console.warn('Invalid font style specified: "' + style + '"'); style = undefined; } const font = { family: valueOrDefault(options.family, fallback.family), lineHeight: toLineHeight(valueOrDefault(options.lineHeight, fallback.lineHeight), size), size, style, weight: valueOrDefault(options.weight, fallback.weight), string: '' }; font.string = toFontString(font); return font; } /** * Evaluates the given `inputs` sequentially and returns the first defined value. * @param inputs - An array of values, falling back to the last value. * @param context - If defined and the current value is a function, the value * is called with `context` as first argument and the result becomes the new input. * @param index - If defined and the current value is an array, the value * at `index` become the new input. * @param info - object to return information about resolution in * @param info.cacheable - Will be set to `false` if option is not cacheable. * @since 2.7.0 */ export function resolve(inputs: Array, context?: object, index?: number, info?: { cacheable: boolean }) { let cacheable = true; let i: number, ilen: number, value: unknown; for (i = 0, ilen = inputs.length; i < ilen; ++i) { value = inputs[i]; if (value === undefined) { continue; } if (context !== undefined && typeof value === 'function') { value = value(context); cacheable = false; } if (index !== undefined && isArray(value)) { value = value[index % value.length]; cacheable = false; } if (value !== undefined) { if (info && !cacheable) { info.cacheable = false; } return value; } } } /** * @param minmax * @param grace * @param beginAtZero * @private */ export function _addGrace(minmax: { min: number; max: number; }, grace: number | string, beginAtZero: boolean) { const {min, max} = minmax; const change = toDimension(grace, (max - min) / 2); const keepZero = (value: number, add: number) => beginAtZero && value === 0 ? 0 : value + add; return { min: keepZero(min, -Math.abs(change)), max: keepZero(max, change) }; } /** * Create a context inheriting parentContext * @param parentContext * @param context * @returns */ export function createContext(parentContext: null, context: T): T; export function createContext(parentContext: P, context: T): P & T; export function createContext(parentContext: object, context: object) { return Object.assign(Object.create(parentContext), context); } ================================================ FILE: src/helpers/helpers.rtl.ts ================================================ export interface RTLAdapter { x(x: number): number; setWidth(w: number): void; textAlign(align: 'center' | 'left' | 'right'): 'center' | 'left' | 'right'; xPlus(x: number, value: number): number; leftForLtr(x: number, itemWidth: number): number; } const getRightToLeftAdapter = function(rectX: number, width: number): RTLAdapter { return { x(x) { return rectX + rectX + width - x; }, setWidth(w) { width = w; }, textAlign(align) { if (align === 'center') { return align; } return align === 'right' ? 'left' : 'right'; }, xPlus(x, value) { return x - value; }, leftForLtr(x, itemWidth) { return x - itemWidth; }, }; }; const getLeftToRightAdapter = function(): RTLAdapter { return { x(x) { return x; }, setWidth(w) { // eslint-disable-line no-unused-vars }, textAlign(align) { return align; }, xPlus(x, value) { return x + value; }, leftForLtr(x, _itemWidth) { // eslint-disable-line @typescript-eslint/no-unused-vars return x; }, }; }; export function getRtlAdapter(rtl: boolean, rectX: number, width: number) { return rtl ? getRightToLeftAdapter(rectX, width) : getLeftToRightAdapter(); } export function overrideTextDirection(ctx: CanvasRenderingContext2D, direction: 'ltr' | 'rtl') { let style: CSSStyleDeclaration, original: [string, string]; if (direction === 'ltr' || direction === 'rtl') { style = ctx.canvas.style; original = [ style.getPropertyValue('direction'), style.getPropertyPriority('direction'), ]; style.setProperty('direction', direction, 'important'); (ctx as { prevTextDirection?: [string, string] }).prevTextDirection = original; } } export function restoreTextDirection(ctx: CanvasRenderingContext2D, original?: [string, string]) { if (original !== undefined) { delete (ctx as { prevTextDirection?: [string, string] }).prevTextDirection; ctx.canvas.style.setProperty('direction', original[0], original[1]); } } ================================================ FILE: src/helpers/helpers.segment.js ================================================ import {_angleBetween, _angleDiff, _isBetween, _normalizeAngle} from './helpers.math.js'; import {createContext} from './helpers.options.js'; import {isPatternOrGradient} from './helpers.color.js'; /** * @typedef { import('../elements/element.line.js').default } LineElement * @typedef { import('../elements/element.point.js').default } PointElement * @typedef {{start: number, end: number, loop: boolean, style?: any}} Segment */ function propertyFn(property) { if (property === 'angle') { return { between: _angleBetween, compare: _angleDiff, normalize: _normalizeAngle, }; } return { between: _isBetween, compare: (a, b) => a - b, normalize: x => x }; } function normalizeSegment({start, end, count, loop, style}) { return { start: start % count, end: end % count, loop: loop && (end - start + 1) % count === 0, style }; } function getSegment(segment, points, bounds) { const {property, start: startBound, end: endBound} = bounds; const {between, normalize} = propertyFn(property); const count = points.length; // eslint-disable-next-line prefer-const let {start, end, loop} = segment; let i, ilen; if (loop) { start += count; end += count; for (i = 0, ilen = count; i < ilen; ++i) { if (!between(normalize(points[start % count][property]), startBound, endBound)) { break; } start--; end--; } start %= count; end %= count; } if (end < start) { end += count; } return {start, end, loop, style: segment.style}; } /** * Returns the sub-segment(s) of a line segment that fall in the given bounds * @param {object} segment * @param {number} segment.start - start index of the segment, referring the points array * @param {number} segment.end - end index of the segment, referring the points array * @param {boolean} segment.loop - indicates that the segment is a loop * @param {object} [segment.style] - segment style * @param {PointElement[]} points - the points that this segment refers to * @param {object} [bounds] * @param {string} bounds.property - the property of a `PointElement` we are bounding. `x`, `y` or `angle`. * @param {number} bounds.start - start value of the property * @param {number} bounds.end - end value of the property * @private **/ export function _boundSegment(segment, points, bounds) { if (!bounds) { return [segment]; } const {property, start: startBound, end: endBound} = bounds; const count = points.length; const {compare, between, normalize} = propertyFn(property); const {start, end, loop, style} = getSegment(segment, points, bounds); const result = []; let inside = false; let subStart = null; let value, point, prevValue; const startIsBefore = () => between(startBound, prevValue, value) && compare(startBound, prevValue) !== 0; const endIsBefore = () => compare(endBound, value) === 0 || between(endBound, prevValue, value); const shouldStart = () => inside || startIsBefore(); const shouldStop = () => !inside || endIsBefore(); for (let i = start, prev = start; i <= end; ++i) { point = points[i % count]; if (point.skip) { continue; } value = normalize(point[property]); if (value === prevValue) { continue; } inside = between(value, startBound, endBound); if (subStart === null && shouldStart()) { subStart = compare(value, startBound) === 0 ? i : prev; } if (subStart !== null && shouldStop()) { result.push(normalizeSegment({start: subStart, end: i, loop, count, style})); subStart = null; } prev = i; prevValue = value; } if (subStart !== null) { result.push(normalizeSegment({start: subStart, end, loop, count, style})); } return result; } /** * Returns the segments of the line that are inside given bounds * @param {LineElement} line * @param {object} [bounds] * @param {string} bounds.property - the property we are bounding with. `x`, `y` or `angle`. * @param {number} bounds.start - start value of the `property` * @param {number} bounds.end - end value of the `property` * @private */ export function _boundSegments(line, bounds) { const result = []; const segments = line.segments; for (let i = 0; i < segments.length; i++) { const sub = _boundSegment(segments[i], line.points, bounds); if (sub.length) { result.push(...sub); } } return result; } /** * Find start and end index of a line. */ function findStartAndEnd(points, count, loop, spanGaps) { let start = 0; let end = count - 1; if (loop && !spanGaps) { // loop and not spanning gaps, first find a gap to start from while (start < count && !points[start].skip) { start++; } } // find first non skipped point (after the first gap possibly) while (start < count && points[start].skip) { start++; } // if we looped to count, start needs to be 0 start %= count; if (loop) { // loop will go past count, if start > 0 end += start; } while (end > start && points[end % count].skip) { end--; } // end could be more than count, normalize end %= count; return {start, end}; } /** * Compute solid segments from Points, when spanGaps === false * @param {PointElement[]} points - the points * @param {number} start - start index * @param {number} max - max index (can go past count on a loop) * @param {boolean} loop - boolean indicating that this would be a loop if no gaps are found */ function solidSegments(points, start, max, loop) { const count = points.length; const result = []; let last = start; let prev = points[start]; let end; for (end = start + 1; end <= max; ++end) { const cur = points[end % count]; if (cur.skip || cur.stop) { if (!prev.skip) { loop = false; result.push({start: start % count, end: (end - 1) % count, loop}); // @ts-ignore start = last = cur.stop ? end : null; } } else { last = end; if (prev.skip) { start = end; } } prev = cur; } if (last !== null) { result.push({start: start % count, end: last % count, loop}); } return result; } /** * Compute the continuous segments that define the whole line * There can be skipped points within a segment, if spanGaps is true. * @param {LineElement} line * @param {object} [segmentOptions] * @return {Segment[]} * @private */ export function _computeSegments(line, segmentOptions) { const points = line.points; const spanGaps = line.options.spanGaps; const count = points.length; if (!count) { return []; } const loop = !!line._loop; const {start, end} = findStartAndEnd(points, count, loop, spanGaps); if (spanGaps === true) { return splitByStyles(line, [{start, end, loop}], points, segmentOptions); } const max = end < start ? end + count : end; const completeLoop = !!line._fullLoop && start === 0 && end === count - 1; return splitByStyles(line, solidSegments(points, start, max, completeLoop), points, segmentOptions); } /** * @param {Segment[]} segments * @param {PointElement[]} points * @param {object} [segmentOptions] * @return {Segment[]} */ function splitByStyles(line, segments, points, segmentOptions) { if (!segmentOptions || !segmentOptions.setContext || !points) { return segments; } return doSplitByStyles(line, segments, points, segmentOptions); } /** * @param {LineElement} line * @param {Segment[]} segments * @param {PointElement[]} points * @param {object} [segmentOptions] * @return {Segment[]} */ function doSplitByStyles(line, segments, points, segmentOptions) { const chartContext = line._chart.getContext(); const baseStyle = readStyle(line.options); const {_datasetIndex: datasetIndex, options: {spanGaps}} = line; const count = points.length; const result = []; let prevStyle = baseStyle; let start = segments[0].start; let i = start; function addStyle(s, e, l, st) { const dir = spanGaps ? -1 : 1; if (s === e) { return; } // Style can not start/end on a skipped point, adjust indices accordingly s += count; while (points[s % count].skip) { s -= dir; } while (points[e % count].skip) { e += dir; } if (s % count !== e % count) { result.push({start: s % count, end: e % count, loop: l, style: st}); prevStyle = st; start = e % count; } } for (const segment of segments) { start = spanGaps ? start : segment.start; let prev = points[start % count]; let style; for (i = start + 1; i <= segment.end; i++) { const pt = points[i % count]; style = readStyle(segmentOptions.setContext(createContext(chartContext, { type: 'segment', p0: prev, p1: pt, p0DataIndex: (i - 1) % count, p1DataIndex: i % count, datasetIndex }))); if (styleChanged(style, prevStyle)) { addStyle(start, i - 1, segment.loop, prevStyle); } prev = pt; prevStyle = style; } if (start < i - 1) { addStyle(start, i - 1, segment.loop, prevStyle); } } return result; } function readStyle(options) { return { backgroundColor: options.backgroundColor, borderCapStyle: options.borderCapStyle, borderDash: options.borderDash, borderDashOffset: options.borderDashOffset, borderJoinStyle: options.borderJoinStyle, borderWidth: options.borderWidth, borderColor: options.borderColor }; } function styleChanged(style, prevStyle) { if (!prevStyle) { return false; } const cache = []; const replacer = function(key, value) { if (!isPatternOrGradient(value)) { return value; } if (!cache.includes(value)) { cache.push(value); } return cache.indexOf(value); }; return JSON.stringify(style, replacer) !== JSON.stringify(prevStyle, replacer); } ================================================ FILE: src/helpers/index.ts ================================================ export * from './helpers.color.js'; export * from './helpers.core.js'; export * from './helpers.canvas.js'; export * from './helpers.collection.js'; export * from './helpers.config.js'; export * from './helpers.curve.js'; export * from './helpers.dom.js'; export {default as easingEffects} from './helpers.easing.js'; export * from './helpers.extras.js'; export * from './helpers.interpolation.js'; export * from './helpers.intl.js'; export * from './helpers.options.js'; export * from './helpers.math.js'; export * from './helpers.rtl.js'; export * from './helpers.segment.js'; export * from './helpers.dataset.js'; ================================================ FILE: src/index.ts ================================================ export * from './controllers/index.js'; export * from './core/index.js'; export * from './elements/index.js'; export * from './platform/index.js'; export * from './plugins/index.js'; export * from './scales/index.js'; import * as controllers from './controllers/index.js'; import * as elements from './elements/index.js'; import * as plugins from './plugins/index.js'; import * as scales from './scales/index.js'; export { controllers, elements, plugins, scales, }; export const registerables = [ controllers, elements, plugins, scales, ]; ================================================ FILE: src/index.umd.ts ================================================ // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck /** * @namespace Chart */ import Chart from './core/core.controller.js'; import * as helpers from './helpers/index.js'; import _adapters from './core/core.adapters.js'; import Animation from './core/core.animation.js'; import animator from './core/core.animator.js'; import Animations from './core/core.animations.js'; import * as controllers from './controllers/index.js'; import DatasetController from './core/core.datasetController.js'; import Element from './core/core.element.js'; import * as elements from './elements/index.js'; import Interaction from './core/core.interaction.js'; import layouts from './core/core.layouts.js'; import * as platforms from './platform/index.js'; import * as plugins from './plugins/index.js'; import registry from './core/core.registry.js'; import Scale from './core/core.scale.js'; import * as scales from './scales/index.js'; import Ticks from './core/core.ticks.js'; // Register built-ins Chart.register(controllers, scales, elements, plugins); Chart.helpers = {...helpers}; Chart._adapters = _adapters; Chart.Animation = Animation; Chart.Animations = Animations; Chart.animator = animator; Chart.controllers = registry.controllers.items; Chart.DatasetController = DatasetController; Chart.Element = Element; Chart.elements = elements; Chart.Interaction = Interaction; Chart.layouts = layouts; Chart.platforms = platforms; Chart.Scale = Scale; Chart.Ticks = Ticks; // Compatibility with ESM extensions Object.assign(Chart, controllers, scales, elements, plugins, platforms); Chart.Chart = Chart; if (typeof window !== 'undefined') { window.Chart = Chart; } export default Chart; ================================================ FILE: src/platform/index.js ================================================ import {_isDomSupported} from '../helpers/index.js'; import BasePlatform from './platform.base.js'; import BasicPlatform from './platform.basic.js'; import DomPlatform from './platform.dom.js'; export function _detectPlatform(canvas) { if (!_isDomSupported() || (typeof OffscreenCanvas !== 'undefined' && canvas instanceof OffscreenCanvas)) { return BasicPlatform; } return DomPlatform; } export {BasePlatform, BasicPlatform, DomPlatform}; ================================================ FILE: src/platform/platform.base.js ================================================ /** * @typedef { import('../core/core.controller.js').default } Chart */ /** * Abstract class that allows abstracting platform dependencies away from the chart. */ export default class BasePlatform { /** * Called at chart construction time, returns a context2d instance implementing * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. * @param {HTMLCanvasElement} canvas - The canvas from which to acquire context (platform specific) * @param {number} [aspectRatio] - The chart options */ acquireContext(canvas, aspectRatio) {} // eslint-disable-line no-unused-vars /** * Called at chart destruction time, releases any resources associated to the context * previously returned by the acquireContext() method. * @param {CanvasRenderingContext2D} context - The context2d instance * @returns {boolean} true if the method succeeded, else false */ releaseContext(context) { // eslint-disable-line no-unused-vars return false; } /** * Registers the specified listener on the given chart. * @param {Chart} chart - Chart from which to listen for event * @param {string} type - The ({@link ChartEvent}) type to listen for * @param {function} listener - Receives a notification (an object that implements * the {@link ChartEvent} interface) when an event of the specified type occurs. */ addEventListener(chart, type, listener) {} // eslint-disable-line no-unused-vars /** * Removes the specified listener previously registered with addEventListener. * @param {Chart} chart - Chart from which to remove the listener * @param {string} type - The ({@link ChartEvent}) type to remove * @param {function} listener - The listener function to remove from the event target. */ removeEventListener(chart, type, listener) {} // eslint-disable-line no-unused-vars /** * @returns {number} the current devicePixelRatio of the device this platform is connected to. */ getDevicePixelRatio() { return 1; } /** * Returns the maximum size in pixels of given canvas element. * @param {HTMLCanvasElement} element * @param {number} [width] - content width of parent element * @param {number} [height] - content height of parent element * @param {number} [aspectRatio] - aspect ratio to maintain */ getMaximumSize(element, width, height, aspectRatio) { width = Math.max(0, width || element.width); height = height || element.height; return { width, height: Math.max(0, aspectRatio ? Math.floor(width / aspectRatio) : height) }; } /** * @param {HTMLCanvasElement} canvas * @returns {boolean} true if the canvas is attached to the platform, false if not. */ isAttached(canvas) { // eslint-disable-line no-unused-vars return true; } /** * Updates config with platform specific requirements * @param {import('../core/core.config.js').default} config */ updateConfig(config) { // eslint-disable-line no-unused-vars // no-op } } ================================================ FILE: src/platform/platform.basic.js ================================================ /** * Platform fallback implementation (minimal). * @see https://github.com/chartjs/Chart.js/pull/4591#issuecomment-319575939 */ import BasePlatform from './platform.base.js'; /** * Platform class for charts without access to the DOM or to many element properties * This platform is used by default for any chart passed an OffscreenCanvas. * @extends BasePlatform */ export default class BasicPlatform extends BasePlatform { acquireContext(item) { // To prevent canvas fingerprinting, some add-ons undefine the getContext // method, for example: https://github.com/kkapsner/CanvasBlocker // https://github.com/chartjs/Chart.js/issues/2807 return item && item.getContext && item.getContext('2d') || null; } updateConfig(config) { config.options.animation = false; } } ================================================ FILE: src/platform/platform.dom.js ================================================ /** * Chart.Platform implementation for targeting a web browser */ import BasePlatform from './platform.base.js'; import {_getParentNode, getRelativePosition, supportsEventListenerOptions, readUsedSize, getMaximumSize} from '../helpers/helpers.dom.js'; import {throttled} from '../helpers/helpers.extras.js'; import {isNullOrUndef} from '../helpers/helpers.core.js'; /** * @typedef { import('../core/core.controller.js').default } Chart */ const EXPANDO_KEY = '$chartjs'; /** * DOM event types -> Chart.js event types. * Note: only events with different types are mapped. * @see https://developer.mozilla.org/en-US/docs/Web/Events */ const EVENT_TYPES = { touchstart: 'mousedown', touchmove: 'mousemove', touchend: 'mouseup', pointerenter: 'mouseenter', pointerdown: 'mousedown', pointermove: 'mousemove', pointerup: 'mouseup', pointerleave: 'mouseout', pointerout: 'mouseout' }; const isNullOrEmpty = value => value === null || value === ''; /** * Initializes the canvas style and render size without modifying the canvas display size, * since responsiveness is handled by the controller.resize() method. The config is used * to determine the aspect ratio to apply in case no explicit height has been specified. * @param {HTMLCanvasElement} canvas * @param {number} [aspectRatio] */ function initCanvas(canvas, aspectRatio) { const style = canvas.style; // NOTE(SB) canvas.getAttribute('width') !== canvas.width: in the first case it // returns null or '' if no explicit value has been set to the canvas attribute. const renderHeight = canvas.getAttribute('height'); const renderWidth = canvas.getAttribute('width'); // Chart.js modifies some canvas values that we want to restore on destroy canvas[EXPANDO_KEY] = { initial: { height: renderHeight, width: renderWidth, style: { display: style.display, height: style.height, width: style.width } } }; // Force canvas to display as block to avoid extra space caused by inline // elements, which would interfere with the responsive resize process. // https://github.com/chartjs/Chart.js/issues/2538 style.display = style.display || 'block'; // Include possible borders in the size style.boxSizing = style.boxSizing || 'border-box'; if (isNullOrEmpty(renderWidth)) { const displayWidth = readUsedSize(canvas, 'width'); if (displayWidth !== undefined) { canvas.width = displayWidth; } } if (isNullOrEmpty(renderHeight)) { if (canvas.style.height === '') { // If no explicit render height and style height, let's apply the aspect ratio, // which one can be specified by the user but also by charts as default option // (i.e. options.aspectRatio). If not specified, use canvas aspect ratio of 2. canvas.height = canvas.width / (aspectRatio || 2); } else { const displayHeight = readUsedSize(canvas, 'height'); if (displayHeight !== undefined) { canvas.height = displayHeight; } } } return canvas; } // Default passive to true as expected by Chrome for 'touchstart' and 'touchend' events. // https://github.com/chartjs/Chart.js/issues/4287 const eventListenerOptions = supportsEventListenerOptions ? {passive: true} : false; function addListener(node, type, listener) { if (node) { node.addEventListener(type, listener, eventListenerOptions); } } function removeListener(chart, type, listener) { if (chart && chart.canvas) { chart.canvas.removeEventListener(type, listener, eventListenerOptions); } } function fromNativeEvent(event, chart) { const type = EVENT_TYPES[event.type] || event.type; const {x, y} = getRelativePosition(event, chart); return { type, chart, native: event, x: x !== undefined ? x : null, y: y !== undefined ? y : null, }; } function nodeListContains(nodeList, canvas) { for (const node of nodeList) { if (node === canvas || node.contains(canvas)) { return true; } } } function createAttachObserver(chart, type, listener) { const canvas = chart.canvas; const observer = new MutationObserver(entries => { let trigger = false; for (const entry of entries) { trigger = trigger || nodeListContains(entry.addedNodes, canvas); trigger = trigger && !nodeListContains(entry.removedNodes, canvas); } if (trigger) { listener(); } }); observer.observe(document, {childList: true, subtree: true}); return observer; } function createDetachObserver(chart, type, listener) { const canvas = chart.canvas; const observer = new MutationObserver(entries => { let trigger = false; for (const entry of entries) { trigger = trigger || nodeListContains(entry.removedNodes, canvas); trigger = trigger && !nodeListContains(entry.addedNodes, canvas); } if (trigger) { listener(); } }); observer.observe(document, {childList: true, subtree: true}); return observer; } const drpListeningCharts = new Map(); let oldDevicePixelRatio = 0; function onWindowResize() { const dpr = window.devicePixelRatio; if (dpr === oldDevicePixelRatio) { return; } oldDevicePixelRatio = dpr; drpListeningCharts.forEach((resize, chart) => { if (chart.currentDevicePixelRatio !== dpr) { resize(); } }); } function listenDevicePixelRatioChanges(chart, resize) { if (!drpListeningCharts.size) { window.addEventListener('resize', onWindowResize); } drpListeningCharts.set(chart, resize); } function unlistenDevicePixelRatioChanges(chart) { drpListeningCharts.delete(chart); if (!drpListeningCharts.size) { window.removeEventListener('resize', onWindowResize); } } function createResizeObserver(chart, type, listener) { const canvas = chart.canvas; const container = canvas && _getParentNode(canvas); if (!container) { return; } const resize = throttled((width, height) => { const w = container.clientWidth; listener(width, height); if (w < container.clientWidth) { // If the container size shrank during chart resize, let's assume // scrollbar appeared. So we resize again with the scrollbar visible - // effectively making chart smaller and the scrollbar hidden again. // Because we are inside `throttled`, and currently `ticking`, scroll // events are ignored during this whole 2 resize process. // If we assumed wrong and something else happened, we are resizing // twice in a frame (potential performance issue) listener(); } }, window); // @ts-ignore until https://github.com/microsoft/TypeScript/issues/37861 implemented const observer = new ResizeObserver(entries => { const entry = entries[0]; const width = entry.contentRect.width; const height = entry.contentRect.height; // When its container's display is set to 'none' the callback will be called with a // size of (0, 0), which will cause the chart to lose its original height, so skip // resizing in such case. if (width === 0 && height === 0) { return; } resize(width, height); }); observer.observe(container); listenDevicePixelRatioChanges(chart, resize); return observer; } function releaseObserver(chart, type, observer) { if (observer) { observer.disconnect(); } if (type === 'resize') { unlistenDevicePixelRatioChanges(chart); } } function createProxyAndListen(chart, type, listener) { const canvas = chart.canvas; const proxy = throttled((event) => { // This case can occur if the chart is destroyed while waiting // for the throttled function to occur. We prevent crashes by checking // for a destroyed chart if (chart.ctx !== null) { listener(fromNativeEvent(event, chart)); } }, chart); addListener(canvas, type, proxy); return proxy; } /** * Platform class for charts that can access the DOM and global window/document properties * @extends BasePlatform */ export default class DomPlatform extends BasePlatform { /** * @param {HTMLCanvasElement} canvas * @param {number} [aspectRatio] * @return {CanvasRenderingContext2D|null} */ acquireContext(canvas, aspectRatio) { // To prevent canvas fingerprinting, some add-ons undefine the getContext // method, for example: https://github.com/kkapsner/CanvasBlocker // https://github.com/chartjs/Chart.js/issues/2807 const context = canvas && canvas.getContext && canvas.getContext('2d'); // `instanceof HTMLCanvasElement/CanvasRenderingContext2D` fails when the canvas is // inside an iframe or when running in a protected environment. We could guess the // types from their toString() value but let's keep things flexible and assume it's // a sufficient condition if the canvas has a context2D which has canvas as `canvas`. // https://github.com/chartjs/Chart.js/issues/3887 // https://github.com/chartjs/Chart.js/issues/4102 // https://github.com/chartjs/Chart.js/issues/4152 if (context && context.canvas === canvas) { // Load platform resources on first chart creation, to make it possible to // import the library before setting platform options. initCanvas(canvas, aspectRatio); return context; } return null; } /** * @param {CanvasRenderingContext2D} context */ releaseContext(context) { const canvas = context.canvas; if (!canvas[EXPANDO_KEY]) { return false; } const initial = canvas[EXPANDO_KEY].initial; ['height', 'width'].forEach((prop) => { const value = initial[prop]; if (isNullOrUndef(value)) { canvas.removeAttribute(prop); } else { canvas.setAttribute(prop, value); } }); const style = initial.style || {}; Object.keys(style).forEach((key) => { canvas.style[key] = style[key]; }); // The canvas render size might have been changed (and thus the state stack discarded), // we can't use save() and restore() to restore the initial state. So make sure that at // least the canvas context is reset to the default state by setting the canvas width. // https://www.w3.org/TR/2011/WD-html5-20110525/the-canvas-element.html // eslint-disable-next-line no-self-assign canvas.width = canvas.width; delete canvas[EXPANDO_KEY]; return true; } /** * * @param {Chart} chart * @param {string} type * @param {function} listener */ addEventListener(chart, type, listener) { // Can have only one listener per type, so make sure previous is removed this.removeEventListener(chart, type); const proxies = chart.$proxies || (chart.$proxies = {}); const handlers = { attach: createAttachObserver, detach: createDetachObserver, resize: createResizeObserver }; const handler = handlers[type] || createProxyAndListen; proxies[type] = handler(chart, type, listener); } /** * @param {Chart} chart * @param {string} type */ removeEventListener(chart, type) { const proxies = chart.$proxies || (chart.$proxies = {}); const proxy = proxies[type]; if (!proxy) { return; } const handlers = { attach: releaseObserver, detach: releaseObserver, resize: releaseObserver }; const handler = handlers[type] || removeListener; handler(chart, type, proxy); proxies[type] = undefined; } getDevicePixelRatio() { return window.devicePixelRatio; } /** * @param {HTMLCanvasElement} canvas * @param {number} [width] - content width of parent element * @param {number} [height] - content height of parent element * @param {number} [aspectRatio] - aspect ratio to maintain */ getMaximumSize(canvas, width, height, aspectRatio) { return getMaximumSize(canvas, width, height, aspectRatio); } /** * @param {HTMLCanvasElement} canvas */ isAttached(canvas) { const container = canvas && _getParentNode(canvas); return !!(container && container.isConnected); } } ================================================ FILE: src/plugins/index.js ================================================ export {default as Colors} from './plugin.colors.js'; export {default as Decimation} from './plugin.decimation.js'; export {default as Filler} from './plugin.filler/index.js'; export {default as Legend} from './plugin.legend.js'; export {default as SubTitle} from './plugin.subtitle.js'; export {default as Title} from './plugin.title.js'; export {default as Tooltip} from './plugin.tooltip.js'; ================================================ FILE: src/plugins/plugin.colors.ts ================================================ import {DoughnutController, PolarAreaController, defaults} from '../index.js'; import type {Chart, ChartDataset} from '../types.js'; export interface ColorsPluginOptions { enabled?: boolean; forceOverride?: boolean; } interface ColorsDescriptor { backgroundColor?: unknown; borderColor?: unknown; } const BORDER_COLORS = [ 'rgb(54, 162, 235)', // blue 'rgb(255, 99, 132)', // red 'rgb(255, 159, 64)', // orange 'rgb(255, 205, 86)', // yellow 'rgb(75, 192, 192)', // green 'rgb(153, 102, 255)', // purple 'rgb(201, 203, 207)' // grey ]; // Border colors with 50% transparency const BACKGROUND_COLORS = /* #__PURE__ */ BORDER_COLORS.map(color => color.replace('rgb(', 'rgba(').replace(')', ', 0.5)')); function getBorderColor(i: number) { return BORDER_COLORS[i % BORDER_COLORS.length]; } function getBackgroundColor(i: number) { return BACKGROUND_COLORS[i % BACKGROUND_COLORS.length]; } function colorizeDefaultDataset(dataset: ChartDataset, i: number) { dataset.borderColor = getBorderColor(i); dataset.backgroundColor = getBackgroundColor(i); return ++i; } function colorizeDoughnutDataset(dataset: ChartDataset, i: number) { dataset.backgroundColor = dataset.data.map(() => getBorderColor(i++)); return i; } function colorizePolarAreaDataset(dataset: ChartDataset, i: number) { dataset.backgroundColor = dataset.data.map(() => getBackgroundColor(i++)); return i; } function getColorizer(chart: Chart) { let i = 0; return (dataset: ChartDataset, datasetIndex: number) => { const controller = chart.getDatasetMeta(datasetIndex).controller; if (controller instanceof DoughnutController) { i = colorizeDoughnutDataset(dataset, i); } else if (controller instanceof PolarAreaController) { i = colorizePolarAreaDataset(dataset, i); } else if (controller) { i = colorizeDefaultDataset(dataset, i); } }; } function containsColorsDefinitions( descriptors: ColorsDescriptor[] | Record ) { let k: number | string; for (k in descriptors) { if (descriptors[k].borderColor || descriptors[k].backgroundColor) { return true; } } return false; } function containsColorsDefinition( descriptor: ColorsDescriptor ) { return descriptor && (descriptor.borderColor || descriptor.backgroundColor); } function containsDefaultColorsDefenitions() { return defaults.borderColor !== 'rgba(0,0,0,0.1)' || defaults.backgroundColor !== 'rgba(0,0,0,0.1)'; } export default { id: 'colors', defaults: { enabled: true, forceOverride: false } as ColorsPluginOptions, beforeLayout(chart: Chart, _args, options: ColorsPluginOptions) { if (!options.enabled) { return; } const { data: {datasets}, options: chartOptions } = chart.config; const {elements} = chartOptions; const containsColorDefenition = ( containsColorsDefinitions(datasets) || containsColorsDefinition(chartOptions) || (elements && containsColorsDefinitions(elements)) || containsDefaultColorsDefenitions()); if (!options.forceOverride && containsColorDefenition) { return; } const colorizer = getColorizer(chart); datasets.forEach(colorizer); } }; ================================================ FILE: src/plugins/plugin.decimation.js ================================================ import {_limitValue, _lookupByKey, isNullOrUndef, resolve} from '../helpers/index.js'; function lttbDecimation(data, start, count, availableWidth, options) { /** * Implementation of the Largest Triangle Three Buckets algorithm. * * This implementation is based on the original implementation by Sveinn Steinarsson * in https://github.com/sveinn-steinarsson/flot-downsample/blob/master/jquery.flot.downsample.js * * The original implementation is MIT licensed. */ const samples = options.samples || availableWidth; // There are less points than the threshold, returning the whole array if (samples >= count) { return data.slice(start, start + count); } const decimated = []; const bucketWidth = (count - 2) / (samples - 2); let sampledIndex = 0; const endIndex = start + count - 1; // Starting from offset let a = start; let i, maxAreaPoint, maxArea, area, nextA; decimated[sampledIndex++] = data[a]; for (i = 0; i < samples - 2; i++) { let avgX = 0; let avgY = 0; let j; // Adding offset const avgRangeStart = Math.floor((i + 1) * bucketWidth) + 1 + start; const avgRangeEnd = Math.min(Math.floor((i + 2) * bucketWidth) + 1, count) + start; const avgRangeLength = avgRangeEnd - avgRangeStart; for (j = avgRangeStart; j < avgRangeEnd; j++) { avgX += data[j].x; avgY += data[j].y; } avgX /= avgRangeLength; avgY /= avgRangeLength; // Adding offset const rangeOffs = Math.floor(i * bucketWidth) + 1 + start; const rangeTo = Math.min(Math.floor((i + 1) * bucketWidth) + 1, count) + start; const {x: pointAx, y: pointAy} = data[a]; // Note that this is changed from the original algorithm which initializes these // values to 1. The reason for this change is that if the area is small, nextA // would never be set and thus a crash would occur in the next loop as `a` would become // `undefined`. Since the area is always positive, but could be 0 in the case of a flat trace, // initializing with a negative number is the correct solution. maxArea = area = -1; for (j = rangeOffs; j < rangeTo; j++) { area = 0.5 * Math.abs( (pointAx - avgX) * (data[j].y - pointAy) - (pointAx - data[j].x) * (avgY - pointAy) ); if (area > maxArea) { maxArea = area; maxAreaPoint = data[j]; nextA = j; } } decimated[sampledIndex++] = maxAreaPoint; a = nextA; } // Include the last point decimated[sampledIndex++] = data[endIndex]; return decimated; } function minMaxDecimation(data, start, count, availableWidth) { let avgX = 0; let countX = 0; let i, point, x, y, prevX, minIndex, maxIndex, startIndex, minY, maxY; const decimated = []; const endIndex = start + count - 1; const xMin = data[start].x; const xMax = data[endIndex].x; const dx = xMax - xMin; for (i = start; i < start + count; ++i) { point = data[i]; x = (point.x - xMin) / dx * availableWidth; y = point.y; const truncX = x | 0; if (truncX === prevX) { // Determine `minY` / `maxY` and `avgX` while we stay within same x-position if (y < minY) { minY = y; minIndex = i; } else if (y > maxY) { maxY = y; maxIndex = i; } // For first point in group, countX is `0`, so average will be `x` / 1. // Use point.x here because we're computing the average data `x` value avgX = (countX * avgX + point.x) / ++countX; } else { // Push up to 4 points, 3 for the last interval and the first point for this interval const lastIndex = i - 1; if (!isNullOrUndef(minIndex) && !isNullOrUndef(maxIndex)) { // The interval is defined by 4 points: start, min, max, end. // The starting point is already considered at this point, so we need to determine which // of the other points to add. We need to sort these points to ensure the decimated data // is still sorted and then ensure there are no duplicates. const intermediateIndex1 = Math.min(minIndex, maxIndex); const intermediateIndex2 = Math.max(minIndex, maxIndex); if (intermediateIndex1 !== startIndex && intermediateIndex1 !== lastIndex) { decimated.push({ ...data[intermediateIndex1], x: avgX, }); } if (intermediateIndex2 !== startIndex && intermediateIndex2 !== lastIndex) { decimated.push({ ...data[intermediateIndex2], x: avgX }); } } // lastIndex === startIndex will occur when a range has only 1 point which could // happen with very uneven data if (i > 0 && lastIndex !== startIndex) { // Last point in the previous interval decimated.push(data[lastIndex]); } // Start of the new interval decimated.push(point); prevX = truncX; countX = 0; minY = maxY = y; minIndex = maxIndex = startIndex = i; } } return decimated; } function cleanDecimatedDataset(dataset) { if (dataset._decimated) { const data = dataset._data; delete dataset._decimated; delete dataset._data; Object.defineProperty(dataset, 'data', { configurable: true, enumerable: true, writable: true, value: data, }); } } function cleanDecimatedData(chart) { chart.data.datasets.forEach((dataset) => { cleanDecimatedDataset(dataset); }); } function getStartAndCountOfVisiblePointsSimplified(meta, points) { const pointCount = points.length; let start = 0; let count; const {iScale} = meta; const {min, max, minDefined, maxDefined} = iScale.getUserBounds(); if (minDefined) { start = _limitValue(_lookupByKey(points, iScale.axis, min).lo, 0, pointCount - 1); } if (maxDefined) { count = _limitValue(_lookupByKey(points, iScale.axis, max).hi + 1, start, pointCount) - start; } else { count = pointCount - start; } return {start, count}; } export default { id: 'decimation', defaults: { algorithm: 'min-max', enabled: false, }, beforeElementsUpdate: (chart, args, options) => { if (!options.enabled) { // The decimation plugin may have been previously enabled. Need to remove old `dataset._data` handlers cleanDecimatedData(chart); return; } // Assume the entire chart is available to show a few more points than needed const availableWidth = chart.width; chart.data.datasets.forEach((dataset, datasetIndex) => { const {_data, indexAxis} = dataset; const meta = chart.getDatasetMeta(datasetIndex); const data = _data || dataset.data; if (resolve([indexAxis, chart.options.indexAxis]) === 'y') { // Decimation is only supported for lines that have an X indexAxis return; } if (!meta.controller.supportsDecimation) { // Only line datasets are supported return; } const xAxis = chart.scales[meta.xAxisID]; if (xAxis.type !== 'linear' && xAxis.type !== 'time') { // Only linear interpolation is supported return; } if (chart.options.parsing) { // Plugin only supports data that does not need parsing return; } let {start, count} = getStartAndCountOfVisiblePointsSimplified(meta, data); const threshold = options.threshold || 4 * availableWidth; if (count <= threshold) { // No decimation is required until we are above this threshold cleanDecimatedDataset(dataset); return; } if (isNullOrUndef(_data)) { // First time we are seeing this dataset // We override the 'data' property with a setter that stores the // raw data in _data, but reads the decimated data from _decimated dataset._data = data; delete dataset.data; Object.defineProperty(dataset, 'data', { configurable: true, enumerable: true, get: function() { return this._decimated; }, set: function(d) { this._data = d; } }); } // Point the chart to the decimated data let decimated; switch (options.algorithm) { case 'lttb': decimated = lttbDecimation(data, start, count, availableWidth, options); break; case 'min-max': decimated = minMaxDecimation(data, start, count, availableWidth); break; default: throw new Error(`Unsupported decimation algorithm '${options.algorithm}'`); } dataset._decimated = decimated; }); }, destroy(chart) { cleanDecimatedData(chart); } }; ================================================ FILE: src/plugins/plugin.filler/filler.drawing.js ================================================ import {clipArea, unclipArea, getDatasetClipArea} from '../../helpers/index.js'; import {_findSegmentEnd, _getBounds, _segments} from './filler.segment.js'; import {_getTarget} from './filler.target.js'; export function _drawfill(ctx, source, area) { const target = _getTarget(source); const {chart, index, line, scale, axis} = source; const lineOpts = line.options; const fillOption = lineOpts.fill; const color = lineOpts.backgroundColor; const {above = color, below = color} = fillOption || {}; const meta = chart.getDatasetMeta(index); const clip = getDatasetClipArea(chart, meta); if (target && line.points.length) { clipArea(ctx, area); doFill(ctx, {line, target, above, below, area, scale, axis, clip}); unclipArea(ctx); } } function doFill(ctx, cfg) { const {line, target, above, below, area, scale, clip} = cfg; const property = line._loop ? 'angle' : cfg.axis; ctx.save(); let fillColor = below; if (below !== above) { if (property === 'x') { clipVertical(ctx, target, area.top); fill(ctx, {line, target, color: above, scale, property, clip}); ctx.restore(); ctx.save(); clipVertical(ctx, target, area.bottom); } else if (property === 'y') { clipHorizontal(ctx, target, area.left); fill(ctx, {line, target, color: below, scale, property, clip}); ctx.restore(); ctx.save(); clipHorizontal(ctx, target, area.right); fillColor = above; } } fill(ctx, {line, target, color: fillColor, scale, property, clip}); ctx.restore(); } function clipVertical(ctx, target, clipY) { const {segments, points} = target; let first = true; let lineLoop = false; ctx.beginPath(); for (const segment of segments) { const {start, end} = segment; const firstPoint = points[start]; const lastPoint = points[_findSegmentEnd(start, end, points)]; if (first) { ctx.moveTo(firstPoint.x, firstPoint.y); first = false; } else { ctx.lineTo(firstPoint.x, clipY); ctx.lineTo(firstPoint.x, firstPoint.y); } lineLoop = !!target.pathSegment(ctx, segment, {move: lineLoop}); if (lineLoop) { ctx.closePath(); } else { ctx.lineTo(lastPoint.x, clipY); } } ctx.lineTo(target.first().x, clipY); ctx.closePath(); ctx.clip(); } function clipHorizontal(ctx, target, clipX) { const {segments, points} = target; let first = true; let lineLoop = false; ctx.beginPath(); for (const segment of segments) { const {start, end} = segment; const firstPoint = points[start]; const lastPoint = points[_findSegmentEnd(start, end, points)]; if (first) { ctx.moveTo(firstPoint.x, firstPoint.y); first = false; } else { ctx.lineTo(clipX, firstPoint.y); ctx.lineTo(firstPoint.x, firstPoint.y); } lineLoop = !!target.pathSegment(ctx, segment, {move: lineLoop}); if (lineLoop) { ctx.closePath(); } else { ctx.lineTo(clipX, lastPoint.y); } } ctx.lineTo(clipX, target.first().y); ctx.closePath(); ctx.clip(); } function fill(ctx, cfg) { const {line, target, property, color, scale, clip} = cfg; const segments = _segments(line, target, property); for (const {source: src, target: tgt, start, end} of segments) { const {style: {backgroundColor = color} = {}} = src; const notShape = target !== true; ctx.save(); ctx.fillStyle = backgroundColor; clipBounds(ctx, scale, clip, notShape && _getBounds(property, start, end)); ctx.beginPath(); const lineLoop = !!line.pathSegment(ctx, src); let loop; if (notShape) { if (lineLoop) { ctx.closePath(); } else { interpolatedLineTo(ctx, target, end, property); } const targetLoop = !!target.pathSegment(ctx, tgt, {move: lineLoop, reverse: true}); loop = lineLoop && targetLoop; if (!loop) { interpolatedLineTo(ctx, target, start, property); } } ctx.closePath(); ctx.fill(loop ? 'evenodd' : 'nonzero'); ctx.restore(); } } function clipBounds(ctx, scale, clip, bounds) { const chartArea = scale.chart.chartArea; const {property, start, end} = bounds || {}; if (property === 'x' || property === 'y') { let left, top, right, bottom; if (property === 'x') { left = start; top = chartArea.top; right = end; bottom = chartArea.bottom; } else { left = chartArea.left; top = start; right = chartArea.right; bottom = end; } ctx.beginPath(); if (clip) { left = Math.max(left, clip.left); right = Math.min(right, clip.right); top = Math.max(top, clip.top); bottom = Math.min(bottom, clip.bottom); } ctx.rect(left, top, right - left, bottom - top); ctx.clip(); } } function interpolatedLineTo(ctx, target, point, property) { const interpolatedPoint = target.interpolate(point, property); if (interpolatedPoint) { ctx.lineTo(interpolatedPoint.x, interpolatedPoint.y); } } ================================================ FILE: src/plugins/plugin.filler/filler.helper.js ================================================ /** * @typedef { import('../../core/core.controller.js').default } Chart * @typedef { import('../../core/core.scale.js').default } Scale * @typedef { import('../../elements/element.point.js').default } PointElement */ import {LineElement} from '../../elements/index.js'; import {isArray} from '../../helpers/index.js'; import {_pointsFromSegments} from './filler.segment.js'; /** * @param {PointElement[] | { x: number; y: number; }} boundary * @param {LineElement} line * @return {LineElement?} */ export function _createBoundaryLine(boundary, line) { let points = []; let _loop = false; if (isArray(boundary)) { _loop = true; // @ts-ignore points = boundary; } else { points = _pointsFromSegments(boundary, line); } return points.length ? new LineElement({ points, options: {tension: 0}, _loop, _fullLoop: _loop }) : null; } export function _shouldApplyFill(source) { return source && source.fill !== false; } ================================================ FILE: src/plugins/plugin.filler/filler.options.js ================================================ import {isObject, isFinite, valueOrDefault} from '../../helpers/helpers.core.js'; /** * @typedef { import('../../core/core.scale.js').default } Scale * @typedef { import('../../elements/element.line.js').default } LineElement * @typedef { import('../../types/index.js').FillTarget } FillTarget * @typedef { import('../../types/index.js').ComplexFillTarget } ComplexFillTarget */ export function _resolveTarget(sources, index, propagate) { const source = sources[index]; let fill = source.fill; const visited = [index]; let target; if (!propagate) { return fill; } while (fill !== false && visited.indexOf(fill) === -1) { if (!isFinite(fill)) { return fill; } target = sources[fill]; if (!target) { return false; } if (target.visible) { return fill; } visited.push(fill); fill = target.fill; } return false; } /** * @param {LineElement} line * @param {number} index * @param {number} count */ export function _decodeFill(line, index, count) { /** @type {string | {value: number}} */ const fill = parseFillOption(line); if (isObject(fill)) { return isNaN(fill.value) ? false : fill; } let target = parseFloat(fill); if (isFinite(target) && Math.floor(target) === target) { return decodeTargetIndex(fill[0], index, target, count); } return ['origin', 'start', 'end', 'stack', 'shape'].indexOf(fill) >= 0 && fill; } function decodeTargetIndex(firstCh, index, target, count) { if (firstCh === '-' || firstCh === '+') { target = index + target; } if (target === index || target < 0 || target >= count) { return false; } return target; } /** * @param {FillTarget | ComplexFillTarget} fill * @param {Scale} scale * @returns {number | null} */ export function _getTargetPixel(fill, scale) { let pixel = null; if (fill === 'start') { pixel = scale.bottom; } else if (fill === 'end') { pixel = scale.top; } else if (isObject(fill)) { // @ts-ignore pixel = scale.getPixelForValue(fill.value); } else if (scale.getBasePixel) { pixel = scale.getBasePixel(); } return pixel; } /** * @param {FillTarget | ComplexFillTarget} fill * @param {Scale} scale * @param {number} startValue * @returns {number | undefined} */ export function _getTargetValue(fill, scale, startValue) { let value; if (fill === 'start') { value = startValue; } else if (fill === 'end') { value = scale.options.reverse ? scale.min : scale.max; } else if (isObject(fill)) { // @ts-ignore value = fill.value; } else { value = scale.getBaseValue(); } return value; } /** * @param {LineElement} line */ function parseFillOption(line) { const options = line.options; const fillOption = options.fill; let fill = valueOrDefault(fillOption && fillOption.target, fillOption); if (fill === undefined) { fill = !!options.backgroundColor; } if (fill === false || fill === null) { return false; } if (fill === true) { return 'origin'; } return fill; } ================================================ FILE: src/plugins/plugin.filler/filler.segment.js ================================================ import {_boundSegment, _boundSegments, _normalizeAngle} from '../../helpers/index.js'; export function _segments(line, target, property) { const segments = line.segments; const points = line.points; const tpoints = target.points; const parts = []; for (const segment of segments) { let {start, end} = segment; end = _findSegmentEnd(start, end, points); const bounds = _getBounds(property, points[start], points[end], segment.loop); if (!target.segments) { // Special case for boundary not supporting `segments` (simpleArc) // Bounds are provided as `target` for partial circle, or undefined for full circle parts.push({ source: segment, target: bounds, start: points[start], end: points[end] }); continue; } // Get all segments from `target` that intersect the bounds of current segment of `line` const targetSegments = _boundSegments(target, bounds); for (const tgt of targetSegments) { const subBounds = _getBounds(property, tpoints[tgt.start], tpoints[tgt.end], tgt.loop); const fillSources = _boundSegment(segment, points, subBounds); for (const fillSource of fillSources) { parts.push({ source: fillSource, target: tgt, start: { [property]: _getEdge(bounds, subBounds, 'start', Math.max) }, end: { [property]: _getEdge(bounds, subBounds, 'end', Math.min) } }); } } } return parts; } export function _getBounds(property, first, last, loop) { if (loop) { return; } let start = first[property]; let end = last[property]; if (property === 'angle') { start = _normalizeAngle(start); end = _normalizeAngle(end); } return {property, start, end}; } export function _pointsFromSegments(boundary, line) { const {x = null, y = null} = boundary || {}; const linePoints = line.points; const points = []; line.segments.forEach(({start, end}) => { end = _findSegmentEnd(start, end, linePoints); const first = linePoints[start]; const last = linePoints[end]; if (y !== null) { points.push({x: first.x, y}); points.push({x: last.x, y}); } else if (x !== null) { points.push({x, y: first.y}); points.push({x, y: last.y}); } }); return points; } export function _findSegmentEnd(start, end, points) { for (;end > start; end--) { const point = points[end]; if (!isNaN(point.x) && !isNaN(point.y)) { break; } } return end; } function _getEdge(a, b, prop, fn) { if (a && b) { return fn(a[prop], b[prop]); } return a ? a[prop] : b ? b[prop] : 0; } ================================================ FILE: src/plugins/plugin.filler/filler.target.js ================================================ import {isFinite} from '../../helpers/index.js'; import {_createBoundaryLine} from './filler.helper.js'; import {_getTargetPixel, _getTargetValue} from './filler.options.js'; import {_buildStackLine} from './filler.target.stack.js'; import {simpleArc} from './simpleArc.js'; /** * @typedef { import('../../core/core.controller.js').default } Chart * @typedef { import('../../core/core.scale.js').default } Scale * @typedef { import('../../elements/element.point.js').default } PointElement */ export function _getTarget(source) { const {chart, fill, line} = source; if (isFinite(fill)) { return getLineByIndex(chart, fill); } if (fill === 'stack') { return _buildStackLine(source); } if (fill === 'shape') { return true; } const boundary = computeBoundary(source); if (boundary instanceof simpleArc) { return boundary; } return _createBoundaryLine(boundary, line); } /** * @param {Chart} chart * @param {number} index */ function getLineByIndex(chart, index) { const meta = chart.getDatasetMeta(index); const visible = meta && chart.isDatasetVisible(index); return visible ? meta.dataset : null; } function computeBoundary(source) { const scale = source.scale || {}; if (scale.getPointPositionForValue) { return computeCircularBoundary(source); } return computeLinearBoundary(source); } function computeLinearBoundary(source) { const {scale = {}, fill} = source; const pixel = _getTargetPixel(fill, scale); if (isFinite(pixel)) { const horizontal = scale.isHorizontal(); return { x: horizontal ? pixel : null, y: horizontal ? null : pixel }; } return null; } function computeCircularBoundary(source) { const {scale, fill} = source; const options = scale.options; const length = scale.getLabels().length; const start = options.reverse ? scale.max : scale.min; const value = _getTargetValue(fill, scale, start); const target = []; if (options.grid.circular) { const center = scale.getPointPositionForValue(0, start); return new simpleArc({ x: center.x, y: center.y, radius: scale.getDistanceFromCenterForValue(value) }); } for (let i = 0; i < length; ++i) { target.push(scale.getPointPositionForValue(i, value)); } return target; } ================================================ FILE: src/plugins/plugin.filler/filler.target.stack.js ================================================ /** * @typedef { import('../../core/core.controller.js').default } Chart * @typedef { import('../../core/core.scale.js').default } Scale * @typedef { import('../../elements/element.point.js').default } PointElement */ import {LineElement} from '../../elements/index.js'; import {_isBetween} from '../../helpers/index.js'; import {_createBoundaryLine} from './filler.helper.js'; /** * @param {{ chart: Chart; scale: Scale; index: number; line: LineElement; }} source * @return {LineElement} */ export function _buildStackLine(source) { const {scale, index, line} = source; const points = []; const segments = line.segments; const sourcePoints = line.points; const linesBelow = getLinesBelow(scale, index); linesBelow.push(_createBoundaryLine({x: null, y: scale.bottom}, line)); for (let i = 0; i < segments.length; i++) { const segment = segments[i]; for (let j = segment.start; j <= segment.end; j++) { addPointsBelow(points, sourcePoints[j], linesBelow); } } return new LineElement({points, options: {}}); } /** * @param {Scale} scale * @param {number} index * @return {LineElement[]} */ function getLinesBelow(scale, index) { const below = []; const metas = scale.getMatchingVisibleMetas('line'); for (let i = 0; i < metas.length; i++) { const meta = metas[i]; if (meta.index === index) { break; } if (!meta.hidden) { below.unshift(meta.dataset); } } return below; } /** * @param {PointElement[]} points * @param {PointElement} sourcePoint * @param {LineElement[]} linesBelow */ function addPointsBelow(points, sourcePoint, linesBelow) { const postponed = []; for (let j = 0; j < linesBelow.length; j++) { const line = linesBelow[j]; const {first, last, point} = findPoint(line, sourcePoint, 'x'); if (!point || (first && last)) { continue; } if (first) { // First point of a segment -> need to add another point before this, postponed.unshift(point); } else { points.push(point); if (!last) { // In the middle of a segment, no need to add more points. break; } } } points.push(...postponed); } /** * @param {LineElement} line * @param {PointElement} sourcePoint * @param {string} property * @returns {{point?: PointElement, first?: boolean, last?: boolean}} */ function findPoint(line, sourcePoint, property) { const point = line.interpolate(sourcePoint, property); if (!point) { return {}; } const pointValue = point[property]; const segments = line.segments; const linePoints = line.points; let first = false; let last = false; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; const firstValue = linePoints[segment.start][property]; const lastValue = linePoints[segment.end][property]; if (_isBetween(pointValue, firstValue, lastValue)) { first = pointValue === firstValue; last = pointValue === lastValue; break; } } return {first, last, point}; } ================================================ FILE: src/plugins/plugin.filler/index.js ================================================ /** * Plugin based on discussion from the following Chart.js issues: * @see https://github.com/chartjs/Chart.js/issues/2380#issuecomment-279961569 * @see https://github.com/chartjs/Chart.js/issues/2440#issuecomment-256461897 */ import LineElement from '../../elements/element.line.js'; import {_drawfill} from './filler.drawing.js'; import {_shouldApplyFill} from './filler.helper.js'; import {_decodeFill, _resolveTarget} from './filler.options.js'; export default { id: 'filler', afterDatasetsUpdate(chart, _args, options) { const count = (chart.data.datasets || []).length; const sources = []; let meta, i, line, source; for (i = 0; i < count; ++i) { meta = chart.getDatasetMeta(i); line = meta.dataset; source = null; if (line && line.options && line instanceof LineElement) { source = { visible: chart.isDatasetVisible(i), index: i, fill: _decodeFill(line, i, count), chart, axis: meta.controller.options.indexAxis, scale: meta.vScale, line, }; } meta.$filler = source; sources.push(source); } for (i = 0; i < count; ++i) { source = sources[i]; if (!source || source.fill === false) { continue; } source.fill = _resolveTarget(sources, i, options.propagate); } }, beforeDraw(chart, _args, options) { const draw = options.drawTime === 'beforeDraw'; const metasets = chart.getSortedVisibleDatasetMetas(); const area = chart.chartArea; for (let i = metasets.length - 1; i >= 0; --i) { const source = metasets[i].$filler; if (!source) { continue; } source.line.updateControlPoints(area, source.axis); if (draw && source.fill) { _drawfill(chart.ctx, source, area); } } }, beforeDatasetsDraw(chart, _args, options) { if (options.drawTime !== 'beforeDatasetsDraw') { return; } const metasets = chart.getSortedVisibleDatasetMetas(); for (let i = metasets.length - 1; i >= 0; --i) { const source = metasets[i].$filler; if (_shouldApplyFill(source)) { _drawfill(chart.ctx, source, chart.chartArea); } } }, beforeDatasetDraw(chart, args, options) { const source = args.meta.$filler; if (!_shouldApplyFill(source) || options.drawTime !== 'beforeDatasetDraw') { return; } _drawfill(chart.ctx, source, chart.chartArea); }, defaults: { propagate: true, drawTime: 'beforeDatasetDraw' } }; ================================================ FILE: src/plugins/plugin.filler/simpleArc.js ================================================ import {TAU} from '../../helpers/index.js'; // TODO: use elements.ArcElement instead export class simpleArc { constructor(opts) { this.x = opts.x; this.y = opts.y; this.radius = opts.radius; } pathSegment(ctx, bounds, opts) { const {x, y, radius} = this; bounds = bounds || {start: 0, end: TAU}; ctx.arc(x, y, radius, bounds.end, bounds.start, true); return !opts.bounds; } interpolate(point) { const {x, y, radius} = this; const angle = point.angle; return { x: x + Math.cos(angle) * radius, y: y + Math.sin(angle) * radius, angle }; } } ================================================ FILE: src/plugins/plugin.legend.js ================================================ import defaults from '../core/core.defaults.js'; import Element from '../core/core.element.js'; import layouts from '../core/core.layouts.js'; import {addRoundedRectPath, drawPointLegend, renderText} from '../helpers/helpers.canvas.js'; import { _isBetween, callback as call, clipArea, getRtlAdapter, overrideTextDirection, restoreTextDirection, toFont, toPadding, unclipArea, valueOrDefault, } from '../helpers/index.js'; import {_alignStartEnd, _textX, _toLeftRightCenter} from '../helpers/helpers.extras.js'; import {toTRBLCorners} from '../helpers/helpers.options.js'; /** * @typedef { import('../types/index.js').ChartEvent } ChartEvent */ const getBoxSize = (labelOpts, fontSize) => { let {boxHeight = fontSize, boxWidth = fontSize} = labelOpts; if (labelOpts.usePointStyle) { boxHeight = Math.min(boxHeight, fontSize); boxWidth = labelOpts.pointStyleWidth || Math.min(boxWidth, fontSize); } return { boxWidth, boxHeight, itemHeight: Math.max(fontSize, boxHeight) }; }; const itemsEqual = (a, b) => a !== null && b !== null && a.datasetIndex === b.datasetIndex && a.index === b.index; export class Legend extends Element { /** * @param {{ ctx: any; options: any; chart: any; }} config */ constructor(config) { super(); this._added = false; // Contains hit boxes for each dataset (in dataset order) this.legendHitBoxes = []; /** * @private */ this._hoveredItem = null; // Are we in doughnut mode which has a different data type this.doughnutMode = false; this.chart = config.chart; this.options = config.options; this.ctx = config.ctx; this.legendItems = undefined; this.columnSizes = undefined; this.lineWidths = undefined; this.maxHeight = undefined; this.maxWidth = undefined; this.top = undefined; this.bottom = undefined; this.left = undefined; this.right = undefined; this.height = undefined; this.width = undefined; this._margins = undefined; this.position = undefined; this.weight = undefined; this.fullSize = undefined; } update(maxWidth, maxHeight, margins) { this.maxWidth = maxWidth; this.maxHeight = maxHeight; this._margins = margins; this.setDimensions(); this.buildLabels(); this.fit(); } setDimensions() { if (this.isHorizontal()) { this.width = this.maxWidth; this.left = this._margins.left; this.right = this.width; } else { this.height = this.maxHeight; this.top = this._margins.top; this.bottom = this.height; } } buildLabels() { const labelOpts = this.options.labels || {}; let legendItems = call(labelOpts.generateLabels, [this.chart], this) || []; if (labelOpts.filter) { legendItems = legendItems.filter((item) => labelOpts.filter(item, this.chart.data)); } if (labelOpts.sort) { legendItems = legendItems.sort((a, b) => labelOpts.sort(a, b, this.chart.data)); } if (this.options.reverse) { legendItems.reverse(); } this.legendItems = legendItems; } fit() { const {options, ctx} = this; // The legend may not be displayed for a variety of reasons including // the fact that the defaults got set to `false`. // When the legend is not displayed, there are no guarantees that the options // are correctly formatted so we need to bail out as early as possible. if (!options.display) { this.width = this.height = 0; return; } const labelOpts = options.labels; const labelFont = toFont(labelOpts.font); const fontSize = labelFont.size; const titleHeight = this._computeTitleHeight(); const {boxWidth, itemHeight} = getBoxSize(labelOpts, fontSize); let width, height; ctx.font = labelFont.string; if (this.isHorizontal()) { width = this.maxWidth; // fill all the width height = this._fitRows(titleHeight, fontSize, boxWidth, itemHeight) + 10; } else { height = this.maxHeight; // fill all the height width = this._fitCols(titleHeight, labelFont, boxWidth, itemHeight) + 10; } this.width = Math.min(width, options.maxWidth || this.maxWidth); this.height = Math.min(height, options.maxHeight || this.maxHeight); } /** * @private */ _fitRows(titleHeight, fontSize, boxWidth, itemHeight) { const {ctx, maxWidth, options: {labels: {padding}}} = this; const hitboxes = this.legendHitBoxes = []; // Width of each line of legend boxes. Labels wrap onto multiple lines when there are too many to fit on one const lineWidths = this.lineWidths = [0]; const lineHeight = itemHeight + padding; let totalHeight = titleHeight; ctx.textAlign = 'left'; ctx.textBaseline = 'middle'; let row = -1; let top = -lineHeight; this.legendItems.forEach((legendItem, i) => { const itemWidth = boxWidth + (fontSize / 2) + ctx.measureText(legendItem.text).width; if (i === 0 || lineWidths[lineWidths.length - 1] + itemWidth + 2 * padding > maxWidth) { totalHeight += lineHeight; lineWidths[lineWidths.length - (i > 0 ? 0 : 1)] = 0; top += lineHeight; row++; } hitboxes[i] = {left: 0, top, row, width: itemWidth, height: itemHeight}; lineWidths[lineWidths.length - 1] += itemWidth + padding; }); return totalHeight; } _fitCols(titleHeight, labelFont, boxWidth, _itemHeight) { const {ctx, maxHeight, options: {labels: {padding}}} = this; const hitboxes = this.legendHitBoxes = []; const columnSizes = this.columnSizes = []; const heightLimit = maxHeight - titleHeight; let totalWidth = padding; let currentColWidth = 0; let currentColHeight = 0; let left = 0; let col = 0; this.legendItems.forEach((legendItem, i) => { const {itemWidth, itemHeight} = calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight); // If too tall, go to new column if (i > 0 && currentColHeight + itemHeight + 2 * padding > heightLimit) { totalWidth += currentColWidth + padding; columnSizes.push({width: currentColWidth, height: currentColHeight}); // previous column size left += currentColWidth + padding; col++; currentColWidth = currentColHeight = 0; } // Store the hitbox width and height here. Final position will be updated in `draw` hitboxes[i] = {left, top: currentColHeight, col, width: itemWidth, height: itemHeight}; // Get max width currentColWidth = Math.max(currentColWidth, itemWidth); currentColHeight += itemHeight + padding; }); totalWidth += currentColWidth; columnSizes.push({width: currentColWidth, height: currentColHeight}); // previous column size return totalWidth; } adjustHitBoxes() { if (!this.options.display) { return; } const titleHeight = this._computeTitleHeight(); const {legendHitBoxes: hitboxes, options: {align, labels: {padding}, rtl}} = this; const rtlHelper = getRtlAdapter(rtl, this.left, this.width); if (this.isHorizontal()) { let row = 0; let left = _alignStartEnd(align, this.left + padding, this.right - this.lineWidths[row]); for (const hitbox of hitboxes) { if (row !== hitbox.row) { row = hitbox.row; left = _alignStartEnd(align, this.left + padding, this.right - this.lineWidths[row]); } hitbox.top += this.top + titleHeight + padding; hitbox.left = rtlHelper.leftForLtr(rtlHelper.x(left), hitbox.width); left += hitbox.width + padding; } } else { let col = 0; let top = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - this.columnSizes[col].height); for (const hitbox of hitboxes) { if (hitbox.col !== col) { col = hitbox.col; top = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - this.columnSizes[col].height); } hitbox.top = top; hitbox.left += this.left + padding; hitbox.left = rtlHelper.leftForLtr(rtlHelper.x(hitbox.left), hitbox.width); top += hitbox.height + padding; } } } isHorizontal() { return this.options.position === 'top' || this.options.position === 'bottom'; } draw() { if (this.options.display) { const ctx = this.ctx; clipArea(ctx, this); this._draw(); unclipArea(ctx); } } /** * @private */ _draw() { const {options: opts, columnSizes, lineWidths, ctx} = this; const {align, labels: labelOpts} = opts; const defaultColor = defaults.color; const rtlHelper = getRtlAdapter(opts.rtl, this.left, this.width); const labelFont = toFont(labelOpts.font); const {padding} = labelOpts; const fontSize = labelFont.size; const halfFontSize = fontSize / 2; let cursor; this.drawTitle(); // Canvas setup ctx.textAlign = rtlHelper.textAlign('left'); ctx.textBaseline = 'middle'; ctx.lineWidth = 0.5; ctx.font = labelFont.string; const {boxWidth, boxHeight, itemHeight} = getBoxSize(labelOpts, fontSize); // current position const drawLegendBox = function(x, y, legendItem) { if (isNaN(boxWidth) || boxWidth <= 0 || isNaN(boxHeight) || boxHeight < 0) { return; } // Set the ctx for the box ctx.save(); const lineWidth = valueOrDefault(legendItem.lineWidth, 1); ctx.fillStyle = valueOrDefault(legendItem.fillStyle, defaultColor); ctx.lineCap = valueOrDefault(legendItem.lineCap, 'butt'); ctx.lineDashOffset = valueOrDefault(legendItem.lineDashOffset, 0); ctx.lineJoin = valueOrDefault(legendItem.lineJoin, 'miter'); ctx.lineWidth = lineWidth; ctx.strokeStyle = valueOrDefault(legendItem.strokeStyle, defaultColor); ctx.setLineDash(valueOrDefault(legendItem.lineDash, [])); if (labelOpts.usePointStyle) { // Recalculate x and y for drawPoint() because its expecting // x and y to be center of figure (instead of top left) const drawOptions = { radius: boxHeight * Math.SQRT2 / 2, pointStyle: legendItem.pointStyle, rotation: legendItem.rotation, borderWidth: lineWidth }; const centerX = rtlHelper.xPlus(x, boxWidth / 2); const centerY = y + halfFontSize; // Draw pointStyle as legend symbol drawPointLegend(ctx, drawOptions, centerX, centerY, labelOpts.pointStyleWidth && boxWidth); } else { // Draw box as legend symbol // Adjust position when boxHeight < fontSize (want it centered) const yBoxTop = y + Math.max((fontSize - boxHeight) / 2, 0); const xBoxLeft = rtlHelper.leftForLtr(x, boxWidth); const borderRadius = toTRBLCorners(legendItem.borderRadius); ctx.beginPath(); if (Object.values(borderRadius).some(v => v !== 0)) { addRoundedRectPath(ctx, { x: xBoxLeft, y: yBoxTop, w: boxWidth, h: boxHeight, radius: borderRadius, }); } else { ctx.rect(xBoxLeft, yBoxTop, boxWidth, boxHeight); } ctx.fill(); if (lineWidth !== 0) { ctx.stroke(); } } ctx.restore(); }; const fillText = function(x, y, legendItem) { renderText(ctx, legendItem.text, x, y + (itemHeight / 2), labelFont, { strikethrough: legendItem.hidden, textAlign: rtlHelper.textAlign(legendItem.textAlign) }); }; // Horizontal const isHorizontal = this.isHorizontal(); const titleHeight = this._computeTitleHeight(); if (isHorizontal) { cursor = { x: _alignStartEnd(align, this.left + padding, this.right - lineWidths[0]), y: this.top + padding + titleHeight, line: 0 }; } else { cursor = { x: this.left + padding, y: _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - columnSizes[0].height), line: 0 }; } overrideTextDirection(this.ctx, opts.textDirection); const lineHeight = itemHeight + padding; this.legendItems.forEach((legendItem, i) => { ctx.strokeStyle = legendItem.fontColor; // for strikethrough effect ctx.fillStyle = legendItem.fontColor; // render in correct colour const textWidth = ctx.measureText(legendItem.text).width; const textAlign = rtlHelper.textAlign(legendItem.textAlign || (legendItem.textAlign = labelOpts.textAlign)); const width = boxWidth + halfFontSize + textWidth; let x = cursor.x; let y = cursor.y; rtlHelper.setWidth(this.width); if (isHorizontal) { if (i > 0 && x + width + padding > this.right) { y = cursor.y += lineHeight; cursor.line++; x = cursor.x = _alignStartEnd(align, this.left + padding, this.right - lineWidths[cursor.line]); } } else if (i > 0 && y + lineHeight > this.bottom) { x = cursor.x = x + columnSizes[cursor.line].width + padding; cursor.line++; y = cursor.y = _alignStartEnd(align, this.top + titleHeight + padding, this.bottom - columnSizes[cursor.line].height); } const realX = rtlHelper.x(x); drawLegendBox(realX, y, legendItem); x = _textX(textAlign, x + boxWidth + halfFontSize, isHorizontal ? x + width : this.right, opts.rtl); // Fill the actual label fillText(rtlHelper.x(x), y, legendItem); if (isHorizontal) { cursor.x += width + padding; } else if (typeof legendItem.text !== 'string') { const fontLineHeight = labelFont.lineHeight; cursor.y += calculateLegendItemHeight(legendItem, fontLineHeight) + padding; } else { cursor.y += lineHeight; } }); restoreTextDirection(this.ctx, opts.textDirection); } /** * @protected */ drawTitle() { const opts = this.options; const titleOpts = opts.title; const titleFont = toFont(titleOpts.font); const titlePadding = toPadding(titleOpts.padding); if (!titleOpts.display) { return; } const rtlHelper = getRtlAdapter(opts.rtl, this.left, this.width); const ctx = this.ctx; const position = titleOpts.position; const halfFontSize = titleFont.size / 2; const topPaddingPlusHalfFontSize = titlePadding.top + halfFontSize; let y; // These defaults are used when the legend is vertical. // When horizontal, they are computed below. let left = this.left; let maxWidth = this.width; if (this.isHorizontal()) { // Move left / right so that the title is above the legend lines maxWidth = Math.max(...this.lineWidths); y = this.top + topPaddingPlusHalfFontSize; left = _alignStartEnd(opts.align, left, this.right - maxWidth); } else { // Move down so that the title is above the legend stack in every alignment const maxHeight = this.columnSizes.reduce((acc, size) => Math.max(acc, size.height), 0); y = topPaddingPlusHalfFontSize + _alignStartEnd(opts.align, this.top, this.bottom - maxHeight - opts.labels.padding - this._computeTitleHeight()); } // Now that we know the left edge of the inner legend box, compute the correct // X coordinate from the title alignment const x = _alignStartEnd(position, left, left + maxWidth); // Canvas setup ctx.textAlign = rtlHelper.textAlign(_toLeftRightCenter(position)); ctx.textBaseline = 'middle'; ctx.strokeStyle = titleOpts.color; ctx.fillStyle = titleOpts.color; ctx.font = titleFont.string; renderText(ctx, titleOpts.text, x, y, titleFont); } /** * @private */ _computeTitleHeight() { const titleOpts = this.options.title; const titleFont = toFont(titleOpts.font); const titlePadding = toPadding(titleOpts.padding); return titleOpts.display ? titleFont.lineHeight + titlePadding.height : 0; } /** * @private */ _getLegendItemAt(x, y) { let i, hitBox, lh; if (_isBetween(x, this.left, this.right) && _isBetween(y, this.top, this.bottom)) { // See if we are touching one of the dataset boxes lh = this.legendHitBoxes; for (i = 0; i < lh.length; ++i) { hitBox = lh[i]; if (_isBetween(x, hitBox.left, hitBox.left + hitBox.width) && _isBetween(y, hitBox.top, hitBox.top + hitBox.height)) { // Touching an element return this.legendItems[i]; } } } return null; } /** * Handle an event * @param {ChartEvent} e - The event to handle */ handleEvent(e) { const opts = this.options; if (!isListened(e.type, opts)) { return; } // Chart event already has relative position in it const hoveredItem = this._getLegendItemAt(e.x, e.y); if (e.type === 'mousemove' || e.type === 'mouseout') { const previous = this._hoveredItem; const sameItem = itemsEqual(previous, hoveredItem); if (previous && !sameItem) { call(opts.onLeave, [e, previous, this], this); } this._hoveredItem = hoveredItem; if (hoveredItem && !sameItem) { call(opts.onHover, [e, hoveredItem, this], this); } } else if (hoveredItem) { call(opts.onClick, [e, hoveredItem, this], this); } } } function calculateItemSize(boxWidth, labelFont, ctx, legendItem, _itemHeight) { const itemWidth = calculateItemWidth(legendItem, boxWidth, labelFont, ctx); const itemHeight = calculateItemHeight(_itemHeight, legendItem, labelFont.lineHeight); return {itemWidth, itemHeight}; } function calculateItemWidth(legendItem, boxWidth, labelFont, ctx) { let legendItemText = legendItem.text; if (legendItemText && typeof legendItemText !== 'string') { legendItemText = legendItemText.reduce((a, b) => a.length > b.length ? a : b); } return boxWidth + (labelFont.size / 2) + ctx.measureText(legendItemText).width; } function calculateItemHeight(_itemHeight, legendItem, fontLineHeight) { let itemHeight = _itemHeight; if (typeof legendItem.text !== 'string') { itemHeight = calculateLegendItemHeight(legendItem, fontLineHeight); } return itemHeight; } function calculateLegendItemHeight(legendItem, fontLineHeight) { const labelHeight = legendItem.text ? legendItem.text.length : 0; return fontLineHeight * labelHeight; } function isListened(type, opts) { if ((type === 'mousemove' || type === 'mouseout') && (opts.onHover || opts.onLeave)) { return true; } if (opts.onClick && (type === 'click' || type === 'mouseup')) { return true; } return false; } export default { id: 'legend', /** * For tests * @private */ _element: Legend, start(chart, _args, options) { const legend = chart.legend = new Legend({ctx: chart.ctx, options, chart}); layouts.configure(chart, legend, options); layouts.addBox(chart, legend); }, stop(chart) { layouts.removeBox(chart, chart.legend); delete chart.legend; }, // During the beforeUpdate step, the layout configuration needs to run // This ensures that if the legend position changes (via an option update) // the layout system respects the change. See https://github.com/chartjs/Chart.js/issues/7527 beforeUpdate(chart, _args, options) { const legend = chart.legend; layouts.configure(chart, legend, options); legend.options = options; }, // The labels need to be built after datasets are updated to ensure that colors // and other styling are correct. See https://github.com/chartjs/Chart.js/issues/6968 afterUpdate(chart) { const legend = chart.legend; legend.buildLabels(); legend.adjustHitBoxes(); }, afterEvent(chart, args) { if (!args.replay) { chart.legend.handleEvent(args.event); } }, defaults: { display: true, position: 'top', align: 'center', fullSize: true, reverse: false, weight: 1000, // a callback that will handle onClick(e, legendItem, legend) { const index = legendItem.datasetIndex; const ci = legend.chart; if (ci.isDatasetVisible(index)) { ci.hide(index); legendItem.hidden = true; } else { ci.show(index); legendItem.hidden = false; } }, onHover: null, onLeave: null, labels: { color: (ctx) => ctx.chart.options.color, boxWidth: 40, padding: 10, // Generates labels shown in the legend // Valid properties to return: // text : text to display // fillStyle : fill of coloured box // strokeStyle: stroke of coloured box // hidden : if this legend item refers to a hidden item // lineCap : cap style for line // lineDash // lineDashOffset : // lineJoin : // lineWidth : generateLabels(chart) { const datasets = chart.data.datasets; const {labels: {usePointStyle, pointStyle, textAlign, color, useBorderRadius, borderRadius}} = chart.legend.options; return chart._getSortedDatasetMetas().map((meta) => { const style = meta.controller.getStyle(usePointStyle ? 0 : undefined); const borderWidth = toPadding(style.borderWidth); return { text: datasets[meta.index].label, fillStyle: style.backgroundColor, fontColor: color, hidden: !meta.visible, lineCap: style.borderCapStyle, lineDash: style.borderDash, lineDashOffset: style.borderDashOffset, lineJoin: style.borderJoinStyle, lineWidth: (borderWidth.width + borderWidth.height) / 4, strokeStyle: style.borderColor, pointStyle: pointStyle || style.pointStyle, rotation: style.rotation, textAlign: textAlign || style.textAlign, borderRadius: useBorderRadius && (borderRadius || style.borderRadius), // Below is extra data used for toggling the datasets datasetIndex: meta.index }; }, this); } }, title: { color: (ctx) => ctx.chart.options.color, display: false, position: 'center', text: '', } }, descriptors: { _scriptable: (name) => !name.startsWith('on'), labels: { _scriptable: (name) => !['generateLabels', 'filter', 'sort'].includes(name), } }, }; ================================================ FILE: src/plugins/plugin.subtitle.js ================================================ import {Title} from './plugin.title.js'; import layouts from '../core/core.layouts.js'; const map = new WeakMap(); export default { id: 'subtitle', start(chart, _args, options) { const title = new Title({ ctx: chart.ctx, options, chart }); layouts.configure(chart, title, options); layouts.addBox(chart, title); map.set(chart, title); }, stop(chart) { layouts.removeBox(chart, map.get(chart)); map.delete(chart); }, beforeUpdate(chart, _args, options) { const title = map.get(chart); layouts.configure(chart, title, options); title.options = options; }, defaults: { align: 'center', display: false, font: { weight: 'normal', }, fullSize: true, padding: 0, position: 'top', text: '', weight: 1500 // by default greater than legend (1000) and smaller than title (2000) }, defaultRoutes: { color: 'color' }, descriptors: { _scriptable: true, _indexable: false, }, }; ================================================ FILE: src/plugins/plugin.title.js ================================================ import Element from '../core/core.element.js'; import layouts from '../core/core.layouts.js'; import {PI, isArray, toPadding, toFont} from '../helpers/index.js'; import {_toLeftRightCenter, _alignStartEnd} from '../helpers/helpers.extras.js'; import {renderText} from '../helpers/helpers.canvas.js'; export class Title extends Element { /** * @param {{ ctx: any; options: any; chart: any; }} config */ constructor(config) { super(); this.chart = config.chart; this.options = config.options; this.ctx = config.ctx; this._padding = undefined; this.top = undefined; this.bottom = undefined; this.left = undefined; this.right = undefined; this.width = undefined; this.height = undefined; this.position = undefined; this.weight = undefined; this.fullSize = undefined; } update(maxWidth, maxHeight) { const opts = this.options; this.left = 0; this.top = 0; if (!opts.display) { this.width = this.height = this.right = this.bottom = 0; return; } this.width = this.right = maxWidth; this.height = this.bottom = maxHeight; const lineCount = isArray(opts.text) ? opts.text.length : 1; this._padding = toPadding(opts.padding); const textSize = lineCount * toFont(opts.font).lineHeight + this._padding.height; if (this.isHorizontal()) { this.height = textSize; } else { this.width = textSize; } } isHorizontal() { const pos = this.options.position; return pos === 'top' || pos === 'bottom'; } _drawArgs(offset) { const {top, left, bottom, right, options} = this; const align = options.align; let rotation = 0; let maxWidth, titleX, titleY; if (this.isHorizontal()) { titleX = _alignStartEnd(align, left, right); titleY = top + offset; maxWidth = right - left; } else { if (options.position === 'left') { titleX = left + offset; titleY = _alignStartEnd(align, bottom, top); rotation = PI * -0.5; } else { titleX = right - offset; titleY = _alignStartEnd(align, top, bottom); rotation = PI * 0.5; } maxWidth = bottom - top; } return {titleX, titleY, maxWidth, rotation}; } draw() { const ctx = this.ctx; const opts = this.options; if (!opts.display) { return; } const fontOpts = toFont(opts.font); const lineHeight = fontOpts.lineHeight; const offset = lineHeight / 2 + this._padding.top; const {titleX, titleY, maxWidth, rotation} = this._drawArgs(offset); renderText(ctx, opts.text, 0, 0, fontOpts, { color: opts.color, maxWidth, rotation, textAlign: _toLeftRightCenter(opts.align), textBaseline: 'middle', translation: [titleX, titleY], }); } } function createTitle(chart, titleOpts) { const title = new Title({ ctx: chart.ctx, options: titleOpts, chart }); layouts.configure(chart, title, titleOpts); layouts.addBox(chart, title); chart.titleBlock = title; } export default { id: 'title', /** * For tests * @private */ _element: Title, start(chart, _args, options) { createTitle(chart, options); }, stop(chart) { const titleBlock = chart.titleBlock; layouts.removeBox(chart, titleBlock); delete chart.titleBlock; }, beforeUpdate(chart, _args, options) { const title = chart.titleBlock; layouts.configure(chart, title, options); title.options = options; }, defaults: { align: 'center', display: false, font: { weight: 'bold', }, fullSize: true, padding: 10, position: 'top', text: '', weight: 2000 // by default greater than legend (1000) to be above }, defaultRoutes: { color: 'color' }, descriptors: { _scriptable: true, _indexable: false, }, }; ================================================ FILE: src/plugins/plugin.tooltip.js ================================================ import Animations from '../core/core.animations.js'; import Element from '../core/core.element.js'; import {addRoundedRectPath} from '../helpers/helpers.canvas.js'; import {each, noop, isNullOrUndef, isArray, _elementsEqual, isObject} from '../helpers/helpers.core.js'; import {toFont, toPadding, toTRBLCorners} from '../helpers/helpers.options.js'; import {getRtlAdapter, overrideTextDirection, restoreTextDirection} from '../helpers/helpers.rtl.js'; import {distanceBetweenPoints, _limitValue} from '../helpers/helpers.math.js'; import {createContext, drawPoint} from '../helpers/index.js'; /** * @typedef { import('../platform/platform.base.js').Chart } Chart * @typedef { import('../types/index.js').ChartEvent } ChartEvent * @typedef { import('../types/index.js').ActiveElement } ActiveElement * @typedef { import('../core/core.interaction.js').InteractionItem } InteractionItem */ const positioners = { /** * Average mode places the tooltip at the average position of the elements shown */ average(items) { if (!items.length) { return false; } let i, len; let xSet = new Set(); let y = 0; let count = 0; for (i = 0, len = items.length; i < len; ++i) { const el = items[i].element; if (el && el.hasValue()) { const pos = el.tooltipPosition(); xSet.add(pos.x); y += pos.y; ++count; } } // No visible items where found, return false so we don't have to divide by 0 which reduces in NaN if (count === 0 || xSet.size === 0) { return false; } const xAverage = [...xSet].reduce((a, b) => a + b) / xSet.size; return { x: xAverage, y: y / count }; }, /** * Gets the tooltip position nearest of the item nearest to the event position */ nearest(items, eventPosition) { if (!items.length) { return false; } let x = eventPosition.x; let y = eventPosition.y; let minDistance = Number.POSITIVE_INFINITY; let i, len, nearestElement; for (i = 0, len = items.length; i < len; ++i) { const el = items[i].element; if (el && el.hasValue()) { const center = el.getCenterPoint(); const d = distanceBetweenPoints(eventPosition, center); if (d < minDistance) { minDistance = d; nearestElement = el; } } } if (nearestElement) { const tp = nearestElement.tooltipPosition(); x = tp.x; y = tp.y; } return { x, y }; } }; // Helper to push or concat based on if the 2nd parameter is an array or not function pushOrConcat(base, toPush) { if (toPush) { if (isArray(toPush)) { // base = base.concat(toPush); Array.prototype.push.apply(base, toPush); } else { base.push(toPush); } } return base; } /** * Returns array of strings split by newline * @param {*} str - The value to split by newline. * @returns {string|string[]} value if newline present - Returned from String split() method * @function */ function splitNewlines(str) { if ((typeof str === 'string' || str instanceof String) && str.indexOf('\n') > -1) { return str.split('\n'); } return str; } /** * Private helper to create a tooltip item model * @param {Chart} chart * @param {ActiveElement} item - {element, index, datasetIndex} to create the tooltip item for * @return new tooltip item */ function createTooltipItem(chart, item) { const {element, datasetIndex, index} = item; const controller = chart.getDatasetMeta(datasetIndex).controller; const {label, value} = controller.getLabelAndValue(index); return { chart, label, parsed: controller.getParsed(index), raw: chart.data.datasets[datasetIndex].data[index], formattedValue: value, dataset: controller.getDataset(), dataIndex: index, datasetIndex, element }; } /** * Get the size of the tooltip */ function getTooltipSize(tooltip, options) { const ctx = tooltip.chart.ctx; const {body, footer, title} = tooltip; const {boxWidth, boxHeight} = options; const bodyFont = toFont(options.bodyFont); const titleFont = toFont(options.titleFont); const footerFont = toFont(options.footerFont); const titleLineCount = title.length; const footerLineCount = footer.length; const bodyLineItemCount = body.length; const padding = toPadding(options.padding); let height = padding.height; let width = 0; // Count of all lines in the body let combinedBodyLength = body.reduce((count, bodyItem) => count + bodyItem.before.length + bodyItem.lines.length + bodyItem.after.length, 0); combinedBodyLength += tooltip.beforeBody.length + tooltip.afterBody.length; if (titleLineCount) { height += titleLineCount * titleFont.lineHeight + (titleLineCount - 1) * options.titleSpacing + options.titleMarginBottom; } if (combinedBodyLength) { // Body lines may include some extra height depending on boxHeight const bodyLineHeight = options.displayColors ? Math.max(boxHeight, bodyFont.lineHeight) : bodyFont.lineHeight; height += bodyLineItemCount * bodyLineHeight + (combinedBodyLength - bodyLineItemCount) * bodyFont.lineHeight + (combinedBodyLength - 1) * options.bodySpacing; } if (footerLineCount) { height += options.footerMarginTop + footerLineCount * footerFont.lineHeight + (footerLineCount - 1) * options.footerSpacing; } // Title width let widthPadding = 0; const maxLineWidth = function(line) { width = Math.max(width, ctx.measureText(line).width + widthPadding); }; ctx.save(); ctx.font = titleFont.string; each(tooltip.title, maxLineWidth); // Body width ctx.font = bodyFont.string; each(tooltip.beforeBody.concat(tooltip.afterBody), maxLineWidth); // Body lines may include some extra width due to the color box widthPadding = options.displayColors ? (boxWidth + 2 + options.boxPadding) : 0; each(body, (bodyItem) => { each(bodyItem.before, maxLineWidth); each(bodyItem.lines, maxLineWidth); each(bodyItem.after, maxLineWidth); }); // Reset back to 0 widthPadding = 0; // Footer width ctx.font = footerFont.string; each(tooltip.footer, maxLineWidth); ctx.restore(); // Add padding width += padding.width; return {width, height}; } function determineYAlign(chart, size) { const {y, height} = size; if (y < height / 2) { return 'top'; } else if (y > (chart.height - height / 2)) { return 'bottom'; } return 'center'; } function doesNotFitWithAlign(xAlign, chart, options, size) { const {x, width} = size; const caret = options.caretSize + options.caretPadding; if (xAlign === 'left' && x + width + caret > chart.width) { return true; } if (xAlign === 'right' && x - width - caret < 0) { return true; } } function determineXAlign(chart, options, size, yAlign) { const {x, width} = size; const {width: chartWidth, chartArea: {left, right}} = chart; let xAlign = 'center'; if (yAlign === 'center') { xAlign = x <= (left + right) / 2 ? 'left' : 'right'; } else if (x <= width / 2) { xAlign = 'left'; } else if (x >= chartWidth - width / 2) { xAlign = 'right'; } if (doesNotFitWithAlign(xAlign, chart, options, size)) { xAlign = 'center'; } return xAlign; } /** * Helper to get the alignment of a tooltip given the size */ function determineAlignment(chart, options, size) { const yAlign = size.yAlign || options.yAlign || determineYAlign(chart, size); return { xAlign: size.xAlign || options.xAlign || determineXAlign(chart, options, size, yAlign), yAlign }; } function alignX(size, xAlign) { let {x, width} = size; if (xAlign === 'right') { x -= width; } else if (xAlign === 'center') { x -= (width / 2); } return x; } function alignY(size, yAlign, paddingAndSize) { // eslint-disable-next-line prefer-const let {y, height} = size; if (yAlign === 'top') { y += paddingAndSize; } else if (yAlign === 'bottom') { y -= height + paddingAndSize; } else { y -= (height / 2); } return y; } /** * Helper to get the location a tooltip needs to be placed at given the initial position (via the vm) and the size and alignment */ function getBackgroundPoint(options, size, alignment, chart) { const {caretSize, caretPadding, cornerRadius} = options; const {xAlign, yAlign} = alignment; const paddingAndSize = caretSize + caretPadding; const {topLeft, topRight, bottomLeft, bottomRight} = toTRBLCorners(cornerRadius); let x = alignX(size, xAlign); const y = alignY(size, yAlign, paddingAndSize); if (yAlign === 'center') { if (xAlign === 'left') { x += paddingAndSize; } else if (xAlign === 'right') { x -= paddingAndSize; } } else if (xAlign === 'left') { x -= Math.max(topLeft, bottomLeft) + caretSize; } else if (xAlign === 'right') { x += Math.max(topRight, bottomRight) + caretSize; } return { x: _limitValue(x, 0, chart.width - size.width), y: _limitValue(y, 0, chart.height - size.height) }; } function getAlignedX(tooltip, align, options) { const padding = toPadding(options.padding); return align === 'center' ? tooltip.x + tooltip.width / 2 : align === 'right' ? tooltip.x + tooltip.width - padding.right : tooltip.x + padding.left; } /** * Helper to build before and after body lines */ function getBeforeAfterBodyLines(callback) { return pushOrConcat([], splitNewlines(callback)); } function createTooltipContext(parent, tooltip, tooltipItems) { return createContext(parent, { tooltip, tooltipItems, type: 'tooltip' }); } function overrideCallbacks(callbacks, context) { const override = context && context.dataset && context.dataset.tooltip && context.dataset.tooltip.callbacks; return override ? callbacks.override(override) : callbacks; } const defaultCallbacks = { // Args are: (tooltipItems, data) beforeTitle: noop, title(tooltipItems) { if (tooltipItems.length > 0) { const item = tooltipItems[0]; const labels = item.chart.data.labels; const labelCount = labels ? labels.length : 0; if (this && this.options && this.options.mode === 'dataset') { return item.dataset.label || ''; } else if (item.label) { return item.label; } else if (labelCount > 0 && item.dataIndex < labelCount) { return labels[item.dataIndex]; } } return ''; }, afterTitle: noop, // Args are: (tooltipItems, data) beforeBody: noop, // Args are: (tooltipItem, data) beforeLabel: noop, label(tooltipItem) { if (this && this.options && this.options.mode === 'dataset') { return tooltipItem.label + ': ' + tooltipItem.formattedValue || tooltipItem.formattedValue; } let label = tooltipItem.dataset.label || ''; if (label) { label += ': '; } const value = tooltipItem.formattedValue; if (!isNullOrUndef(value)) { label += value; } return label; }, labelColor(tooltipItem) { const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); const options = meta.controller.getStyle(tooltipItem.dataIndex); return { borderColor: options.borderColor, backgroundColor: options.backgroundColor, borderWidth: options.borderWidth, borderDash: options.borderDash, borderDashOffset: options.borderDashOffset, borderRadius: 0, }; }, labelTextColor() { return this.options.bodyColor; }, labelPointStyle(tooltipItem) { const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); const options = meta.controller.getStyle(tooltipItem.dataIndex); return { pointStyle: options.pointStyle, rotation: options.rotation, }; }, afterLabel: noop, // Args are: (tooltipItems, data) afterBody: noop, // Args are: (tooltipItems, data) beforeFooter: noop, footer: noop, afterFooter: noop }; /** * Invoke callback from object with context and arguments. * If callback returns `undefined`, then will be invoked default callback. * @param {Record} callbacks * @param {keyof typeof defaultCallbacks} name * @param {*} ctx * @param {*} arg * @returns {any} */ function invokeCallbackWithFallback(callbacks, name, ctx, arg) { const result = callbacks[name].call(ctx, arg); if (typeof result === 'undefined') { return defaultCallbacks[name].call(ctx, arg); } return result; } export class Tooltip extends Element { /** * @namespace Chart.Tooltip.positioners */ static positioners = positioners; constructor(config) { super(); this.opacity = 0; this._active = []; this._eventPosition = undefined; this._size = undefined; this._cachedAnimations = undefined; this._tooltipItems = []; this.$animations = undefined; this.$context = undefined; this.chart = config.chart; this.options = config.options; this.dataPoints = undefined; this.title = undefined; this.beforeBody = undefined; this.body = undefined; this.afterBody = undefined; this.footer = undefined; this.xAlign = undefined; this.yAlign = undefined; this.x = undefined; this.y = undefined; this.height = undefined; this.width = undefined; this.caretX = undefined; this.caretY = undefined; // TODO: V4, make this private, rename to `_labelStyles`, and combine with `labelPointStyles` // and `labelTextColors` to create a single variable this.labelColors = undefined; this.labelPointStyles = undefined; this.labelTextColors = undefined; } initialize(options) { this.options = options; this._cachedAnimations = undefined; this.$context = undefined; } /** * @private */ _resolveAnimations() { const cached = this._cachedAnimations; if (cached) { return cached; } const chart = this.chart; const options = this.options.setContext(this.getContext()); const opts = options.enabled && chart.options.animation && options.animations; const animations = new Animations(this.chart, opts); if (opts._cacheable) { this._cachedAnimations = Object.freeze(animations); } return animations; } /** * @protected */ getContext() { return this.$context || (this.$context = createTooltipContext(this.chart.getContext(), this, this._tooltipItems)); } getTitle(context, options) { const {callbacks} = options; const beforeTitle = invokeCallbackWithFallback(callbacks, 'beforeTitle', this, context); const title = invokeCallbackWithFallback(callbacks, 'title', this, context); const afterTitle = invokeCallbackWithFallback(callbacks, 'afterTitle', this, context); let lines = []; lines = pushOrConcat(lines, splitNewlines(beforeTitle)); lines = pushOrConcat(lines, splitNewlines(title)); lines = pushOrConcat(lines, splitNewlines(afterTitle)); return lines; } getBeforeBody(tooltipItems, options) { return getBeforeAfterBodyLines( invokeCallbackWithFallback(options.callbacks, 'beforeBody', this, tooltipItems) ); } getBody(tooltipItems, options) { const {callbacks} = options; const bodyItems = []; each(tooltipItems, (context) => { const bodyItem = { before: [], lines: [], after: [] }; const scoped = overrideCallbacks(callbacks, context); pushOrConcat(bodyItem.before, splitNewlines(invokeCallbackWithFallback(scoped, 'beforeLabel', this, context))); pushOrConcat(bodyItem.lines, invokeCallbackWithFallback(scoped, 'label', this, context)); pushOrConcat(bodyItem.after, splitNewlines(invokeCallbackWithFallback(scoped, 'afterLabel', this, context))); bodyItems.push(bodyItem); }); return bodyItems; } getAfterBody(tooltipItems, options) { return getBeforeAfterBodyLines( invokeCallbackWithFallback(options.callbacks, 'afterBody', this, tooltipItems) ); } // Get the footer and beforeFooter and afterFooter lines getFooter(tooltipItems, options) { const {callbacks} = options; const beforeFooter = invokeCallbackWithFallback(callbacks, 'beforeFooter', this, tooltipItems); const footer = invokeCallbackWithFallback(callbacks, 'footer', this, tooltipItems); const afterFooter = invokeCallbackWithFallback(callbacks, 'afterFooter', this, tooltipItems); let lines = []; lines = pushOrConcat(lines, splitNewlines(beforeFooter)); lines = pushOrConcat(lines, splitNewlines(footer)); lines = pushOrConcat(lines, splitNewlines(afterFooter)); return lines; } /** * @private */ _createItems(options) { const active = this._active; const data = this.chart.data; const labelColors = []; const labelPointStyles = []; const labelTextColors = []; let tooltipItems = []; let i, len; for (i = 0, len = active.length; i < len; ++i) { tooltipItems.push(createTooltipItem(this.chart, active[i])); } // If the user provided a filter function, use it to modify the tooltip items if (options.filter) { tooltipItems = tooltipItems.filter((element, index, array) => options.filter(element, index, array, data)); } // If the user provided a sorting function, use it to modify the tooltip items if (options.itemSort) { tooltipItems = tooltipItems.sort((a, b) => options.itemSort(a, b, data)); } // Determine colors for boxes each(tooltipItems, (context) => { const scoped = overrideCallbacks(options.callbacks, context); labelColors.push(invokeCallbackWithFallback(scoped, 'labelColor', this, context)); labelPointStyles.push(invokeCallbackWithFallback(scoped, 'labelPointStyle', this, context)); labelTextColors.push(invokeCallbackWithFallback(scoped, 'labelTextColor', this, context)); }); this.labelColors = labelColors; this.labelPointStyles = labelPointStyles; this.labelTextColors = labelTextColors; this.dataPoints = tooltipItems; return tooltipItems; } update(changed, replay) { const options = this.options.setContext(this.getContext()); const active = this._active; let properties; let tooltipItems = []; if (!active.length) { if (this.opacity !== 0) { properties = { opacity: 0 }; } } else { const position = positioners[options.position].call(this, active, this._eventPosition); tooltipItems = this._createItems(options); this.title = this.getTitle(tooltipItems, options); this.beforeBody = this.getBeforeBody(tooltipItems, options); this.body = this.getBody(tooltipItems, options); this.afterBody = this.getAfterBody(tooltipItems, options); this.footer = this.getFooter(tooltipItems, options); const size = this._size = getTooltipSize(this, options); const positionAndSize = Object.assign({}, position, size); const alignment = determineAlignment(this.chart, options, positionAndSize); const backgroundPoint = getBackgroundPoint(options, positionAndSize, alignment, this.chart); this.xAlign = alignment.xAlign; this.yAlign = alignment.yAlign; properties = { opacity: 1, x: backgroundPoint.x, y: backgroundPoint.y, width: size.width, height: size.height, caretX: position.x, caretY: position.y }; } this._tooltipItems = tooltipItems; this.$context = undefined; if (properties) { this._resolveAnimations().update(this, properties); } if (changed && options.external) { options.external.call(this, {chart: this.chart, tooltip: this, replay}); } } drawCaret(tooltipPoint, ctx, size, options) { const caretPosition = this.getCaretPosition(tooltipPoint, size, options); ctx.lineTo(caretPosition.x1, caretPosition.y1); ctx.lineTo(caretPosition.x2, caretPosition.y2); ctx.lineTo(caretPosition.x3, caretPosition.y3); } getCaretPosition(tooltipPoint, size, options) { const {xAlign, yAlign} = this; const {caretSize, cornerRadius} = options; const {topLeft, topRight, bottomLeft, bottomRight} = toTRBLCorners(cornerRadius); const {x: ptX, y: ptY} = tooltipPoint; const {width, height} = size; let x1, x2, x3, y1, y2, y3; if (yAlign === 'center') { y2 = ptY + (height / 2); if (xAlign === 'left') { x1 = ptX; x2 = x1 - caretSize; // Left draws bottom -> top, this y1 is on the bottom y1 = y2 + caretSize; y3 = y2 - caretSize; } else { x1 = ptX + width; x2 = x1 + caretSize; // Right draws top -> bottom, thus y1 is on the top y1 = y2 - caretSize; y3 = y2 + caretSize; } x3 = x1; } else { if (xAlign === 'left') { x2 = ptX + Math.max(topLeft, bottomLeft) + (caretSize); } else if (xAlign === 'right') { x2 = ptX + width - Math.max(topRight, bottomRight) - caretSize; } else { x2 = this.caretX; } if (yAlign === 'top') { y1 = ptY; y2 = y1 - caretSize; // Top draws left -> right, thus x1 is on the left x1 = x2 - caretSize; x3 = x2 + caretSize; } else { y1 = ptY + height; y2 = y1 + caretSize; // Bottom draws right -> left, thus x1 is on the right x1 = x2 + caretSize; x3 = x2 - caretSize; } y3 = y1; } return {x1, x2, x3, y1, y2, y3}; } drawTitle(pt, ctx, options) { const title = this.title; const length = title.length; let titleFont, titleSpacing, i; if (length) { const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width); pt.x = getAlignedX(this, options.titleAlign, options); ctx.textAlign = rtlHelper.textAlign(options.titleAlign); ctx.textBaseline = 'middle'; titleFont = toFont(options.titleFont); titleSpacing = options.titleSpacing; ctx.fillStyle = options.titleColor; ctx.font = titleFont.string; for (i = 0; i < length; ++i) { ctx.fillText(title[i], rtlHelper.x(pt.x), pt.y + titleFont.lineHeight / 2); pt.y += titleFont.lineHeight + titleSpacing; // Line Height and spacing if (i + 1 === length) { pt.y += options.titleMarginBottom - titleSpacing; // If Last, add margin, remove spacing } } } } /** * @private */ _drawColorBox(ctx, pt, i, rtlHelper, options) { const labelColor = this.labelColors[i]; const labelPointStyle = this.labelPointStyles[i]; const {boxHeight, boxWidth} = options; const bodyFont = toFont(options.bodyFont); const colorX = getAlignedX(this, 'left', options); const rtlColorX = rtlHelper.x(colorX); const yOffSet = boxHeight < bodyFont.lineHeight ? (bodyFont.lineHeight - boxHeight) / 2 : 0; const colorY = pt.y + yOffSet; if (options.usePointStyle) { const drawOptions = { radius: Math.min(boxWidth, boxHeight) / 2, // fit the circle in the box pointStyle: labelPointStyle.pointStyle, rotation: labelPointStyle.rotation, borderWidth: 1 }; // Recalculate x and y for drawPoint() because its expecting // x and y to be center of figure (instead of top left) const centerX = rtlHelper.leftForLtr(rtlColorX, boxWidth) + boxWidth / 2; const centerY = colorY + boxHeight / 2; // Fill the point with white so that colours merge nicely if the opacity is < 1 ctx.strokeStyle = options.multiKeyBackground; ctx.fillStyle = options.multiKeyBackground; drawPoint(ctx, drawOptions, centerX, centerY); // Draw the point ctx.strokeStyle = labelColor.borderColor; ctx.fillStyle = labelColor.backgroundColor; drawPoint(ctx, drawOptions, centerX, centerY); } else { // Border ctx.lineWidth = isObject(labelColor.borderWidth) ? Math.max(...Object.values(labelColor.borderWidth)) : (labelColor.borderWidth || 1); // TODO, v4 remove fallback ctx.strokeStyle = labelColor.borderColor; ctx.setLineDash(labelColor.borderDash || []); ctx.lineDashOffset = labelColor.borderDashOffset || 0; // Fill a white rect so that colours merge nicely if the opacity is < 1 const outerX = rtlHelper.leftForLtr(rtlColorX, boxWidth); const innerX = rtlHelper.leftForLtr(rtlHelper.xPlus(rtlColorX, 1), boxWidth - 2); const borderRadius = toTRBLCorners(labelColor.borderRadius); if (Object.values(borderRadius).some(v => v !== 0)) { ctx.beginPath(); ctx.fillStyle = options.multiKeyBackground; addRoundedRectPath(ctx, { x: outerX, y: colorY, w: boxWidth, h: boxHeight, radius: borderRadius, }); ctx.fill(); ctx.stroke(); // Inner square ctx.fillStyle = labelColor.backgroundColor; ctx.beginPath(); addRoundedRectPath(ctx, { x: innerX, y: colorY + 1, w: boxWidth - 2, h: boxHeight - 2, radius: borderRadius, }); ctx.fill(); } else { // Normal rect ctx.fillStyle = options.multiKeyBackground; ctx.fillRect(outerX, colorY, boxWidth, boxHeight); ctx.strokeRect(outerX, colorY, boxWidth, boxHeight); // Inner square ctx.fillStyle = labelColor.backgroundColor; ctx.fillRect(innerX, colorY + 1, boxWidth - 2, boxHeight - 2); } } // restore fillStyle ctx.fillStyle = this.labelTextColors[i]; } drawBody(pt, ctx, options) { const {body} = this; const {bodySpacing, bodyAlign, displayColors, boxHeight, boxWidth, boxPadding} = options; const bodyFont = toFont(options.bodyFont); let bodyLineHeight = bodyFont.lineHeight; let xLinePadding = 0; const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width); const fillLineOfText = function(line) { ctx.fillText(line, rtlHelper.x(pt.x + xLinePadding), pt.y + bodyLineHeight / 2); pt.y += bodyLineHeight + bodySpacing; }; const bodyAlignForCalculation = rtlHelper.textAlign(bodyAlign); let bodyItem, textColor, lines, i, j, ilen, jlen; ctx.textAlign = bodyAlign; ctx.textBaseline = 'middle'; ctx.font = bodyFont.string; pt.x = getAlignedX(this, bodyAlignForCalculation, options); // Before body lines ctx.fillStyle = options.bodyColor; each(this.beforeBody, fillLineOfText); xLinePadding = displayColors && bodyAlignForCalculation !== 'right' ? bodyAlign === 'center' ? (boxWidth / 2 + boxPadding) : (boxWidth + 2 + boxPadding) : 0; // Draw body lines now for (i = 0, ilen = body.length; i < ilen; ++i) { bodyItem = body[i]; textColor = this.labelTextColors[i]; ctx.fillStyle = textColor; each(bodyItem.before, fillLineOfText); lines = bodyItem.lines; // Draw Legend-like boxes if needed if (displayColors && lines.length) { this._drawColorBox(ctx, pt, i, rtlHelper, options); bodyLineHeight = Math.max(bodyFont.lineHeight, boxHeight); } for (j = 0, jlen = lines.length; j < jlen; ++j) { fillLineOfText(lines[j]); // Reset for any lines that don't include colorbox bodyLineHeight = bodyFont.lineHeight; } each(bodyItem.after, fillLineOfText); } // Reset back to 0 for after body xLinePadding = 0; bodyLineHeight = bodyFont.lineHeight; // After body lines each(this.afterBody, fillLineOfText); pt.y -= bodySpacing; // Remove last body spacing } drawFooter(pt, ctx, options) { const footer = this.footer; const length = footer.length; let footerFont, i; if (length) { const rtlHelper = getRtlAdapter(options.rtl, this.x, this.width); pt.x = getAlignedX(this, options.footerAlign, options); pt.y += options.footerMarginTop; ctx.textAlign = rtlHelper.textAlign(options.footerAlign); ctx.textBaseline = 'middle'; footerFont = toFont(options.footerFont); ctx.fillStyle = options.footerColor; ctx.font = footerFont.string; for (i = 0; i < length; ++i) { ctx.fillText(footer[i], rtlHelper.x(pt.x), pt.y + footerFont.lineHeight / 2); pt.y += footerFont.lineHeight + options.footerSpacing; } } } drawBackground(pt, ctx, tooltipSize, options) { const {xAlign, yAlign} = this; const {x, y} = pt; const {width, height} = tooltipSize; const {topLeft, topRight, bottomLeft, bottomRight} = toTRBLCorners(options.cornerRadius); ctx.fillStyle = options.backgroundColor; ctx.strokeStyle = options.borderColor; ctx.lineWidth = options.borderWidth; ctx.beginPath(); ctx.moveTo(x + topLeft, y); if (yAlign === 'top') { this.drawCaret(pt, ctx, tooltipSize, options); } ctx.lineTo(x + width - topRight, y); ctx.quadraticCurveTo(x + width, y, x + width, y + topRight); if (yAlign === 'center' && xAlign === 'right') { this.drawCaret(pt, ctx, tooltipSize, options); } ctx.lineTo(x + width, y + height - bottomRight); ctx.quadraticCurveTo(x + width, y + height, x + width - bottomRight, y + height); if (yAlign === 'bottom') { this.drawCaret(pt, ctx, tooltipSize, options); } ctx.lineTo(x + bottomLeft, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - bottomLeft); if (yAlign === 'center' && xAlign === 'left') { this.drawCaret(pt, ctx, tooltipSize, options); } ctx.lineTo(x, y + topLeft); ctx.quadraticCurveTo(x, y, x + topLeft, y); ctx.closePath(); ctx.fill(); if (options.borderWidth > 0) { ctx.stroke(); } } /** * Update x/y animation targets when _active elements are animating too * @private */ _updateAnimationTarget(options) { const chart = this.chart; const anims = this.$animations; const animX = anims && anims.x; const animY = anims && anims.y; if (animX || animY) { const position = positioners[options.position].call(this, this._active, this._eventPosition); if (!position) { return; } const size = this._size = getTooltipSize(this, options); const positionAndSize = Object.assign({}, position, this._size); const alignment = determineAlignment(chart, options, positionAndSize); const point = getBackgroundPoint(options, positionAndSize, alignment, chart); if (animX._to !== point.x || animY._to !== point.y) { this.xAlign = alignment.xAlign; this.yAlign = alignment.yAlign; this.width = size.width; this.height = size.height; this.caretX = position.x; this.caretY = position.y; this._resolveAnimations().update(this, point); } } } /** * Determine if the tooltip will draw anything * @returns {boolean} True if the tooltip will render */ _willRender() { return !!this.opacity; } draw(ctx) { const options = this.options.setContext(this.getContext()); let opacity = this.opacity; if (!opacity) { return; } this._updateAnimationTarget(options); const tooltipSize = { width: this.width, height: this.height }; const pt = { x: this.x, y: this.y }; // IE11/Edge does not like very small opacities, so snap to 0 opacity = Math.abs(opacity) < 1e-3 ? 0 : opacity; const padding = toPadding(options.padding); // Truthy/falsey value for empty tooltip const hasTooltipContent = this.title.length || this.beforeBody.length || this.body.length || this.afterBody.length || this.footer.length; if (options.enabled && hasTooltipContent) { ctx.save(); ctx.globalAlpha = opacity; // Draw Background this.drawBackground(pt, ctx, tooltipSize, options); overrideTextDirection(ctx, options.textDirection); pt.y += padding.top; // Titles this.drawTitle(pt, ctx, options); // Body this.drawBody(pt, ctx, options); // Footer this.drawFooter(pt, ctx, options); restoreTextDirection(ctx, options.textDirection); ctx.restore(); } } /** * Get active elements in the tooltip * @returns {Array} Array of elements that are active in the tooltip */ getActiveElements() { return this._active || []; } /** * Set active elements in the tooltip * @param {array} activeElements Array of active datasetIndex/index pairs. * @param {object} eventPosition Synthetic event position used in positioning */ setActiveElements(activeElements, eventPosition) { const lastActive = this._active; const active = activeElements.map(({datasetIndex, index}) => { const meta = this.chart.getDatasetMeta(datasetIndex); if (!meta) { throw new Error('Cannot find a dataset at index ' + datasetIndex); } return { datasetIndex, element: meta.data[index], index, }; }); const changed = !_elementsEqual(lastActive, active); const positionChanged = this._positionChanged(active, eventPosition); if (changed || positionChanged) { this._active = active; this._eventPosition = eventPosition; this._ignoreReplayEvents = true; this.update(true); } } /** * Handle an event * @param {ChartEvent} e - The event to handle * @param {boolean} [replay] - This is a replayed event (from update) * @param {boolean} [inChartArea] - The event is inside chartArea * @returns {boolean} true if the tooltip changed */ handleEvent(e, replay, inChartArea = true) { if (replay && this._ignoreReplayEvents) { return false; } this._ignoreReplayEvents = false; const options = this.options; const lastActive = this._active || []; const active = this._getActiveElements(e, lastActive, replay, inChartArea); // When there are multiple items shown, but the tooltip position is nearest mode // an update may need to be made because our position may have changed even though // the items are the same as before. const positionChanged = this._positionChanged(active, e); // Remember Last Actives const changed = replay || !_elementsEqual(active, lastActive) || positionChanged; // Only handle target event on tooltip change if (changed) { this._active = active; if (options.enabled || options.external) { this._eventPosition = { x: e.x, y: e.y }; this.update(true, replay); } } return changed; } /** * Helper for determining the active elements for event * @param {ChartEvent} e - The event to handle * @param {InteractionItem[]} lastActive - Previously active elements * @param {boolean} [replay] - This is a replayed event (from update) * @param {boolean} [inChartArea] - The event is inside chartArea * @returns {InteractionItem[]} - Active elements * @private */ _getActiveElements(e, lastActive, replay, inChartArea) { const options = this.options; if (e.type === 'mouseout') { return []; } if (!inChartArea) { // Let user control the active elements outside chartArea. Eg. using Legend. // But make sure that active elements are still valid. return lastActive.filter(i => this.chart.data.datasets[i.datasetIndex] && this.chart.getDatasetMeta(i.datasetIndex).controller.getParsed(i.index) !== undefined ); } // Find Active Elements for tooltips const active = this.chart.getElementsAtEventForMode(e, options.mode, options, replay); if (options.reverse) { active.reverse(); } return active; } /** * Determine if the active elements + event combination changes the * tooltip position * @param {array} active - Active elements * @param {ChartEvent} e - Event that triggered the position change * @returns {boolean} True if the position has changed */ _positionChanged(active, e) { const {caretX, caretY, options} = this; const position = positioners[options.position].call(this, active, e); return position !== false && (caretX !== position.x || caretY !== position.y); } } export default { id: 'tooltip', _element: Tooltip, positioners, afterInit(chart, _args, options) { if (options) { chart.tooltip = new Tooltip({chart, options}); } }, beforeUpdate(chart, _args, options) { if (chart.tooltip) { chart.tooltip.initialize(options); } }, reset(chart, _args, options) { if (chart.tooltip) { chart.tooltip.initialize(options); } }, afterDraw(chart) { const tooltip = chart.tooltip; if (tooltip && tooltip._willRender()) { const args = { tooltip }; if (chart.notifyPlugins('beforeTooltipDraw', {...args, cancelable: true}) === false) { return; } tooltip.draw(chart.ctx); chart.notifyPlugins('afterTooltipDraw', args); } }, afterEvent(chart, args) { if (chart.tooltip) { // If the event is replayed from `update`, we should evaluate with the final positions. const useFinalPosition = args.replay; if (chart.tooltip.handleEvent(args.event, useFinalPosition, args.inChartArea)) { // notify chart about the change, so it will render args.changed = true; } } }, defaults: { enabled: true, external: null, position: 'average', backgroundColor: 'rgba(0,0,0,0.8)', titleColor: '#fff', titleFont: { weight: 'bold', }, titleSpacing: 2, titleMarginBottom: 6, titleAlign: 'left', bodyColor: '#fff', bodySpacing: 2, bodyFont: { }, bodyAlign: 'left', footerColor: '#fff', footerSpacing: 2, footerMarginTop: 6, footerFont: { weight: 'bold', }, footerAlign: 'left', padding: 6, caretPadding: 2, caretSize: 5, cornerRadius: 6, boxHeight: (ctx, opts) => opts.bodyFont.size, boxWidth: (ctx, opts) => opts.bodyFont.size, multiKeyBackground: '#fff', displayColors: true, boxPadding: 0, borderColor: 'rgba(0,0,0,0)', borderWidth: 0, animation: { duration: 400, easing: 'easeOutQuart', }, animations: { numbers: { type: 'number', properties: ['x', 'y', 'width', 'height', 'caretX', 'caretY'], }, opacity: { easing: 'linear', duration: 200 } }, callbacks: defaultCallbacks }, defaultRoutes: { bodyFont: 'font', footerFont: 'font', titleFont: 'font' }, descriptors: { _scriptable: (name) => name !== 'filter' && name !== 'itemSort' && name !== 'external', _indexable: false, callbacks: { _scriptable: false, _indexable: false, }, animation: { _fallback: false }, animations: { _fallback: 'animation' } }, // Resolve additionally from `interaction` options and defaults. additionalOptionScopes: ['interaction'] }; ================================================ FILE: src/scales/index.js ================================================ export {default as CategoryScale} from './scale.category.js'; export {default as LinearScale} from './scale.linear.js'; export {default as LogarithmicScale} from './scale.logarithmic.js'; export {default as RadialLinearScale} from './scale.radialLinear.js'; export {default as TimeScale} from './scale.time.js'; export {default as TimeSeriesScale} from './scale.timeseries.js'; ================================================ FILE: src/scales/scale.category.js ================================================ import Scale from '../core/core.scale.js'; import {isNullOrUndef, valueOrDefault, _limitValue} from '../helpers/index.js'; const addIfString = (labels, raw, index, addedLabels) => { if (typeof raw === 'string') { index = labels.push(raw) - 1; addedLabels.unshift({index, label: raw}); } else if (isNaN(raw)) { index = null; } return index; }; function findOrAddLabel(labels, raw, index, addedLabels) { const first = labels.indexOf(raw); if (first === -1) { return addIfString(labels, raw, index, addedLabels); } const last = labels.lastIndexOf(raw); return first !== last ? index : first; } const validIndex = (index, max) => index === null ? null : _limitValue(Math.round(index), 0, max); function _getLabelForValue(value) { const labels = this.getLabels(); if (value >= 0 && value < labels.length) { return labels[value]; } return value; } export default class CategoryScale extends Scale { static id = 'category'; /** * @type {any} */ static defaults = { ticks: { callback: _getLabelForValue } }; constructor(cfg) { super(cfg); /** @type {number} */ this._startValue = undefined; this._valueRange = 0; this._addedLabels = []; } init(scaleOptions) { const added = this._addedLabels; if (added.length) { const labels = this.getLabels(); for (const {index, label} of added) { if (labels[index] === label) { labels.splice(index, 1); } } this._addedLabels = []; } super.init(scaleOptions); } parse(raw, index) { if (isNullOrUndef(raw)) { return null; } const labels = this.getLabels(); index = isFinite(index) && labels[index] === raw ? index : findOrAddLabel(labels, raw, valueOrDefault(index, raw), this._addedLabels); return validIndex(index, labels.length - 1); } determineDataLimits() { const {minDefined, maxDefined} = this.getUserBounds(); let {min, max} = this.getMinMax(true); if (this.options.bounds === 'ticks') { if (!minDefined) { min = 0; } if (!maxDefined) { max = this.getLabels().length - 1; } } this.min = min; this.max = max; } buildTicks() { const min = this.min; const max = this.max; const offset = this.options.offset; const ticks = []; let labels = this.getLabels(); // If we are viewing some subset of labels, slice the original array labels = (min === 0 && max === labels.length - 1) ? labels : labels.slice(min, max + 1); this._valueRange = Math.max(labels.length - (offset ? 0 : 1), 1); this._startValue = this.min - (offset ? 0.5 : 0); for (let value = min; value <= max; value++) { ticks.push({value}); } return ticks; } getLabelForValue(value) { return _getLabelForValue.call(this, value); } /** * @protected */ configure() { super.configure(); if (!this.isHorizontal()) { // For backward compatibility, vertical category scale reverse is inverted. this._reversePixels = !this._reversePixels; } } // Used to get data value locations. Value can either be an index or a numerical value getPixelForValue(value) { if (typeof value !== 'number') { value = this.parse(value); } return value === null ? NaN : this.getPixelForDecimal((value - this._startValue) / this._valueRange); } // Must override base implementation because it calls getPixelForValue // and category scale can have duplicate values getPixelForTick(index) { const ticks = this.ticks; if (index < 0 || index > ticks.length - 1) { return null; } return this.getPixelForValue(ticks[index].value); } getValueForPixel(pixel) { return Math.round(this._startValue + this.getDecimalForPixel(pixel) * this._valueRange); } getBasePixel() { return this.bottom; } } ================================================ FILE: src/scales/scale.linear.js ================================================ import {isFinite} from '../helpers/helpers.core.js'; import LinearScaleBase from './scale.linearbase.js'; import Ticks from '../core/core.ticks.js'; import {toRadians} from '../helpers/index.js'; export default class LinearScale extends LinearScaleBase { static id = 'linear'; /** * @type {any} */ static defaults = { ticks: { callback: Ticks.formatters.numeric } }; determineDataLimits() { const {min, max} = this.getMinMax(true); this.min = isFinite(min) ? min : 0; this.max = isFinite(max) ? max : 1; // Common base implementation to handle min, max, beginAtZero this.handleTickRangeOptions(); } /** * Returns the maximum number of ticks based on the scale dimension * @protected */ computeTickLimit() { const horizontal = this.isHorizontal(); const length = horizontal ? this.width : this.height; const minRotation = toRadians(this.options.ticks.minRotation); const ratio = (horizontal ? Math.sin(minRotation) : Math.cos(minRotation)) || 0.001; const tickFont = this._resolveTickFontOptions(0); return Math.ceil(length / Math.min(40, tickFont.lineHeight / ratio)); } // Utils getPixelForValue(value) { return value === null ? NaN : this.getPixelForDecimal((value - this._startValue) / this._valueRange); } getValueForPixel(pixel) { return this._startValue + this.getDecimalForPixel(pixel) * this._valueRange; } } ================================================ FILE: src/scales/scale.linearbase.js ================================================ import {isNullOrUndef} from '../helpers/helpers.core.js'; import {almostEquals, almostWhole, niceNum, _decimalPlaces, _setMinAndMaxByKey, sign, toRadians} from '../helpers/helpers.math.js'; import Scale from '../core/core.scale.js'; import {formatNumber} from '../helpers/helpers.intl.js'; /** * Generate a set of linear ticks for an axis * 1. If generationOptions.min, generationOptions.max, and generationOptions.step are defined: * if (max - min) / step is an integer, ticks are generated as [min, min + step, ..., max] * Note that the generationOptions.maxCount setting is respected in this scenario * * 2. If generationOptions.min, generationOptions.max, and generationOptions.count is defined * spacing = (max - min) / count * Ticks are generated as [min, min + spacing, ..., max] * * 3. If generationOptions.count is defined * spacing = (niceMax - niceMin) / count * * 4. Compute optimal spacing of ticks using niceNum algorithm * * @param generationOptions the options used to generate the ticks * @param dataRange the range of the data * @returns {object[]} array of tick objects */ function generateTicks(generationOptions, dataRange) { const ticks = []; // To get a "nice" value for the tick spacing, we will use the appropriately named // "nice number" algorithm. See https://stackoverflow.com/questions/8506881/nice-label-algorithm-for-charts-with-minimum-ticks // for details. const MIN_SPACING = 1e-14; const {bounds, step, min, max, precision, count, maxTicks, maxDigits, includeBounds} = generationOptions; const unit = step || 1; const maxSpaces = maxTicks - 1; const {min: rmin, max: rmax} = dataRange; const minDefined = !isNullOrUndef(min); const maxDefined = !isNullOrUndef(max); const countDefined = !isNullOrUndef(count); const minSpacing = (rmax - rmin) / (maxDigits + 1); let spacing = niceNum((rmax - rmin) / maxSpaces / unit) * unit; let factor, niceMin, niceMax, numSpaces; // Beyond MIN_SPACING floating point numbers being to lose precision // such that we can't do the math necessary to generate ticks if (spacing < MIN_SPACING && !minDefined && !maxDefined) { return [{value: rmin}, {value: rmax}]; } numSpaces = Math.ceil(rmax / spacing) - Math.floor(rmin / spacing); if (numSpaces > maxSpaces) { // If the calculated num of spaces exceeds maxNumSpaces, recalculate it spacing = niceNum(numSpaces * spacing / maxSpaces / unit) * unit; } if (!isNullOrUndef(precision)) { // If the user specified a precision, round to that number of decimal places factor = Math.pow(10, precision); spacing = Math.ceil(spacing * factor) / factor; } if (bounds === 'ticks') { niceMin = Math.floor(rmin / spacing) * spacing; niceMax = Math.ceil(rmax / spacing) * spacing; } else { niceMin = rmin; niceMax = rmax; } if (minDefined && maxDefined && step && almostWhole((max - min) / step, spacing / 1000)) { // Case 1: If min, max and stepSize are set and they make an evenly spaced scale use it. // spacing = step; // numSpaces = (max - min) / spacing; // Note that we round here to handle the case where almostWhole translated an FP error numSpaces = Math.round(Math.min((max - min) / spacing, maxTicks)); spacing = (max - min) / numSpaces; niceMin = min; niceMax = max; } else if (countDefined) { // Cases 2 & 3, we have a count specified. Handle optional user defined edges to the range. // Sometimes these are no-ops, but it makes the code a lot clearer // and when a user defined range is specified, we want the correct ticks niceMin = minDefined ? min : niceMin; niceMax = maxDefined ? max : niceMax; numSpaces = count - 1; spacing = (niceMax - niceMin) / numSpaces; } else { // Case 4 numSpaces = (niceMax - niceMin) / spacing; // If very close to our rounded value, use it. if (almostEquals(numSpaces, Math.round(numSpaces), spacing / 1000)) { numSpaces = Math.round(numSpaces); } else { numSpaces = Math.ceil(numSpaces); } } // The spacing will have changed in cases 1, 2, and 3 so the factor cannot be computed // until this point const decimalPlaces = Math.max( _decimalPlaces(spacing), _decimalPlaces(niceMin) ); factor = Math.pow(10, isNullOrUndef(precision) ? decimalPlaces : precision); niceMin = Math.round(niceMin * factor) / factor; niceMax = Math.round(niceMax * factor) / factor; let j = 0; if (minDefined) { if (includeBounds && niceMin !== min) { ticks.push({value: min}); if (niceMin < min) { j++; // Skip niceMin } // If the next nice tick is close to min, skip it if (almostEquals(Math.round((niceMin + j * spacing) * factor) / factor, min, relativeLabelSize(min, minSpacing, generationOptions))) { j++; } } else if (niceMin < min) { j++; } } for (; j < numSpaces; ++j) { const tickValue = Math.round((niceMin + j * spacing) * factor) / factor; if (maxDefined && tickValue > max) { break; } ticks.push({value: tickValue}); } if (maxDefined && includeBounds && niceMax !== max) { // If the previous tick is too close to max, replace it with max, else add max if (ticks.length && almostEquals(ticks[ticks.length - 1].value, max, relativeLabelSize(max, minSpacing, generationOptions))) { ticks[ticks.length - 1].value = max; } else { ticks.push({value: max}); } } else if (!maxDefined || niceMax === max) { ticks.push({value: niceMax}); } return ticks; } function relativeLabelSize(value, minSpacing, {horizontal, minRotation}) { const rad = toRadians(minRotation); const ratio = (horizontal ? Math.sin(rad) : Math.cos(rad)) || 0.001; const length = 0.75 * minSpacing * ('' + value).length; return Math.min(minSpacing / ratio, length); } export default class LinearScaleBase extends Scale { constructor(cfg) { super(cfg); /** @type {number} */ this.start = undefined; /** @type {number} */ this.end = undefined; /** @type {number} */ this._startValue = undefined; /** @type {number} */ this._endValue = undefined; this._valueRange = 0; } parse(raw, index) { // eslint-disable-line no-unused-vars if (isNullOrUndef(raw)) { return null; } if ((typeof raw === 'number' || raw instanceof Number) && !isFinite(+raw)) { return null; } return +raw; } handleTickRangeOptions() { const {beginAtZero} = this.options; const {minDefined, maxDefined} = this.getUserBounds(); let {min, max} = this; const setMin = v => (min = minDefined ? min : v); const setMax = v => (max = maxDefined ? max : v); if (beginAtZero) { const minSign = sign(min); const maxSign = sign(max); if (minSign < 0 && maxSign < 0) { setMax(0); } else if (minSign > 0 && maxSign > 0) { setMin(0); } } if (min === max) { let offset = max === 0 ? 1 : Math.abs(max * 0.05); setMax(max + offset); if (!beginAtZero) { setMin(min - offset); } } this.min = min; this.max = max; } getTickLimit() { const tickOpts = this.options.ticks; // eslint-disable-next-line prefer-const let {maxTicksLimit, stepSize} = tickOpts; let maxTicks; if (stepSize) { maxTicks = Math.ceil(this.max / stepSize) - Math.floor(this.min / stepSize) + 1; if (maxTicks > 1000) { console.warn(`scales.${this.id}.ticks.stepSize: ${stepSize} would result generating up to ${maxTicks} ticks. Limiting to 1000.`); maxTicks = 1000; } } else { maxTicks = this.computeTickLimit(); maxTicksLimit = maxTicksLimit || 11; } if (maxTicksLimit) { maxTicks = Math.min(maxTicksLimit, maxTicks); } return maxTicks; } /** * @protected */ computeTickLimit() { return Number.POSITIVE_INFINITY; } buildTicks() { const opts = this.options; const tickOpts = opts.ticks; // Figure out what the max number of ticks we can support it is based on the size of // the axis area. For now, we say that the minimum tick spacing in pixels must be 40 // We also limit the maximum number of ticks to 11 which gives a nice 10 squares on // the graph. Make sure we always have at least 2 ticks let maxTicks = this.getTickLimit(); maxTicks = Math.max(2, maxTicks); const numericGeneratorOptions = { maxTicks, bounds: opts.bounds, min: opts.min, max: opts.max, precision: tickOpts.precision, step: tickOpts.stepSize, count: tickOpts.count, maxDigits: this._maxDigits(), horizontal: this.isHorizontal(), minRotation: tickOpts.minRotation || 0, includeBounds: tickOpts.includeBounds !== false }; const dataRange = this._range || this; const ticks = generateTicks(numericGeneratorOptions, dataRange); // At this point, we need to update our max and min given the tick values, // since we probably have expanded the range of the scale if (opts.bounds === 'ticks') { _setMinAndMaxByKey(ticks, this, 'value'); } if (opts.reverse) { ticks.reverse(); this.start = this.max; this.end = this.min; } else { this.start = this.min; this.end = this.max; } return ticks; } /** * @protected */ configure() { const ticks = this.ticks; let start = this.min; let end = this.max; super.configure(); if (this.options.offset && ticks.length) { const offset = (end - start) / Math.max(ticks.length - 1, 1) / 2; start -= offset; end += offset; } this._startValue = start; this._endValue = end; this._valueRange = end - start; } getLabelForValue(value) { return formatNumber(value, this.chart.options.locale, this.options.ticks.format); } } ================================================ FILE: src/scales/scale.logarithmic.js ================================================ import {finiteOrDefault, isFinite} from '../helpers/helpers.core.js'; import {formatNumber} from '../helpers/helpers.intl.js'; import {_setMinAndMaxByKey, log10} from '../helpers/helpers.math.js'; import Scale from '../core/core.scale.js'; import LinearScaleBase from './scale.linearbase.js'; import Ticks from '../core/core.ticks.js'; const log10Floor = v => Math.floor(log10(v)); const changeExponent = (v, m) => Math.pow(10, log10Floor(v) + m); function isMajor(tickVal) { const remain = tickVal / (Math.pow(10, log10Floor(tickVal))); return remain === 1; } function steps(min, max, rangeExp) { const rangeStep = Math.pow(10, rangeExp); const start = Math.floor(min / rangeStep); const end = Math.ceil(max / rangeStep); return end - start; } function startExp(min, max) { const range = max - min; let rangeExp = log10Floor(range); while (steps(min, max, rangeExp) > 10) { rangeExp++; } while (steps(min, max, rangeExp) < 10) { rangeExp--; } return Math.min(rangeExp, log10Floor(min)); } /** * Generate a set of logarithmic ticks * @param generationOptions the options used to generate the ticks * @param dataRange the range of the data * @returns {object[]} array of tick objects */ function generateTicks(generationOptions, {min, max}) { min = finiteOrDefault(generationOptions.min, min); const ticks = []; const minExp = log10Floor(min); let exp = startExp(min, max); let precision = exp < 0 ? Math.pow(10, Math.abs(exp)) : 1; const stepSize = Math.pow(10, exp); const base = minExp > exp ? Math.pow(10, minExp) : 0; const start = Math.round((min - base) * precision) / precision; const offset = Math.floor((min - base) / stepSize / 10) * stepSize * 10; let significand = Math.floor((start - offset) / Math.pow(10, exp)); let value = finiteOrDefault(generationOptions.min, Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision); while (value < max) { ticks.push({value, major: isMajor(value), significand}); if (significand >= 10) { significand = significand < 15 ? 15 : 20; } else { significand++; } if (significand >= 20) { exp++; significand = 2; precision = exp >= 0 ? 1 : precision; } value = Math.round((base + offset + significand * Math.pow(10, exp)) * precision) / precision; } const lastTick = finiteOrDefault(generationOptions.max, value); ticks.push({value: lastTick, major: isMajor(lastTick), significand}); return ticks; } export default class LogarithmicScale extends Scale { static id = 'logarithmic'; /** * @type {any} */ static defaults = { ticks: { callback: Ticks.formatters.logarithmic, major: { enabled: true } } }; constructor(cfg) { super(cfg); /** @type {number} */ this.start = undefined; /** @type {number} */ this.end = undefined; /** @type {number} */ this._startValue = undefined; this._valueRange = 0; } parse(raw, index) { const value = LinearScaleBase.prototype.parse.apply(this, [raw, index]); if (value === 0) { this._zero = true; return undefined; } return isFinite(value) && value > 0 ? value : null; } determineDataLimits() { const {min, max} = this.getMinMax(true); this.min = isFinite(min) ? Math.max(0, min) : null; this.max = isFinite(max) ? Math.max(0, max) : null; if (this.options.beginAtZero) { this._zero = true; } // if data has `0` in it or `beginAtZero` is true, min (non zero) value is at bottom // of scale, and it does not equal suggestedMin, lower the min bound by one exp. if (this._zero && this.min !== this._suggestedMin && !isFinite(this._userMin)) { this.min = min === changeExponent(this.min, 0) ? changeExponent(this.min, -1) : changeExponent(this.min, 0); } this.handleTickRangeOptions(); } handleTickRangeOptions() { const {minDefined, maxDefined} = this.getUserBounds(); let min = this.min; let max = this.max; const setMin = v => (min = minDefined ? min : v); const setMax = v => (max = maxDefined ? max : v); if (min === max) { if (min <= 0) { // includes null setMin(1); setMax(10); } else { setMin(changeExponent(min, -1)); setMax(changeExponent(max, +1)); } } if (min <= 0) { setMin(changeExponent(max, -1)); } if (max <= 0) { setMax(changeExponent(min, +1)); } this.min = min; this.max = max; } buildTicks() { const opts = this.options; const generationOptions = { min: this._userMin, max: this._userMax }; const ticks = generateTicks(generationOptions, this); // At this point, we need to update our max and min given the tick values, // since we probably have expanded the range of the scale if (opts.bounds === 'ticks') { _setMinAndMaxByKey(ticks, this, 'value'); } if (opts.reverse) { ticks.reverse(); this.start = this.max; this.end = this.min; } else { this.start = this.min; this.end = this.max; } return ticks; } /** * @param {number} value * @return {string} */ getLabelForValue(value) { return value === undefined ? '0' : formatNumber(value, this.chart.options.locale, this.options.ticks.format); } /** * @protected */ configure() { const start = this.min; super.configure(); this._startValue = log10(start); this._valueRange = log10(this.max) - log10(start); } getPixelForValue(value) { if (value === undefined || value === 0) { value = this.min; } if (value === null || isNaN(value)) { return NaN; } return this.getPixelForDecimal(value === this.min ? 0 : (log10(value) - this._startValue) / this._valueRange); } getValueForPixel(pixel) { const decimal = this.getDecimalForPixel(pixel); return Math.pow(10, this._startValue + decimal * this._valueRange); } } ================================================ FILE: src/scales/scale.radialLinear.js ================================================ import defaults from '../core/core.defaults.js'; import {_longestText, addRoundedRectPath, renderText, _isPointInArea} from '../helpers/helpers.canvas.js'; import {HALF_PI, TAU, toDegrees, toRadians, _normalizeAngle, PI} from '../helpers/helpers.math.js'; import LinearScaleBase from './scale.linearbase.js'; import Ticks from '../core/core.ticks.js'; import {valueOrDefault, isArray, isFinite, callback as callCallback, isNullOrUndef} from '../helpers/helpers.core.js'; import {createContext, toFont, toPadding, toTRBLCorners} from '../helpers/helpers.options.js'; function getTickBackdropHeight(opts) { const tickOpts = opts.ticks; if (tickOpts.display && opts.display) { const padding = toPadding(tickOpts.backdropPadding); return valueOrDefault(tickOpts.font && tickOpts.font.size, defaults.font.size) + padding.height; } return 0; } function measureLabelSize(ctx, font, label) { label = isArray(label) ? label : [label]; return { w: _longestText(ctx, font.string, label), h: label.length * font.lineHeight }; } function determineLimits(angle, pos, size, min, max) { if (angle === min || angle === max) { return { start: pos - (size / 2), end: pos + (size / 2) }; } else if (angle < min || angle > max) { return { start: pos - size, end: pos }; } return { start: pos, end: pos + size }; } /** * Helper function to fit a radial linear scale with point labels */ function fitWithPointLabels(scale) { // Right, this is really confusing and there is a lot of maths going on here // The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9 // // Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif // // Solution: // // We assume the radius of the polygon is half the size of the canvas at first // at each index we check if the text overlaps. // // Where it does, we store that angle and that index. // // After finding the largest index and angle we calculate how much we need to remove // from the shape radius to move the point inwards by that x. // // We average the left and right distances to get the maximum shape radius that can fit in the box // along with labels. // // Once we have that, we can find the centre point for the chart, by taking the x text protrusion // on each side, removing that from the size, halving it and adding the left x protrusion width. // // This will mean we have a shape fitted to the canvas, as large as it can be with the labels // and position it in the most space efficient manner // // https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif // Get maximum radius of the polygon. Either half the height (minus the text width) or half the width. // Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points const orig = { l: scale.left + scale._padding.left, r: scale.right - scale._padding.right, t: scale.top + scale._padding.top, b: scale.bottom - scale._padding.bottom }; const limits = Object.assign({}, orig); const labelSizes = []; const padding = []; const valueCount = scale._pointLabels.length; const pointLabelOpts = scale.options.pointLabels; const additionalAngle = pointLabelOpts.centerPointLabels ? PI / valueCount : 0; for (let i = 0; i < valueCount; i++) { const opts = pointLabelOpts.setContext(scale.getPointLabelContext(i)); padding[i] = opts.padding; const pointPosition = scale.getPointPosition(i, scale.drawingArea + padding[i], additionalAngle); const plFont = toFont(opts.font); const textSize = measureLabelSize(scale.ctx, plFont, scale._pointLabels[i]); labelSizes[i] = textSize; const angleRadians = _normalizeAngle(scale.getIndexAngle(i) + additionalAngle); const angle = Math.round(toDegrees(angleRadians)); const hLimits = determineLimits(angle, pointPosition.x, textSize.w, 0, 180); const vLimits = determineLimits(angle, pointPosition.y, textSize.h, 90, 270); updateLimits(limits, orig, angleRadians, hLimits, vLimits); } scale.setCenterPoint( orig.l - limits.l, limits.r - orig.r, orig.t - limits.t, limits.b - orig.b ); // Now that text size is determined, compute the full positions scale._pointLabelItems = buildPointLabelItems(scale, labelSizes, padding); } function updateLimits(limits, orig, angle, hLimits, vLimits) { const sin = Math.abs(Math.sin(angle)); const cos = Math.abs(Math.cos(angle)); let x = 0; let y = 0; if (hLimits.start < orig.l) { x = (orig.l - hLimits.start) / sin; limits.l = Math.min(limits.l, orig.l - x); } else if (hLimits.end > orig.r) { x = (hLimits.end - orig.r) / sin; limits.r = Math.max(limits.r, orig.r + x); } if (vLimits.start < orig.t) { y = (orig.t - vLimits.start) / cos; limits.t = Math.min(limits.t, orig.t - y); } else if (vLimits.end > orig.b) { y = (vLimits.end - orig.b) / cos; limits.b = Math.max(limits.b, orig.b + y); } } function createPointLabelItem(scale, index, itemOpts) { const outerDistance = scale.drawingArea; const {extra, additionalAngle, padding, size} = itemOpts; const pointLabelPosition = scale.getPointPosition(index, outerDistance + extra + padding, additionalAngle); const angle = Math.round(toDegrees(_normalizeAngle(pointLabelPosition.angle + HALF_PI))); const y = yForAngle(pointLabelPosition.y, size.h, angle); const textAlign = getTextAlignForAngle(angle); const left = leftForTextAlign(pointLabelPosition.x, size.w, textAlign); return { // if to draw or overlapped visible: true, // Text position x: pointLabelPosition.x, y, // Text rendering data textAlign, // Bounding box left, top: y, right: left + size.w, bottom: y + size.h }; } function isNotOverlapped(item, area) { if (!area) { return true; } const {left, top, right, bottom} = item; const apexesInArea = _isPointInArea({x: left, y: top}, area) || _isPointInArea({x: left, y: bottom}, area) || _isPointInArea({x: right, y: top}, area) || _isPointInArea({x: right, y: bottom}, area); return !apexesInArea; } function buildPointLabelItems(scale, labelSizes, padding) { const items = []; const valueCount = scale._pointLabels.length; const opts = scale.options; const {centerPointLabels, display} = opts.pointLabels; const itemOpts = { extra: getTickBackdropHeight(opts) / 2, additionalAngle: centerPointLabels ? PI / valueCount : 0 }; let area; for (let i = 0; i < valueCount; i++) { itemOpts.padding = padding[i]; itemOpts.size = labelSizes[i]; const item = createPointLabelItem(scale, i, itemOpts); items.push(item); if (display === 'auto') { item.visible = isNotOverlapped(item, area); if (item.visible) { area = item; } } } return items; } function getTextAlignForAngle(angle) { if (angle === 0 || angle === 180) { return 'center'; } else if (angle < 180) { return 'left'; } return 'right'; } function leftForTextAlign(x, w, align) { if (align === 'right') { x -= w; } else if (align === 'center') { x -= (w / 2); } return x; } function yForAngle(y, h, angle) { if (angle === 90 || angle === 270) { y -= (h / 2); } else if (angle > 270 || angle < 90) { y -= h; } return y; } function drawPointLabelBox(ctx, opts, item) { const {left, top, right, bottom} = item; const {backdropColor} = opts; if (!isNullOrUndef(backdropColor)) { const borderRadius = toTRBLCorners(opts.borderRadius); const padding = toPadding(opts.backdropPadding); ctx.fillStyle = backdropColor; const backdropLeft = left - padding.left; const backdropTop = top - padding.top; const backdropWidth = right - left + padding.width; const backdropHeight = bottom - top + padding.height; if (Object.values(borderRadius).some(v => v !== 0)) { ctx.beginPath(); addRoundedRectPath(ctx, { x: backdropLeft, y: backdropTop, w: backdropWidth, h: backdropHeight, radius: borderRadius, }); ctx.fill(); } else { ctx.fillRect(backdropLeft, backdropTop, backdropWidth, backdropHeight); } } } function drawPointLabels(scale, labelCount) { const {ctx, options: {pointLabels}} = scale; for (let i = labelCount - 1; i >= 0; i--) { const item = scale._pointLabelItems[i]; if (!item.visible) { // overlapping continue; } const optsAtIndex = pointLabels.setContext(scale.getPointLabelContext(i)); drawPointLabelBox(ctx, optsAtIndex, item); const plFont = toFont(optsAtIndex.font); const {x, y, textAlign} = item; renderText( ctx, scale._pointLabels[i], x, y + (plFont.lineHeight / 2), plFont, { color: optsAtIndex.color, textAlign: textAlign, textBaseline: 'middle' } ); } } function pathRadiusLine(scale, radius, circular, labelCount) { const {ctx} = scale; if (circular) { // Draw circular arcs between the points ctx.arc(scale.xCenter, scale.yCenter, radius, 0, TAU); } else { // Draw straight lines connecting each index let pointPosition = scale.getPointPosition(0, radius); ctx.moveTo(pointPosition.x, pointPosition.y); for (let i = 1; i < labelCount; i++) { pointPosition = scale.getPointPosition(i, radius); ctx.lineTo(pointPosition.x, pointPosition.y); } } } function drawRadiusLine(scale, gridLineOpts, radius, labelCount, borderOpts) { const ctx = scale.ctx; const circular = gridLineOpts.circular; const {color, lineWidth} = gridLineOpts; if ((!circular && !labelCount) || !color || !lineWidth || radius < 0) { return; } ctx.save(); ctx.strokeStyle = color; ctx.lineWidth = lineWidth; ctx.setLineDash(borderOpts.dash || []); ctx.lineDashOffset = borderOpts.dashOffset; ctx.beginPath(); pathRadiusLine(scale, radius, circular, labelCount); ctx.closePath(); ctx.stroke(); ctx.restore(); } function createPointLabelContext(parent, index, label) { return createContext(parent, { label, index, type: 'pointLabel' }); } export default class RadialLinearScale extends LinearScaleBase { static id = 'radialLinear'; /** * @type {any} */ static defaults = { display: true, // Boolean - Whether to animate scaling the chart from the centre animate: true, position: 'chartArea', angleLines: { display: true, lineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, grid: { circular: false }, startAngle: 0, // label settings ticks: { // Boolean - Show a backdrop to the scale label showLabelBackdrop: true, callback: Ticks.formatters.numeric }, pointLabels: { backdropColor: undefined, // Number - The backdrop padding above & below the label in pixels backdropPadding: 2, // Boolean - if true, show point labels display: true, // Number - Point label font size in pixels font: { size: 10 }, // Function - Used to convert point labels callback(label) { return label; }, // Number - Additionl padding between scale and pointLabel padding: 5, // Boolean - if true, center point labels to slices in polar chart centerPointLabels: false } }; static defaultRoutes = { 'angleLines.color': 'borderColor', 'pointLabels.color': 'color', 'ticks.color': 'color' }; static descriptors = { angleLines: { _fallback: 'grid' } }; constructor(cfg) { super(cfg); /** @type {number} */ this.xCenter = undefined; /** @type {number} */ this.yCenter = undefined; /** @type {number} */ this.drawingArea = undefined; /** @type {string[]} */ this._pointLabels = []; this._pointLabelItems = []; } setDimensions() { // Set the unconstrained dimension before label rotation const padding = this._padding = toPadding(getTickBackdropHeight(this.options) / 2); const w = this.width = this.maxWidth - padding.width; const h = this.height = this.maxHeight - padding.height; this.xCenter = Math.floor(this.left + w / 2 + padding.left); this.yCenter = Math.floor(this.top + h / 2 + padding.top); this.drawingArea = Math.floor(Math.min(w, h) / 2); } determineDataLimits() { const {min, max} = this.getMinMax(false); this.min = isFinite(min) && !isNaN(min) ? min : 0; this.max = isFinite(max) && !isNaN(max) ? max : 0; // Common base implementation to handle min, max, beginAtZero this.handleTickRangeOptions(); } /** * Returns the maximum number of ticks based on the scale dimension * @protected */ computeTickLimit() { return Math.ceil(this.drawingArea / getTickBackdropHeight(this.options)); } generateTickLabels(ticks) { LinearScaleBase.prototype.generateTickLabels.call(this, ticks); // Point labels this._pointLabels = this.getLabels() .map((value, index) => { const label = callCallback(this.options.pointLabels.callback, [value, index], this); return label || label === 0 ? label : ''; }) .filter((v, i) => this.chart.getDataVisibility(i)); } fit() { const opts = this.options; if (opts.display && opts.pointLabels.display) { fitWithPointLabels(this); } else { this.setCenterPoint(0, 0, 0, 0); } } setCenterPoint(leftMovement, rightMovement, topMovement, bottomMovement) { this.xCenter += Math.floor((leftMovement - rightMovement) / 2); this.yCenter += Math.floor((topMovement - bottomMovement) / 2); this.drawingArea -= Math.min(this.drawingArea / 2, Math.max(leftMovement, rightMovement, topMovement, bottomMovement)); } getIndexAngle(index) { const angleMultiplier = TAU / (this._pointLabels.length || 1); const startAngle = this.options.startAngle || 0; return _normalizeAngle(index * angleMultiplier + toRadians(startAngle)); } getDistanceFromCenterForValue(value) { if (isNullOrUndef(value)) { return NaN; } // Take into account half font size + the yPadding of the top value const scalingFactor = this.drawingArea / (this.max - this.min); if (this.options.reverse) { return (this.max - value) * scalingFactor; } return (value - this.min) * scalingFactor; } getValueForDistanceFromCenter(distance) { if (isNullOrUndef(distance)) { return NaN; } const scaledDistance = distance / (this.drawingArea / (this.max - this.min)); return this.options.reverse ? this.max - scaledDistance : this.min + scaledDistance; } getPointLabelContext(index) { const pointLabels = this._pointLabels || []; if (index >= 0 && index < pointLabels.length) { const pointLabel = pointLabels[index]; return createPointLabelContext(this.getContext(), index, pointLabel); } } getPointPosition(index, distanceFromCenter, additionalAngle = 0) { const angle = this.getIndexAngle(index) - HALF_PI + additionalAngle; return { x: Math.cos(angle) * distanceFromCenter + this.xCenter, y: Math.sin(angle) * distanceFromCenter + this.yCenter, angle }; } getPointPositionForValue(index, value) { return this.getPointPosition(index, this.getDistanceFromCenterForValue(value)); } getBasePosition(index) { return this.getPointPositionForValue(index || 0, this.getBaseValue()); } getPointLabelPosition(index) { const {left, top, right, bottom} = this._pointLabelItems[index]; return { left, top, right, bottom, }; } /** * @protected */ drawBackground() { const {backgroundColor, grid: {circular}} = this.options; if (backgroundColor) { const ctx = this.ctx; ctx.save(); ctx.beginPath(); pathRadiusLine(this, this.getDistanceFromCenterForValue(this._endValue), circular, this._pointLabels.length); ctx.closePath(); ctx.fillStyle = backgroundColor; ctx.fill(); ctx.restore(); } } /** * @protected */ drawGrid() { const ctx = this.ctx; const opts = this.options; const {angleLines, grid, border} = opts; const labelCount = this._pointLabels.length; let i, offset, position; if (opts.pointLabels.display) { drawPointLabels(this, labelCount); } if (grid.display) { this.ticks.forEach((tick, index) => { if (index !== 0 || (index === 0 && this.min < 0)) { offset = this.getDistanceFromCenterForValue(tick.value); const context = this.getContext(index); const optsAtIndex = grid.setContext(context); const optsAtIndexBorder = border.setContext(context); drawRadiusLine(this, optsAtIndex, offset, labelCount, optsAtIndexBorder); } }); } if (angleLines.display) { ctx.save(); for (i = labelCount - 1; i >= 0; i--) { const optsAtIndex = angleLines.setContext(this.getPointLabelContext(i)); const {color, lineWidth} = optsAtIndex; if (!lineWidth || !color) { continue; } ctx.lineWidth = lineWidth; ctx.strokeStyle = color; ctx.setLineDash(optsAtIndex.borderDash); ctx.lineDashOffset = optsAtIndex.borderDashOffset; offset = this.getDistanceFromCenterForValue(opts.reverse ? this.min : this.max); position = this.getPointPosition(i, offset); ctx.beginPath(); ctx.moveTo(this.xCenter, this.yCenter); ctx.lineTo(position.x, position.y); ctx.stroke(); } ctx.restore(); } } /** * @protected */ drawBorder() {} /** * @protected */ drawLabels() { const ctx = this.ctx; const opts = this.options; const tickOpts = opts.ticks; if (!tickOpts.display) { return; } const startAngle = this.getIndexAngle(0); let offset, width; ctx.save(); ctx.translate(this.xCenter, this.yCenter); ctx.rotate(startAngle); ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; this.ticks.forEach((tick, index) => { if ((index === 0 && this.min >= 0) && !opts.reverse) { return; } const optsAtIndex = tickOpts.setContext(this.getContext(index)); const tickFont = toFont(optsAtIndex.font); offset = this.getDistanceFromCenterForValue(this.ticks[index].value); if (optsAtIndex.showLabelBackdrop) { ctx.font = tickFont.string; width = ctx.measureText(tick.label).width; ctx.fillStyle = optsAtIndex.backdropColor; const padding = toPadding(optsAtIndex.backdropPadding); ctx.fillRect( -width / 2 - padding.left, -offset - tickFont.size / 2 - padding.top, width + padding.width, tickFont.size + padding.height ); } renderText(ctx, tick.label, 0, -offset, tickFont, { color: optsAtIndex.color, strokeColor: optsAtIndex.textStrokeColor, strokeWidth: optsAtIndex.textStrokeWidth, }); }); ctx.restore(); } /** * @protected */ drawTitle() {} } ================================================ FILE: src/scales/scale.time.js ================================================ import adapters from '../core/core.adapters.js'; import {callback as call, isFinite, isNullOrUndef, mergeIf, valueOrDefault} from '../helpers/helpers.core.js'; import {toRadians, isNumber, _limitValue} from '../helpers/helpers.math.js'; import Scale from '../core/core.scale.js'; import {_arrayUnique, _filterBetween, _lookup} from '../helpers/helpers.collection.js'; /** * @typedef { import('../core/core.adapters.js').TimeUnit } Unit * @typedef {{common: boolean, size: number, steps?: number}} Interval * @typedef { import('../core/core.adapters.js').DateAdapter } DateAdapter */ /** * @type {Object} */ const INTERVALS = { millisecond: {common: true, size: 1, steps: 1000}, second: {common: true, size: 1000, steps: 60}, minute: {common: true, size: 60000, steps: 60}, hour: {common: true, size: 3600000, steps: 24}, day: {common: true, size: 86400000, steps: 30}, week: {common: false, size: 604800000, steps: 4}, month: {common: true, size: 2.628e9, steps: 12}, quarter: {common: false, size: 7.884e9, steps: 4}, year: {common: true, size: 3.154e10} }; /** * @type {Unit[]} */ const UNITS = /** @type Unit[] */ /* #__PURE__ */ (Object.keys(INTERVALS)); /** * @param {number} a * @param {number} b */ function sorter(a, b) { return a - b; } /** * @param {TimeScale} scale * @param {*} input * @return {number} */ function parse(scale, input) { if (isNullOrUndef(input)) { return null; } const adapter = scale._adapter; const {parser, round, isoWeekday} = scale._parseOpts; let value = input; if (typeof parser === 'function') { value = parser(value); } // Only parse if it's not a timestamp already if (!isFinite(value)) { value = typeof parser === 'string' ? adapter.parse(value, parser) : adapter.parse(value); } if (value === null) { return null; } if (round) { value = round === 'week' && (isNumber(isoWeekday) || isoWeekday === true) ? adapter.startOf(value, 'isoWeek', isoWeekday) : adapter.startOf(value, round); } return +value; } /** * Figures out what unit results in an appropriate number of auto-generated ticks * @param {Unit} minUnit * @param {number} min * @param {number} max * @param {number} capacity * @return {object} */ function determineUnitForAutoTicks(minUnit, min, max, capacity) { const ilen = UNITS.length; for (let i = UNITS.indexOf(minUnit); i < ilen - 1; ++i) { const interval = INTERVALS[UNITS[i]]; const factor = interval.steps ? interval.steps : Number.MAX_SAFE_INTEGER; if (interval.common && Math.ceil((max - min) / (factor * interval.size)) <= capacity) { return UNITS[i]; } } return UNITS[ilen - 1]; } /** * Figures out what unit to format a set of ticks with * @param {TimeScale} scale * @param {number} numTicks * @param {Unit} minUnit * @param {number} min * @param {number} max * @return {Unit} */ function determineUnitForFormatting(scale, numTicks, minUnit, min, max) { for (let i = UNITS.length - 1; i >= UNITS.indexOf(minUnit); i--) { const unit = UNITS[i]; if (INTERVALS[unit].common && scale._adapter.diff(max, min, unit) >= numTicks - 1) { return unit; } } return UNITS[minUnit ? UNITS.indexOf(minUnit) : 0]; } /** * @param {Unit} unit * @return {object} */ function determineMajorUnit(unit) { for (let i = UNITS.indexOf(unit) + 1, ilen = UNITS.length; i < ilen; ++i) { if (INTERVALS[UNITS[i]].common) { return UNITS[i]; } } } /** * @param {object} ticks * @param {number} time * @param {number[]} [timestamps] - if defined, snap to these timestamps */ function addTick(ticks, time, timestamps) { if (!timestamps) { ticks[time] = true; } else if (timestamps.length) { const {lo, hi} = _lookup(timestamps, time); const timestamp = timestamps[lo] >= time ? timestamps[lo] : timestamps[hi]; ticks[timestamp] = true; } } /** * @param {TimeScale} scale * @param {object[]} ticks * @param {object} map * @param {Unit} majorUnit * @return {object[]} */ function setMajorTicks(scale, ticks, map, majorUnit) { const adapter = scale._adapter; const first = +adapter.startOf(ticks[0].value, majorUnit); const last = ticks[ticks.length - 1].value; let major, index; for (major = first; major <= last; major = +adapter.add(major, 1, majorUnit)) { index = map[major]; if (index >= 0) { ticks[index].major = true; } } return ticks; } /** * @param {TimeScale} scale * @param {number[]} values * @param {Unit|undefined} [majorUnit] * @return {object[]} */ function ticksFromTimestamps(scale, values, majorUnit) { const ticks = []; /** @type {Object} */ const map = {}; const ilen = values.length; let i, value; for (i = 0; i < ilen; ++i) { value = values[i]; map[value] = i; ticks.push({ value, major: false }); } // We set the major ticks separately from the above loop because calling startOf for every tick // is expensive when there is a large number of ticks return (ilen === 0 || !majorUnit) ? ticks : setMajorTicks(scale, ticks, map, majorUnit); } export default class TimeScale extends Scale { static id = 'time'; /** * @type {any} */ static defaults = { /** * Scale boundary strategy (bypassed by min/max time options) * - `data`: make sure data are fully visible, ticks outside are removed * - `ticks`: make sure ticks are fully visible, data outside are truncated * @see https://github.com/chartjs/Chart.js/pull/4556 * @since 2.7.0 */ bounds: 'data', adapters: {}, time: { parser: false, // false == a pattern string from or a custom callback that converts its argument to a timestamp unit: false, // false == automatic or override with week, month, year, etc. round: false, // none, or override with week, month, year, etc. isoWeekday: false, // override week start day minUnit: 'millisecond', displayFormats: {} }, ticks: { /** * Ticks generation input values: * - 'auto': generates "optimal" ticks based on scale size and time options. * - 'data': generates ticks from data (including labels from data {t|x|y} objects). * - 'labels': generates ticks from user given `data.labels` values ONLY. * @see https://github.com/chartjs/Chart.js/pull/4507 * @since 2.7.0 */ source: 'auto', callback: false, major: { enabled: false } } }; /** * @param {object} props */ constructor(props) { super(props); /** @type {{data: number[], labels: number[], all: number[]}} */ this._cache = { data: [], labels: [], all: [] }; /** @type {Unit} */ this._unit = 'day'; /** @type {Unit=} */ this._majorUnit = undefined; this._offsets = {}; this._normalized = false; this._parseOpts = undefined; } init(scaleOpts, opts = {}) { const time = scaleOpts.time || (scaleOpts.time = {}); /** @type {DateAdapter} */ const adapter = this._adapter = new adapters._date(scaleOpts.adapters.date); adapter.init(opts); // Backward compatibility: before introducing adapter, `displayFormats` was // supposed to contain *all* unit/string pairs but this can't be resolved // when loading the scale (adapters are loaded afterward), so let's populate // missing formats on update mergeIf(time.displayFormats, adapter.formats()); this._parseOpts = { parser: time.parser, round: time.round, isoWeekday: time.isoWeekday }; super.init(scaleOpts); this._normalized = opts.normalized; } /** * @param {*} raw * @param {number?} [index] * @return {number} */ parse(raw, index) { // eslint-disable-line no-unused-vars if (raw === undefined) { return null; } return parse(this, raw); } beforeLayout() { super.beforeLayout(); this._cache = { data: [], labels: [], all: [] }; } determineDataLimits() { const options = this.options; const adapter = this._adapter; const unit = options.time.unit || 'day'; // eslint-disable-next-line prefer-const let {min, max, minDefined, maxDefined} = this.getUserBounds(); /** * @param {object} bounds */ function _applyBounds(bounds) { if (!minDefined && !isNaN(bounds.min)) { min = Math.min(min, bounds.min); } if (!maxDefined && !isNaN(bounds.max)) { max = Math.max(max, bounds.max); } } // If we have user provided `min` and `max` labels / data bounds can be ignored if (!minDefined || !maxDefined) { // Labels are always considered, when user did not force bounds _applyBounds(this._getLabelBounds()); // If `bounds` is `'ticks'` and `ticks.source` is `'labels'`, // data bounds are ignored (and don't need to be determined) if (options.bounds !== 'ticks' || options.ticks.source !== 'labels') { _applyBounds(this.getMinMax(false)); } } min = isFinite(min) && !isNaN(min) ? min : +adapter.startOf(Date.now(), unit); max = isFinite(max) && !isNaN(max) ? max : +adapter.endOf(Date.now(), unit) + 1; // Make sure that max is strictly higher than min (required by the timeseries lookup table) this.min = Math.min(min, max - 1); this.max = Math.max(min + 1, max); } /** * @private */ _getLabelBounds() { const arr = this.getLabelTimestamps(); let min = Number.POSITIVE_INFINITY; let max = Number.NEGATIVE_INFINITY; if (arr.length) { min = arr[0]; max = arr[arr.length - 1]; } return {min, max}; } /** * @return {object[]} */ buildTicks() { const options = this.options; const timeOpts = options.time; const tickOpts = options.ticks; const timestamps = tickOpts.source === 'labels' ? this.getLabelTimestamps() : this._generate(); if (options.bounds === 'ticks' && timestamps.length) { this.min = this._userMin || timestamps[0]; this.max = this._userMax || timestamps[timestamps.length - 1]; } const min = this.min; const max = this.max; const ticks = _filterBetween(timestamps, min, max); // PRIVATE // determineUnitForFormatting relies on the number of ticks so we don't use it when // autoSkip is enabled because we don't yet know what the final number of ticks will be this._unit = timeOpts.unit || (tickOpts.autoSkip ? determineUnitForAutoTicks(timeOpts.minUnit, this.min, this.max, this._getLabelCapacity(min)) : determineUnitForFormatting(this, ticks.length, timeOpts.minUnit, this.min, this.max)); this._majorUnit = !tickOpts.major.enabled || this._unit === 'year' ? undefined : determineMajorUnit(this._unit); this.initOffsets(timestamps); if (options.reverse) { ticks.reverse(); } return ticksFromTimestamps(this, ticks, this._majorUnit); } afterAutoSkip() { // Offsets for bar charts need to be handled with the auto skipped // ticks. Once ticks have been skipped, we re-compute the offsets. if (this.options.offsetAfterAutoskip) { this.initOffsets(this.ticks.map(tick => +tick.value)); } } /** * Returns the start and end offsets from edges in the form of {start, end} * where each value is a relative width to the scale and ranges between 0 and 1. * They add extra margins on the both sides by scaling down the original scale. * Offsets are added when the `offset` option is true. * @param {number[]} timestamps * @protected */ initOffsets(timestamps = []) { let start = 0; let end = 0; let first, last; if (this.options.offset && timestamps.length) { first = this.getDecimalForValue(timestamps[0]); if (timestamps.length === 1) { start = 1 - first; } else { start = (this.getDecimalForValue(timestamps[1]) - first) / 2; } last = this.getDecimalForValue(timestamps[timestamps.length - 1]); if (timestamps.length === 1) { end = last; } else { end = (last - this.getDecimalForValue(timestamps[timestamps.length - 2])) / 2; } } const limit = timestamps.length < 3 ? 0.5 : 0.25; start = _limitValue(start, 0, limit); end = _limitValue(end, 0, limit); this._offsets = {start, end, factor: 1 / (start + 1 + end)}; } /** * Generates a maximum of `capacity` timestamps between min and max, rounded to the * `minor` unit using the given scale time `options`. * Important: this method can return ticks outside the min and max range, it's the * responsibility of the calling code to clamp values if needed. * @protected */ _generate() { const adapter = this._adapter; const min = this.min; const max = this.max; const options = this.options; const timeOpts = options.time; // @ts-ignore const minor = timeOpts.unit || determineUnitForAutoTicks(timeOpts.minUnit, min, max, this._getLabelCapacity(min)); const stepSize = valueOrDefault(options.ticks.stepSize, 1); const weekday = minor === 'week' ? timeOpts.isoWeekday : false; const hasWeekday = isNumber(weekday) || weekday === true; const ticks = {}; let first = min; let time, count; // For 'week' unit, handle the first day of week option if (hasWeekday) { first = +adapter.startOf(first, 'isoWeek', weekday); } // Align first ticks on unit first = +adapter.startOf(first, hasWeekday ? 'day' : minor); // Prevent browser from freezing in case user options request millions of milliseconds if (adapter.diff(max, min, minor) > 100000 * stepSize) { throw new Error(min + ' and ' + max + ' are too far apart with stepSize of ' + stepSize + ' ' + minor); } const timestamps = options.ticks.source === 'data' && this.getDataTimestamps(); for (time = first, count = 0; time < max; time = +adapter.add(time, stepSize, minor), count++) { addTick(ticks, time, timestamps); } if (time === max || options.bounds === 'ticks' || count === 1) { addTick(ticks, time, timestamps); } // @ts-ignore return Object.keys(ticks).sort(sorter).map(x => +x); } /** * @param {number} value * @return {string} */ getLabelForValue(value) { const adapter = this._adapter; const timeOpts = this.options.time; if (timeOpts.tooltipFormat) { return adapter.format(value, timeOpts.tooltipFormat); } return adapter.format(value, timeOpts.displayFormats.datetime); } /** * @param {number} value * @param {string|undefined} format * @return {string} */ format(value, format) { const options = this.options; const formats = options.time.displayFormats; const unit = this._unit; const fmt = format || formats[unit]; return this._adapter.format(value, fmt); } /** * Function to format an individual tick mark * @param {number} time * @param {number} index * @param {object[]} ticks * @param {string|undefined} [format] * @return {string} * @private */ _tickFormatFunction(time, index, ticks, format) { const options = this.options; const formatter = options.ticks.callback; if (formatter) { return call(formatter, [time, index, ticks], this); } const formats = options.time.displayFormats; const unit = this._unit; const majorUnit = this._majorUnit; const minorFormat = unit && formats[unit]; const majorFormat = majorUnit && formats[majorUnit]; const tick = ticks[index]; const major = majorUnit && majorFormat && tick && tick.major; return this._adapter.format(time, format || (major ? majorFormat : minorFormat)); } /** * @param {object[]} ticks */ generateTickLabels(ticks) { let i, ilen, tick; for (i = 0, ilen = ticks.length; i < ilen; ++i) { tick = ticks[i]; tick.label = this._tickFormatFunction(tick.value, i, ticks); } } /** * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC) * @return {number} */ getDecimalForValue(value) { return value === null ? NaN : (value - this.min) / (this.max - this.min); } /** * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC) * @return {number} */ getPixelForValue(value) { const offsets = this._offsets; const pos = this.getDecimalForValue(value); return this.getPixelForDecimal((offsets.start + pos) * offsets.factor); } /** * @param {number} pixel * @return {number} */ getValueForPixel(pixel) { const offsets = this._offsets; const pos = this.getDecimalForPixel(pixel) / offsets.factor - offsets.end; return this.min + pos * (this.max - this.min); } /** * @param {string} label * @return {{w:number, h:number}} * @private */ _getLabelSize(label) { const ticksOpts = this.options.ticks; const tickLabelWidth = this.ctx.measureText(label).width; const angle = toRadians(this.isHorizontal() ? ticksOpts.maxRotation : ticksOpts.minRotation); const cosRotation = Math.cos(angle); const sinRotation = Math.sin(angle); const tickFontSize = this._resolveTickFontOptions(0).size; return { w: (tickLabelWidth * cosRotation) + (tickFontSize * sinRotation), h: (tickLabelWidth * sinRotation) + (tickFontSize * cosRotation) }; } /** * @param {number} exampleTime * @return {number} * @private */ _getLabelCapacity(exampleTime) { const timeOpts = this.options.time; const displayFormats = timeOpts.displayFormats; // pick the longest format (milliseconds) for guesstimation const format = displayFormats[timeOpts.unit] || displayFormats.millisecond; const exampleLabel = this._tickFormatFunction(exampleTime, 0, ticksFromTimestamps(this, [exampleTime], this._majorUnit), format); const size = this._getLabelSize(exampleLabel); // subtract 1 - if offset then there's one less label than tick // if not offset then one half label padding is added to each end leaving room for one less label const capacity = Math.floor(this.isHorizontal() ? this.width / size.w : this.height / size.h) - 1; return capacity > 0 ? capacity : 1; } /** * @protected */ getDataTimestamps() { let timestamps = this._cache.data || []; let i, ilen; if (timestamps.length) { return timestamps; } const metas = this.getMatchingVisibleMetas(); if (this._normalized && metas.length) { return (this._cache.data = metas[0].controller.getAllParsedValues(this)); } for (i = 0, ilen = metas.length; i < ilen; ++i) { timestamps = timestamps.concat(metas[i].controller.getAllParsedValues(this)); } return (this._cache.data = this.normalize(timestamps)); } /** * @protected */ getLabelTimestamps() { const timestamps = this._cache.labels || []; let i, ilen; if (timestamps.length) { return timestamps; } const labels = this.getLabels(); for (i = 0, ilen = labels.length; i < ilen; ++i) { timestamps.push(parse(this, labels[i])); } return (this._cache.labels = this._normalized ? timestamps : this.normalize(timestamps)); } /** * @param {number[]} values * @protected */ normalize(values) { // It seems to be somewhat faster to do sorting first return _arrayUnique(values.sort(sorter)); } } ================================================ FILE: src/scales/scale.timeseries.js ================================================ import TimeScale from './scale.time.js'; import {_lookupByKey} from '../helpers/helpers.collection.js'; /** * Linearly interpolates the given source `val` using the table. If value is out of bounds, values * at edges are used for the interpolation. * @param {object} table * @param {number} val * @param {boolean} [reverse] lookup time based on position instead of vice versa * @return {object} */ function interpolate(table, val, reverse) { let lo = 0; let hi = table.length - 1; let prevSource, nextSource, prevTarget, nextTarget; if (reverse) { if (val >= table[lo].pos && val <= table[hi].pos) { ({lo, hi} = _lookupByKey(table, 'pos', val)); } ({pos: prevSource, time: prevTarget} = table[lo]); ({pos: nextSource, time: nextTarget} = table[hi]); } else { if (val >= table[lo].time && val <= table[hi].time) { ({lo, hi} = _lookupByKey(table, 'time', val)); } ({time: prevSource, pos: prevTarget} = table[lo]); ({time: nextSource, pos: nextTarget} = table[hi]); } const span = nextSource - prevSource; return span ? prevTarget + (nextTarget - prevTarget) * (val - prevSource) / span : prevTarget; } class TimeSeriesScale extends TimeScale { static id = 'timeseries'; /** * @type {any} */ static defaults = TimeScale.defaults; /** * @param {object} props */ constructor(props) { super(props); /** @type {object[]} */ this._table = []; /** @type {number} */ this._minPos = undefined; /** @type {number} */ this._tableRange = undefined; } /** * @protected */ initOffsets() { const timestamps = this._getTimestampsForTable(); const table = this._table = this.buildLookupTable(timestamps); this._minPos = interpolate(table, this.min); this._tableRange = interpolate(table, this.max) - this._minPos; super.initOffsets(timestamps); } /** * Returns an array of {time, pos} objects used to interpolate a specific `time` or position * (`pos`) on the scale, by searching entries before and after the requested value. `pos` is * a decimal between 0 and 1: 0 being the start of the scale (left or top) and 1 the other * extremity (left + width or top + height). Note that it would be more optimized to directly * store pre-computed pixels, but the scale dimensions are not guaranteed at the time we need * to create the lookup table. The table ALWAYS contains at least two items: min and max. * @param {number[]} timestamps * @return {object[]} * @protected */ buildLookupTable(timestamps) { const {min, max} = this; const items = []; const table = []; let i, ilen, prev, curr, next; for (i = 0, ilen = timestamps.length; i < ilen; ++i) { curr = timestamps[i]; if (curr >= min && curr <= max) { items.push(curr); } } if (items.length < 2) { // In case there is less that 2 timestamps between min and max, the scale is defined by min and max return [ {time: min, pos: 0}, {time: max, pos: 1} ]; } for (i = 0, ilen = items.length; i < ilen; ++i) { next = items[i + 1]; prev = items[i - 1]; curr = items[i]; // only add points that breaks the scale linearity if (Math.round((next + prev) / 2) !== curr) { table.push({time: curr, pos: i / (ilen - 1)}); } } return table; } /** * Generates all timestamps defined in the data. * Important: this method can return ticks outside the min and max range, it's the * responsibility of the calling code to clamp values if needed. * @protected */ _generate() { const min = this.min; const max = this.max; let timestamps = super.getDataTimestamps(); if (!timestamps.includes(min) || !timestamps.length) { timestamps.splice(0, 0, min); } if (!timestamps.includes(max) || timestamps.length === 1) { timestamps.push(max); } return timestamps.sort((a, b) => a - b); } /** * Returns all timestamps * @return {number[]} * @private */ _getTimestampsForTable() { let timestamps = this._cache.all || []; if (timestamps.length) { return timestamps; } const data = this.getDataTimestamps(); const label = this.getLabelTimestamps(); if (data.length && label.length) { // If combining labels and data (data might not contain all labels), // we need to recheck uniqueness and sort timestamps = this.normalize(data.concat(label)); } else { timestamps = data.length ? data : label; } timestamps = this._cache.all = timestamps; return timestamps; } /** * @param {number} value - Milliseconds since epoch (1 January 1970 00:00:00 UTC) * @return {number} */ getDecimalForValue(value) { return (interpolate(this._table, value) - this._minPos) / this._tableRange; } /** * @param {number} pixel * @return {number} */ getValueForPixel(pixel) { const offsets = this._offsets; const decimal = this.getDecimalForPixel(pixel) / offsets.factor - offsets.end; return interpolate(this._table, decimal * this._tableRange + this._minPos, true); } } export default TimeSeriesScale; ================================================ FILE: src/types/animation.d.ts ================================================ import {Chart} from './index.js'; import {AnyObject} from './basic.js'; export declare class Animation { constructor(cfg: AnyObject, target: AnyObject, prop: string, to?: unknown); active(): boolean; update(cfg: AnyObject, to: unknown, date: number): void; cancel(): void; tick(date: number): void; readonly _to: unknown; } export interface AnimationEvent { chart: Chart; numSteps: number; initial: boolean; currentStep: number; } export declare class Animator { listen(chart: Chart, event: 'complete' | 'progress', cb: (event: AnimationEvent) => void): void; add(chart: Chart, items: readonly Animation[]): void; has(chart: Chart): boolean; start(chart: Chart): void; running(chart: Chart): boolean; stop(chart: Chart): void; remove(chart: Chart): boolean; } export declare class Animations { constructor(chart: Chart, animations: AnyObject); configure(animations: AnyObject): void; update(target: AnyObject, values: AnyObject): undefined | boolean; } ================================================ FILE: src/types/basic.d.ts ================================================ export type AnyObject = Record; export type EmptyObject = Record; ================================================ FILE: src/types/color.d.ts ================================================ export type Color = string | CanvasGradient | CanvasPattern; ================================================ FILE: src/types/geometric.d.ts ================================================ export interface ChartArea { top: number; left: number; right: number; bottom: number; width: number; height: number; } export interface Point { x: number | null; y: number | null; } export type TRBL = { top: number; right: number; bottom: number; left: number; } export type TRBLCorners = { topLeft: number; topRight: number; bottomLeft: number; bottomRight: number; }; export type CornerRadius = number | Partial; export type RoundedRect = { x: number; y: number; w: number; h: number; radius?: CornerRadius } export type Padding = Partial | number | Point; export interface SplinePoint { x: number; y: number; skip?: boolean; // Both Bezier and monotone interpolations have these fields // but they are added in different spots cp1x?: number; cp1y?: number; cp2x?: number; cp2y?: number; } ================================================ FILE: src/types/index.d.ts ================================================ /* eslint-disable @typescript-eslint/ban-types */ import {DeepPartial, DistributiveArray, UnionToIntersection} from './utils.js'; import {TimeUnit} from '../core/core.adapters.js'; import PointElement from '../elements/element.point.js'; import {EasingFunction} from '../helpers/helpers.easing.js'; import {AnimationEvent} from './animation.js'; import {AnyObject, EmptyObject} from './basic.js'; import {Color} from './color.js'; import Element from '../core/core.element.js'; import {ChartArea, Padding, Point} from './geometric.js'; import {LayoutItem, LayoutPosition} from './layout.js'; import {ColorsPluginOptions} from '../plugins/plugin.colors.js'; export {EasingFunction} from '../helpers/helpers.easing.js'; export {default as ArcElement, ArcProps} from '../elements/element.arc.js'; export {default as PointElement, PointProps} from '../elements/element.point.js'; export {Animation, Animations, Animator, AnimationEvent} from './animation.js'; export {Color} from './color.js'; export {ChartArea, Point, TRBL} from './geometric.js'; export {LayoutItem, LayoutPosition} from './layout.js'; export interface ScriptableContext { active: boolean; chart: Chart; dataIndex: number; dataset: UnionToIntersection>; datasetIndex: number; type: string; mode: string; parsed: UnionToIntersection>; raw: unknown; } export interface ScriptableLineSegmentContext { type: 'segment', p0: PointElement, p1: PointElement, p0DataIndex: number, p1DataIndex: number, datasetIndex: number } export type Scriptable = T | ((ctx: TContext, options: AnyObject) => T | undefined); export type ScriptableOptions = { [P in keyof T]: Scriptable }; export type ScriptableAndScriptableOptions = Scriptable | ScriptableOptions; export type ScriptableAndArray = readonly T[] | Scriptable; export type ScriptableAndArrayOptions = { [P in keyof T]: ScriptableAndArray }; export interface ParsingOptions { /** * How to parse the dataset. The parsing can be disabled by specifying parsing: false at chart options or dataset. If parsing is disabled, data must be sorted and in the formats the associated chart type and scales use internally. */ parsing: { [key: string]: string; } | false; /** * Chart.js is fastest if you provide data with indices that are unique, sorted, and consistent across datasets and provide the normalized: true option to let Chart.js know that you have done so. */ normalized: boolean; } export interface ControllerDatasetOptions extends ParsingOptions { /** * The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. * @default 'x' */ indexAxis: 'x' | 'y'; /** * How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. 0 = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` */ clip: number | ChartArea | false; /** * The label for the dataset which appears in the legend and tooltips. */ label: string; /** * The drawing order of dataset. Also affects order for stacking, tooltip and legend. */ order: number; /** * The ID of the group to which this dataset belongs to (when stacked, each group will be a separate stack). */ stack: string; /** * Configures the visibility state of the dataset. Set it to true, to hide the dataset from the chart. * @default false */ hidden: boolean; } export interface BarControllerDatasetOptions extends ControllerDatasetOptions, ScriptableAndArrayOptions>, ScriptableAndArrayOptions>, AnimationOptions<'bar'> { /** * The ID of the x axis to plot this dataset on. */ xAxisID: string; /** * The ID of the y axis to plot this dataset on. */ yAxisID: string; /** * Percent (0-1) of the available width each bar should be within the category width. 1.0 will take the whole category width and put the bars right next to each other. * @default 0.9 */ barPercentage: number; /** * Percent (0-1) of the available width each category should be within the sample width. * @default 0.8 */ categoryPercentage: number; /** * Manually set width of each bar in pixels. If set to 'flex', it computes "optimal" sample widths that globally arrange bars side by side. If not set (default), bars are equally sized based on the smallest interval. */ barThickness: number | 'flex'; /** * Set this to ensure that bars are not sized thicker than this. */ maxBarThickness: number; /** * Set this to ensure that bars have a minimum length in pixels. */ minBarLength: number; /** * Point style for the legend * @default 'circle; */ pointStyle: PointStyle; /** * Should the bars be grouped on index axis * @default true */ grouped: boolean; } export interface BarControllerChartOptions { /** * Should null or undefined values be omitted from drawing */ skipNull?: boolean; } export type BarController = DatasetController export declare const BarController: ChartComponent & { prototype: BarController; new (chart: Chart, datasetIndex: number): BarController; }; export interface BubbleControllerDatasetOptions extends ControllerDatasetOptions, ScriptableAndArrayOptions>, ScriptableAndArrayOptions> { /** * The ID of the x axis to plot this dataset on. */ xAxisID: string; /** * The ID of the y axis to plot this dataset on. */ yAxisID: string; } export interface BubbleDataPoint extends Point { /** * Bubble radius in pixels (not scaled). */ r?: number; } export type BubbleController = DatasetController export declare const BubbleController: ChartComponent & { prototype: BubbleController; new (chart: Chart, datasetIndex: number): BubbleController; }; export interface LineControllerDatasetOptions extends ControllerDatasetOptions, ScriptableAndArrayOptions>, ScriptableAndArrayOptions>, ScriptableOptions, ScriptableContext<'line'>>, ScriptableAndArrayOptions>, ScriptableOptions, ScriptableContext<'line'>>, ScriptableAndArrayOptions>, AnimationOptions<'line'> { /** * The ID of the x axis to plot this dataset on. */ xAxisID: string; /** * The ID of the y axis to plot this dataset on. */ yAxisID: string; /** * If true, lines will be drawn between points with no or null data. If false, points with NaN data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. * @default false */ spanGaps: boolean | number; showLine: boolean; } export interface LineControllerChartOptions { /** * If true, lines will be drawn between points with no or null data. If false, points with NaN data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. * @default false */ spanGaps: boolean | number; /** * If false, the lines between points are not drawn. * @default true */ showLine: boolean; } export type LineController = DatasetController export declare const LineController: ChartComponent & { prototype: LineController; new (chart: Chart, datasetIndex: number): LineController; }; export type ScatterControllerDatasetOptions = LineControllerDatasetOptions; export type ScatterDataPoint = Point export type ScatterControllerChartOptions = LineControllerChartOptions; export type ScatterController = LineController export declare const ScatterController: ChartComponent & { prototype: ScatterController; new (chart: Chart, datasetIndex: number): ScatterController; }; export interface DoughnutControllerDatasetOptions extends ControllerDatasetOptions, ScriptableAndArrayOptions>, ScriptableAndArrayOptions>, AnimationOptions<'doughnut'> { /** * Sweep to allow arcs to cover. * @default 360 */ circumference: number; /** * Arc offset (in pixels). */ offset: number | number[]; /** * Starting angle to draw this dataset from. * @default 0 */ rotation: number; /** * The relative thickness of the dataset. Providing a value for weight will cause the pie or doughnut dataset to be drawn with a thickness relative to the sum of all the dataset weight values. * @default 1 */ weight: number; /** * Similar to the `offset` option, but applies to all arcs. This can be used to to add spaces * between arcs * @default 0 */ spacing: number; } export interface DoughnutAnimationOptions extends AnimationSpec<'doughnut'> { /** * If true, the chart will animate in with a rotation animation. This property is in the options.animation object. * @default true */ animateRotate: boolean; /** * If true, will animate scaling the chart from the center outwards. * @default false */ animateScale: boolean; } export interface DoughnutControllerChartOptions { /** * Sweep to allow arcs to cover. * @default 360 */ circumference: number; /** * The portion of the chart that is cut out of the middle. ('50%' - for doughnut, 0 - for pie) * String ending with '%' means percentage, number means pixels. * @default 50 */ cutout: Scriptable>; /** * Arc offset (in pixels). */ offset: number | number[]; /** * The outer radius of the chart. String ending with '%' means percentage of maximum radius, number means pixels. * @default '100%' */ radius: Scriptable>; /** * Starting angle to draw arcs from. * @default 0 */ rotation: number; /** * Spacing between the arcs * @default 0 */ spacing: number; animation: false | DoughnutAnimationOptions; } export type DoughnutDataPoint = number; export interface DoughnutController extends DatasetController { readonly innerRadius: number; readonly outerRadius: number; readonly offsetX: number; readonly offsetY: number; calculateTotal(): number; calculateCircumference(value: number): number; } export declare const DoughnutController: ChartComponent & { prototype: DoughnutController; new (chart: Chart, datasetIndex: number): DoughnutController; }; export interface DoughnutMetaExtensions { total: number; } export type PieControllerDatasetOptions = DoughnutControllerDatasetOptions; export type PieControllerChartOptions = DoughnutControllerChartOptions; export type PieAnimationOptions = DoughnutAnimationOptions; export type PieDataPoint = DoughnutDataPoint; export type PieMetaExtensions = DoughnutMetaExtensions; export type PieController = DoughnutController export declare const PieController: ChartComponent & { prototype: PieController; new (chart: Chart, datasetIndex: number): PieController; }; export interface PolarAreaControllerDatasetOptions extends DoughnutControllerDatasetOptions { /** * Arc angle to cover. - for polar only * @default circumference / (arc count) */ angle: number; } export type PolarAreaAnimationOptions = DoughnutAnimationOptions; export interface PolarAreaControllerChartOptions { /** * Starting angle to draw arcs for the first item in a dataset. In degrees, 0 is at top. * @default 0 */ startAngle: number; animation: false | PolarAreaAnimationOptions; } export interface PolarAreaController extends DoughnutController { countVisibleElements(): number; } export declare const PolarAreaController: ChartComponent & { prototype: PolarAreaController; new (chart: Chart, datasetIndex: number): PolarAreaController; }; export interface RadarControllerDatasetOptions extends ControllerDatasetOptions, ScriptableAndArrayOptions>, ScriptableAndArrayOptions>, AnimationOptions<'radar'> { /** * The ID of the x axis to plot this dataset on. */ xAxisID: string; /** * The ID of the y axis to plot this dataset on. */ yAxisID: string; /** * If true, lines will be drawn between points with no or null data. If false, points with NaN data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. */ spanGaps: boolean | number; /** * If false, the line is not drawn for this dataset. */ showLine: boolean; } export type RadarControllerChartOptions = LineControllerChartOptions; export type RadarController = DatasetController export declare const RadarController: ChartComponent & { prototype: RadarController; new (chart: Chart, datasetIndex: number): RadarController; }; interface ChartMetaClip { left: number | boolean; top: number | boolean; right: number | boolean; bottom: number | boolean; disabled: boolean; } interface ChartMetaCommon { type: string; controller: DatasetController; order: number; label: string; index: number; visible: boolean; stack: number; indexAxis: 'x' | 'y'; data: TElement[]; dataset?: TDatasetElement; hidden: boolean; xAxisID?: string; yAxisID?: string; rAxisID?: string; iAxisID: string; vAxisID: string; xScale?: Scale; yScale?: Scale; rScale?: Scale; iScale?: Scale; vScale?: Scale; _sorted: boolean; _stacked: boolean | 'single'; _parsed: unknown[]; _clip: ChartMetaClip; } export type ChartMeta< TType extends ChartType = ChartType, TElement extends Element = Element, TDatasetElement extends Element = Element, > = DeepPartial< { [key in ChartType]: ChartTypeRegistry[key]['metaExtensions'] }[TType] > & ChartMetaCommon; export interface ActiveDataPoint { datasetIndex: number; index: number; } export interface ActiveElement extends ActiveDataPoint { element: Element; } export declare class Chart< TType extends ChartType = ChartType, TData = DefaultDataPoint, TLabel = unknown > { readonly platform: BasePlatform; readonly id: string; readonly canvas: HTMLCanvasElement; readonly ctx: CanvasRenderingContext2D; readonly config: ChartConfiguration | ChartConfigurationCustomTypesPerDataset; readonly width: number; readonly height: number; readonly aspectRatio: number; readonly boxes: LayoutItem[]; readonly currentDevicePixelRatio: number; readonly chartArea: ChartArea; readonly scales: { [key: string]: Scale }; readonly attached: boolean; readonly legend?: LegendElement; // Only available if legend plugin is registered and enabled readonly tooltip?: TooltipModel; // Only available if tooltip plugin is registered and enabled data: ChartData; options: ChartOptions; constructor(item: ChartItem, config: ChartConfiguration | ChartConfigurationCustomTypesPerDataset); clear(): this; stop(): this; resize(width?: number, height?: number): void; ensureScalesHaveIDs(): void; buildOrUpdateScales(): void; buildOrUpdateControllers(): void; reset(): void; update(mode?: UpdateMode | ((ctx: { datasetIndex: number }) => UpdateMode)): void; render(): void; draw(): void; isPointInArea(point: Point): boolean; getElementsAtEventForMode(e: Event, mode: string, options: InteractionOptions, useFinalPosition: boolean): InteractionItem[]; getSortedVisibleDatasetMetas(): ChartMeta[]; getDatasetMeta(datasetIndex: number): ChartMeta; getVisibleDatasetCount(): number; isDatasetVisible(datasetIndex: number): boolean; setDatasetVisibility(datasetIndex: number, visible: boolean): void; toggleDataVisibility(index: number): void; getDataVisibility(index: number): boolean; hide(datasetIndex: number, dataIndex?: number): void; show(datasetIndex: number, dataIndex?: number): void; getActiveElements(): ActiveElement[]; setActiveElements(active: ActiveDataPoint[]): void; destroy(): void; toBase64Image(type?: string, quality?: unknown): string; bindEvents(): void; unbindEvents(): void; updateHoverStyle(items: InteractionItem[], mode: 'dataset', enabled: boolean): void; notifyPlugins(hook: string, args?: AnyObject): boolean | void; isPluginEnabled(pluginId: string): boolean; getContext(): { chart: Chart, type: string }; static readonly defaults: Defaults; static readonly overrides: Overrides; static readonly version: string; static readonly instances: { [key: string]: Chart }; static readonly registry: Registry; static getChart(key: string | CanvasRenderingContext2D | HTMLCanvasElement): Chart | undefined; static register(...items: ChartComponentLike[]): void; static unregister(...items: ChartComponentLike[]): void; } export declare const registerables: readonly ChartComponentLike[]; export declare type ChartItem = | string | CanvasRenderingContext2D | HTMLCanvasElement | { canvas: HTMLCanvasElement } | ArrayLike; export declare enum UpdateModeEnum { resize = 'resize', reset = 'reset', none = 'none', hide = 'hide', show = 'show', default = 'default', active = 'active' } export type UpdateMode = keyof typeof UpdateModeEnum; export declare class DatasetController< TType extends ChartType = ChartType, TElement extends Element = Element, TDatasetElement extends Element = Element, TParsedData = ParsedDataType, > { constructor(chart: Chart, datasetIndex: number); readonly chart: Chart; readonly index: number; readonly _cachedMeta: ChartMeta; enableOptionSharing: boolean; // If true, the controller supports the decimation // plugin. Defaults to `false` for all controllers // except the LineController supportsDecimation: boolean; linkScales(): void; getAllParsedValues(scale: Scale): number[]; protected getLabelAndValue(index: number): { label: string; value: string }; updateElements(elements: TElement[], start: number, count: number, mode: UpdateMode): void; update(mode: UpdateMode): void; updateIndex(datasetIndex: number): void; protected getMaxOverflow(): boolean | number; draw(): void; reset(): void; getDataset(): ChartDataset; getMeta(): ChartMeta; getScaleForId(scaleID: string): Scale | undefined; configure(): void; initialize(): void; addElements(): void; buildOrUpdateElements(resetNewElements?: boolean): void; getStyle(index: number, active: boolean): AnyObject; protected resolveDatasetElementOptions(mode: UpdateMode): AnyObject; protected resolveDataElementOptions(index: number, mode: UpdateMode): AnyObject; /** * Utility for checking if the options are shared and should be animated separately. * @protected */ protected getSharedOptions(options: AnyObject): undefined | AnyObject; /** * Utility for determining if `options` should be included in the updated properties * @protected */ protected includeOptions(mode: UpdateMode, sharedOptions: AnyObject): boolean; /** * Utility for updating an element with new properties, using animations when appropriate. * @protected */ protected updateElement(element: TElement | TDatasetElement, index: number | undefined, properties: AnyObject, mode: UpdateMode): void; /** * Utility to animate the shared options, that are potentially affecting multiple elements. * @protected */ protected updateSharedOptions(sharedOptions: AnyObject, mode: UpdateMode, newOptions: AnyObject): void; removeHoverStyle(element: TElement, datasetIndex: number, index: number): void; setHoverStyle(element: TElement, datasetIndex: number, index: number): void; parse(start: number, count: number): void; protected parsePrimitiveData(meta: ChartMeta, data: AnyObject[], start: number, count: number): AnyObject[]; protected parseArrayData(meta: ChartMeta, data: AnyObject[], start: number, count: number): AnyObject[]; protected parseObjectData(meta: ChartMeta, data: AnyObject[], start: number, count: number): AnyObject[]; protected getParsed(index: number): TParsedData; protected applyStack(scale: Scale, parsed: unknown[]): number; protected updateRangeFromParsed( range: { min: number; max: number }, scale: Scale, parsed: unknown[], stack: boolean | string ): void; protected getMinMax(scale: Scale, canStack?: boolean): { min: number; max: number }; } export interface DatasetControllerChartComponent extends ChartComponent { defaults: { datasetElementType?: string | null | false; dataElementType?: string | null | false; }; } export interface Defaults extends CoreChartOptions, ElementChartOptions, PluginChartOptions { scale: ScaleOptionsByType; scales: { [key in ScaleType]: ScaleOptionsByType; }; set(values: AnyObject): AnyObject; set(scope: string, values: AnyObject): AnyObject; get(scope: string): AnyObject; describe(scope: string, values: AnyObject): AnyObject; override(scope: string, values: AnyObject): AnyObject; /** * Routes the named defaults to fallback to another scope/name. * This routing is useful when those target values, like defaults.color, are changed runtime. * If the values would be copied, the runtime change would not take effect. By routing, the * fallback is evaluated at each access, so its always up to date. * * Example: * * defaults.route('elements.arc', 'backgroundColor', '', 'color') * - reads the backgroundColor from defaults.color when undefined locally * * @param scope Scope this route applies to. * @param name Property name that should be routed to different namespace when not defined here. * @param targetScope The namespace where those properties should be routed to. * Empty string ('') is the root of defaults. * @param targetName The target name in the target scope the property should be routed to. */ route(scope: string, name: string, targetScope: string, targetName: string): void; } export type Overrides = { [key in ChartType]: CoreChartOptions & ElementChartOptions & PluginChartOptions & DatasetChartOptions & ScaleChartOptions & ChartTypeRegistry[key]['chartOptions']; } export declare const defaults: Defaults; export interface InteractionOptions { axis?: string; intersect?: boolean; includeInvisible?: boolean; } export interface InteractionItem { element: Element; datasetIndex: number; index: number; } export type InteractionModeFunction = ( chart: Chart, e: ChartEvent, options: InteractionOptions, useFinalPosition?: boolean ) => InteractionItem[]; export interface InteractionModeMap { /** * Returns items at the same index. If the options.intersect parameter is true, we only return items if we intersect something * If the options.intersect mode is false, we find the nearest item and return the items at the same index as that item */ index: InteractionModeFunction; /** * Returns items in the same dataset. If the options.intersect parameter is true, we only return items if we intersect something * If the options.intersect is false, we find the nearest item and return the items in that dataset */ dataset: InteractionModeFunction; /** * Point mode returns all elements that hit test based on the event position * of the event */ point: InteractionModeFunction; /** * nearest mode returns the element closest to the point */ nearest: InteractionModeFunction; /** * x mode returns the elements that hit-test at the current x coordinate */ x: InteractionModeFunction; /** * y mode returns the elements that hit-test at the current y coordinate */ y: InteractionModeFunction; } export type InteractionMode = keyof InteractionModeMap; export declare const Interaction: { modes: InteractionModeMap; /** * Helper function to select candidate elements for interaction */ evaluateInteractionItems( chart: Chart, axis: InteractionAxis, position: Point, handler: (element: Element & VisualElement, datasetIndex: number, index: number) => void, intersect?: boolean ): InteractionItem[]; }; export declare const layouts: { /** * Register a box to a chart. * A box is simply a reference to an object that requires layout. eg. Scales, Legend, Title. * @param {Chart} chart - the chart to use * @param {LayoutItem} item - the item to add to be laid out */ addBox(chart: Chart, item: LayoutItem): void; /** * Remove a layoutItem from a chart * @param {Chart} chart - the chart to remove the box from * @param {LayoutItem} layoutItem - the item to remove from the layout */ removeBox(chart: Chart, layoutItem: LayoutItem): void; /** * Sets (or updates) options on the given `item`. * @param {Chart} chart - the chart in which the item lives (or will be added to) * @param {LayoutItem} item - the item to configure with the given options * @param options - the new item options. */ configure( chart: Chart, item: LayoutItem, options: { fullSize?: number; position?: LayoutPosition; weight?: number } ): void; /** * Fits boxes of the given chart into the given size by having each box measure itself * then running a fitting algorithm * @param {Chart} chart - the chart * @param {number} width - the width to fit into * @param {number} height - the height to fit into */ update(chart: Chart, width: number, height: number): void; }; export interface Plugin extends ExtendedPlugin { id: string; /** * The events option defines the browser events that the plugin should listen. * @default ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'] */ events?: (keyof HTMLElementEventMap)[] /** * @desc Called when plugin is installed for this chart instance. This hook is also invoked for disabled plugins (options === false). * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. * @since 3.0.0 */ install?(chart: Chart, args: EmptyObject, options: O): void; /** * @desc Called when a plugin is starting. This happens when chart is created or plugin is enabled. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. * @since 3.0.0 */ start?(chart: Chart, args: EmptyObject, options: O): void; /** * @desc Called when a plugin stopping. This happens when chart is destroyed or plugin is disabled. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. * @since 3.0.0 */ stop?(chart: Chart, args: EmptyObject, options: O): void; /** * @desc Called before initializing `chart`. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. */ beforeInit?(chart: Chart, args: EmptyObject, options: O): void; /** * @desc Called after `chart` has been initialized and before the first update. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. */ afterInit?(chart: Chart, args: EmptyObject, options: O): void; /** * @desc Called before updating `chart`. If any plugin returns `false`, the update * is cancelled (and thus subsequent render(s)) until another `update` is triggered. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {UpdateMode} args.mode - The update mode * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart update. */ beforeUpdate?(chart: Chart, args: { mode: UpdateMode, cancelable: true }, options: O): boolean | void; /** * @desc Called after `chart` has been updated and before rendering. Note that this * hook will not be called if the chart update has been previously cancelled. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {UpdateMode} args.mode - The update mode * @param {object} options - The plugin options. */ afterUpdate?(chart: Chart, args: { mode: UpdateMode }, options: O): void; /** * @desc Called during the update process, before any chart elements have been created. * This can be used for data decimation by changing the data array inside a dataset. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. */ beforeElementsUpdate?(chart: Chart, args: EmptyObject, options: O): void; /** * @desc Called during chart reset * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. * @since version 3.0.0 */ reset?(chart: Chart, args: EmptyObject, options: O): void; /** * @desc Called before updating the `chart` datasets. If any plugin returns `false`, * the datasets update is cancelled until another `update` is triggered. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {UpdateMode} args.mode - The update mode. * @param {object} options - The plugin options. * @returns {boolean} false to cancel the datasets update. * @since version 2.1.5 */ beforeDatasetsUpdate?(chart: Chart, args: { mode: UpdateMode }, options: O): boolean | void; /** * @desc Called after the `chart` datasets have been updated. Note that this hook * will not be called if the datasets update has been previously cancelled. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {UpdateMode} args.mode - The update mode. * @param {object} options - The plugin options. * @since version 2.1.5 */ afterDatasetsUpdate?(chart: Chart, args: { mode: UpdateMode, cancelable: true }, options: O): void; /** * @desc Called before updating the `chart` dataset at the given `args.index`. If any plugin * returns `false`, the datasets update is cancelled until another `update` is triggered. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {number} args.index - The dataset index. * @param {object} args.meta - The dataset metadata. * @param {UpdateMode} args.mode - The update mode. * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart datasets drawing. */ beforeDatasetUpdate?(chart: Chart, args: { index: number; meta: ChartMeta, mode: UpdateMode, cancelable: true }, options: O): boolean | void; /** * @desc Called after the `chart` datasets at the given `args.index` has been updated. Note * that this hook will not be called if the datasets update has been previously cancelled. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {number} args.index - The dataset index. * @param {object} args.meta - The dataset metadata. * @param {UpdateMode} args.mode - The update mode. * @param {object} options - The plugin options. */ afterDatasetUpdate?(chart: Chart, args: { index: number; meta: ChartMeta, mode: UpdateMode, cancelable: false }, options: O): void; /** * @desc Called before laying out `chart`. If any plugin returns `false`, * the layout update is cancelled until another `update` is triggered. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart layout. */ beforeLayout?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; /** * @desc Called before scale data limits are calculated. This hook is called separately for each scale in the chart. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {Scale} args.scale - The scale. * @param {object} options - The plugin options. */ beforeDataLimits?(chart: Chart, args: { scale: Scale }, options: O): void; /** * @desc Called after scale data limits are calculated. This hook is called separately for each scale in the chart. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {Scale} args.scale - The scale. * @param {object} options - The plugin options. */ afterDataLimits?(chart: Chart, args: { scale: Scale }, options: O): void; /** * @desc Called before scale builds its ticks. This hook is called separately for each scale in the chart. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {Scale} args.scale - The scale. * @param {object} options - The plugin options. */ beforeBuildTicks?(chart: Chart, args: { scale: Scale }, options: O): void; /** * @desc Called after scale has build its ticks. This hook is called separately for each scale in the chart. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {Scale} args.scale - The scale. * @param {object} options - The plugin options. */ afterBuildTicks?(chart: Chart, args: { scale: Scale }, options: O): void; /** * @desc Called after the `chart` has been laid out. Note that this hook will not * be called if the layout update has been previously cancelled. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. */ afterLayout?(chart: Chart, args: EmptyObject, options: O): void; /** * @desc Called before rendering `chart`. If any plugin returns `false`, * the rendering is cancelled until another `render` is triggered. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart rendering. */ beforeRender?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; /** * @desc Called after the `chart` has been fully rendered (and animation completed). Note * that this hook will not be called if the rendering has been previously cancelled. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. */ afterRender?(chart: Chart, args: EmptyObject, options: O): void; /** * @desc Called before drawing `chart` at every animation frame. If any plugin returns `false`, * the frame drawing is cancelled untilanother `render` is triggered. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart drawing. */ beforeDraw?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; /** * @desc Called after the `chart` has been drawn. Note that this hook will not be called * if the drawing has been previously cancelled. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. */ afterDraw?(chart: Chart, args: EmptyObject, options: O): void; /** * @desc Called before drawing the `chart` datasets. If any plugin returns `false`, * the datasets drawing is cancelled until another `render` is triggered. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart datasets drawing. */ beforeDatasetsDraw?(chart: Chart, args: { cancelable: true }, options: O): boolean | void; /** * @desc Called after the `chart` datasets have been drawn. Note that this hook * will not be called if the datasets drawing has been previously cancelled. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. */ afterDatasetsDraw?(chart: Chart, args: EmptyObject, options: O, cancelable: false): void; /** * @desc Called before drawing the `chart` dataset at the given `args.index` (datasets * are drawn in the reverse order). If any plugin returns `false`, the datasets drawing * is cancelled until another `render` is triggered. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {number} args.index - The dataset index. * @param {object} args.meta - The dataset metadata. * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart datasets drawing. */ beforeDatasetDraw?(chart: Chart, args: { index: number; meta: ChartMeta }, options: O): boolean | void; /** * @desc Called after the `chart` datasets at the given `args.index` have been drawn * (datasets are drawn in the reverse order). Note that this hook will not be called * if the datasets drawing has been previously cancelled. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {number} args.index - The dataset index. * @param {object} args.meta - The dataset metadata. * @param {object} options - The plugin options. */ afterDatasetDraw?(chart: Chart, args: { index: number; meta: ChartMeta }, options: O): void; /** * @desc Called before processing the specified `event`. If any plugin returns `false`, * the event will be discarded. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {ChartEvent} args.event - The event object. * @param {boolean} args.replay - True if this event is replayed from `Chart.update` * @param {boolean} args.inChartArea - The event position is inside chartArea * @param {boolean} [args.changed] - Set to true if the plugin needs a render. Should only be changed to true, because this args object is passed through all plugins. * @param {object} options - The plugin options. */ beforeEvent?(chart: Chart, args: { event: ChartEvent, replay: boolean, changed?: boolean; cancelable: true, inChartArea: boolean }, options: O): boolean | void; /** * @desc Called after the `event` has been consumed. Note that this hook * will not be called if the `event` has been previously discarded. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {ChartEvent} args.event - The event object. * @param {boolean} args.replay - True if this event is replayed from `Chart.update` * @param {boolean} args.inChartArea - The event position is inside chartArea * @param {boolean} [args.changed] - Set to true if the plugin needs a render. Should only be changed to true, because this args object is passed through all plugins. * @param {object} options - The plugin options. */ afterEvent?(chart: Chart, args: { event: ChartEvent, replay: boolean, changed?: boolean, cancelable: false, inChartArea: boolean }, options: O): void; /** * @desc Called after the chart as been resized. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {number} args.size - The new canvas display size (eq. canvas.style width & height). * @param {object} options - The plugin options. */ resize?(chart: Chart, args: { size: { width: number, height: number } }, options: O): void; /** * Called before the chart is being destroyed. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. */ beforeDestroy?(chart: Chart, args: EmptyObject, options: O): void; /** * Called after the chart has been destroyed. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. */ afterDestroy?(chart: Chart, args: EmptyObject, options: O): void; /** * Called after chart is destroyed on all plugins that were installed for that chart. This hook is also invoked for disabled plugins (options === false). * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {object} options - The plugin options. * @since 3.0.0 */ uninstall?(chart: Chart, args: EmptyObject, options: O): void; /** * Default options used in the plugin */ defaults?: Partial; } export declare type ChartComponentLike = ChartComponent | ChartComponent[] | { [key: string]: ChartComponent } | Plugin | Plugin[]; /** * Please use the module's default export which provides a singleton instance * Note: class is exported for typedoc */ export interface Registry { readonly controllers: TypedRegistry; readonly elements: TypedRegistry; readonly plugins: TypedRegistry; readonly scales: TypedRegistry; add(...args: ChartComponentLike[]): void; remove(...args: ChartComponentLike[]): void; addControllers(...args: ChartComponentLike[]): void; addElements(...args: ChartComponentLike[]): void; addPlugins(...args: ChartComponentLike[]): void; addScales(...args: ChartComponentLike[]): void; getController(id: string): DatasetController | undefined; getElement(id: string): Element | undefined; getPlugin(id: string): Plugin | undefined; getScale(id: string): Scale | undefined; } export declare const registry: Registry; export interface Tick { value: number; label?: string | string[]; major?: boolean; } export interface CoreScaleOptions { /** * Controls the axis global visibility (visible when true, hidden when false). When display: 'auto', the axis is visible only if at least one associated dataset is visible. * @default true */ display: boolean | 'auto'; /** * Align pixel values to device pixels */ alignToPixels: boolean; /** * Background color of the scale area. */ backgroundColor: Color; /** * Reverse the scale. * @default false */ reverse: boolean; /** * Clip the dataset drawing against the size of the scale instead of chart area. * @default true */ clip: boolean; /** * The weight used to sort the axis. Higher weights are further away from the chart area. * @default true */ weight: number; /** * User defined minimum value for the scale, overrides minimum value from data. */ min: unknown; /** * User defined maximum value for the scale, overrides maximum value from data. */ max: unknown; /** * Adjustment used when calculating the maximum data value. */ suggestedMin: unknown; /** * Adjustment used when calculating the minimum data value. */ suggestedMax: unknown; /** * Callback called before the update process starts. */ beforeUpdate(axis: Scale): void; /** * Callback that runs before dimensions are set. */ beforeSetDimensions(axis: Scale): void; /** * Callback that runs after dimensions are set. */ afterSetDimensions(axis: Scale): void; /** * Callback that runs before data limits are determined. */ beforeDataLimits(axis: Scale): void; /** * Callback that runs after data limits are determined. */ afterDataLimits(axis: Scale): void; /** * Callback that runs before ticks are created. */ beforeBuildTicks(axis: Scale): void; /** * Callback that runs after ticks are created. Useful for filtering ticks. */ afterBuildTicks(axis: Scale): void; /** * Callback that runs before ticks are converted into strings. */ beforeTickToLabelConversion(axis: Scale): void; /** * Callback that runs after ticks are converted into strings. */ afterTickToLabelConversion(axis: Scale): void; /** * Callback that runs before tick rotation is determined. */ beforeCalculateLabelRotation(axis: Scale): void; /** * Callback that runs after tick rotation is determined. */ afterCalculateLabelRotation(axis: Scale): void; /** * Callback that runs before the scale fits to the canvas. */ beforeFit(axis: Scale): void; /** * Callback that runs after the scale fits to the canvas. */ afterFit(axis: Scale): void; /** * Callback that runs at the end of the update process. */ afterUpdate(axis: Scale): void; } export interface Scale extends Element, LayoutItem { readonly id: string; readonly type: string; readonly ctx: CanvasRenderingContext2D; readonly chart: Chart; maxWidth: number; maxHeight: number; paddingTop: number; paddingBottom: number; paddingLeft: number; paddingRight: number; axis: string; labelRotation: number; min: number; max: number; ticks: Tick[]; getMatchingVisibleMetas(type?: string): ChartMeta[]; drawTitle(chartArea: ChartArea): void; drawLabels(chartArea: ChartArea): void; drawGrid(chartArea: ChartArea): void; /** * @param {number} pixel * @return {number} */ getDecimalForPixel(pixel: number): number; /** * Utility for getting the pixel location of a percentage of scale * The coordinate (0, 0) is at the upper-left corner of the canvas * @param {number} decimal * @return {number} */ getPixelForDecimal(decimal: number): number; /** * Returns the location of the tick at the given index * The coordinate (0, 0) is at the upper-left corner of the canvas * @param {number} index * @return {number} */ getPixelForTick(index: number): number; /** * Used to get the label to display in the tooltip for the given value * @param {*} value * @return {string} */ getLabelForValue(value: number): string; /** * Returns the grid line width at given value */ getLineWidthForValue(value: number): number; /** * Returns the location of the given data point. Value can either be an index or a numerical value * The coordinate (0, 0) is at the upper-left corner of the canvas * @param {*} value * @param {number} [index] * @return {number} */ getPixelForValue(value: number, index?: number): number; /** * Used to get the data value from a given pixel. This is the inverse of getPixelForValue * The coordinate (0, 0) is at the upper-left corner of the canvas * @param {number} pixel * @return {*} */ getValueForPixel(pixel: number): number | undefined; getBaseValue(): number; /** * Returns the pixel for the minimum chart value * The coordinate (0, 0) is at the upper-left corner of the canvas * @return {number} */ getBasePixel(): number; init(options: O): void; parse(raw: unknown, index?: number): unknown; getUserBounds(): { min: number; max: number; minDefined: boolean; maxDefined: boolean }; getMinMax(canStack: boolean): { min: number; max: number }; getTicks(): Tick[]; getLabels(): string[]; getLabelItems(chartArea?: ChartArea): LabelItem[]; beforeUpdate(): void; configure(): void; afterUpdate(): void; beforeSetDimensions(): void; setDimensions(): void; afterSetDimensions(): void; beforeDataLimits(): void; determineDataLimits(): void; afterDataLimits(): void; beforeBuildTicks(): void; buildTicks(): Tick[]; afterBuildTicks(): void; beforeTickToLabelConversion(): void; generateTickLabels(ticks: Tick[]): void; afterTickToLabelConversion(): void; beforeCalculateLabelRotation(): void; calculateLabelRotation(): void; afterCalculateLabelRotation(): void; beforeFit(): void; fit(): void; afterFit(): void; isFullSize(): boolean; } export declare class Scale { constructor(cfg: {id: string, type: string, ctx: CanvasRenderingContext2D, chart: Chart}); } export interface ScriptableScaleContext { chart: Chart; scale: Scale; index: number; tick: Tick; } export interface ScriptableScalePointLabelContext { chart: Chart; scale: Scale; index: number; label: string; type: string; } export interface RenderTextOpts { /** * The fill color of the text. If unset, the existing * fillStyle property of the canvas is unchanged. */ color?: Color; /** * The width of the strikethrough / underline * @default 2 */ decorationWidth?: number; /** * The max width of the text in pixels */ maxWidth?: number; /** * A rotation to be applied to the canvas * This is applied after the translation is applied */ rotation?: number; /** * Apply a strikethrough effect to the text */ strikethrough?: boolean; /** * The color of the text stroke. If unset, the existing * strokeStyle property of the context is unchanged */ strokeColor?: Color; /** * The text stroke width. If unset, the existing * lineWidth property of the context is unchanged */ strokeWidth?: number; /** * The text alignment to use. If unset, the existing * textAlign property of the context is unchanged */ textAlign?: CanvasTextAlign; /** * The text baseline to use. If unset, the existing * textBaseline property of the context is unchanged */ textBaseline?: CanvasTextBaseline; /** * If specified, a translation to apply to the context */ translation?: [number, number]; /** * Underline the text */ underline?: boolean; /** * Dimensions for drawing the label backdrop */ backdrop?: BackdropOptions; } export interface BackdropOptions { /** * Left position of backdrop as pixel */ left: number; /** * Top position of backdrop as pixel */ top: number; /** * Width of backdrop in pixels */ width: number; /** * Height of backdrop in pixels */ height: number; /** * Color of label backdrops. */ color: Scriptable; } export interface LabelItem { label: string | string[]; font: CanvasFontSpec; textOffset: number; options: RenderTextOpts; } export declare const Ticks: { formatters: { /** * Formatter for value labels * @param value the value to display * @return {string|string[]} the label to display */ values(value: unknown): string | string[]; /** * Formatter for numeric ticks * @param tickValue the value to be formatted * @param index the position of the tickValue parameter in the ticks array * @param ticks the list of ticks being converted * @return string representation of the tickValue parameter */ numeric(this: Scale, tickValue: number, index: number, ticks: { value: number }[]): string; /** * Formatter for logarithmic ticks * @param tickValue the value to be formatted * @param index the position of the tickValue parameter in the ticks array * @param ticks the list of ticks being converted * @return string representation of the tickValue parameter */ logarithmic(this: Scale, tickValue: number, index: number, ticks: { value: number }[]): string; }; }; export interface TypedRegistry { /** * @param {ChartComponent} item * @returns {string} The scope where items defaults were registered to. */ register(item: ChartComponent): string; get(id: string): T | undefined; unregister(item: ChartComponent): void; } export interface ChartEvent { type: | 'contextmenu' | 'mouseenter' | 'mousedown' | 'mousemove' | 'mouseup' | 'mouseout' | 'click' | 'dblclick' | 'keydown' | 'keypress' | 'keyup' | 'resize'; native: Event | null; x: number | null; y: number | null; } export interface ChartComponent { id: string; defaults?: AnyObject; defaultRoutes?: { [property: string]: string }; beforeRegister?(): void; afterRegister?(): void; beforeUnregister?(): void; afterUnregister?(): void; } export type InteractionAxis = 'x' | 'y' | 'xy' | 'r'; export interface CoreInteractionOptions { /** * Sets which elements appear in the tooltip. See Interaction Modes for details. * @default 'nearest' */ mode: InteractionMode; /** * if true, the hover mode only applies when the mouse position intersects an item on the chart. * @default true */ intersect: boolean; /** * Defines which directions are used in calculating distances. Defaults to 'x' for 'index' mode and 'xy' in dataset and 'nearest' modes. */ axis: InteractionAxis; /** * if true, the invisible points that are outside of the chart area will also be included when evaluating interactions. * @default false */ includeInvisible: boolean; } export interface CoreChartOptions extends ParsingOptions, AnimationOptions { datasets: { [key in ChartType]: ChartTypeRegistry[key]['datasetOptions'] } /** * The base axis of the chart. 'x' for vertical charts and 'y' for horizontal charts. * @default 'x' */ indexAxis: 'x' | 'y'; /** * How to clip relative to chartArea. Positive value allows overflow, negative value clips that many pixels inside chartArea. 0 = clip at chartArea. Clipping can also be configured per side: `clip: {left: 5, top: false, right: -2, bottom: 0}` */ clip: number | ChartArea | false; /** * base color * @see Defaults.color */ color: Scriptable>; /** * base background color * @see Defaults.backgroundColor */ backgroundColor: ScriptableAndArray>; /** * base hover background color * @see Defaults.hoverBackgroundColor */ hoverBackgroundColor: ScriptableAndArray>; /** * base border color * @see Defaults.borderColor */ borderColor: ScriptableAndArray>; /** * base hover border color * @see Defaults.hoverBorderColor */ hoverBorderColor: ScriptableAndArray>; /** * base font * @see Defaults.font */ font: Partial; /** * Resizes the chart canvas when its container does (important note...). * @default true */ responsive: boolean; /** * Maintain the original canvas aspect ratio (width / height) when resizing. For this option to work properly the chart must be in its own dedicated container. * @default true */ maintainAspectRatio: boolean; /** * Delay the resize update by give amount of milliseconds. This can ease the resize process by debouncing update of the elements. * @default 0 */ resizeDelay: number; /** * Canvas aspect ratio (i.e. width / height, a value of 1 representing a square canvas). Note that this option is ignored if the height is explicitly defined either as attribute or via the style. * @default 2 */ aspectRatio: number; /** * Locale used for number formatting (using `Intl.NumberFormat`). * @default user's browser setting */ locale: string; /** * Called when a resize occurs. Gets passed two arguments: the chart instance and the new size. */ onResize(chart: Chart, size: { width: number; height: number }): void; /** * Override the window's default devicePixelRatio. * @default window.devicePixelRatio */ devicePixelRatio: number; interaction: CoreInteractionOptions; hover: CoreInteractionOptions; /** * The events option defines the browser events that the chart should listen to for tooltips and hovering. * @default ['mousemove', 'mouseout', 'click', 'touchstart', 'touchmove'] */ events: (keyof HTMLElementEventMap)[] /** * Called when any of the events fire. Passed the event, an array of active elements (bars, points, etc), and the chart. */ onHover(event: ChartEvent, elements: ActiveElement[], chart: Chart): void; /** * Called if the event is of type 'mouseup' or 'click'. Passed the event, an array of active elements, and the chart. */ onClick(event: ChartEvent, elements: ActiveElement[], chart: Chart): void; layout: Partial<{ autoPadding: boolean; padding: Scriptable>; }>; } export type AnimationSpec = { /** * The number of milliseconds an animation takes. * @default 1000 */ duration?: Scriptable>; /** * Easing function to use * @default 'easeOutQuart' */ easing?: Scriptable>; /** * Delay before starting the animations. * @default 0 */ delay?: Scriptable>; /** * If set to true, the animations loop endlessly. * @default false */ loop?: Scriptable>; } export type AnimationsSpec = { [name: string]: false | AnimationSpec & { properties: string[]; /** * Type of property, determines the interpolator used. Possible values: 'number', 'color' and 'boolean'. Only really needed for 'color', because typeof does not get that right. */ type: 'color' | 'number' | 'boolean'; fn: (from: T, to: T, factor: number) => T; /** * Start value for the animation. Current value is used when undefined */ from: Scriptable>; /** * */ to: Scriptable>; } } export type TransitionSpec = { animation: AnimationSpec; animations: AnimationsSpec; } export type TransitionsSpec = { [mode: string]: TransitionSpec } export type AnimationOptions = { animation: false | AnimationSpec & { /** * Callback called on each step of an animation. */ onProgress?: (this: Chart, event: AnimationEvent) => void; /** * Callback called when all animations are completed. */ onComplete?: (this: Chart, event: AnimationEvent) => void; }; animations: AnimationsSpec; transitions: TransitionsSpec; }; export interface FontSpec { /** * Default font family for all text, follows CSS font-family options. * @default "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif" */ family: string; /** * Default font size (in px) for text. Does not apply to radialLinear scale point labels. * @default 12 */ size: number; /** * Default font style. Does not apply to tooltip title or footer. Does not apply to chart title. Follows CSS font-style options (i.e. normal, italic, oblique, initial, inherit) * @default 'normal' */ style: 'normal' | 'italic' | 'oblique' | 'initial' | 'inherit'; /** * Default font weight (boldness). (see MDN). */ weight: 'normal' | 'bold' | 'lighter' | 'bolder' | number | null; /** * Height of an individual line of text (see MDN). * @default 1.2 */ lineHeight: number | string; } export interface CanvasFontSpec extends FontSpec { string: string; } export type TextAlign = 'left' | 'center' | 'right'; export type Align = 'start' | 'center' | 'end'; export interface VisualElement { draw(ctx: CanvasRenderingContext2D, area?: ChartArea): void; inRange(mouseX: number, mouseY: number, useFinalPosition?: boolean): boolean; inXRange(mouseX: number, useFinalPosition?: boolean): boolean; inYRange(mouseY: number, useFinalPosition?: boolean): boolean; getCenterPoint(useFinalPosition?: boolean): Point; getRange?(axis: 'x' | 'y'): number; } export interface CommonElementOptions { borderWidth: number; borderColor: Color; backgroundColor: Color; } export interface CommonHoverOptions { hoverBorderWidth: number; hoverBorderColor: Color; hoverBackgroundColor: Color; } export interface Segment { start: number; end: number; loop: boolean; } export interface ArcBorderRadius { outerStart: number; outerEnd: number; innerStart: number; innerEnd: number; } export interface ArcOptions extends CommonElementOptions { /** * If true, Arc can take up 100% of a circular graph without any visual split or cut. This option doesn't support borderRadius and borderJoinStyle miter * @default true */ selfJoin: boolean; /** * Arc stroke alignment. */ borderAlign: 'center' | 'inner'; /** * Line dash. See MDN. * @default [] */ borderDash: number[]; /** * Line dash offset. See MDN. * @default 0.0 */ borderDashOffset: number; /** * Line join style. See MDN. Default is 'round' when `borderAlign` is 'inner', else 'bevel'. */ borderJoinStyle: CanvasLineJoin; /** * Sets the border radius for arcs * @default 0 */ borderRadius: number | ArcBorderRadius; /** * Arc offset (in pixels). */ offset: number; /** * If false, Arc will be flat. * @default true */ circular: boolean; /** * Spacing between arcs */ spacing: number } export interface ArcHoverOptions extends CommonHoverOptions { hoverBorderDash: number[]; hoverBorderDashOffset: number; hoverOffset: number; } export interface LineProps { points: Point[] } export interface LineOptions extends CommonElementOptions { /** * Line cap style. See MDN. * @default 'butt' */ borderCapStyle: CanvasLineCap; /** * Line dash. See MDN. * @default [] */ borderDash: number[]; /** * Line dash offset. See MDN. * @default 0.0 */ borderDashOffset: number; /** * Line join style. See MDN. * @default 'miter' */ borderJoinStyle: CanvasLineJoin; /** * true to keep Bézier control inside the chart, false for no restriction. * @default true */ capBezierPoints: boolean; /** * Interpolation mode to apply. * @default 'default' */ cubicInterpolationMode: 'default' | 'monotone'; /** * Bézier curve tension (0 for no Bézier curves). * @default 0 */ tension: number; /** * true to show the line as a stepped line (tension will be ignored). * @default false */ stepped: 'before' | 'after' | 'middle' | boolean; /** * Both line and radar charts support a fill option on the dataset object which can be used to create area between two datasets or a dataset and a boundary, i.e. the scale origin, start or end */ fill: FillTarget | ComplexFillTarget; /** * If true, lines will be drawn between points with no or null data. If false, points with NaN data will create a break in the line. Can also be a number specifying the maximum gap length to span. The unit of the value depends on the scale used. */ spanGaps: boolean | number; segment: { backgroundColor: Scriptable, borderColor: Scriptable, borderCapStyle: Scriptable; borderDash: Scriptable; borderDashOffset: Scriptable; borderJoinStyle: Scriptable; borderWidth: Scriptable; }; } export interface LineHoverOptions extends CommonHoverOptions { hoverBorderCapStyle: CanvasLineCap; hoverBorderDash: number[]; hoverBorderDashOffset: number; hoverBorderJoinStyle: CanvasLineJoin; } export interface LineElement extends Element, VisualElement { updateControlPoints(chartArea: ChartArea, indexAxis?: 'x' | 'y'): void; points: Point[]; readonly segments: Segment[]; first(): Point | false; last(): Point | false; interpolate(point: Point, property: 'x' | 'y'): undefined | Point | Point[]; pathSegment(ctx: CanvasRenderingContext2D, segment: Segment, params: AnyObject): undefined | boolean; path(ctx: CanvasRenderingContext2D): boolean; } export declare const LineElement: ChartComponent & { prototype: LineElement; new (cfg: AnyObject): LineElement; }; export type PointStyle = | 'circle' | 'cross' | 'crossRot' | 'dash' | 'line' | 'rect' | 'rectRounded' | 'rectRot' | 'star' | 'triangle' | false | HTMLImageElement | HTMLCanvasElement; export interface PointOptions extends CommonElementOptions { /** * Point radius * @default 3 */ radius: number; /** * Extra radius added to point radius for hit detection. * @default 1 */ hitRadius: number; /** * Point style * @default 'circle; */ pointStyle: PointStyle; /** * Point rotation (in degrees). * @default 0 */ rotation: number; /** * Draw the active elements over the other elements of the dataset, * @default true */ drawActiveElementsOnTop: boolean; } export interface PointHoverOptions extends CommonHoverOptions { /** * Point radius when hovered. * @default 4 */ hoverRadius: number; } export interface PointPrefixedOptions { /** * The fill color for points. */ pointBackgroundColor: Color; /** * The border color for points. */ pointBorderColor: Color; /** * The width of the point border in pixels. */ pointBorderWidth: number; /** * The pixel size of the non-displayed point that reacts to mouse events. */ pointHitRadius: number; /** * The radius of the point shape. If set to 0, the point is not rendered. */ pointRadius: number; /** * The rotation of the point in degrees. */ pointRotation: number; /** * Style of the point. */ pointStyle: PointStyle; } export interface PointPrefixedHoverOptions { /** * Point background color when hovered. */ pointHoverBackgroundColor: Color; /** * Point border color when hovered. */ pointHoverBorderColor: Color; /** * Border width of point when hovered. */ pointHoverBorderWidth: number; /** * The radius of the point when hovered. */ pointHoverRadius: number; } export interface BarProps extends Point { base: number; horizontal: boolean; width: number; height: number; } export interface BarOptions extends Omit { /** * The base value for the bar in data units along the value axis. */ base: number; /** * Skipped (excluded) border: 'start', 'end', 'left', 'right', 'bottom', 'top', 'middle', false (none) or true (all). * @default 'start' */ borderSkipped: 'start' | 'end' | 'left' | 'right' | 'bottom' | 'top' | 'middle' | boolean; /** * Border radius * @default 0 */ borderRadius: number | BorderRadius; /** * Amount to inflate the rectangle(s). This can be used to hide artifacts between bars. * Unit is pixels. 'auto' translates to 0.33 pixels when barPercentage * categoryPercentage is 1, else 0. * @default 'auto' */ inflateAmount: number | 'auto'; /** * Width of the border, number for all sides, object to specify width for each side specifically * @default 0 */ borderWidth: number | { top?: number, right?: number, bottom?: number, left?: number }; } export interface BorderRadius { topLeft: number; topRight: number; bottomLeft: number; bottomRight: number; } export interface BarHoverOptions extends CommonHoverOptions { hoverBorderRadius: number | BorderRadius; } export interface BarElement< T extends BarProps = BarProps, O extends BarOptions = BarOptions > extends Element, VisualElement {} export declare const BarElement: ChartComponent & { prototype: BarElement; new (cfg: AnyObject): BarElement; }; export interface ElementOptionsByType { arc: ScriptableAndArrayOptions>; bar: ScriptableAndArrayOptions>; line: ScriptableAndArrayOptions>; point: ScriptableAndArrayOptions>; } export type ElementChartOptions = { elements: ElementOptionsByType }; export declare class BasePlatform { /** * Called at chart construction time, returns a context2d instance implementing * the [W3C Canvas 2D Context API standard]{@link https://www.w3.org/TR/2dcontext/}. * @param {HTMLCanvasElement} canvas - The canvas from which to acquire context (platform specific) * @param options - The chart options */ acquireContext( canvas: HTMLCanvasElement, options?: CanvasRenderingContext2DSettings ): CanvasRenderingContext2D | null; /** * Called at chart destruction time, releases any resources associated to the context * previously returned by the acquireContext() method. * @param {CanvasRenderingContext2D} context - The context2d instance * @returns {boolean} true if the method succeeded, else false */ releaseContext(context: CanvasRenderingContext2D): boolean; /** * Registers the specified listener on the given chart. * @param {Chart} chart - Chart from which to listen for event * @param {string} type - The ({@link ChartEvent}) type to listen for * @param listener - Receives a notification (an object that implements * the {@link ChartEvent} interface) when an event of the specified type occurs. */ addEventListener(chart: Chart, type: string, listener: (e: ChartEvent) => void): void; /** * Removes the specified listener previously registered with addEventListener. * @param {Chart} chart - Chart from which to remove the listener * @param {string} type - The ({@link ChartEvent}) type to remove * @param listener - The listener function to remove from the event target. */ removeEventListener(chart: Chart, type: string, listener: (e: ChartEvent) => void): void; /** * @returns {number} the current devicePixelRatio of the device this platform is connected to. */ getDevicePixelRatio(): number; /** * @param {HTMLCanvasElement} canvas - The canvas for which to calculate the maximum size * @param {number} [width] - Parent element's content width * @param {number} [height] - Parent element's content height * @param {number} [aspectRatio] - The aspect ratio to maintain * @returns { width: number, height: number } the maximum size available. */ getMaximumSize(canvas: HTMLCanvasElement, width?: number, height?: number, aspectRatio?: number): { width: number, height: number }; /** * @param {HTMLCanvasElement} canvas * @returns {boolean} true if the canvas is attached to the platform, false if not. */ isAttached(canvas: HTMLCanvasElement): boolean; /** * Updates config with platform specific requirements * @param {ChartConfiguration | ChartConfigurationCustomTypes} config */ updateConfig(config: ChartConfiguration | ChartConfigurationCustomTypesPerDataset): void; } export declare class BasicPlatform extends BasePlatform {} export declare class DomPlatform extends BasePlatform {} export declare const Decimation: Plugin; export declare const enum DecimationAlgorithm { lttb = 'lttb', minmax = 'min-max', } interface BaseDecimationOptions { enabled: boolean; threshold?: number; } interface LttbDecimationOptions extends BaseDecimationOptions { algorithm: DecimationAlgorithm.lttb | 'lttb'; samples?: number; } interface MinMaxDecimationOptions extends BaseDecimationOptions { algorithm: DecimationAlgorithm.minmax | 'min-max'; } export type DecimationOptions = LttbDecimationOptions | MinMaxDecimationOptions; export declare const Filler: Plugin; export interface FillerOptions { drawTime: 'beforeDraw' | 'beforeDatasetDraw' | 'beforeDatasetsDraw'; propagate: boolean; } export type FillTarget = number | string | { value: number } | 'start' | 'end' | 'origin' | 'stack' | 'shape' | boolean; export interface ComplexFillTarget { /** * The accepted values are the same as the filling mode values, so you may use absolute and relative dataset indexes and/or boundaries. */ target: FillTarget; /** * If no color is set, the default color will be the background color of the chart. */ above: Color; /** * Same as the above. */ below: Color; } export interface FillerControllerDatasetOptions { /** * Both line and radar charts support a fill option on the dataset object which can be used to create area between two datasets or a dataset and a boundary, i.e. the scale origin, start or end */ fill: FillTarget | ComplexFillTarget; } export declare const Legend: Plugin; export interface LegendItem { /** * Label that will be displayed */ text: string; /** * Border radius of the legend box * @since 3.1.0 */ borderRadius?: number | BorderRadius; /** * Index of the associated dataset */ datasetIndex?: number; /** * Index the associated label in the labels array */ index?: number /** * Fill style of the legend box */ fillStyle?: Color; /** * Font color for the text * Defaults to LegendOptions.labels.color */ fontColor?: Color; /** * If true, this item represents a hidden dataset. Label will be rendered with a strike-through effect */ hidden?: boolean; /** * For box border. * @see https://developer.mozilla.org/en/docs/Web/API/CanvasRenderingContext2D/lineCap */ lineCap?: CanvasLineCap; /** * For box border. * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash */ lineDash?: number[]; /** * For box border. * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineDashOffset */ lineDashOffset?: number; /** * For box border. * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin */ lineJoin?: CanvasLineJoin; /** * Width of box border */ lineWidth?: number; /** * Stroke style of the legend box */ strokeStyle?: Color; /** * Point style of the legend box (only used if usePointStyle is true) */ pointStyle?: PointStyle; /** * Rotation of the point in degrees (only used if usePointStyle is true) */ rotation?: number; /** * Text alignment */ textAlign?: TextAlign; } export interface LegendElement extends Element>, LayoutItem { chart: Chart; ctx: CanvasRenderingContext2D; legendItems?: LegendItem[]; options: LegendOptions; fit(): void; } export interface LegendOptions { /** * Is the legend shown? * @default true */ display: boolean; /** * Position of the legend. * @default 'top' */ position: LayoutPosition; /** * Alignment of the legend. * @default 'center' */ align: Align; /** * Maximum height of the legend, in pixels */ maxHeight: number; /** * Maximum width of the legend, in pixels */ maxWidth: number; /** * Marks that this box should take the full width/height of the canvas (moving other boxes). This is unlikely to need to be changed in day-to-day use. * @default true */ fullSize: boolean; /** * Legend will show datasets in reverse order. * @default false */ reverse: boolean; /** * A callback that is called when a click event is registered on a label item. */ onClick(this: LegendElement, e: ChartEvent, legendItem: LegendItem, legend: LegendElement): void; /** * A callback that is called when a 'mousemove' event is registered on top of a label item */ onHover(this: LegendElement, e: ChartEvent, legendItem: LegendItem, legend: LegendElement): void; /** * A callback that is called when a 'mousemove' event is registered outside of a previously hovered label item. */ onLeave(this: LegendElement, e: ChartEvent, legendItem: LegendItem, legend: LegendElement): void; labels: { /** * Width of colored box. * @default 40 */ boxWidth: number; /** * Height of the coloured box. * @default fontSize */ boxHeight: number; /** * Color of label * @see Defaults.color */ color: Color; /** * Font of label * @see Defaults.font */ font: ScriptableAndScriptableOptions, ScriptableChartContext>; /** * Padding between rows of colored boxes. * @default 10 */ padding: number; /** * If usePointStyle is true, the width of the point style used for the legend. */ pointStyleWidth: number; /** * Generates legend items for each thing in the legend. Default implementation returns the text + styling for the color box. See Legend Item for details. */ generateLabels(chart: Chart): LegendItem[]; /** * Filters legend items out of the legend. Receives 2 parameters, a Legend Item and the chart data */ filter(item: LegendItem, data: ChartData): boolean; /** * Sorts the legend items */ sort(a: LegendItem, b: LegendItem, data: ChartData): number; /** * Override point style for the legend. Only applies if usePointStyle is true */ pointStyle: PointStyle; /** * Text alignment */ textAlign?: TextAlign; /** * Label style will match corresponding point style (size is based on the minimum value between boxWidth and font.size). * @default false */ usePointStyle: boolean; /** * Label borderRadius will match corresponding borderRadius. * @default false */ useBorderRadius: boolean; /** * Override the borderRadius to use. * @default undefined */ borderRadius: number; }; /** * true for rendering the legends from right to left. */ rtl: boolean; /** * This will force the text direction 'rtl' or 'ltr' on the canvas for rendering the legend, regardless of the css specified on the canvas * @default canvas's default */ textDirection: string; title: { /** * Is the legend title displayed. * @default false */ display: boolean; /** * Color of title * @see Defaults.color */ color: Color; /** * see Fonts */ font: ScriptableAndScriptableOptions, ScriptableChartContext>; position: 'center' | 'start' | 'end'; padding?: number | ChartArea; /** * The string title. */ text: string; }; } export declare const SubTitle: Plugin; export declare const Title: Plugin; export interface TitleOptions { /** * Alignment of the title. * @default 'center' */ align: Align; /** * Is the title shown? * @default false */ display: boolean; /** * Position of title * @default 'top' */ position: 'top' | 'left' | 'bottom' | 'right'; /** * Color of text * @see Defaults.color */ color: Color; font: ScriptableAndScriptableOptions, ScriptableChartContext>; /** * Marks that this box should take the full width/height of the canvas (moving other boxes). If set to `false`, places the box above/beside the * chart area * @default true */ fullSize: boolean; /** * Adds padding above and below the title text if a single number is specified. It is also possible to change top and bottom padding separately. */ padding: number | { top: number; bottom: number }; /** * Title text to display. If specified as an array, text is rendered on multiple lines. */ text: string | string[]; } export type TooltipXAlignment = 'left' | 'center' | 'right'; export type TooltipYAlignment = 'top' | 'center' | 'bottom'; export interface TooltipLabelStyle { borderColor: Color; backgroundColor: Color; /** * Width of border line * @since 3.1.0 */ borderWidth?: number; /** * Border dash * @since 3.1.0 */ borderDash?: [number, number]; /** * Border dash offset * @since 3.1.0 */ borderDashOffset?: number; /** * borderRadius * @since 3.1.0 */ borderRadius?: number | BorderRadius; } export interface TooltipModel extends Element> { readonly chart: Chart; // The items that we are rendering in the tooltip. See Tooltip Item Interface section dataPoints: TooltipItem[]; // Positioning xAlign: TooltipXAlignment; yAlign: TooltipYAlignment; // X and Y properties are the top left of the tooltip x: number; y: number; width: number; height: number; // Where the tooltip points to caretX: number; caretY: number; // Body // The body lines that need to be rendered // Each object contains 3 parameters // before: string[] // lines of text before the line with the color square // lines: string[]; // lines of text to render as the main item with color square // after: string[]; // lines of text to render after the main lines body: { before: string[]; lines: string[]; after: string[] }[]; // lines of text that appear after the title but before the body beforeBody: string[]; // line of text that appear after the body and before the footer afterBody: string[]; // Title // lines of text that form the title title: string[]; // Footer // lines of text that form the footer footer: string[]; // Styles to render for each item in body[]. This is the styling of the squares in the tooltip labelColors: TooltipLabelStyle[]; labelTextColors: Color[]; labelPointStyles: { pointStyle: PointStyle; rotation: number }[]; // 0 opacity is a hidden tooltip opacity: number; // tooltip options options: TooltipOptions; getActiveElements(): ActiveElement[]; setActiveElements(active: ActiveDataPoint[], eventPosition: Point): void; } export interface TooltipPosition extends Point { xAlign?: TooltipXAlignment; yAlign?: TooltipYAlignment; } export type TooltipPositionerFunction = ( this: TooltipModel, items: readonly ActiveElement[], eventPosition: Point ) => TooltipPosition | false; export interface TooltipPositionerMap { average: TooltipPositionerFunction; nearest: TooltipPositionerFunction; } export type TooltipPositioner = keyof TooltipPositionerMap; export interface Tooltip extends Plugin { readonly positioners: TooltipPositionerMap; } export declare const Tooltip: Tooltip; export interface TooltipDatasetCallbacks< TType extends ChartType, Model = TooltipModel, Item = TooltipItem> { beforeLabel(this: Model, tooltipItem: Item): string | string[] | void; label(this: Model, tooltipItem: Item): string | string[] | void; afterLabel(this: Model, tooltipItem: Item): string | string[] | void; labelColor(this: Model, tooltipItem: Item): TooltipLabelStyle | void; labelTextColor(this: Model, tooltipItem: Item): Color | void; labelPointStyle(this: Model, tooltipItem: Item): { pointStyle: PointStyle; rotation: number } | void; } export interface TooltipCallbacks< TType extends ChartType, Model = TooltipModel, Item = TooltipItem> extends TooltipDatasetCallbacks { beforeTitle(this: Model, tooltipItems: Item[]): string | string[] | void; title(this: Model, tooltipItems: Item[]): string | string[] | void; afterTitle(this: Model, tooltipItems: Item[]): string | string[] | void; beforeBody(this: Model, tooltipItems: Item[]): string | string[] | void; afterBody(this: Model, tooltipItems: Item[]): string | string[] | void; beforeLabel(this: Model, tooltipItem: Item): string | string[] | void; label(this: Model, tooltipItem: Item): string | string[] | void; afterLabel(this: Model, tooltipItem: Item): string | string[] | void; labelColor(this: Model, tooltipItem: Item): TooltipLabelStyle | void; labelTextColor(this: Model, tooltipItem: Item): Color | void; labelPointStyle(this: Model, tooltipItem: Item): { pointStyle: PointStyle; rotation: number } | void; beforeFooter(this: Model, tooltipItems: Item[]): string | string[] | void; footer(this: Model, tooltipItems: Item[]): string | string[] | void; afterFooter(this: Model, tooltipItems: Item[]): string | string[] | void; } export interface ExtendedPlugin< TType extends ChartType, O = AnyObject, Model = TooltipModel> { /** * @desc Called before drawing the `tooltip`. If any plugin returns `false`, * the tooltip drawing is cancelled until another `render` is triggered. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {Tooltip} args.tooltip - The tooltip. * @param {object} options - The plugin options. * @returns {boolean} `false` to cancel the chart tooltip drawing. */ beforeTooltipDraw?(chart: Chart, args: { tooltip: Model, cancelable: true }, options: O): boolean | void; /** * @desc Called after drawing the `tooltip`. Note that this hook will not * be called if the tooltip drawing has been previously cancelled. * @param {Chart} chart - The chart instance. * @param {object} args - The call arguments. * @param {Tooltip} args.tooltip - The tooltip. * @param {object} options - The plugin options. */ afterTooltipDraw?(chart: Chart, args: { tooltip: Model }, options: O): void; } export interface ScriptableTooltipContext { chart: UnionToIntersection>; tooltip: UnionToIntersection>; tooltipItems: TooltipItem[]; } export interface TooltipOptions extends CoreInteractionOptions { /** * Are on-canvas tooltips enabled? * @default true */ enabled: Scriptable>; /** * See external tooltip section. */ external(this: TooltipModel, args: { chart: Chart; tooltip: TooltipModel }): void; /** * The mode for positioning the tooltip */ position: Scriptable> /** * Override the tooltip alignment calculations */ xAlign: Scriptable>; yAlign: Scriptable>; /** * Sort tooltip items. */ itemSort: (a: TooltipItem, b: TooltipItem, data: ChartData) => number; filter: (e: TooltipItem, index: number, array: TooltipItem[], data: ChartData) => boolean; /** * Background color of the tooltip. * @default 'rgba(0, 0, 0, 0.8)' */ backgroundColor: Scriptable>; /** * Padding between the color box and the text. * @default 1 */ boxPadding: number; /** * Color of title * @default '#fff' */ titleColor: Scriptable>; /** * See Fonts * @default {weight: 'bold'} */ titleFont: ScriptableAndScriptableOptions, ScriptableTooltipContext>; /** * Spacing to add to top and bottom of each title line. * @default 2 */ titleSpacing: Scriptable>; /** * Margin to add on bottom of title section. * @default 6 */ titleMarginBottom: Scriptable>; /** * Horizontal alignment of the title text lines. * @default 'left' */ titleAlign: Scriptable>; /** * Spacing to add to top and bottom of each tooltip item. * @default 2 */ bodySpacing: Scriptable>; /** * Color of body * @default '#fff' */ bodyColor: Scriptable>; /** * See Fonts. * @default {} */ bodyFont: ScriptableAndScriptableOptions, ScriptableTooltipContext>; /** * Horizontal alignment of the body text lines. * @default 'left' */ bodyAlign: Scriptable>; /** * Spacing to add to top and bottom of each footer line. * @default 2 */ footerSpacing: Scriptable>; /** * Margin to add before drawing the footer. * @default 6 */ footerMarginTop: Scriptable>; /** * Color of footer * @default '#fff' */ footerColor: Scriptable>; /** * See Fonts * @default {weight: 'bold'} */ footerFont: ScriptableAndScriptableOptions, ScriptableTooltipContext>; /** * Horizontal alignment of the footer text lines. * @default 'left' */ footerAlign: Scriptable>; /** * Padding to add to the tooltip * @default 6 */ padding: Scriptable>; /** * Extra distance to move the end of the tooltip arrow away from the tooltip point. * @default 2 */ caretPadding: Scriptable>; /** * Size, in px, of the tooltip arrow. * @default 5 */ caretSize: Scriptable>; /** * Radius of tooltip corner curves. * @default 6 */ cornerRadius: Scriptable>; /** * Color to draw behind the colored boxes when multiple items are in the tooltip. * @default '#fff' */ multiKeyBackground: Scriptable>; /** * If true, color boxes are shown in the tooltip. * @default true */ displayColors: Scriptable>; /** * Width of the color box if displayColors is true. * @default bodyFont.size */ boxWidth: Scriptable>; /** * Height of the color box if displayColors is true. * @default bodyFont.size */ boxHeight: Scriptable>; /** * Use the corresponding point style (from dataset options) instead of color boxes, ex: star, triangle etc. (size is based on the minimum value between boxWidth and boxHeight) * @default false */ usePointStyle: Scriptable>; /** * Color of the border. * @default 'rgba(0, 0, 0, 0)' */ borderColor: Scriptable>; /** * Size of the border. * @default 0 */ borderWidth: Scriptable>; /** * true for rendering the legends from right to left. */ rtl: Scriptable>; /** * This will force the text direction 'rtl' or 'ltr on the canvas for rendering the tooltips, regardless of the css specified on the canvas * @default canvas's default */ textDirection: Scriptable>; animation: AnimationSpec | false; animations: AnimationsSpec | false; callbacks: TooltipCallbacks; } export interface TooltipDatasetOptions { callbacks: TooltipDatasetCallbacks; } export interface TooltipItem { /** * The chart the tooltip is being shown on */ chart: Chart; /** * Label for the tooltip */ label: string; /** * Parsed data values for the given `dataIndex` and `datasetIndex` */ parsed: UnionToIntersection>; /** * Raw data values for the given `dataIndex` and `datasetIndex` */ raw: unknown; /** * Formatted value for the tooltip */ formattedValue: string; /** * The dataset the item comes from */ dataset: UnionToIntersection>; /** * Index of the dataset the item comes from */ datasetIndex: number; /** * Index of this data item in the dataset */ dataIndex: number; /** * The chart element (point, arc, bar, etc.) for this tooltip item */ element: Element; } export interface PluginDatasetOptionsByType { tooltip: TooltipDatasetOptions; } export interface PluginOptionsByType { colors: ColorsPluginOptions; decimation: DecimationOptions; filler: FillerOptions; legend: LegendOptions; subtitle: TitleOptions; title: TitleOptions; tooltip: TooltipOptions; } export interface PluginChartOptions { plugins: PluginOptionsByType; } export interface BorderOptions { /** * @default true */ display: boolean /** * @default [] */ dash: Scriptable; /** * @default 0 */ dashOffset: Scriptable; color: Color; width: number; z: number; } export interface GridLineOptions { /** * @default true */ display: boolean; /** * @default false */ circular: boolean; /** * @default 'rgba(0, 0, 0, 0.1)' */ color: ScriptableAndArray; /** * @default 1 */ lineWidth: ScriptableAndArray; /** * @default true */ drawOnChartArea: boolean; /** * @default true */ drawTicks: boolean; /** * @default [] */ tickBorderDash: Scriptable; /** * @default 0 */ tickBorderDashOffset: Scriptable; /** * @default 'rgba(0, 0, 0, 0.1)' */ tickColor: ScriptableAndArray; /** * @default 10 */ tickLength: number; /** * @default 1 */ tickWidth: number; /** * @default false */ offset: boolean; /** * @default 0 */ z: number; } export interface TickOptions { /** * Color of label backdrops. * @default 'rgba(255, 255, 255, 0.75)' */ backdropColor: Scriptable; /** * Padding of tick backdrop. * @default 2 */ backdropPadding: number | ChartArea; /** * Returns the string representation of the tick value as it should be displayed on the chart. See callback. */ callback: (this: Scale, tickValue: number | string, index: number, ticks: Tick[]) => string | string[] | number | number[] | null | undefined; /** * If true, show tick labels. * @default true */ display: boolean; /** * Color of tick * @see Defaults.color */ color: ScriptableAndArray; /** * see Fonts */ font: ScriptableAndScriptableOptions, ScriptableScaleContext>; /** * Sets the offset of the tick labels from the axis */ padding: number; /** * If true, draw a background behind the tick labels. * @default false */ showLabelBackdrop: Scriptable; /** * The color of the stroke around the text. * @default undefined */ textStrokeColor: Scriptable; /** * Stroke width around the text. * @default 0 */ textStrokeWidth: Scriptable; /** * z-index of tick layer. Useful when ticks are drawn on chart area. Values <= 0 are drawn under datasets, > 0 on top. * @default 0 */ z: number; major: { /** * If true, major ticks are generated. A major tick will affect autoskipping and major will be defined on ticks in the scriptable options context. * @default false */ enabled: boolean; }; } export type CartesianTickOptions = TickOptions & { /** * The number of ticks to examine when deciding how many labels will fit. Setting a smaller value will be faster, but may be less accurate when there is large variability in label length. * @default ticks.length */ sampleSize: number; /** * The label alignment * @default 'center' */ align: Align | 'inner'; /** * If true, automatically calculates how many labels can be shown and hides labels accordingly. Labels will be rotated up to maxRotation before skipping any. Turn autoSkip off to show all labels no matter what. * @default true */ autoSkip: boolean; /** * Padding between the ticks on the horizontal axis when autoSkip is enabled. * @default 0 */ autoSkipPadding: number; /** * How is the label positioned perpendicular to the axis direction. * This only applies when the rotation is 0 and the axis position is one of "top", "left", "right", or "bottom" * @default 'near' */ crossAlign: 'near' | 'center' | 'far'; /** * Should the defined `min` and `max` values be presented as ticks even if they are not "nice". * @default: true */ includeBounds: boolean; /** * Distance in pixels to offset the label from the centre point of the tick (in the x direction for the x axis, and the y direction for the y axis). Note: this can cause labels at the edges to be cropped by the edge of the canvas * @default 0 */ labelOffset: number; /** * Minimum rotation for tick labels. Note: Only applicable to horizontal scales. * @default 0 */ minRotation: number; /** * Maximum rotation for tick labels when rotating to condense labels. Note: Rotation doesn't occur until necessary. Note: Only applicable to horizontal scales. * @default 50 */ maxRotation: number; /** * Flips tick labels around axis, displaying the labels inside the chart instead of outside. Note: Only applicable to vertical scales. * @default false */ mirror: boolean; /** * Padding between the tick label and the axis. When set on a vertical axis, this applies in the horizontal (X) direction. When set on a horizontal axis, this applies in the vertical (Y) direction. * @default 0 */ padding: number; /** * Maximum number of ticks and gridlines to show. * @default 11 */ maxTicksLimit: number; } export interface ScriptableCartesianScaleContext { scale: keyof CartesianScaleTypeRegistry; type: string; } export interface ScriptableChartContext { chart: Chart; type: string; } export interface CartesianScaleOptions extends CoreScaleOptions { /** * Scale boundary strategy (bypassed by min/max time options) * - `data`: make sure data are fully visible, ticks outside are removed * - `ticks`: make sure ticks are fully visible, data outside are truncated * @since 2.7.0 * @default 'ticks' */ bounds: 'ticks' | 'data'; /** * Position of the axis. */ position: 'left' | 'top' | 'right' | 'bottom' | 'center' | { [scale: string]: number }; /** * Stack group. Axes at the same `position` with same `stack` are stacked. */ stack?: string; /** * Weight of the scale in stack group. Used to determine the amount of allocated space for the scale within the group. * @default 1 */ stackWeight?: number; /** * Which type of axis this is. Possible values are: 'x', 'y', 'r'. If not set, this is inferred from the first character of the ID which should be 'x', 'y' or 'r'. */ axis: 'x' | 'y' | 'r'; /** * User defined minimum value for the scale, overrides minimum value from data. */ min: number; /** * User defined maximum value for the scale, overrides maximum value from data. */ max: number; /** * If true, extra space is added to the both edges and the axis is scaled to fit into the chart area. This is set to true for a bar chart by default. * @default false */ offset: boolean; grid: Partial; border: BorderOptions; /** Options for the scale title. */ title: { /** If true, displays the axis title. */ display: boolean; /** Alignment of the axis title. */ align: Align; /** The text for the title, e.g. "# of People" or "Response Choices". */ text: string | string[]; /** Color of the axis label. */ color: Color; /** The color of the text stroke for the axis label.*/ strokeColor?: Color; /** The text stroke width for the axis label.*/ strokeWidth?: number; /** Information about the axis title font. */ font: ScriptableAndScriptableOptions, ScriptableCartesianScaleContext>; /** Padding to apply around scale labels. */ padding: number | { /** Padding on the (relative) top side of this axis label. */ top: number; /** Padding on the (relative) bottom side of this axis label. */ bottom: number; /** This is a shorthand for defining top/bottom to the same values. */ y: number; }; }; /** * If true, data will be comprised between datasets of data * @default false */ stacked?: boolean | 'single'; ticks: CartesianTickOptions; } export type CategoryScaleOptions = Omit & { min: string | number; max: string | number; labels: string[] | string[][]; }; export type CategoryScale = Scale export declare const CategoryScale: ChartComponent & { prototype: CategoryScale; new (cfg: AnyObject): CategoryScale; }; export type LinearScaleOptions = CartesianScaleOptions & { /** * if true, scale will include 0 if it is not already included. * @default true */ beginAtZero: boolean; /** * Adjustment used when calculating the minimum data value. */ suggestedMin?: number; /** * Adjustment used when calculating the maximum data value. */ suggestedMax?: number; /** * Percentage (string ending with %) or amount (number) for added room in the scale range above and below data. */ grace?: string | number; ticks: { /** * The Intl.NumberFormat options used by the default label formatter */ format: Intl.NumberFormatOptions; /** * if defined and stepSize is not specified, the step size will be rounded to this many decimal places. */ precision: number; /** * User defined fixed step size for the scale */ stepSize: number; /** * User defined count of ticks */ count: number; }; }; export type LinearScale = Scale export declare const LinearScale: ChartComponent & { prototype: LinearScale; new (cfg: AnyObject): LinearScale; }; export type LogarithmicScaleOptions = CartesianScaleOptions & { /** * Adjustment used when calculating the maximum data value. */ suggestedMin?: number; /** * Adjustment used when calculating the minimum data value. */ suggestedMax?: number; ticks: { /** * The Intl.NumberFormat options used by the default label formatter */ format: Intl.NumberFormatOptions; }; }; export type LogarithmicScale = Scale export declare const LogarithmicScale: ChartComponent & { prototype: LogarithmicScale; new (cfg: AnyObject): LogarithmicScale; }; export type TimeScaleTimeOptions = { /** * Custom parser for dates. */ parser: string | ((v: unknown) => number); /** * If defined, dates will be rounded to the start of this unit. See Time Units below for the allowed units. */ round: false | TimeUnit; /** * If boolean and true and the unit is set to 'week', then the first day of the week will be Monday. Otherwise, it will be Sunday. * If `number`, the index of the first day of the week (0 - Sunday, 6 - Saturday). * @default false */ isoWeekday: boolean | number; /** * Sets how different time units are displayed. */ displayFormats: { [key: string]: string; }; /** * The format string to use for the tooltip. */ tooltipFormat: string; /** * If defined, will force the unit to be a certain type. See Time Units section below for details. * @default false */ unit: false | TimeUnit; /** * The minimum display format to be used for a time unit. * @default 'millisecond' */ minUnit: TimeUnit; }; export type TimeScaleTickOptions = { /** * Ticks generation input values: * - 'auto': generates "optimal" ticks based on scale size and time options. * - 'data': generates ticks from data (including labels from data `{t|x|y}` objects). * - 'labels': generates ticks from user given `data.labels` values ONLY. * @see https://github.com/chartjs/Chart.js/pull/4507 * @since 2.7.0 * @default 'auto' */ source: 'labels' | 'auto' | 'data'; /** * The number of units between grid lines. * @default 1 */ stepSize: number; }; export type TimeScaleOptions = Omit & { min: string | number; max: string | number; suggestedMin: string | number; suggestedMax: string | number; /** * Scale boundary strategy (bypassed by min/max time options) * - `data`: make sure data are fully visible, ticks outside are removed * - `ticks`: make sure ticks are fully visible, data outside are truncated * @since 2.7.0 * @default 'data' */ bounds: 'ticks' | 'data'; /** * If true, bar chart offsets are computed with skipped tick sizes * @since 3.8.0 * @default false */ offsetAfterAutoskip: boolean; /** * options for creating a new adapter instance */ adapters: { date: unknown; }; time: TimeScaleTimeOptions; ticks: TimeScaleTickOptions; }; export interface TimeScale extends Scale { format(value: number, format?: string): string; getDataTimestamps(): number[]; getLabelTimestamps(): string[]; normalize(values: number[]): number[]; } export declare const TimeScale: ChartComponent & { prototype: TimeScale; new (cfg: AnyObject): TimeScale; }; export type TimeSeriesScale = TimeScale export declare const TimeSeriesScale: ChartComponent & { prototype: TimeSeriesScale; new (cfg: AnyObject): TimeSeriesScale; }; export type RadialTickOptions = TickOptions & { /** * The Intl.NumberFormat options used by the default label formatter */ format: Intl.NumberFormatOptions; /** * Maximum number of ticks and gridlines to show. * @default 11 */ maxTicksLimit: number; /** * if defined and stepSize is not specified, the step size will be rounded to this many decimal places. */ precision: number; /** * User defined fixed step size for the scale. */ stepSize: number; /** * User defined number of ticks */ count: number; } export type RadialLinearScaleOptions = CoreScaleOptions & { animate: boolean; startAngle: number; angleLines: { /** * if true, angle lines are shown. * @default true */ display: boolean; /** * Color of angled lines. * @default 'rgba(0, 0, 0, 0.1)' */ color: Scriptable; /** * Width of angled lines. * @default 1 */ lineWidth: Scriptable; /** * Length and spacing of dashes on angled lines. See MDN. * @default [] */ borderDash: Scriptable; /** * Offset for line dashes. See MDN. * @default 0 */ borderDashOffset: Scriptable; }; /** * if true, scale will include 0 if it is not already included. * @default false */ beginAtZero: boolean; grid: Partial; /** * User defined minimum number for the scale, overrides minimum value from data. */ min: number; /** * User defined maximum number for the scale, overrides maximum value from data. */ max: number; pointLabels: { /** * Background color of the point label. * @default undefined */ backdropColor: Scriptable; /** * Padding of label backdrop. * @default 2 */ backdropPadding: Scriptable; /** * Border radius * @default 0 * @since 3.8.0 */ borderRadius: Scriptable; /** * if true, point labels are shown. When `display: 'auto'`, the label is hidden if it overlaps with another label. * @default true */ display: boolean | 'auto'; /** * Color of label * @see Defaults.color */ color: Scriptable; /** */ font: ScriptableAndScriptableOptions, ScriptableScalePointLabelContext>; /** * Callback function to transform data labels to point labels. The default implementation simply returns the current string. */ callback: (label: string, index: number) => string | string[] | number | number[]; /** * Padding around the pointLabels * @default 5 */ padding: Scriptable; /** * if true, point labels are centered. * @default false */ centerPointLabels: boolean; }; /** * Adjustment used when calculating the maximum data value. */ suggestedMax: number; /** * Adjustment used when calculating the minimum data value. */ suggestedMin: number; ticks: RadialTickOptions; }; export interface RadialLinearScale extends Scale { xCenter: number; yCenter: number; readonly drawingArea: number; setCenterPoint(leftMovement: number, rightMovement: number, topMovement: number, bottomMovement: number): void; getIndexAngle(index: number): number; getDistanceFromCenterForValue(value: number): number; getValueForDistanceFromCenter(distance: number): number; getPointPosition(index: number, distanceFromCenter: number): { x: number; y: number; angle: number }; getPointPositionForValue(index: number, value: number): { x: number; y: number; angle: number }; getPointLabelPosition(index: number): ChartArea; getBasePosition(index: number): { x: number; y: number; angle: number }; } export declare const RadialLinearScale: ChartComponent & { prototype: RadialLinearScale; new (cfg: AnyObject): RadialLinearScale; }; export interface CartesianScaleTypeRegistry { linear: { options: LinearScaleOptions; }; logarithmic: { options: LogarithmicScaleOptions; }; category: { options: CategoryScaleOptions; }; time: { options: TimeScaleOptions; }; timeseries: { options: TimeScaleOptions; }; } export interface RadialScaleTypeRegistry { radialLinear: { options: RadialLinearScaleOptions; }; } export interface ScaleTypeRegistry extends CartesianScaleTypeRegistry, RadialScaleTypeRegistry { } export type ScaleType = keyof ScaleTypeRegistry; export interface CartesianParsedData extends Point { // Only specified when stacked bars are enabled _stacks?: { // Key is the stack ID which is generally the axis ID [key: string]: { // Inner key is the datasetIndex [key: number]: number; } } } export interface BarParsedData extends CartesianParsedData { // Only specified if floating bars are show _custom?: { barStart: number; barEnd: number; start: number; end: number; min: number; max: number; } } export interface BubbleParsedData extends CartesianParsedData { // The bubble radius value _custom: number; } export interface RadialParsedData { r: number; } export interface ChartTypeRegistry { bar: { chartOptions: BarControllerChartOptions; datasetOptions: BarControllerDatasetOptions; defaultDataPoint: number | [number, number] | null; metaExtensions: {}; parsedDataType: BarParsedData, scales: keyof CartesianScaleTypeRegistry; }; line: { chartOptions: LineControllerChartOptions; datasetOptions: LineControllerDatasetOptions & FillerControllerDatasetOptions; defaultDataPoint: ScatterDataPoint | number | null; metaExtensions: {}; parsedDataType: CartesianParsedData; scales: keyof CartesianScaleTypeRegistry; }; scatter: { chartOptions: ScatterControllerChartOptions; datasetOptions: ScatterControllerDatasetOptions; defaultDataPoint: ScatterDataPoint | number | null; metaExtensions: {}; parsedDataType: CartesianParsedData; scales: keyof CartesianScaleTypeRegistry; }; bubble: { chartOptions: unknown; datasetOptions: BubbleControllerDatasetOptions; defaultDataPoint: BubbleDataPoint; metaExtensions: {}; parsedDataType: BubbleParsedData; scales: keyof CartesianScaleTypeRegistry; }; pie: { chartOptions: PieControllerChartOptions; datasetOptions: PieControllerDatasetOptions; defaultDataPoint: PieDataPoint; metaExtensions: PieMetaExtensions; parsedDataType: number; scales: keyof CartesianScaleTypeRegistry; }; doughnut: { chartOptions: DoughnutControllerChartOptions; datasetOptions: DoughnutControllerDatasetOptions; defaultDataPoint: DoughnutDataPoint; metaExtensions: DoughnutMetaExtensions; parsedDataType: number; scales: keyof CartesianScaleTypeRegistry; }; polarArea: { chartOptions: PolarAreaControllerChartOptions; datasetOptions: PolarAreaControllerDatasetOptions; defaultDataPoint: number; metaExtensions: {}; parsedDataType: RadialParsedData; scales: keyof RadialScaleTypeRegistry; }; radar: { chartOptions: RadarControllerChartOptions; datasetOptions: RadarControllerDatasetOptions & FillerControllerDatasetOptions; defaultDataPoint: number | null; metaExtensions: {}; parsedDataType: RadialParsedData; scales: keyof RadialScaleTypeRegistry; }; } export type ChartType = keyof ChartTypeRegistry; export type ScaleOptionsByType = { [key in ScaleType]: { type: key } & ScaleTypeRegistry[key]['options'] }[TScale] ; // Convenience alias for creating and manipulating scale options in user code export type ScaleOptions = DeepPartial>; export type DatasetChartOptions = { [key in TType]: { datasets: ChartTypeRegistry[key]['datasetOptions']; }; }; export type ScaleChartOptions = { scales: { [key: string]: ScaleOptionsByType; }; }; export type ChartOptions = Exclude< DeepPartial< CoreChartOptions & ElementChartOptions & PluginChartOptions & DatasetChartOptions & ScaleChartOptions & ChartTypeRegistry[TType]['chartOptions'] >, DeepPartial >; export type DefaultDataPoint = DistributiveArray; export type ParsedDataType = ChartTypeRegistry[TType]['parsedDataType']; export interface ChartDatasetProperties { type?: TType; data: TData; } export interface ChartDatasetPropertiesCustomTypesPerDataset { type: TType; data: TData; } export type ChartDataset< TType extends ChartType = ChartType, TData = DefaultDataPoint > = DeepPartial< { [key in ChartType]: { type: key } & ChartTypeRegistry[key]['datasetOptions'] }[TType] > & DeepPartial< PluginDatasetOptionsByType > & ChartDatasetProperties; export type ChartDatasetCustomTypesPerDataset< TType extends ChartType = ChartType, TData = DefaultDataPoint > = DeepPartial< { [key in ChartType]: { type: key } & ChartTypeRegistry[key]['datasetOptions'] }[TType] > & DeepPartial< PluginDatasetOptionsByType > & ChartDatasetPropertiesCustomTypesPerDataset; /** * TData represents the data point type. If unspecified, a default is provided * based on the chart type. * TLabel represents the label type */ export interface ChartData< TType extends ChartType = ChartType, TData = DefaultDataPoint, TLabel = unknown > { labels?: TLabel[]; xLabels?: TLabel[]; yLabels?: TLabel[]; datasets: ChartDataset[]; } export interface ChartDataCustomTypesPerDataset< TType extends ChartType = ChartType, TData = DefaultDataPoint, TLabel = unknown > { labels?: TLabel[]; xLabels?: TLabel[]; yLabels?: TLabel[]; datasets: ChartDatasetCustomTypesPerDataset[]; } export interface ChartConfiguration< TType extends ChartType = ChartType, TData = DefaultDataPoint, TLabel = unknown > { type: TType; data: ChartData; options?: ChartOptions | undefined; plugins?: Plugin[]; platform?: typeof BasePlatform; } export interface ChartConfigurationCustomTypesPerDataset< TType extends ChartType = ChartType, TData = DefaultDataPoint, TLabel = unknown > { data: ChartDataCustomTypesPerDataset; options?: ChartOptions | undefined; plugins?: Plugin[]; } ================================================ FILE: src/types/layout.d.ts ================================================ import {ChartArea} from './geometric.js'; export type LayoutPosition = 'left' | 'top' | 'right' | 'bottom' | 'center' | 'chartArea' | {[scaleId: string]: number}; export interface LayoutItem { /** * The position of the item in the chart layout. Possible values are */ position: LayoutPosition; /** * The weight used to sort the item. Higher weights are further away from the chart area */ weight: number; /** * if true, and the item is horizontal, then push vertical boxes down */ fullSize: boolean; /** * Width of item. Must be valid after update() */ width: number; /** * Height of item. Must be valid after update() */ height: number; /** * Left edge of the item. Set by layout system and cannot be used in update */ left: number; /** * Top edge of the item. Set by layout system and cannot be used in update */ top: number; /** * Right edge of the item. Set by layout system and cannot be used in update */ right: number; /** * Bottom edge of the item. Set by layout system and cannot be used in update */ bottom: number; /** * Called before the layout process starts */ beforeLayout?(): void; /** * Draws the element */ draw(chartArea: ChartArea): void; /** * Returns an object with padding on the edges */ getPadding?(): ChartArea; /** * returns true if the layout item is horizontal (ie. top or bottom) */ isHorizontal(): boolean; /** * Takes two parameters: width and height. * @param width * @param height */ update(width: number, height: number, margins?: ChartArea): void; } ================================================ FILE: src/types/utils.d.ts ================================================ /* eslint-disable @typescript-eslint/ban-types */ // DeepPartial implementation taken from the utility-types NPM package, which is // Copyright (c) 2016 Piotr Witek (http://piotrwitek.github.io) // and used under the terms of the MIT license export type DeepPartial = T extends Function ? T : T extends Array ? _DeepPartialArray : T extends object ? _DeepPartialObject : T | undefined; type _DeepPartialArray = Array> type _DeepPartialObject = { [P in keyof T]?: DeepPartial }; export type DistributiveArray = [T] extends [unknown] ? Array : never // https://stackoverflow.com/a/50375286 export type UnionToIntersection = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never; export type AllKeys = T extends any ? keyof T : never; export type PickType> = T extends { [k in K]?: any } ? T[K] : undefined; export type Merge = { [k in AllKeys]: PickType; }; ================================================ FILE: src/types.ts ================================================ /** * Temporary entry point of the types at the time of the transition. * After transition done need to remove it in favor of index.ts */ export * from './index.js'; /** * Explicitly re-exporting to resolve the ambiguity. */ export { BarController, BubbleController, DoughnutController, LineController, PieController, PolarAreaController, RadarController, ScatterController, Animation, Animations, Chart, DatasetController, Interaction, Scale, Ticks, defaults, layouts, registry, ArcElement, BarElement, LineElement, PointElement, BasePlatform, BasicPlatform, DomPlatform, Decimation, Filler, Legend, SubTitle, Title, Tooltip, CategoryScale, LinearScale, LogarithmicScale, RadialLinearScale, TimeScale, TimeSeriesScale, PluginOptionsByType, ElementOptionsByType, ChartDatasetProperties, UpdateModeEnum, registerables } from './types/index.js'; export * from './types/index.js'; ================================================ FILE: test/.eslintrc.yml ================================================ env: jasmine: true globals: acquireChart: true afterEvent: true Chart: true moment: true waitForResize: true rules: max-statements: ["warn", 50] ================================================ FILE: test/BasicChartWebWorker.js ================================================ // This file is a basic example of using a chart inside a web worker. // All it creates a new chart from a transferred OffscreenCanvas and then assert that the correct platform type was // used. // Receives messages with data of type: { type: 'initialize', canvas: OffscreenCanvas } // Sends messages with data of types: { type: 'success' } | { type: 'error', errorMessage: string } // eslint-disable-next-line no-undef importScripts('../src/chart.umd.min.js'); onmessage = function(event) { try { const {type, canvas} = event.data; if (type !== 'initialize') { throw new Error('invalid message type received by worker: ' + type); } const chart = new Chart(canvas); if (!(chart.platform instanceof Chart.platforms.BasicPlatform)) { throw new Error('did not use basic platform for chart in web worker'); } postMessage({type: 'success'}); } catch (error) { postMessage({type: 'error', errorMessage: error.stack}); } }; ================================================ FILE: test/fixtures/controller.bar/aligned-pixels.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a'], datasets: [{ data: [-1] }, { data: [1] }] }, options: { indexAxis: 'y', events: [], backgroundColor: 'navy', devicePixelRatio: 1.25, scales: { x: {display: false, alignToPixels: true}, y: {display: false, stacked: true} } } }, options: { canvas: { width: 100, height: 500 } } }; ================================================ FILE: test/fixtures/controller.bar/backgroundColor/indexable.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: [ '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#000000' ] }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], } ] }, options: { elements: { bar: { backgroundColor: [ '#ff88ff', '#888888', '#ff8800', '#00ff88', '#8800ff', '#ffff88' ] } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/backgroundColor/loopable.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 3, 4, 5, 6], backgroundColor: [ '#ff0000', '#00ff00', '#0000ff' ] }, { // option in element (fallback) data: [6, 5, 4, 3, 2, 1], } ] }, options: { elements: { bar: { backgroundColor: [ '#000000', '#888888' ] } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/backgroundColor/scriptable.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 0 ? '#00ff00' : value > -8 ? '#0000ff' : '#ff00ff'; } }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5] } ] }, options: { elements: { bar: { backgroundColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff00ff' : value > 0 ? '#0000ff' : value > -8 ? '#ff0000' : '#00ff00'; } } }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/backgroundColor/value.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: '#ff0000' }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], } ] }, options: { elements: { bar: { backgroundColor: '#00ff00' } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/bar-animation-hide-show.js ================================================ const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 512; const ctx = canvas.getContext('2d'); module.exports = { config: { type: 'bar', data: { labels: [0], datasets: [ { data: [1], backgroundColor: 'rgba(255,0,0,0.5)' }, { data: [2], backgroundColor: 'rgba(0,0,255,0.5)' }, { data: [3], backgroundColor: 'rgba(0,255,0,0.5)' } ] }, options: { animation: { duration: 14000, easing: 'linear' }, events: [], scales: { x: {display: false}, y: {display: false, max: 4} } } }, options: { canvas: { height: 512, width: 512 }, run: function(chart) { const animator = Chart.animator; const anims = animator._getAnims(chart); // disable animator const backup = animator._refresh; animator._refresh = function() { }; return new Promise((resolve) => { window.requestAnimationFrame(() => { // make sure previous animation is finished animator._update(Date.now() * 2); chart.hide(1); let start = anims.items[0]._start; for (let i = 0; i < 8; i++) { animator._update(start + i * 2000); let x = i % 4 * 128; let y = Math.floor(i / 4) * 128; ctx.drawImage(chart.canvas, x, y, 128, 128); } // make sure previous animation is finished animator._update(Date.now() * 2); chart.show(1); start = anims.items[0]._start; for (let i = 0; i < 8; i++) { animator._update(start + i * 2000); let x = i % 4 * 128; let y = Math.floor(2 + i / 4) * 128; ctx.drawImage(chart.canvas, x, y, 128, 128); } Chart.helpers.clearCanvas(chart.canvas); chart.ctx.drawImage(canvas, 0, 0); animator._refresh = backup; resolve(); }); }); } } }; ================================================ FILE: test/fixtures/controller.bar/bar-base-value.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 3, 4], datasets: [ { data: [5, 20, 10, 11], base: 10, backgroundColor: '#00ff00', borderColor: '#ff0000', borderWidth: 2, } ] }, options: { scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/bar-default-begin-at-zero.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 3, 4], datasets: [ { data: [5, 20, 1, 10], backgroundColor: '#00ff00', borderColor: '#ff0000' } ] }, options: { scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/bar-thickness-absolute.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2017", "2018", "2019", "2024", "2025"], "datasets": [{ "backgroundColor": "rgba(255, 99, 132, 0.5)", "barPercentage": 1, "categoryPercentage": 1, "barThickness": 128, "data": [1, null, 3, 4, 5] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "offset": true, "display": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-flex-offset.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2017", "2018", "2020", "2024", "2038"], "datasets": [{ "backgroundColor": "#FF6384", "barPercentage": 1, "categoryPercentage": 1, "barThickness": "flex", "data": [1, null, 3, 4, 5] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "offset": true, "display": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-flex-single-reverse.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "backgroundColor": "#FF6384", "barThickness": "flex", "barPercentage": 1, "categoryPercentage": 1, "data": [1] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "display": false, "offset": false, "time": { "parser": "YYYY" }, "reverse": true, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-flex-single.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "backgroundColor": "#FF6384", "barThickness": "flex", "barPercentage": 1, "categoryPercentage": 1, "data": [1] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "display": false, "offset": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-flex.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2017", "2018", "2020", "2024", "2038"], "datasets": [{ "backgroundColor": "#FF6384", "barPercentage": 1, "categoryPercentage": 1, "barThickness": "flex", "data": [1, null, 3, 4, 5] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "display": false, "offset": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-max.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "backgroundColor": "#FF6384", "barPercentage": 1, "categoryPercentage": 1, "maxBarThickness": 8, "data": [1, null, 3, 4, 5] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "display": false, "offset": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-min-interval-multi.json ================================================ { "config": { "type": "bar", "data": { "datasets": [{ "backgroundColor": "#FF6384", "barPercentage": 1, "categoryPercentage": 1, "data": [{"x": "2001", "y": 1}, {"x": "2099", "y": 5}] }, { "backgroundColor": "#8463FF", "barPercentage": 1, "categoryPercentage": 1, "data": [{"x": "2019", "y": 2}, {"x": "2020", "y": 3}] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "display": false, "min": "2000", "max": "2100", "time": { "parser": "YYYY" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-min-interval.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "backgroundColor": "#FF6384", "barPercentage": 1, "categoryPercentage": 1, "data": [1, null, 3, 4, 5] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "display": false, "offset": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-multiple.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "backgroundColor": "#FF6384", "data": [1, null, 3, 4, 5] }, { "backgroundColor": "#36A2EB", "data": [5, 4, 3, null, 1] }, { "backgroundColor": "#FFCE56", "data": [3, 5, 2, null, 4] }] }, "options": { "responsive": false, "datasets": { "bar": { "barPercentage": 1, "categoryPercentage": 1 } }, "scales": { "x": { "type": "time", "display": false, "offset": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-no-overlap.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "backgroundColor": "#FF6384", "data": [ {"y": "1", "x": "2016"}, {"y": "2", "x": "2017"}, {"y": "3", "x": "2017-08"}, {"y": "4", "x": "2024"}, {"y": "5", "x": "2030"} ] }] }, "options": { "responsive": false, "datasets": { "bar": { "barPercentage": 1, "categoryPercentage": 1 } }, "scales": { "x": { "type": "time", "display": false, "offset": false, "time": { "parser": "YYYY-MM" }, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-offset.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "backgroundColor": "#FF6384", "data": [1, null, 3, 4, 5] }, { "backgroundColor": "#36A2EB", "data": [5, 4, 3, null, 1] }, { "backgroundColor": "#FFCE56", "data": [3, 5, 2, null, 4] }] }, "options": { "responsive": false, "datasets": { "bar": { "barPercentage": 1, "categoryPercentage": 1 } }, "scales": { "x": { "type": "time", "offset": true, "display": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-per-dataset-stacked.json ================================================ { "config": { "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "type": "bar", "barThickness": 16, "backgroundColor": "#FF6384", "data": [1, null, 3, 4, 5] }, { "type": "bar", "barThickness": 8, "backgroundColor": "#36A2EB", "data": [5, 4, 3, null, 1] }, { "type": "bar", "barThickness": 4, "backgroundColor": "#FFCE56", "data": [3, 5, 2, null, 4] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "offset": true, "stacked": true, "display": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "stacked": true, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-per-dataset.json ================================================ { "config": { "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "type": "bar", "barThickness": 16, "backgroundColor": "#FF6384", "data": [1, null, 3, 4, 5] }, { "type": "bar", "barThickness": 8, "backgroundColor": "#36A2EB", "data": [5, 4, 3, null, 1] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "offset": true, "display": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-reverse.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "backgroundColor": "#FF6384", "data": [1, null, 3, 4, 5] }, { "backgroundColor": "#36A2EB", "data": [5, 4, 3, null, 1] }, { "backgroundColor": "#FFCE56", "data": [3, 5, 2, null, 4] }] }, "options": { "responsive": false, "datasets": { "bar": { "barPercentage": 1, "categoryPercentage": 1 } }, "scales": { "x": { "type": "time", "display": false, "offset": false, "time": { "parser": "YYYY" }, "reverse": true, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-single-xy.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "barPercentage": 1, "categoryPercentage": 1, "backgroundColor": "#FF6384", "data": [{"x": "2022", "y": 42}] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "display": false, "offset": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-single.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "barPercentage": 1, "categoryPercentage": 1, "backgroundColor": "#FF6384", "data": [1] }] }, "options": { "responsive": false, "scales": { "x": { "type": "time", "display": false, "offset": false, "time": { "parser": "YYYY" }, "min": "2013", "ticks": { "source": "labels" } }, "y": { "display": false, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/bar-thickness-stacked.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "backgroundColor": "#FF6384", "data": [1, null, 3, 4, 5] }, { "backgroundColor": "#36A2EB", "data": [5, 4, 3, null, 1] }, { "backgroundColor": "#FFCE56", "data": [3, 5, 2, null, 4] }] }, "options": { "responsive": false, "datasets": { "bar": { "barPercentage": 1, "categoryPercentage": 1 } }, "scales": { "x": { "type": "time", "stacked": true, "display": false, "offset": false, "time": { "parser": "YYYY" }, "ticks": { "source": "labels" } }, "y": { "display": false, "stacked": true, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/baseLine/bottom.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b'], datasets: [{ backgroundColor: '#AAFFCC', borderColor: '#0000FF', borderWidth: 1, data: [1, 2] }] }, options: { scales: { x: { display: false }, y: { ticks: { display: false }, grid: { color: function(context) { return context.tick.value === 0 ? 'red' : 'transparent'; }, lineWidth: 5, tickLength: 0 }, } }, maintainAspectRatio: false } }, options: { canvas: { width: 128, height: 128 } } }; ================================================ FILE: test/fixtures/controller.bar/baseLine/left.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b'], datasets: [{ backgroundColor: '#AAFFCC', borderColor: '#0000FF', borderWidth: 1, data: [1, 2] }] }, options: { indexAxis: 'y', scales: { y: { display: false }, x: { ticks: { display: false }, grid: { color: function(context) { return context.tick.value === 0 ? 'red' : 'transparent'; }, lineWidth: 5, tickLength: 0 }, } }, maintainAspectRatio: false } }, options: { canvas: { width: 128, height: 128 } } }; ================================================ FILE: test/fixtures/controller.bar/baseLine/mid-x.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b'], datasets: [{ backgroundColor: '#AAFFCC', borderColor: '#0000FF', borderWidth: 1, data: [1, -1] }] }, options: { indexAxis: 'y', scales: { y: { display: false }, x: { ticks: { display: false }, grid: { color: function(context) { return context.tick.value === 0 ? 'red' : 'transparent'; }, lineWidth: 5, tickLength: 0 }, } }, maintainAspectRatio: false } }, options: { canvas: { width: 128, height: 128 } } }; ================================================ FILE: test/fixtures/controller.bar/baseLine/mid-y.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b'], datasets: [{ backgroundColor: '#AAFFCC', borderColor: '#0000FF', borderWidth: 1, data: [1, -1] }] }, options: { scales: { x: { display: false }, y: { ticks: { display: false }, grid: { color: function(context) { return context.tick.value === 0 ? 'red' : 'transparent'; }, lineWidth: 5, tickLength: 0 }, } }, maintainAspectRatio: false } }, options: { canvas: { width: 128, height: 128 } } }; ================================================ FILE: test/fixtures/controller.bar/baseLine/right.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b'], datasets: [{ backgroundColor: '#AAFFCC', borderColor: '#0000FF', borderWidth: 1, data: [-1, -2] }] }, options: { indexAxis: 'y', scales: { y: { display: false }, x: { ticks: { display: false }, grid: { color: function(context) { return context.tick.value === 0 ? 'red' : 'transparent'; }, lineWidth: 5, tickLength: 0, borderWidth: 0 }, } }, maintainAspectRatio: false } }, options: { canvas: { width: 128, height: 128 } } }; ================================================ FILE: test/fixtures/controller.bar/baseLine/top.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b'], datasets: [{ backgroundColor: '#AAFFCC', borderColor: '#0000FF', borderWidth: 1, data: [-1, -2] }] }, options: { scales: { x: { display: false }, y: { ticks: { display: false }, grid: { color: function(context) { return context.tick.value === 0 ? 'red' : 'transparent'; }, borderWidth: 0, lineWidth: 5, tickLength: 0 }, } }, maintainAspectRatio: false } }, options: { canvas: { width: 128, height: 128 } } }; ================================================ FILE: test/fixtures/controller.bar/baseLine/value-x.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b'], datasets: [{ backgroundColor: '#AAFFCC', borderColor: '#0000FF', borderWidth: 1, data: [1, 3] }] }, options: { base: 2, indexAxis: 'y', scales: { y: { display: false }, x: { ticks: { display: false }, grid: { color: function(context) { return context.tick.value === 2 ? 'red' : 'transparent'; }, lineWidth: 5, tickLength: 0 }, } }, maintainAspectRatio: false } }, options: { canvas: { width: 128, height: 128 } } }; ================================================ FILE: test/fixtures/controller.bar/baseLine/value-y.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b'], datasets: [{ backgroundColor: '#AAFFCC', borderColor: '#0000FF', borderWidth: 1, data: [1, 3] }] }, options: { base: 2, scales: { x: { display: false }, y: { ticks: { display: false }, grid: { color: function(context) { return context.tick.value === 2 ? 'red' : 'transparent'; }, lineWidth: 5, tickLength: 0 }, } }, maintainAspectRatio: false } }, options: { canvas: { width: 128, height: 128 } } }; ================================================ FILE: test/fixtures/controller.bar/borderColor/border+dpr.js ================================================ module.exports = { threshold: 0, tolerance: 0, config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5, 6], datasets: [ { // option in dataset data: [5, 4, 3, 2, 3, 4, 5], }, ] }, options: { events: [], devicePixelRatio: 1.5, barPercentage: 1, categoryPercentage: 1, backgroundColor: 'black', borderColor: 'black', borderWidth: 8, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 501 } } }; ================================================ FILE: test/fixtures/controller.bar/borderColor/indexable.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: [ '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#000000' ] }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: [ '#ff88ff', '#888888', '#ff8800', '#00ff88', '#8800ff', '#ffff88' ], borderWidth: 8 } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderColor/scriptable.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 0 ? '#00ff00' : value > -8 ? '#0000ff' : '#ff00ff'; } }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5] } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff00ff' : value > 0 ? '#0000ff' : value > -8 ? '#ff0000' : '#00ff00'; }, borderWidth: 8 } }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderColor/value.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: '#ff0000' }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: '#00ff00', borderWidth: 8 } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-mixed-chart.js ================================================ module.exports = { threshold: 0.01, config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { backgroundColor: 'red', data: [12, 19, 12, 5, 4, 12], }, { backgroundColor: 'green', data: [12, 19, -4, 5, 8, 3], type: 'line' }, { backgroundColor: 'blue', data: [7, 11, -12, 12, 0, -7], } ] }, options: { elements: { bar: { borderRadius: Number.MAX_VALUE, borderWidth: 2, } }, scales: { x: {display: false, stacked: true}, y: {display: false, stacked: true} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderRadius/border-radius-stacked-number-with-order.js ================================================ module.exports = { threshold: 0.01, config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { backgroundColor: 'red', data: [12, 19, 12, 5, 4, 12], order: 2, }, { backgroundColor: 'green', data: [12, 19, -4, 5, 8, 3], order: 1, }, { backgroundColor: 'blue', data: [7, 11, -12, 12, 0, -7], order: 0, } ] }, options: { elements: { bar: { borderRadius: Number.MAX_VALUE, borderWidth: 2, } }, scales: { x: {display: false, stacked: true}, y: {display: false, stacked: true} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderRadius/border-radius-stacked-number.js ================================================ module.exports = { threshold: 0.01, config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { backgroundColor: 'red', data: [12, 19, 12, 5, 4, 12], }, { backgroundColor: 'green', data: [12, 19, -4, 5, 8, 3], }, { backgroundColor: 'blue', data: [7, 11, -12, 12, 0, -7], } ] }, options: { elements: { bar: { borderRadius: Number.MAX_VALUE, borderWidth: 2, } }, scales: { x: {display: false, stacked: true}, y: {display: false, stacked: true} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderRadius/border-radius.js ================================================ module.exports = { threshold: 0.01, config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderWidth: 2, borderRadius: 5 }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], borderSkipped: false, borderRadius: Number.MAX_VALUE } ] }, options: { indexAxis: 'y', elements: { bar: { backgroundColor: '#AAAAAA80', borderColor: '#80808080', borderWidth: {bottom: 6, left: 15, top: 6, right: 15} } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderRadius/no-spacing.js ================================================ module.exports = { threshold: 0.01, config: { type: 'bar', data: { labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'], datasets: [ { data: [9, 25, 13, 17, 12, 21, 20, 19, 6, 12, 14, 20], categoryPercentage: 1, barPercentage: 1, backgroundColor: '#2E5C76', borderWidth: 2, borderColor: '#377395', borderRadius: 5, }, ] }, options: { devicePixelRatio: 1.25, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderSkipped/indexable.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderSkipped: [ 'top', 'top', 'right', 'right', 'bottom', 'left' ] }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: '#888', borderWidth: 8, borderSkipped: [ 'bottom', 'bottom', 'left', 'left', 'top', 'right' ] } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderSkipped/middle.js ================================================ module.exports = { threshold: 0.01, config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { backgroundColor: 'red', data: [12, 19, 12, 5, 4, 12], }, { backgroundColor: 'green', data: [12, 19, -4, 5, 8, 3], }, { backgroundColor: 'blue', data: [7, 11, -12, 12, 0, -7], } ] }, options: { borderRadius: Number.MAX_VALUE, borderSkipped: 'middle', borderWidth: 2, scales: { x: {display: false, stacked: true}, y: {display: false, stacked: true} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderSkipped/scriptable.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderSkipped: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? 'left' : value > 0 ? 'right' : value > -8 ? 'top' : 'bottom'; } }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5] } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: '#888', borderSkipped: function(ctx) { var index = ctx.dataIndex; return index > 4 ? 'left' : index > 3 ? 'right' : index > 1 ? 'top' : 'bottom'; }, borderWidth: 8 } }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderSkipped/value.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3], datasets: [ { // option in dataset data: [0, 5, -10, null], borderSkipped: 'top' }, { // option in dataset data: [0, 5, -10, null], borderSkipped: 'right' }, { // option in dataset data: [0, 5, -10, null], borderSkipped: 'bottom' }, { // option in element (fallback) data: [0, 5, -10, null], }, { // option in dataset data: [0, 5, -10, null], borderSkipped: false } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: '#888', borderSkipped: 'left', borderWidth: 8 } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderWidth/indexable-object.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderSkipped: false, borderWidth: [ {}, {bottom: 1, left: 1, top: 1, right: 1}, {bottom: 1, left: 2, top: 1, right: 2}, {bottom: 1, left: 3, top: 1, right: 3}, {bottom: 1, left: 4, top: 1, right: 4}, {bottom: 1, left: 5, top: 1, right: 5} ] }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: '#80808080', borderSkipped: false, borderWidth: [ {bottom: 1, left: 5, top: 1, right: 5}, {bottom: 1, left: 4, top: 1, right: 4}, {bottom: 1, left: 3, top: 1, right: 3}, {bottom: 1, left: 2, top: 1, right: 2}, {bottom: 1, left: 1, top: 1, right: 1}, {} ] } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderWidth/indexable.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderWidth: [ 0, 1, 2, 3, 4, 5 ] }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: '#888', borderWidth: [ 5, 4, 3, 2, 1, 0 ] } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderWidth/negative.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderWidth: -2 }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], }, { data: [0, 5, 10, null, -10, -5], borderWidth: {left: -5, top: -5, bottom: -5, right: -5}, borderSkipped: false }, { data: [0, 5, 10, null, -10, -5], borderWidth: {} }, ] }, options: { elements: { bar: { backgroundColor: '#888', borderColor: '#f00', borderWidth: -4 } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderWidth/object.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderSkipped: false, borderWidth: {bottom: 1, left: 2, top: 3, right: 4} }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: '#888', borderSkipped: false, borderWidth: {bottom: 4, left: 3, top: 2, right: 1} } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderWidth/scriptable-object.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderSkipped: false, borderWidth: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return {top: Math.abs(value)}; } }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5] } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: '#80808080', borderSkipped: false, borderWidth: function(ctx) { return {left: ctx.dataIndex * 2}; } } }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderWidth/scriptable.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderWidth: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return Math.abs(value); } }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5] } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: '#888', borderWidth: function(ctx) { return ctx.dataIndex * 2; } } }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/borderWidth/value.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderWidth: 2 }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], } ] }, options: { elements: { bar: { backgroundColor: 'transparent', borderColor: '#888', borderWidth: 4 } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/chart-area-clip.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 3, 4], datasets: [ { data: [5, 20, -5, -20], borderColor: '#ff0000' } ] }, options: { layout: { padding: { left: 0, right: 0, top: 50, bottom: 50 } }, elements: { bar: { backgroundColor: '#00ff00', borderWidth: 8 } }, scales: { x: {display: false}, y: {display: false, min: -10, max: 10} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/data/object-index-axis-y.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ label: '# of Votes', data: {a: 1, b: 3, c: 2} }] }, options: { indexAxis: 'y' } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/data/object.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b', 'c'], datasets: [ { data: {a: 10, b: 2, c: -5}, backgroundColor: '#ff0000' }, { data: {a: 8, b: 12, c: 5}, backgroundColor: '#00ff00' } ] }, options: { scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/data/parsing.js ================================================ const data = [{x: 'Jan', net: 100, cogs: 50, gm: 50}, {x: 'Feb', net: 120, cogs: 55, gm: 75}]; module.exports = { config: { type: 'bar', data: { labels: ['Jan', 'Feb'], datasets: [{ label: 'Net sales', backgroundColor: 'blue', data: data, parsing: { yAxisKey: 'net' } }, { label: 'Cost of goods sold', backgroundColor: 'red', data: data, parsing: { yAxisKey: 'cogs' } }, { label: 'Gross margin', backgroundColor: 'green', data: data, parsing: { yAxisKey: 'gm' } }] }, options: { scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/floatBar/data-as-objects-horizontal.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b', 'c'], datasets: [ { data: [{y: 'b', x: [2, 8]}, {y: 'c', x: [2, 5]}], backgroundColor: '#ff0000' }, { data: [{y: 'a', x: 10}, {y: 'c', x: [6, 10]}], backgroundColor: '#00ff00' } ] }, options: { indexAxis: 'y', scales: { x: {display: false, min: 0}, y: {display: false, stacked: true} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/floatBar/data-as-objects.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b', 'c'], datasets: [ { data: [{x: 'b', y: [2, 8]}, {x: 'c', y: [2, 5]}], backgroundColor: '#ff0000' }, { data: [{x: 'a', y: 10}, {x: 'c', y: [6, 10]}], backgroundColor: '#00ff00' } ] }, options: { scales: { x: {display: false, stacked: true}, y: {display: false, min: 0} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/floatBar/float-bar-horizontal.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2030", "2034", "2038", "2042"], "datasets": [{ "backgroundColor": "#FF6384", "data": [11, [6,2], [-4,-7], -2] }, { "backgroundColor": "#36A2EB", "data": [[1,2], [3,4], [-2,-3], [1,4]] }, { "backgroundColor": "#FFCE56", "data": [[0,1], [1,2], [-2,-1], [1,-7]] }] }, "options": { "indexAxis": "y", "scales": { "x": { "display": false }, "y": { "display": false } } } }, "debug": false, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/floatBar/float-bar-stacked-horizontal.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2030", "2034", "2038", "2042"], "datasets": [{ "backgroundColor": "#FF6384", "data": [11, [6,2], [-4,-7], -2] }, { "backgroundColor": "#36A2EB", "data": [[1,2], [3,4], [-2,-3], [1,4]] }, { "backgroundColor": "#FFCE56", "data": [[0,1], [1,2], [-2,-1], [1,-7]] }] }, "options": { "indexAxis": "y", "scales": { "x": { "display": false, "stacked": true }, "y": { "display": false, "stacked": true } } } }, "debug": false, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/floatBar/float-bar-stacked.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2030", "2034", "2038", "2042"], "datasets": [{ "backgroundColor": "#FF6384", "data": [11, [6,2], [-4,-7], -2] }, { "backgroundColor": "#36A2EB", "data": [[1,2], [3,4], [-2,-3], [1,4]] }, { "backgroundColor": "#FFCE56", "data": [[0,1], [1,2], [-2,-1], [1,-7]] }] }, "options": { "scales": { "x": { "display": false, "stacked": true }, "y": { "display": false, "stacked": true } } } }, "debug": false, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/floatBar/float-bar.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2030", "2034", "2038", "2042"], "datasets": [{ "backgroundColor": "#FF6384", "data": [11, [6,2], [-4,-7], -2] }, { "backgroundColor": "#36A2EB", "data": [[1,2], [3,4], [-2,-3], [1,4]] }, { "backgroundColor": "#FFCE56", "data": [[0,1], [1,2], [-2,-1], [1,-7]] }] }, "options": { "scales": { "x": { "display": false }, "y": { "display": false } } } }, "debug": false, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/horizontal-borders.js ================================================ module.exports = { threshold: 0.01, config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderWidth: 2 }, { // option in element (fallback) data: [0, 5, 10, null, -10, -5], borderSkipped: false } ] }, options: { indexAxis: 'y', elements: { bar: { backgroundColor: '#AAAAAA80', borderColor: '#80808080', borderWidth: {bottom: 6, left: 15, top: 6, right: 15} } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/minBarLength/horizontal-neg.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2], datasets: [ { data: [0, -0.01, -30], backgroundColor: '#00ff00', borderColor: '#000', borderWidth: 4, minBarLength: 20 } ] }, options: { indexAxis: 'y', scales: { x: { ticks: { display: false } }, y: {display: false} } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/minBarLength/horizontal-pos.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2], datasets: [ { data: [0, 0.01, 30], backgroundColor: '#00ff00', borderColor: '#000', borderWidth: 4, minBarLength: 20 } ] }, options: { indexAxis: 'y', scales: { x: { ticks: { display: false } }, y: {display: false} } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/minBarLength/horizontal-stacked-no-overlap.js ================================================ const minBarLength = 50; module.exports = { config: { type: 'bar', data: { labels: [1, 2, 3, 4], datasets: [ { data: [1, -1, 1, 20], backgroundColor: '#bb000066', minBarLength }, { data: [1, -1, -1, -20], backgroundColor: '#00bb0066', minBarLength }, { data: [1, -1, 1, 40], backgroundColor: '#0000bb66', minBarLength }, { data: [1, -1, -1, -40], backgroundColor: '#00000066', minBarLength } ] }, options: { indexAxis: 'y', scales: { x: { display: false, stacked: true }, y: { type: 'linear', position: 'left', stacked: true, ticks: { display: false } } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/minBarLength/horizontal-stacked.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4], datasets: [{ data: [0, 0.01, 30], backgroundColor: '#00ff00', borderColor: '#000', borderWidth: 4, minBarLength: 20, xAxisID: 'x2', }] }, options: { indexAxis: 'y', scales: { x: { stack: 'demo', ticks: { display: false } }, x2: { type: 'linear', position: 'bottom', stack: 'demo', stackWeight: 1, ticks: { display: false } }, y: {display: false}, } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/minBarLength/horizontal.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4], datasets: [ { data: [0, -0.01, 0.01, 30, -30], backgroundColor: '#00ff00', borderColor: '#000', borderSkipped: ctx => ctx.raw === 0 ? false : 'start', borderWidth: 4, minBarLength: 20 } ] }, options: { indexAxis: 'y', scales: { x: { ticks: { display: false } }, y: {display: false} } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/minBarLength/vertical-neg.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2], datasets: [ { data: [0, -0.01, -30], backgroundColor: '#00ff00', borderColor: '#000', borderWidth: 4, minBarLength: 20 } ] }, options: { scales: { x: {display: false}, y: { ticks: { display: false } } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/minBarLength/vertical-pos.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2], datasets: [ { data: [0, 0.01, 30], backgroundColor: '#00ff00', borderColor: '#000', borderWidth: 4, minBarLength: 20 } ] }, options: { scales: { x: {display: false}, y: { ticks: { display: false } } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/minBarLength/vertical-stacked-no-overlap.js ================================================ const minBarLength = 50; module.exports = { config: { type: 'bar', data: { labels: [1, 2, 3, 4], datasets: [ { data: [1, -1, 1, 20], backgroundColor: '#bb000066', minBarLength }, { data: [1, -1, -1, -20], backgroundColor: '#00bb0066', minBarLength }, { data: [1, -1, 1, 40], backgroundColor: '#0000bb66', minBarLength }, { data: [1, -1, -1, -40], backgroundColor: '#00000066', minBarLength } ] }, options: { scales: { x: { display: false, stacked: true }, y: { type: 'linear', position: 'left', stacked: true, ticks: { display: false } } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/minBarLength/vertical-stacked.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4], datasets: [{ data: [0, 0.01, 30], backgroundColor: '#00ff00', borderColor: '#000', borderWidth: 4, minBarLength: 20, yAxisID: 'y2', }] }, options: { scales: { x: {display: false}, y: { stack: 'demo', ticks: { display: false } }, y2: { type: 'linear', position: 'left', stack: 'demo', stackWeight: 1, ticks: { display: false } } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/minBarLength/vertical.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4], datasets: [ { data: [0, -0.01, 0.01, 30, -30], backgroundColor: '#00ff00', borderColor: '#000', borderSkipped: ctx => ctx.raw === 0 ? false : 'start', borderWidth: 4, minBarLength: 20 } ] }, options: { scales: { x: {display: false}, y: { ticks: { display: false } } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/not-grouped/mixed.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9281', config: { type: 'bar', data: { labels: [0, 1, 2], datasets: [ { label: 'data 1', data: [1, 2, 2], backgroundColor: 'rgb(255,0,0,0.7)', grouped: true }, { label: 'data 2', data: [4, 4, 1], backgroundColor: 'rgb(0,255,0,0.7)', grouped: true }, { label: 'data 3', data: [2, 1, 3], backgroundColor: 'rgb(0,0,255,0.7)', grouped: false } ] }, options: { scales: { x: {display: false}, y: {display: false} } } }, }; ================================================ FILE: test/fixtures/controller.bar/not-grouped/on-time.js ================================================ const data1 = [ { x: '2017-11-02T20:30:00', y: 27 }, { x: '2017-11-03T20:53:00', y: 30 }, { x: '2017-11-06T05:46:00', y: 19 }, { x: '2017-11-06T21:03:00', y: 28 }, { x: '2017-11-07T20:49:00', y: 29 }, { x: '2017-11-08T21:52:00', y: 33 } ]; const data2 = [ { x: '2017-11-03T13:07:00', y: 45 }, { x: '2017-11-04T04:50:00', y: 40 }, { x: '2017-11-06T12:48:00', y: 38 }, { x: '2017-11-07T12:28:00', y: 42 }, { x: '2017-11-08T12:45:00', y: 51 }, { x: '2017-11-09T05:23:00', y: 57 } ]; const data3 = [ { x: '2017-11-03T16:30:00', y: 32 }, { x: '2017-11-04T11:50:00', y: 34 }, { x: '2017-11-06T18:30:00', y: 28 }, { x: '2017-11-07T15:51:00', y: 31 }, { x: '2017-11-08T17:27:00', y: 36 }, { x: '2017-11-09T06:53:00', y: 31 } ]; module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/5139', config: { type: 'bar', data: { datasets: [ { data: data1, backgroundColor: 'rgb(0,0,255)', }, { data: data2, backgroundColor: 'rgb(255,0,0)', }, { data: data3, backgroundColor: 'rgb(0,255,0)', }, ] }, options: { barThickness: 10, grouped: false, scales: { x: { bounds: 'ticks', type: 'time', offset: false, position: 'bottom', display: true, time: { isoWeekday: true, unit: 'day' }, grid: { offset: false } }, y: { beginAtZero: true, display: false } }, } }, options: { spriteText: true, canvas: { width: 1000, height: 300 } } }; ================================================ FILE: test/fixtures/controller.bar/skipNull/bar-skip-null-object-data.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [ { data: {0: 5, 1: 20, 2: 1, 3: 10}, backgroundColor: '#00ff00', borderColor: '#ff0000' }, { data: {0: 10, 1: null, 2: 1, 3: NaN}, backgroundColor: '#ff0000', borderColor: '#ff0000' } ] }, options: { skipNull: true, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/skipNull/bar-skip-null.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 3, 4], datasets: [ { data: [5, 20, 1, 10], backgroundColor: '#00ff00', borderColor: '#ff0000' }, { data: [10, null, 1, undefined], backgroundColor: '#ff0000', borderColor: '#ff0000' } ] }, options: { skipNull: true, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/skipNull/combinations.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['0', '1', '2', '3', '4', '5', '6', '7'], datasets: [ { data: [null, 1000, null, 1000, null, 1000, null, 1000], backgroundColor: '#00ff00', borderColor: '#ff0000' }, { data: [null, null, 1000, 1000, null, null, 1000, 1000], backgroundColor: '#ff0000', borderColor: '#ff0000' }, { data: [null, null, null, null, 1000, 1000, 1000, 1000], backgroundColor: '#0000ff', borderColor: '#0000ff' } ] }, options: { skipNull: true, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.bar/stacking/issue-9105.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9105', config: { type: 'bar', data: { labels: ['January', 'February', 'March', 'April', 'May', 'June'], datasets: [ { backgroundColor: 'rgba(255,99,132,0.8)', label: 'Dataset 1', data: [12, 19, 3, 5, 2, 3], stack: '0', yAxisID: 'y' }, { backgroundColor: 'rgba(54,162,235,0.8)', label: 'Dataset 2', data: [13, 19, 3, 5, 8, 3], stack: '0', yAxisID: 'y' }, { backgroundColor: 'rgba(75,192,192,0.8)', label: 'Dataset 3', data: [13, 19, 3, 5, 8, 3], stack: '0', yAxisID: 'y' } ] }, options: { plugins: false, scales: { x: { display: false, }, y: { display: false } } } }, options: { run(chart) { chart.data.datasets[1].stack = '1'; chart.update(); } } }; ================================================ FILE: test/fixtures/controller.bar/stacking/logarithmic-strings.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: ['10', '100', '10', '100'], backgroundColor: '#ff0000' }, { data: ['100', '10', '0', '100'], backgroundColor: '#00ff00' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { datasets: { bar: { barPercentage: 1, } }, scales: { x: { type: 'category', display: false, stacked: true, }, y: { type: 'logarithmic', display: false, stacked: true } } } } }; ================================================ FILE: test/fixtures/controller.bar/stacking/logarithmic.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [10, 100, 10, 100], backgroundColor: '#ff0000' }, { data: [100, 10, 0, 100], backgroundColor: '#00ff00' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { datasets: { bar: { barPercentage: 1, } }, scales: { x: { type: 'category', display: false, stacked: true, }, y: { type: 'logarithmic', display: false, stacked: true } } } } }; ================================================ FILE: test/fixtures/controller.bar/stacking/order-default.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "backgroundColor": "#FF6384", "data": [1, null, 3, 4, 5] }, { "backgroundColor": "#36A2EB", "data": [5, 4, 3, null, 1] }, { "backgroundColor": "#FFCE56", "data": [3, 5, 2, null, 4] }] }, "options": { "responsive": false, "scales": { "x": { "display": false, "stacked": true }, "y": { "display": false, "stacked": true, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/stacking/order-specified.json ================================================ { "config": { "type": "bar", "data": { "labels": ["2016", "2018", "2020", "2024", "2030"], "datasets": [{ "backgroundColor": "#FF6384", "data": [1, null, 3, 4, 5], "order": 20 }, { "backgroundColor": "#36A2EB", "data": [5, 4, 3, null, 1], "order": 25 }, { "backgroundColor": "#FFCE56", "data": [3, 5, 2, null, 4], "order": 10 }] }, "options": { "responsive": false, "scales": { "x": { "display": false, "stacked": true }, "y": { "display": false, "stacked": true, "beginAtZero": true } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bar/stacking/remove-dataset.js ================================================ var barChartData = { labels: [0, 1, 2, 3, 4, 5, 6], datasets: [ { backgroundColor: 'red', data: [ // { x: 0, y: 0 }, {x: 1, y: 5}, {x: 2, y: 5}, {x: 3, y: 5}, {x: 4, y: 5}, {x: 5, y: 5}, {x: 6, y: 5} ] }, { backgroundColor: 'blue', data: [ {x: 0, y: 5}, // { x: 1, y: 0 }, {x: 2, y: 5}, {x: 3, y: 5}, {x: 4, y: 5}, {x: 5, y: 5}, {x: 6, y: 5} ] }, { backgroundColor: 'green', data: [ {x: 0, y: 5}, {x: 1, y: 5}, // { x: 2, y: 0 }, {x: 3, y: 5}, {x: 4, y: 5}, {x: 5, y: 5}, {x: 6, y: 5} ] }, { backgroundColor: 'yellow', data: [ {x: 0, y: 5}, {x: 1, y: 5}, {x: 2, y: 5}, // {x: 3, y: 0 }, {x: 4, y: 5}, {x: 5, y: 5}, {x: 6, y: 5} ] }, { backgroundColor: 'purple', data: [ {x: 0, y: 5}, {x: 1, y: 5}, {x: 2, y: 5}, {x: 3, y: 5}, // { x: 4, y: 0 }, {x: 5, y: 5}, {x: 6, y: 5} ] }, { backgroundColor: 'grey', data: [ {x: 0, y: 5}, {x: 1, y: 5}, {x: 2, y: 5}, {x: 3, y: 5}, {x: 4, y: 5}, // { x: 5, y: 0 }, {x: 6, y: 5} ] } ] }; module.exports = { config: { type: 'bar', data: barChartData, options: { scales: { x: { display: false, stacked: true }, y: { display: false, stacked: true } } } }, options: { run(chart) { chart.data.datasets.splice(0, 1); chart.update(); } } }; ================================================ FILE: test/fixtures/controller.bar/stacking/replace-data.js ================================================ var barChartData = { labels: ['January', 'February', 'March'], datasets: [ { label: 'Dataset 1', backgroundColor: 'red', data: [5, 5, 5] }, { label: 'Dataset 2', backgroundColor: 'blue', data: [5, 5, 5] }, { label: 'Dataset 3', backgroundColor: 'green', data: [5, 5, 5] } ] }; module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8614', config: { type: 'bar', data: barChartData, options: { scales: { x: { display: false, stacked: true }, y: { display: false, stacked: true } } } }, options: { run(chart) { chart.data.datasets[1].data = [ {x: 'January', y: 5}, // Februay missing {x: 'March', y: 5} ]; chart.update(); } } }; ================================================ FILE: test/fixtures/controller.bar/stacking/stacked-and-multiple-axis.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [ { label: 'Dataset 1', data: [100, 90, 100, 50, 99, 87, 34], backgroundColor: 'rgba(255,99,132,0.8)', stack: 'a', xAxisID: 'x' }, { label: 'Dataset 2', data: [20, 25, 30, 32, 58, 14, 12], backgroundColor: 'rgba(54,162,235,0.8)', stack: 'b', xAxisID: 'x2' }, { label: 'Dataset 3', data: [80, 30, 40, 60, 70, 80, 47], backgroundColor: 'rgba(75,192,192,0.8)', stack: 'a', xAxisID: 'x3' }, { label: 'Dataset 4', data: [80, 30, 40, 60, 70, 80, 47], backgroundColor: 'rgba(54,162,235,0.8)', stack: 'a', xAxisID: 'x3' }, ] }, options: { plugins: false, barThickness: 'flex', scales: { x: { stacked: true, display: false, }, x2: { labels: ['January 2024', 'February 2024', 'March 2024', 'April 2024', 'May 2024', 'June 2024', 'July 2024'], stacked: true, display: false, }, x3: { labels: ['January 2025', 'February 2025', 'March 2025', 'April 2025', 'May 2025', 'June 2025', 'July 2025'], stacked: true, display: false, }, y: { stacked: true, display: false, } } } }, options: { } }; ================================================ FILE: test/fixtures/controller.bubble/autoPadding-disabled.js ================================================ module.exports = { config: { type: 'bubble', data: { datasets: [{ backgroundColor: 'red', data: [{x: 12, y: 54, r: 22.4}] }, { backgroundColor: 'blue', data: [{x: 18, y: 38, r: 25}] }] }, options: { layout: { autoPadding: false, } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/controller.bubble/clip.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 5, 10, 15, 20, 25, 30, 50, 55, 60], datasets: [{ data: [6, 11, 10, 10, 3, 22, 7, 24], type: 'bubble', label: 'test', borderColor: '#3e95cd', fill: false }] }, options: { scales: { x: {ticks: {display: false}}, y: { min: 8, max: 25, beginAtZero: true, ticks: { display: false } } } } }, options: { canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/controller.bubble/hover-radius-zero.js ================================================ module.exports = { config: { type: 'bubble', data: { labels: [2, 2, 2, 2], datasets: [{ data: [ [1, 1], [1, 2], [1, 3, 20], [1, 4, 20] ] }, { data: [1, 2, 3, 4] }, { data: [{x: 3, y: 1}, {x: 3, y: 2}, {x: 3, y: 3, r: 15}, {x: 3, y: 4, r: 15}] }] }, options: { events: [], radius: 10, hoverRadius: 0, backgroundColor: 'blue', hoverBackgroundColor: 'red', scales: { x: {display: false, bounds: 'data'}, y: {display: false} }, layout: { padding: 24 } } }, options: { canvas: { height: 256, width: 256 }, run(chart) { chart.setActiveElements([ {datasetIndex: 0, index: 1}, {datasetIndex: 0, index: 2}, {datasetIndex: 1, index: 1}, {datasetIndex: 1, index: 2}, {datasetIndex: 2, index: 1}, {datasetIndex: 2, index: 2}, ]); chart.update(); } } }; ================================================ FILE: test/fixtures/controller.bubble/padding-update.js ================================================ module.exports = { config: { type: 'bubble', data: { datasets: [{ backgroundColor: 'red', data: [{x: 12, y: 54, r: 22.4}] }, { backgroundColor: 'blue', data: [{x: 18, y: 38, r: 25}] }] } }, options: { spriteText: true, canvas: { width: 256, height: 256 }, run(chart) { chart.update(); } } }; ================================================ FILE: test/fixtures/controller.bubble/padding.js ================================================ module.exports = { config: { type: 'bubble', data: { datasets: [{ backgroundColor: 'red', data: [{x: 12, y: 54, r: 22.4}] }, { backgroundColor: 'blue', data: [{x: 18, y: 38, r: 25}] }] } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/controller.bubble/point-style.json ================================================ { "config": { "type": "bubble", "data": { "datasets": [{ "data": [ {"x": 0, "y": 3}, {"x": 1, "y": 3}, {"x": 2, "y": 3}, {"x": 3, "y": 3}, {"x": 4, "y": 3}, {"x": 5, "y": 3}, {"x": 6, "y": 3}, {"x": 7, "y": 3}, {"x": 8, "y": 3}, {"x": 9, "y": 3} ], "backgroundColor": "#00ff00", "borderColor": "transparent", "borderWidth": 0, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] }, { "data": [ {"x": 0, "y": 2}, {"x": 1, "y": 2}, {"x": 2, "y": 2}, {"x": 3, "y": 2}, {"x": 4, "y": 2}, {"x": 5, "y": 2}, {"x": 6, "y": 2}, {"x": 7, "y": 2}, {"x": 8, "y": 2}, {"x": 9, "y": 2} ], "backgroundColor": "transparent", "borderColor": "#0000ff", "borderWidth": 1, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] }, { "data": [ {"x": 0, "y": 1}, {"x": 1, "y": 1}, {"x": 2, "y": 1}, {"x": 3, "y": 1}, {"x": 4, "y": 1}, {"x": 5, "y": 1}, {"x": 6, "y": 1}, {"x": 7, "y": 1}, {"x": 8, "y": 1}, {"x": 9, "y": 1} ], "backgroundColor": "#00ff00", "borderColor": "#0000ff", "borderWidth": 1, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] }] }, "options": { "responsive": false, "scales": { "x": {"display": false}, "y": { "display": false, "min": 0, "max": 4 } }, "elements": { "line": { "borderColor": "transparent", "borderWidth": 1, "fill": false }, "point": { "radius": 16 } }, "layout": { "padding": { "left": 24, "right": 24 } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.bubble/radius-data.js ================================================ module.exports = { config: { type: 'bubble', data: { datasets: [{ data: [ {x: 0, y: 5, r: 1}, {x: 1, y: 4, r: 2}, {x: 2, y: 3, r: 6}, {x: 3, y: 2}, {x: 4, y: 1, r: 2}, {x: 5, y: 0, r: NaN}, {x: 6, y: -1, r: undefined}, {x: 7, y: -2, r: null}, {x: 8, y: -3, r: '4'}, {x: 9, y: -4, r: '4px'}, ] }] }, options: { scales: { x: {display: false}, y: {display: false} }, elements: { point: { backgroundColor: '#444', radius: 10 } }, layout: { padding: { left: 24, right: 24 } } } }, options: { canvas: { height: 128, width: 256 } } }; ================================================ FILE: test/fixtures/controller.bubble/radius-scriptable.js ================================================ module.exports = { config: { type: 'bubble', data: { datasets: [{ data: [ {x: 0, y: 0}, {x: 1, y: 0}, {x: 2, y: 0}, {x: 3, y: 0}, {x: 4, y: 0}, {x: 5, y: 0} ], radius: function(ctx) { return ctx.dataset.data[ctx.dataIndex].x * 4; } }] }, options: { scales: { x: {display: false}, y: {display: false} }, elements: { point: { backgroundColor: '#444' } }, layout: { padding: { left: 24, right: 24 } } } }, options: { canvas: { height: 128, width: 256 } } }; ================================================ FILE: test/fixtures/controller.doughnut/backgroundColor/indexable.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], backgroundColor: [ '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#000000' ] }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: [ '#ff88ff', '#888888', '#ff8800', '#00ff88', '#8800ff', '#ffff88' ] } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/backgroundColor/scriptable.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], backgroundColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 6 ? '#00ff00' : value > 2 ? '#0000ff' : '#ff00ff'; } }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 6 ? '#00ff00' : value > 2 ? '#0000ff' : '#ff00ff'; } } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/backgroundColor/value.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], backgroundColor: '#ff0000' }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: '#00ff00' } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderAlign/indexable.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderAlign: [ 'center', 'inner', 'center', 'inner', 'center', 'inner', ], borderColor: '#00ff00' }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#ff0000', borderWidth: 5, borderAlign: [ 'center', 'inner', 'center', 'inner', 'center', 'inner', ] } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderAlign/scriptable.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderAlign: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 'inner' : 'center'; }, borderColor: '#0000ff', }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#ff00ff', borderWidth: 8, borderAlign: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 'center' : 'inner'; } } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderAlign/value.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderAlign: 'inner', borderColor: '#00ff00', }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderAlign: 'center', borderColor: '#0000ff', borderWidth: 4, } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderColor/indexable.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderColor: [ '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#000000' ] }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: [ '#ff88ff', '#888888', '#ff8800', '#00ff88', '#8800ff', '#ffff88' ], borderWidth: 8 } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderColor/scriptable.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 6 ? '#00ff00' : value > 2 ? '#0000ff' : '#ff00ff'; } }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff00ff' : value > 6 ? '#0000ff' : value > 2 ? '#ff0000' : '#00ff00'; }, borderWidth: 8 } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderColor/value.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderColor: '#ff0000' }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#00ff00', borderWidth: 8 } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderDash/scriptable.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [5, 2, 4, 7, 6, 8] } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: 'black', borderWidth: 1, borderDash: function(ctx) { var value = (ctx.dataIndex || 0) % 2; return value === 0 ? [3, 3] : []; } } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderDash/value.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [5, 2, 4, 7, 6, 8], borderAlign: 'inner', borderColor: 'black' }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderWidth: 1, borderDash: [3, 3] } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderJoinStyle/bevel-default.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [0, 2, 4, null, 6, 8], backgroundColor: 'transparent', borderColor: '#000', borderWidth: 10, spacing: 50, }, ] }, options: { } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderJoinStyle/miter.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [0, 2, 4, null, 6, 8], backgroundColor: 'transparent', borderColor: '#000', borderJoinStyle: 'miter', borderWidth: 10, spacing: 50, }, ] }, options: { } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderJoinStyle/round.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [0, 2, 4, null, 6, 8], backgroundColor: 'transparent', borderColor: '#000', borderJoinStyle: 'round', borderWidth: 10, spacing: 50, }, ] }, options: { } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderRadius/scriptable.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderRadius: () => 4, }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderRadius/value-corners.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderRadius: { outerStart: 20, outerEnd: 40, } }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderRadius/value-large-radius.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [60, 15, 33, 44, 12], // Radius is large enough to clip borderRadius: 200, backgroundColor: [ 'rgb(255, 99, 132)', 'rgb(255, 159, 64)', 'rgb(255, 205, 86)', 'rgb(75, 192, 192)', 'rgb(54, 162, 235)' ] }, ] }, // options: { // elements: { // arc: { // backgroundColor: 'transparent', // borderColor: '#888', // } // }, // } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderRadius/value-small-number.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderRadius: 20 }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderWidth/indexable.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderWidth: [ 0, 1, 2, 3, 4, 5 ] }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', borderWidth: [ 5, 4, 3, 2, 1, 0 ] } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderWidth/scriptable.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderWidth: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return Math.abs(value); } }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', borderWidth: function(ctx) { return ctx.dataIndex * 2; } } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/borderWidth/value.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderWidth: 2 }, { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', borderWidth: 4 } }, } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-NaN.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, NaN, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ] }] }, } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-animation-hide-last.js ================================================ const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 512; const ctx = canvas.getContext('2d'); module.exports = { config: { type: 'doughnut', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1], backgroundColor: 'rgba(255, 99, 132, 0.8)', borderWidth: 4, borderColor: 'rgb(255, 99, 132)', }] }, options: { animation: { duration: 0, easing: 'linear', }, responsive: false, plugins: { legend: false, title: false, tooltip: false, filler: false } }, }, options: { canvas: { height: 512, width: 512 }, run: function(chart) { chart.options.animation.duration = 8000; chart.toggleDataVisibility(0); chart.update(); const animator = Chart.animator; // disable animator const backup = animator._refresh; animator._refresh = function() { }; return new Promise((resolve) => { window.requestAnimationFrame(() => { const anims = animator._getAnims(chart); const start = anims.items[0]._start; for (let i = 0; i < 16; i++) { animator._update(start + i * 500); let x = i % 4 * 128; let y = Math.floor(i / 4) * 128; ctx.drawImage(chart.canvas, x, y, 128, 128); } Chart.helpers.clearCanvas(chart.canvas); chart.ctx.drawImage(canvas, 0, 0); animator._refresh = backup; resolve(); }); }); } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-animation.js ================================================ const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 512; const ctx = canvas.getContext('2d'); module.exports = { config: { type: 'doughnut', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, 10, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderWidth: 4, borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ] }] }, options: { animation: { duration: 8000, easing: 'linear' }, responsive: false, plugins: { legend: false, title: false, tooltip: false, filler: false } }, plugins: [{ id: 'hide', afterInit(chart) { chart.toggleDataVisibility(4); } }] }, options: { canvas: { height: 512, width: 512 }, run: function(chart) { const animator = Chart.animator; const anims = animator._getAnims(chart); // disable animator const backup = animator._refresh; animator._refresh = function() { }; return new Promise((resolve) => { window.requestAnimationFrame(() => { const start = anims.items[0]._start; for (let i = 0; i < 16; i++) { animator._update(start + i * 500); let x = i % 4 * 128; let y = Math.floor(i / 4) * 128; ctx.drawImage(chart.canvas, x, y, 128, 128); } Chart.helpers.clearCanvas(chart.canvas); chart.ctx.drawImage(canvas, 0, 0); animator._refresh = backup; resolve(); }); }); } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-border-align-center.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [{ "data": [1, 5, 10, 50, 100], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)", "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)", "rgba(153, 102, 255, 0.8)" ], "borderWidth": 20, "borderColor": [ "rgb(255, 99, 132)", "rgb(54, 162, 235)", "rgb(255, 206, 86)", "rgb(75, 192, 192)", "rgb(153, 102, 255)" ] }] }, "options": { "responsive": false } } } ================================================ FILE: test/fixtures/controller.doughnut/doughnut-border-align-inner.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [{ "data": [1, 5, 10, 50, 100], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)", "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)", "rgba(153, 102, 255, 0.8)" ], "borderWidth": 20, "borderColor": [ "rgb(255, 99, 132)", "rgb(54, 162, 235)", "rgb(255, 206, 86)", "rgb(75, 192, 192)", "rgb(153, 102, 255)" ], "borderAlign": "inner" }] }, "options": { "responsive": false } } } ================================================ FILE: test/fixtures/controller.doughnut/doughnut-circumference-over-2pi.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["A"], "datasets": [{ "data": [100], "backgroundColor": [ "rgba(153, 102, 255, 0.8)" ], "borderWidth": 20, "borderColor": [ "rgb(153, 102, 255)" ] }] }, "options": { "circumference": 400, "responsive": false } } } ================================================ FILE: test/fixtures/controller.doughnut/doughnut-circumference-per-dataset.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, 10, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderWidth: 1, borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ], circumference: 180 }] }, options: { circumference: 57.32, responsive: false } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-circumference.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [{ "data": [1, 5, 10, 50, 100], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)", "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)", "rgba(153, 102, 255, 0.8)" ], "borderWidth": 20, "borderColor": [ "rgb(255, 99, 132)", "rgb(54, 162, 235)", "rgb(255, 206, 86)", "rgb(75, 192, 192)", "rgb(153, 102, 255)" ] }] }, "options": { "circumference": 57.32, "responsive": false } } } ================================================ FILE: test/fixtures/controller.doughnut/doughnut-full-to-semi.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9832', config: { type: 'doughnut', data: { datasets: [{ label: 'Set 1', data: [50, 50, 25], backgroundColor: ['#BF616A', '#D08770', '#EBCB8B'], borderWidth: 0 }, { label: 'Se1 2', data: [50, 50, 25], backgroundColor: ['#BF616A', '#D08770', '#EBCB8B'], borderWidth: 0 }] }, options: { rotation: -90 } }, options: { canvas: { width: 512, height: 512 }, run(chart) { chart.options.circumference = 180; chart.update(); } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-hidden-single.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, 10, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], }, { data: [1, 5, 10, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], }] }, options: { responsive: false, plugins: { legend: false, title: false, tooltip: false, filler: false } }, }, options: { run(chart) { chart.hide(0, 4); chart.hide(1, 2); } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-hidden.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, 10, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderWidth: 4, borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ] }] }, options: { responsive: false, plugins: { legend: false, title: false, tooltip: false, filler: false } }, plugins: [{ id: 'hide', afterInit(chart) { chart.toggleDataVisibility(4); } }] } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-offset.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['Red', 'Blue', 'Yellow'], datasets: [{ data: [12, 4, 6], backgroundColor: ['red', 'blue', 'yellow'] }] }, options: { offset: 40, layout: { padding: 50 } } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-outer-radius-percent.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, 10, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ] }] }, options: { radius: '30%', } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-outer-radius-pixels.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, 10, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ] }] }, options: { radius: 150, } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-parsing.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['Red', 'Blue', 'Yellow'], datasets: [{ data: [ {foo: 12}, {foo: 4}, {foo: 6}, ], backgroundColor: ['red', 'blue', 'yellow'] }] }, options: { parsing: { key: 'foo' } } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-rotation-300.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, 10, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ] }] }, options: { rotation: 300 } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-rotation-circumference-8x8.js ================================================ const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 512; const ctx = canvas.getContext('2d'); module.exports = { config: { type: 'doughnut', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, 10, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ] }] }, options: { rotation: -360, circumference: 180, events: [] } }, options: { canvas: { height: 512, width: 512 }, run: function(chart) { return new Promise((resolve) => { for (let i = 0; i < 64; i++) { const col = i % 8; const row = Math.floor(i / 8); const evenodd = row % 2 ? 1 : -1; chart.options.rotation = col * 45 * evenodd; chart.options.circumference = 360 - row * 45; chart.update(); ctx.drawImage(chart.canvas, col * 64, row * 64, 64, 64); } ctx.strokeStyle = 'red'; ctx.lineWidth = 0.5; ctx.beginPath(); for (let i = 1; i < 8; i++) { ctx.moveTo(i * 64, 0); ctx.lineTo(i * 64, 511); ctx.moveTo(0, i * 64); ctx.lineTo(511, i * 64); } ctx.stroke(); Chart.helpers.clearCanvas(chart.canvas); chart.ctx.drawImage(canvas, 0, 0); resolve(); }); } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-rotation-per-dataset.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, 10, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderWidth: 1, borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ], rotation: -90 }, { data: [1, 5, 10, 50, 100], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderWidth: 1, borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ], rotation: 0 }] }, options: { circumference: 180, responsive: false } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-set-active-elements.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9248', config: { type: 'doughnut', data: { datasets: [ { data: [34, 33, 17, 16], backgroundColor: ['#D92323', '#E45757', '#ED8D8D', '#F5C4C4'] } ] }, options: { events: [], // for easier saving of the fixture only borderWidth: 0, hoverBorderWidth: 4, hoverBorderColor: 'black', cutout: '80%', } }, options: { run(chart) { chart.setActiveElements([{datasetIndex: 0, index: 1}]); chart.update(); } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-spacing-and-offset.js ================================================ module.exports = { config: { type: 'doughnut', data: { datasets: [{ data: [10, 20, 40, 50, 5], label: 'Dataset 1', backgroundColor: [ 'red', 'orange', 'yellow', 'green', 'blue' ] }], labels: [ 'Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5' ], }, options: { spacing: 50, offset: [0, 50, 0, 0, 0], } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-spacing.js ================================================ module.exports = { config: { type: 'doughnut', data: { datasets: [{ data: [10, 20, 40, 50, 5], label: 'Dataset 1', backgroundColor: [ 'red', 'orange', 'yellow', 'green', 'blue' ] }], labels: [ 'Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5' ], }, options: { spacing: 50, } } }; ================================================ FILE: test/fixtures/controller.doughnut/doughnut-weight.json ================================================ { "config": { "type": "doughnut", "data": { "datasets": [{ "data": [ 1, 1 ], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)" ], "borderWidth": 0 }, { "data": [ 2, 1 ], "hidden": true, "borderWidth": 0 }, { "data": [ 3, 3 ], "weight": 3, "backgroundColor": [ "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)" ], "borderWidth": 0 }, { "data": [ 4, 0 ], "weight": 0, "borderWidth": 0 }, { "data": [ 5, 0 ], "weight": -2, "borderWidth": 0 }], "labels": [ "label0", "label1" ] } }, "options": { "canvas": { "height": 500, "width": 500 } } } ================================================ FILE: test/fixtures/controller.doughnut/event-replay.js ================================================ function drawMousePoint(ctx, center) { ctx.beginPath(); ctx.arc(center.x, center.y, 8, 0, Math.PI * 2); ctx.fillStyle = 'yellow'; ctx.fill(); } const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 512; const ctx = canvas.getContext('2d'); module.exports = { config: { type: 'pie', data: { datasets: [{ backgroundColor: ['red', 'green', 'blue'], hoverBackgroundColor: 'black', data: [1, 1, 1] }] } }, options: { canvas: { width: 512, height: 512 }, async run(chart) { ctx.drawImage(chart.canvas, 0, 0, 256, 256); const arc = chart.getDatasetMeta(0).data[0]; const center = arc.getCenterPoint(); await jasmine.triggerMouseEvent(chart, 'mousemove', arc); drawMousePoint(chart.ctx, center); ctx.drawImage(chart.canvas, 256, 0, 256, 256); chart.toggleDataVisibility(0); chart.update(); drawMousePoint(chart.ctx, center); ctx.drawImage(chart.canvas, 0, 256, 256, 256); await jasmine.triggerMouseEvent(chart, 'mouseout', arc); ctx.drawImage(chart.canvas, 256, 256, 256, 256); Chart.helpers.clearCanvas(chart.canvas); chart.ctx.drawImage(canvas, 0, 0); } } }; ================================================ FILE: test/fixtures/controller.doughnut/pie-border-align-center.json ================================================ { "config": { "type": "pie", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [{ "data": [1, 5, 10, 50, 100], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)", "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)", "rgba(153, 102, 255, 0.8)" ], "borderWidth": 20, "borderColor": [ "rgb(255, 99, 132)", "rgb(54, 162, 235)", "rgb(255, 206, 86)", "rgb(75, 192, 192)", "rgb(153, 102, 255)" ] }] }, "options": { "responsive": false } } } ================================================ FILE: test/fixtures/controller.doughnut/pie-border-align-inner.json ================================================ { "config": { "type": "pie", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [{ "data": [1, 5, 10, 50, 100], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)", "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)", "rgba(153, 102, 255, 0.8)" ], "borderWidth": 20, "borderColor": [ "rgb(255, 99, 132)", "rgb(54, 162, 235)", "rgb(255, 206, 86)", "rgb(75, 192, 192)", "rgb(153, 102, 255)" ], "borderAlign": "inner" }] }, "options": { "responsive": false, "plugins": { "legend": false, "title": false, "tooltip": false } } } } ================================================ FILE: test/fixtures/controller.doughnut/pie-circumference.json ================================================ { "config": { "type": "pie", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [{ "data": [1, 5, 10, 50, 100], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)", "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)", "rgba(153, 102, 255, 0.8)" ], "borderWidth": 20, "borderColor": [ "rgb(255, 99, 132)", "rgb(54, 162, 235)", "rgb(255, 206, 86)", "rgb(75, 192, 192)", "rgb(153, 102, 255)" ] }] }, "options": { "circumference": 57.32, "responsive": false } } } ================================================ FILE: test/fixtures/controller.doughnut/pie-offset.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['Red', 'Blue', 'Yellow'], datasets: [{ data: [12, 4, 6], backgroundColor: ['red', 'blue', 'yellow'] }] }, options: { offset: 40, layout: { padding: 50 } } } }; ================================================ FILE: test/fixtures/controller.doughnut/pie-weight.json ================================================ { "config": { "type": "pie", "data": { "datasets": [ { "data": [ 1, 1 ], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)" ], "borderWidth": 0 }, { "data": [ 2, 1 ], "hidden": true, "borderWidth": 0 }, { "data": [ 3, 3 ], "weight": 3, "backgroundColor": [ "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)" ], "borderWidth": 0 }, { "data": [ 4, 0 ], "weight": 0, "borderWidth": 0 }, { "data": [ 5, 0 ], "weight": -2, "borderWidth": 0 } ], "labels": [ "label0", "label1" ] } }, "options": { "canvas": { "height": 500, "width": 500 } } } ================================================ FILE: test/fixtures/controller.doughnut/selfJoin/doughnut.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['Red'], datasets: [ { // option in dataset data: [100], borderWidth: 15, backgroundColor: '#FF0000', borderColor: '#000000', borderAlign: 'center', selfJoin: true } ] } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/selfJoin/pie.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['Red'], datasets: [ { // option in dataset data: [100], borderWidth: 15, backgroundColor: '#FF0000', borderColor: '#000000', borderAlign: 'center', borderJoinStyle: 'round', selfJoin: true } ] } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.doughnut/single-slice-circumference-405.js ================================================ module.exports = { threshold: 0.05, config: { type: 'doughnut', data: { labels: ['A'], datasets: [{ data: [1], backgroundColor: 'rgba(0,0,0,0.3)', borderColor: 'rgba(0,0,0,0.5)', circumference: 405 }] }, }, options: { canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/controller.doughnut/single-slice-offset.js ================================================ module.exports = { config: { type: 'doughnut', data: { labels: ['A'], datasets: [{ data: [385], backgroundColor: 'rgba(0,0,0,0.3)', borderColor: 'rgba(0,0,0,0.5)', }] }, options: { offset: 20 } } }; ================================================ FILE: test/fixtures/controller.doughnut/single-slice-opacity.js ================================================ module.exports = { threshold: 0.05, config: { type: 'doughnut', data: { labels: ['A'], datasets: [{ data: [1], backgroundColor: 'rgba(0,0,0,0.3)', borderColor: 'rgba(0,0,0,0.5)' }] }, }, options: { canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/controller.line/backgroundColor/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [4, 5, 10, null, -10, -5], backgroundColor: function(ctx) { var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#ff00ff'; } }, { // option in element (fallback) data: [-4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: true, backgroundColor: function(ctx) { var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#ff00ff'; } }, point: { backgroundColor: '#0000ff', radius: 10 } }, layout: { padding: 32 }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/backgroundColor/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: '#ff0000' }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: true, backgroundColor: '#00ff00' }, point: { radius: 10 } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderCapStyle/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3], datasets: [ { // option in dataset data: [null, 3, 3], borderCapStyle: function(ctx) { var index = (ctx.datasetIndex % 2); return index === 0 ? 'round' : index === 1 ? 'square' : 'butt'; } }, { // option in element (fallback) data: [null, 2, 2], }, { // option in element (fallback) data: [null, 1, 1], } ] }, options: { elements: { line: { borderCapStyle: function(ctx) { var index = (ctx.datasetIndex % 3); return index === 0 ? 'round' : index === 1 ? 'square' : 'butt'; }, borderColor: '#ff0000', borderWidth: 32, fill: false }, point: { radius: 10, } }, layout: { padding: 32 }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderCapStyle/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3], datasets: [ { // option in dataset data: [null, 3, 3], borderCapStyle: 'round', }, { // option in dataset data: [null, 2, 2], borderCapStyle: 'square', }, { // option in element (fallback) data: [null, 1, 1], } ] }, options: { elements: { line: { borderCapStyle: 'butt', borderColor: '#00ff00', borderWidth: 32, fill: false, }, point: { radius: 10, } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderColor/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [4, 5, 10, null, -10, -5], borderColor: function(ctx) { var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#0000ff'; } }, { // option in element (fallback) data: [-4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { borderColor: function(ctx) { var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#0000ff'; }, borderWidth: 10, fill: false }, point: { borderColor: '#ff0000', borderWidth: 10, radius: 16 } }, layout: { padding: 32 }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderColor/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: '#ff0000' }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { borderColor: '#0000ff', fill: false, }, point: { borderColor: '#0000ff', radius: 10, } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderDash/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [4, 5, 10, null, -10, -5], borderDash: function(ctx) { return ctx.datasetIndex === 0 ? [5] : [10]; } }, { // option in element (fallback) data: [-4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderDash: function(ctx) { return ctx.datasetIndex === 0 ? [5] : [10]; } }, point: { radius: 10, } }, layout: { padding: 32 }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderDash/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: '#ff0000', borderDash: [5] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { borderColor: '#00ff00', borderDash: [10], fill: false, }, point: { radius: 10, } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderDashOffset/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3], datasets: [ { // option in dataset data: [1, 1, 1, 1], borderColor: '#ff0000', borderDash: [20], borderDashOffset: function(ctx) { return ctx.datasetIndex === 0 ? 5.0 : 0.0; } }, { // option in element (fallback) data: [0, 0, 0, 0] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderDash: [20], borderDashOffset: function(ctx) { return ctx.datasetIndex === 0 ? 5.0 : 0.0; }, fill: false, }, point: { radius: 10, } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderDashOffset/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [1, 1, 1, 1, 1, 1], borderColor: '#ff0000', borderDash: [20], borderDashOffset: 5.0 }, { // option in element (fallback) data: [0, 0, 0, 0, 0, 0] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderDash: [20], borderDashOffset: 0.0, // default fill: false, }, point: { radius: 10, } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderJoinStyle/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2], datasets: [ { // option in dataset data: [6, 18, 6], borderColor: '#ff0000', borderJoinStyle: function(ctx) { var index = ctx.datasetIndex % 3; return index === 0 ? 'round' : index === 1 ? 'miter' : 'bevel'; } }, { // option in element (fallback) data: [2, 14, 2], borderColor: '#0000ff', }, { // option in element (fallback) data: [-2, 10, -2] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderJoinStyle: function(ctx) { var index = (ctx.datasetIndex % 3); return index === 0 ? 'round' : index === 1 ? 'miter' : 'bevel'; }, borderWidth: 25, fill: false, tension: 0 } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderJoinStyle/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2], datasets: [ { // option in dataset data: [6, 18, 6], borderColor: '#ff0000', borderJoinStyle: 'round', }, { // option in element (fallback) data: [2, 14, 2], borderColor: '#0000ff', borderJoinStyle: 'bevel', }, { // option in element (fallback) data: [-2, 10, -2] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderJoinStyle: 'miter', borderWidth: 25, fill: false, tension: 0 } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderWidth/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [4, 5, 10, null, -10, -5], borderColor: '#0000ff', borderWidth: function(ctx) { var index = ctx.index; return index % 2 ? 10 : 20; }, pointBorderColor: '#00ff00' }, { // option in element (fallback) data: [-4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { borderColor: '#ff0000', borderWidth: function(ctx) { var index = ctx.index; return index % 2 ? 10 : 20; }, fill: false, }, point: { borderColor: '#00ff00', borderWidth: 5, radius: 10 } }, layout: { padding: 32 }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderWidth/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: '#0000ff', borderWidth: 6 }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { borderColor: '#00ff00', borderWidth: 3, fill: false, }, point: { radius: 10, } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/borderWidth/zero.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: '#0000ff', borderColor: '#0000ff', borderWidth: 0 }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { borderColor: '#00ff00', borderWidth: 3, fill: false, }, point: { backgroundColor: '#00ff00', radius: 10, } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/clip/default-x-max.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [{ "borderColor": "red", "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], "fill": false, "showLine": true, "borderWidth": 20, "pointRadius": 0 }] }, "options": { "responsive": false, "scales": { "x": { "max": 3, "ticks": { "display": false } }, "y": {"ticks": {"display": false}} }, "layout": { "padding": 24 } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.line/clip/default-x-min.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [{ "borderColor": "red", "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], "fill": false, "showLine": true, "borderWidth": 20, "pointRadius": 0 }] }, "options": { "responsive": false, "scales": { "x": { "min": -2, "ticks": { "display": false } }, "y": {"ticks": {"display": false}} }, "layout": { "padding": 24 } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.line/clip/default-x.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [{ "borderColor": "red", "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], "fill": false, "showLine": true, "borderWidth": 20, "pointRadius": 0 }] }, "options": { "responsive": false, "scales": { "x": { "min": -2, "max": 3, "ticks": { "display": false } }, "y": {"ticks": {"display": false}} }, "layout": { "padding": 24 } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.line/clip/default-y-max.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [{ "borderColor": "red", "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], "fill": false, "showLine": true, "borderWidth": 20, "pointRadius": 0 }] }, "options": { "responsive": false, "scales": { "x": {"ticks": {"display": false}}, "y": { "max": 6, "ticks": { "display": false } } }, "layout": { "padding": 24 } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.line/clip/default-y-min.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [{ "borderColor": "red", "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], "fill": false, "showLine": true, "borderWidth": 20, "pointRadius": 0 }] }, "options": { "responsive": false, "scales": { "x": {"ticks": {"display": false}}, "y": { "min": 2, "ticks": { "display": false } } }, "layout": { "padding": 24 } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.line/clip/default-y.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [{ "borderColor": "red", "data": [{"x":-5,"y":5},{"x":-4,"y":6},{"x":-3,"y":7},{"x":-2,"y":6},{"x":-1,"y":5},{"x":0,"y":4},{"x":1,"y":3},{"x":2,"y":2},{"x":3,"y":5},{"x":4,"y":7},{"x":5,"y":9}], "fill": false, "showLine": true, "borderWidth": 20, "pointRadius": 0 }] }, "options": { "responsive": false, "scales": { "x": {"ticks": {"display": false}}, "y": { "min": 2, "max": 6, "ticks": { "display": false } } }, "layout": { "padding": 24 } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.line/clip/false.js ================================================ const data = []; for (let x = 0.95; x < 1.15; x += 0.002) { data.push({x, y: x}); } for (let x = 0.95; x < 1.15; x += 0.001) { data.push({x, y: 2.1 - x}); } module.exports = { config: { type: 'scatter', data: { datasets: [{ clip: false, radius: 8, borderWidth: 0, backgroundColor: (ctx) => ctx.type !== 'data' || ctx.raw.x < 1 || ctx.raw.x > 1.1 ? 'rgba(255,0,0,0.7)' : 'rgba(0,0,255,0.05)', data }] }, options: { plugins: false, scales: { x: { min: 1, max: 1.1 }, y: { min: 1, max: 1.1 }, }, layout: { padding: 32 } } }, options: { spriteText: true, canvas: { height: 240, width: 320 } } }; ================================================ FILE: test/fixtures/controller.line/clip/specified.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [ { "showLine": true, "borderColor": "red", "data": [{"x":-4,"y":-4},{"x":4,"y":4}], "clip": false }, { "showLine": true, "borderColor": "green", "data": [{"x":-4,"y":-5},{"x":4,"y":3}], "clip": 5 }, { "showLine": true, "borderColor": "blue", "data": [{"x":-4,"y":-3},{"x":4,"y":5}], "clip": -5 }, { "showLine": true, "borderColor": "brown", "data": [{"x":-3,"y":-3},{"x":-1,"y":3},{"x":1,"y":-2},{"x":2,"y":3}], "clip": { "top": 8, "left": false, "right": -20, "bottom": -20 } } ] }, "options": { "responsive": false, "scales": { "x": { "min": -2, "max": 2, "ticks": { "display": false } }, "y": { "min": -2, "max": 2, "ticks": { "display": false } } }, "layout": { "padding": 24 }, "elements": { "line": { "fill": false, "borderWidth": 20 }, "point": { "radius": 0 } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.line/cubicInterpolationMode/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 4, 2, 6, 4, 8], borderColor: '#ff0000', cubicInterpolationMode: function(ctx) { return ctx.datasetIndex === 0 ? 'monotone' : 'default'; } }, { // option in element (fallback) data: [2, 6, 4, 8, 6, 10], } ] }, options: { elements: { line: { borderColor: '#00ff00', borderWidth: 20, cubicInterpolationMode: function(ctx) { return ctx.datasetIndex === 0 ? 'monotone' : 'default'; }, fill: false, tension: 0.4 } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/cubicInterpolationMode/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 4, 2, 6, 4, 8], borderColor: '#ff0000', cubicInterpolationMode: 'monotone' }, { // option in element (fallback) data: [2, 6, 4, 8, 6, 10] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderWidth: 20, cubicInterpolationMode: 'default', fill: false, tension: 0.4 } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/fill/no-border.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [12, 19, 3, 5, 2, 3], backgroundColor: '#ff0000', borderWidth: 0, tension: 0.4, fill: true }, ] }, options: { animation: { duration: 1 }, scales: { x: {display: false}, y: {display: false} }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 256, width: 512 }, run() { return new Promise(resolve => setTimeout(resolve, 50)); } } }; ================================================ FILE: test/fixtures/controller.line/fill/order-default.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [3, 1, 2, 0, 8, 1], backgroundColor: '#ff0000' }, { // option in element (fallback) data: [0, 4, 2, 6, 4, 8], backgroundColor: '#00ff00' } ] }, options: { elements: { line: { fill: true }, point: { radius: 0 } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/fill/order.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [3, 1, 2, 0, 8, 1], backgroundColor: '#ff0000', order: 2 }, { data: [0, 4, 2, 6, 4, 8], backgroundColor: '#00ff00', order: 1 } ] }, options: { elements: { line: { fill: true }, point: { radius: 0 } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/fill/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [-2, -6, -4, -8, -6, -10], backgroundColor: '#ff0000', fill: function(ctx) { return ctx.datasetIndex === 0 ? true : false; } }, { // option in element (fallback) data: [0, 4, 2, 6, 4, 8], } ] }, options: { elements: { line: { backgroundColor: '#00ff00', fill: function(ctx) { return ctx.datasetIndex === 0 ? true : false; } } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/fill/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [-2, -6, -4, -8, -6, -10], backgroundColor: '#ff0000', fill: false }, { // option in element (fallback) data: [0, 4, 2, 6, 4, 8], } ] }, options: { elements: { line: { backgroundColor: '#00ff00', fill: true, } }, layout: { padding: 32 }, scales: { x: {display: false}, y: {display: false} }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/issue-8902.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8902', config: { type: 'line', data: { labels: [1, 2, 3, 4, 5, 6, 7, 8], datasets: [{ data: [65, 59, NaN, 48, 56, 57, 40], borderColor: 'rgb(75, 192, 192)', }] }, options: { plugins: false, scales: { x: { type: 'linear', min: 1, max: 3 } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/non-numeric-y.json ================================================ { "config": { "type": "line", "data": { "xLabels": ["January", "February", "March", "April", "May", "June", "July"], "yLabels": ["", "Request Added", "Request Viewed", "Request Accepted", "Request Solved", "Solving Confirmed"], "datasets": [{ "label": "My First dataset", "data": ["", "Request Added", "Request Added", "Request Added", "Request Viewed", "Request Viewed", "Request Viewed"], "fill": false, "borderColor": "red", "backgroundColor": "red" }] }, "options": { "responsive": false, "scales": { "x": {"display": false}, "y": { "type": "category", "display": false } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.line/point-style-offscreen-canvas.json ================================================ { "config": { "type": "line", "data": { "labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "datasets": [{ "borderColor": "transparent", "data": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], "pointBackgroundColor": "#00ff00", "pointBorderColor": "transparent", "pointBorderWidth": 0, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] }, { "borderColor": "transparent", "data": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], "pointBackgroundColor": "transparent", "pointBorderColor": "#0000ff", "pointBorderWidth": 1, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] }, { "borderColor": "transparent", "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "pointBackgroundColor": "#00ff00", "pointBorderColor": "#0000ff", "pointBorderWidth": 1, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] }] }, "options": { "responsive": false, "scales": { "x": {"display": false}, "y": { "display": false, "min": 0, "max": 4 } }, "elements": { "line": { "fill": false }, "point": { "radius": 16 } }, "layout": { "padding": { "left": 24, "right": 24 } } } }, "options": { "canvas": { "height": 256, "width": 512 }, "useOffscreenCanvas": true } } ================================================ FILE: test/fixtures/controller.line/point-style.json ================================================ { "config": { "type": "line", "data": { "labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "datasets": [{ "borderColor": "transparent", "data": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], "pointBackgroundColor": "#00ff00", "pointBorderColor": "transparent", "pointBorderWidth": 0, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] }, { "borderColor": "transparent", "data": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], "pointBackgroundColor": "transparent", "pointBorderColor": "#0000ff", "pointBorderWidth": 1, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] }, { "borderColor": "transparent", "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "pointBackgroundColor": "#00ff00", "pointBorderColor": "#0000ff", "pointBorderWidth": 1, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] }] }, "options": { "responsive": false, "scales": { "x": {"display": false}, "y": { "display": false, "min": 0, "max": 4 } }, "elements": { "line": { "fill": false }, "point": { "radius": 16 } }, "layout": { "padding": { "left": 24, "right": 24 } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/controller.line/pointBackgroundColor/indexable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: [ '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#000000' ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: [ '#ff88ff', '#888888', '#ff8800', '#00ff88', '#8800ff', '#ffff88' ], radius: 10 } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/pointBackgroundColor/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 0 ? '#00ff00' : value > -8 ? '#0000ff' : '#ff00ff'; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff00ff' : value > 0 ? '#0000ff' : value > -8 ? '#ff0000' : '#00ff00'; }, radius: 10, } }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/pointBackgroundColor/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: '#ff0000' }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#00ff00', radius: 10, } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/pointBorderColor/indexable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: [ '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#000000' ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: [ '#ff88ff', '#888888', '#ff8800', '#00ff88', '#8800ff', '#ffff88' ], radius: 10 } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/pointBorderColor/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 0 ? '#00ff00' : value > -8 ? '#0000ff' : '#ff00ff'; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff00ff' : value > 0 ? '#0000ff' : value > -8 ? '#ff0000' : '#00ff00'; }, radius: 10, } }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/pointBorderColor/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#ff0000' }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: '#00ff00', radius: 10, } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/pointBorderWidth/indexable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#00ff00', pointBorderWidth: [ 1, 2, 3, 4, 5, 6 ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: '#ff0000', borderWidth: [ 6, 5, 4, 3, 2, 1 ], radius: 10 } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/pointBorderWidth/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#0000ff', pointBorderWidth: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 10 : value > -4 ? 5 : 2; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: '#ff0000', borderWidth: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 2 : value > -4 ? 5 : 10; }, radius: 10, } }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/pointBorderWidth/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#0000ff', pointBorderWidth: 6 }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: '#00ff00', borderWidth: 3, radius: 10, } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/pointStyle/indexable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5, 6], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5, 0], pointBackgroundColor: '#ff0000', pointBorderColor: '#ff0000', pointStyle: [ 'circle', 'cross', 'crossRot', 'dash', 'line', 'rect', false ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5, -4], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#00ff00', borderColor: '#00ff00', pointStyle: [ 'line', 'rect', 'rectRounded', 'rectRot', 'star', 'triangle' ], radius: 10 } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/pointStyle/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: '#ff0000', pointBorderColor: '#ff0000', pointStyle: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? 'rect' : value > 0 ? 'star' : value > -8 ? 'cross' : 'triangle'; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#0000ff', borderColor: '#0000ff', pointStyle: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? 'triangle' : value > 0 ? 'cross' : value > -8 ? 'star' : 'rect'; }, radius: 10, } }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/pointStyle/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#ff0000', pointStyle: 'star', }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#00ff00', pointStyle: 'rect', radius: 10, } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/radius/indexable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: '#00ff00', pointRadius: [ 1, 2, 3, 4, 5, 6 ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#ff0000', radius: [ 6, 5, 4, 3, 2, 1 ], } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/radius/scriptable-to-value.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['A', 'B', 'C'], datasets: [{ data: [12, 19, 3] }] }, options: { animation: { duration: 0 }, backgroundColor: 'red', radius: () => 20, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 }, run: (chart) => { chart.options.radius = 5; chart.update(); } } }; ================================================ FILE: test/fixtures/controller.line/radius/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: '#0000ff', pointRadius: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 10 : value > -4 ? 5 : 2; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#ff0000', radius: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 2 : value > -4 ? 5 : 10; }, } }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/radius/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: '#0000ff', pointRadius: 6 }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#00ff00', radius: 3, } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/rotation/indexable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#00ff00', pointRotation: [ 0, 30, 60, 90, 120, 150 ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: '#ff0000', borderWidth: 10, pointStyle: 'line', rotation: [ 150, 120, 90, 60, 30, 0 ], } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/rotation/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#0000ff', pointRotation: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 120 : value > -4 ? 60 : 0; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: '#ff0000', rotation: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 0 : value > -4 ? 60 : 120; }, pointStyle: 'line', radius: 10, } }, scales: { x: {display: false}, y: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/rotation/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#0000ff', pointRotation: 90 }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: '#00ff00', pointStyle: 'line', radius: 10, rotation: 0, } }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/segments/gap.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['a', 'b', 'c', 'd', 'e', 'f'], datasets: [{ data: [1, 3, NaN, NaN, 2, 1], borderColor: 'black', segment: { borderColor: ctx => ctx.p0.skip || ctx.p1.skip ? 'red' : undefined, borderDash: ctx => ctx.p0.skip || ctx.p1.skip ? [5, 5] : undefined }, spanGaps: true }] }, options: { scales: { x: {display: false}, y: {display: false} } } } }; ================================================ FILE: test/fixtures/controller.line/segments/gradient.js ================================================ const getGradient = (context) => { const {chart, p0, p1} = context; const ctx = chart.ctx; const {x: x0} = p0.getProps(['x'], true); const {x: x1} = p1.getProps(['x'], true); const gradient = ctx.createLinearGradient(x0, 0, x1, 0); gradient.addColorStop(0, p0.options.backgroundColor); gradient.addColorStop(1, p1.options.backgroundColor); return gradient; }; module.exports = { config: { type: 'line', data: { datasets: [{ data: [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}, {x: 4, y: 4}, {x: 5, y: 5}, {x: 6, y: 6}], pointBackgroundColor: ['red', 'yellow', 'green', 'green', 'blue', 'pink', 'blue'], pointBorderWidth: 0, pointRadius: 10, borderWidth: 5, segment: { borderColor: getGradient, } }] }, options: { scales: { x: {type: 'linear', display: false}, y: {display: false} } } } }; ================================================ FILE: test/fixtures/controller.line/segments/range.js ================================================ function x(ctx, {min = -Infinity, max = Infinity}) { return (ctx.p0.parsed.x >= min || ctx.p1.parsed.x >= min) && (ctx.p0.parsed.x < max && ctx.p1.parsed.x < max); } function y(ctx, {min = -Infinity, max = Infinity}) { return (ctx.p0.parsed.y >= min || ctx.p1.parsed.y >= min) && (ctx.p0.parsed.y < max || ctx.p1.parsed.y < max); } function xy(ctx, xr, yr) { return x(ctx, xr) && y(ctx, yr); } module.exports = { config: { type: 'line', data: { datasets: [{ data: [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}, {x: 4, y: 4}, {x: 5, y: 5}, {x: 6, y: 7}, {x: 7, y: 8}], borderColor: 'black', segment: { borderColor: ctx => x(ctx, {min: 3, max: 4}) ? 'red' : y(ctx, {min: 5}) ? 'green' : xy(ctx, {min: 0}, {max: 1}) ? 'blue' : undefined, borderDash: ctx => x(ctx, {min: 3, max: 4}) || y(ctx, {min: 5}) ? [5, 5] : undefined, } }] }, options: { scales: { x: {type: 'linear', display: false}, y: {display: false} } } } }; ================================================ FILE: test/fixtures/controller.line/segments/single.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['a', 'b', 'c', 'd', 'e', 'f'], datasets: [{ data: [1, 2, 3, 3, 2, 1], borderColor: 'black', segment: { borderColor: 'red', } }] }, options: { scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/controller.line/segments/slope.js ================================================ function slope({p0, p1}) { return (p0.y - p1.y) / (p1.x - p0.x); } module.exports = { config: { type: 'line', data: { labels: ['a', 'b', 'c', 'd', 'e', 'f'], datasets: [{ data: [1, 2, 3, 3, 2, 1], borderColor: 'black', segment: { borderColor: ctx => slope(ctx) > 0 ? 'green' : slope(ctx) < 0 ? 'red' : undefined, borderDash: ctx => slope(ctx) < 0 ? [5, 5] : undefined } }] }, options: { scales: { x: {display: false}, y: {display: false} } } } }; ================================================ FILE: test/fixtures/controller.line/segments/spanGaps.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['a', 'b', 'c', 'd', 'e', 'f'], datasets: [{ data: [1, 3, null, null, 2, 1], segment: { borderColor: ctx => ctx.p1.parsed.x > 2 ? 'red' : undefined, borderDash: ctx => ctx.p1.parsed.x > 3 ? [6, 6] : undefined, }, spanGaps: true }, { data: [0, 2, null, null, 1, 0], segment: { borderColor: ctx => ctx.p1.parsed.x > 2 ? 'red' : undefined, borderDash: ctx => ctx.p1.parsed.x > 3 ? [6, 6] : undefined, }, spanGaps: false }] }, options: { borderColor: 'black', radius: 0, scales: { x: {display: false}, y: {display: false} } } } }; ================================================ FILE: test/fixtures/controller.line/showLine/dataset.js ================================================ module.exports = { description: 'should draw all elements except lines turned off per dataset', config: { type: 'line', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset1', borderColor: 'red', backgroundColor: 'green', showLine: false, fill: false }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { showLine: true, scales: { x: { display: false }, y: { display: false } }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { width: 512, height: 512 } } }; ================================================ FILE: test/fixtures/controller.line/showLine/false.js ================================================ module.exports = { description: 'should draw all elements except lines', config: { type: 'line', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset1', borderColor: 'red', backgroundColor: 'green', fill: true }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { showLine: false, scales: { x: { display: false }, y: { display: false } }, plugins: { legend: false, title: false, tooltip: false, filler: true } } } }; ================================================ FILE: test/fixtures/controller.line/stacking/bounds-data.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['a', 'b'], datasets: [{ borderColor: 'red', data: [50, 75], }, { borderColor: 'blue', data: [25, 50], }] }, options: { scales: { x: { display: false }, y: { stacked: true, bounds: 'data' } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.line/stacking/order-default.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [3, 1, 2, 0, 8, 1], backgroundColor: '#ff0000' }, { // option in element (fallback) data: [0, 4, 2, 6, 4, 8], backgroundColor: '#00ff00' } ] }, options: { elements: { line: { fill: true }, point: { radius: 0 } }, layout: { padding: 32 }, scales: { x: {stacked: true, display: false}, y: {stacked: true, display: false} }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/stacking/order-specified.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [3, 1, 2, 0, 8, 1], backgroundColor: '#ff0000', order: 2 }, { // option in element (fallback) data: [0, 4, 2, 6, 4, 8], backgroundColor: '#00ff00', order: 1 } ] }, options: { elements: { line: { fill: true }, point: { radius: 0 } }, layout: { padding: 32 }, scales: { x: {stacked: true, display: false}, y: {stacked: true, display: false} }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/stacking/single.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2], datasets: [ { data: [0, -1, -1], backgroundColor: '#ff0000', }, { data: [0, 2, 2], backgroundColor: '#00ff00', }, { data: [0, 0, 1], backgroundColor: '#0000ff', } ] }, options: { elements: { line: { fill: '-1', }, point: { radius: 0 } }, layout: { padding: 32 }, plugins: { legend: false, title: false, tooltip: false, filler: true }, scales: { x: {display: false}, y: {display: false, stacked: 'single'} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/stacking/stacked-scatter.js ================================================ module.exports = { config: { type: 'scatter', data: { datasets: [{ label: 'label1', data: [{ x: 0, y: 30 }, { x: 5, y: 35 }, { x: 10, y: 20 }], backgroundColor: '#42A8E4' }, { label: 'label2', data: [{ x: 0, y: 10 }, { x: 5, y: 15 }, { x: 10, y: 15 }], backgroundColor: '#FC3F55' }, { label: 'label3', data: [{ x: 0, y: -15 }, { x: 5, y: -10 }, { x: 10, y: -20 }], backgroundColor: '#FFBE3F' }], }, options: { scales: { x: { display: false, position: 'bottom', }, y: { stacked: true, display: false, position: 'left', }, }, plugins: { legend: false, title: false, tooltip: false, filler: true } }, }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.line/stacking/updates.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9424', config: { type: 'line', data: { labels: [0, 1, 2], datasets: [ { data: [1, 1, 1], stack: 's1', borderColor: '#ff0000', }, { data: [2, 2, 2], stack: 's1', borderColor: '#00ff00', }, { data: [3, 3, 3], stack: 's1', borderColor: '#0000ff', } ] }, options: { borderWidth: 5, scales: { x: {display: false}, y: {display: true, stacked: true} } } }, options: { spriteText: true, run(chart) { chart.data.datasets.splice(1, 0, {data: [1.5, 1.5, 1.5], stack: 's2', borderColor: '#000000'}); chart.update(); } } }; ================================================ FILE: test/fixtures/controller.polarArea/angle-array.json ================================================ { "config": { "type": "polarArea", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [ { "data": [11, 16, 21, 7, 10], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)", "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)", "rgba(153, 102, 255, 0.8)", "rgba(255, 159, 64, 0.8)" ] } ] }, "options": { "elements": { "arc": { "angle": [60.5387, 100.6457, 60.5387, 123.5641, 14.7021] } }, "responsive": false, "scales": { "r": { "display": false } } } } } ================================================ FILE: test/fixtures/controller.polarArea/angle-lines.json ================================================ { "threshold": 0.05, "config": { "type": "polarArea", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [ { "data": [11, 16, 21, 7, 10], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)", "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)", "rgba(153, 102, 255, 0.8)", "rgba(255, 159, 64, 0.8)" ] } ] }, "options": { "responsive": false, "scales": { "r": { "display": true, "angleLines": { "display": true, "color": "#000" }, "ticks": { "display": false } } } } } } ================================================ FILE: test/fixtures/controller.polarArea/angle-undefined.json ================================================ { "config": { "type": "polarArea", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [ { "data": [11, 16, 21, 7, 10], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)", "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)", "rgba(153, 102, 255, 0.8)", "rgba(255, 159, 64, 0.8)" ] } ] }, "options": { "responsive": false, "scales": { "r": { "display": false } } } } } ================================================ FILE: test/fixtures/controller.polarArea/backgroundColor/indexable-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], backgroundColor: [ '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#000000' ] }, ] }, options: { scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/backgroundColor/indexable-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: [ '#ff88ff', '#888888', '#ff8800', '#00ff88', '#8800ff', '#ffff88' ] } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/backgroundColor/scriptable-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], backgroundColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 6 ? '#00ff00' : value > 2 ? '#0000ff' : '#ff00ff'; } }, ] }, options: { scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/backgroundColor/scriptable-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 6 ? '#00ff00' : value > 2 ? '#0000ff' : '#ff00ff'; } } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/backgroundColor/value-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], backgroundColor: '#ff0000' }, ] }, options: { scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/backgroundColor/value-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: '#00ff00' } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/border-align-center.json ================================================ { "config": { "type": "polarArea", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [ { "data": [11, 16, 21, 1, 10], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)", "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)", "rgba(153, 102, 255, 0.8)" ], "borderWidth": 20, "borderColor": [ "rgb(255, 99, 132)", "rgb(54, 162, 235)", "rgb(255, 206, 86)", "rgb(75, 192, 192)", "rgb(153, 102, 255)" ] } ] }, "options": { "elements": { "arc": { "angle": [2.1658, 10.8404, 21.6922, 108.4323, 216.8588] } }, "responsive": false, "scales": { "r": { "display": false } } } } } ================================================ FILE: test/fixtures/controller.polarArea/border-align-inner.json ================================================ { "config": { "type": "polarArea", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [ { "data": [11, 16, 21, 1, 10], "backgroundColor": [ "rgba(255, 99, 132, 0.8)", "rgba(54, 162, 235, 0.8)", "rgba(255, 206, 86, 0.8)", "rgba(75, 192, 192, 0.8)", "rgba(153, 102, 255, 0.8)" ], "borderWidth": 20, "borderColor": [ "rgb(255, 99, 132)", "rgb(54, 162, 235)", "rgb(255, 206, 86)", "rgb(75, 192, 192)", "rgb(153, 102, 255)" ], "borderAlign": "inner" } ] }, "options": { "elements": { "arc": { "angle": [2.1658, 10.8404, 21.6922, 108.4323, 216.8588] } }, "responsive": false, "scales": { "r": { "display": false } } } } } ================================================ FILE: test/fixtures/controller.polarArea/borderAlign/indexable-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderAlign: [ 'center', 'inner', 'center', 'inner', 'center', 'inner', ], borderColor: '#00ff00' }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#ff0000', borderWidth: 5, } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderAlign/indexable-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#ff0000', borderWidth: 5, borderAlign: [ 'center', 'inner', 'center', 'inner', 'center', 'inner', ] } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderAlign/scriptable-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderAlign: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 'inner' : 'center'; }, borderColor: '#0000ff', }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#ff00ff', borderWidth: 8, } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderAlign/scriptable-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#ff00ff', borderWidth: 8, borderAlign: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 'center' : 'inner'; } } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderAlign/value-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderAlign: 'inner', borderColor: '#00ff00', }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#0000ff', borderWidth: 4, } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderAlign/value-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderAlign: 'center', borderColor: '#0000ff', borderWidth: 4, } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderColor/indexable-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderColor: [ '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#000000' ] }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderWidth: 8 } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderColor/indexable-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: [ '#ff88ff', '#888888', '#ff8800', '#00ff88', '#8800ff', '#ffff88' ], borderWidth: 8 } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderColor/scriptable-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 6 ? '#00ff00' : value > 2 ? '#0000ff' : '#ff00ff'; } }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderWidth: 8 } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderColor/scriptable-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff00ff' : value > 6 ? '#0000ff' : value > 2 ? '#ff0000' : '#00ff00'; }, borderWidth: 8 } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderColor/value-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderColor: '#ff0000' }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderWidth: 8 } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderColor/value-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#00ff00', borderWidth: 8 } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderDash/scriptable.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [5, 2, 4, 7, 6, 8] } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: 'black', borderWidth: 1, borderDash: function(ctx) { var value = (ctx.dataIndex || 0) % 2; return value === 0 ? [3, 3] : []; } } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderDash/value.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [5, 2, 4, 7, 6, 8], borderAlign: 'inner', borderColor: 'black' }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderWidth: 1, borderDash: [3, 3] } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderWidth/indexable-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderWidth: [ 0, 1, 2, 3, 4, 5 ] }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderWidth/indexable-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', borderWidth: [ 5, 4, 3, 2, 1, 0 ] } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderWidth/scriptable-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderWidth: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return Math.abs(value); } }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderWidth/scriptable-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', borderWidth: function(ctx) { return ctx.dataIndex * 2; } } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderWidth/value-dataset.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 2, 4, null, 6, 8], borderWidth: 2 }, ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/borderWidth/value-element-options.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in element (fallback) data: [0, 2, 4, null, 6, 8], } ] }, options: { elements: { arc: { backgroundColor: 'transparent', borderColor: '#888', borderWidth: 4 } }, scales: { r: { display: false } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/controller.polarArea/last-slice-animate.js ================================================ const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 512; const ctx = canvas.getContext('2d'); module.exports = { config: { type: 'polarArea', data: { labels: ['A'], datasets: [{ data: [20], backgroundColor: 'red', }] }, options: { animation: { duration: 0, easing: 'linear' }, responsive: false, plugins: { legend: false, title: false, tooltip: false, filler: false }, scales: { r: { ticks: { display: false, } } } }, }, options: { canvas: { height: 512, width: 512 }, run: function(chart) { chart.options.animation.duration = 8000; chart.toggleDataVisibility(0); chart.update(); const animator = Chart.animator; // disable animator const backup = animator._refresh; animator._refresh = function() { }; return new Promise((resolve) => { window.requestAnimationFrame(() => { const anims = animator._getAnims(chart); const start = anims.items[0]._start; for (let i = 0; i < 16; i++) { animator._update(start + i * 500); let x = i % 4 * 128; let y = Math.floor(i / 4) * 128; ctx.drawImage(chart.canvas, x, y, 128, 128); } Chart.helpers.clearCanvas(chart.canvas); chart.ctx.drawImage(canvas, 0, 0); animator._refresh = backup; resolve(); }); }); } } }; ================================================ FILE: test/fixtures/controller.polarArea/parse-object-data.json ================================================ { "config": { "type": "polarArea", "data": { "datasets": [ { "data": [{"id": "Sales", "nested": {"value": 10}}, {"id": "Purchases", "nested": {"value": 20}}], "backgroundColor": ["red", "blue"] } ] }, "options": { "responsive": false, "plugins": { "legend": false }, "parsing": { "key": "nested.value" }, "scales": { "r": { "display": false } } } } } ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/centered-180.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { startAngle: 180, pointLabels: { display: true, centerPointLabels: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/centered-45.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { startAngle: 45, pointLabels: { display: true, centerPointLabels: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/centered.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { pointLabels: { display: true, centerPointLabels: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/default-180.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { startAngle: 180, pointLabels: { display: true, } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/default-45.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { startAngle: 45, pointLabels: { display: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/default.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { pointLabels: { display: true, } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/displayAuto-180.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: new Array(50).fill(5), backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2']) }, options: { scales: { r: { startAngle: 180, pointLabels: { display: 'auto', } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/displayAuto.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: new Array(50).fill(5), backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2']) }, options: { scales: { r: { pointLabels: { display: 'auto', } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/overlapping.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: new Array(50).fill(5), backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: new Array(50).fill(0).map((el, i) => ['label ' + i, 'line 2']) }, options: { scales: { r: { pointLabels: { display: true, } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered-45.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { plugins: { title: { display: true, position: 'bottom', text: 'Chart Title' }, legend: false }, scales: { r: { startAngle: 45, pointLabels: { display: true, centerPointLabels: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/withTitle/bottom-centered.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { plugins: { title: { display: true, position: 'bottom', text: 'Chart Title' }, legend: false }, scales: { r: { pointLabels: { display: true, centerPointLabels: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered-45.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { plugins: { title: { display: true, position: 'left', text: 'Chart Title' }, legend: false }, scales: { r: { startAngle: 45, pointLabels: { display: true, centerPointLabels: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/withTitle/left-centered.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { plugins: { title: { display: true, position: 'left', text: 'Chart Title' }, legend: false }, scales: { r: { pointLabels: { display: true, centerPointLabels: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered-45.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { plugins: { title: { display: true, position: 'right', text: 'Chart Title' }, legend: false }, scales: { r: { startAngle: 45, pointLabels: { display: true, centerPointLabels: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/withTitle/right-centered.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { plugins: { title: { display: true, position: 'right', text: 'Chart Title' }, legend: false }, scales: { r: { pointLabels: { display: true, centerPointLabels: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered-45.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { plugins: { title: { display: true, text: 'Chart Title' }, legend: false }, scales: { r: { startAngle: 45, pointLabels: { display: true, centerPointLabels: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/withTitle/top-centered.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { plugins: { title: { display: true, text: 'Chart Title' }, legend: false }, scales: { r: { pointLabels: { display: true, centerPointLabels: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/withTitle/top-default-45.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { plugins: { title: { display: true, text: 'Chart Title' }, legend: false }, scales: { r: { startAngle: 45, pointLabels: { display: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/pointLabels/withTitle/top-default.js ================================================ module.exports = { config: { type: 'polarArea', data: { datasets: [{ data: [1, 2, 3, 4], backgroundColor: ['#f003', '#0f03', '#00f3', '#0003'] }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { plugins: { title: { display: true, text: 'Chart Title' }, legend: false }, scales: { r: { pointLabels: { display: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.polarArea/polar-area-animation-rotate.js ================================================ const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 512; const ctx = canvas.getContext('2d'); module.exports = { config: { type: 'polarArea', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, 10, 2, 4], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderWidth: 4, borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ] }] }, options: { animation: { animateRotate: true, animateScale: false, duration: 8000, easing: 'linear' }, responsive: false, plugins: { legend: false, title: false, tooltip: false, filler: false }, scales: { r: { ticks: { display: false, } } } }, }, options: { canvas: { height: 512, width: 512 }, run: function(chart) { const animator = Chart.animator; const anims = animator._getAnims(chart); // disable animator const backup = animator._refresh; animator._refresh = function() { }; return new Promise((resolve) => { window.requestAnimationFrame(() => { const start = anims.items[0]._start; for (let i = 0; i < 16; i++) { animator._update(start + i * 500); let x = i % 4 * 128; let y = Math.floor(i / 4) * 128; ctx.drawImage(chart.canvas, x, y, 128, 128); } Chart.helpers.clearCanvas(chart.canvas); chart.ctx.drawImage(canvas, 0, 0); animator._refresh = backup; resolve(); }); }); } } }; ================================================ FILE: test/fixtures/controller.polarArea/polar-area-animation-scale.js ================================================ const canvas = document.createElement('canvas'); canvas.width = 512; canvas.height = 512; const ctx = canvas.getContext('2d'); module.exports = { config: { type: 'polarArea', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 5, 10, 2, 4], backgroundColor: [ 'rgba(255, 99, 132, 0.8)', 'rgba(54, 162, 235, 0.8)', 'rgba(255, 206, 86, 0.8)', 'rgba(75, 192, 192, 0.8)', 'rgba(153, 102, 255, 0.8)' ], borderWidth: 4, borderColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 206, 86)', 'rgb(75, 192, 192)', 'rgb(153, 102, 255)' ] }] }, options: { animation: { animateRotate: false, animateScale: true, duration: 8000, easing: 'linear' }, responsive: false, plugins: { legend: false, title: false, tooltip: false, filler: false }, scales: { r: { ticks: { display: false, } } } }, }, options: { canvas: { height: 512, width: 512 }, run: function(chart) { const animator = Chart.animator; const anims = animator._getAnims(chart); // disable animator const backup = animator._refresh; animator._refresh = function() { }; return new Promise((resolve) => { window.requestAnimationFrame(() => { const start = anims.items[0]._start; for (let i = 0; i < 16; i++) { animator._update(start + i * 500); let x = i % 4 * 128; let y = Math.floor(i / 4) * 128; ctx.drawImage(chart.canvas, x, y, 128, 128); } Chart.helpers.clearCanvas(chart.canvas); chart.ctx.drawImage(canvas, 0, 0); animator._refresh = backup; resolve(); }); }); } } }; ================================================ FILE: test/fixtures/controller.radar/backgroundColor/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: function(ctx) { var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#ff00ff'; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { backgroundColor: function(ctx) { var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#ff00ff'; }, fill: true, }, point: { backgroundColor: '#0000ff', radius: 10 } }, scales: { r: { display: false, min: -15, }, }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/backgroundColor/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: '#ff0000' }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { backgroundColor: '#00ff00', fill: true, }, point: { radius: 10 } }, scales: { r: { display: false, min: -15 } }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderCapStyle/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2], datasets: [ { // option in dataset data: [null, 3, 3], borderCapStyle: function(ctx) { var index = (ctx.datasetIndex % 2); return index === 0 ? 'round' : index === 1 ? 'square' : 'butt'; } }, { // option in element (fallback) data: [null, 2, 2] }, { // option in element (fallback) data: [null, 1, 1] } ] }, options: { elements: { line: { borderCapStyle: function(ctx) { var index = (ctx.datasetIndex % 3); return index === 0 ? 'round' : index === 1 ? 'square' : 'butt'; }, borderColor: '#ff0000', borderWidth: 32, fill: false }, point: { radius: 10 } }, layout: { padding: 32 }, scales: { r: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderCapStyle/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2], datasets: [ { // option in dataset data: [null, 3, 3], borderCapStyle: 'round' }, { // option in dataset data: [null, 2, 2], borderCapStyle: 'square' }, { // option in element (fallback) data: [null, 1, 1] } ] }, options: { elements: { line: { borderCapStyle: 'butt', borderColor: '#00ff00', borderWidth: 32, fill: false }, point: { radius: 10 } }, layout: { padding: 32 }, scales: { r: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderColor/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: function(ctx) { var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#0000ff'; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { borderColor: function(ctx) { var index = ctx.index; return index === 0 ? '#ff0000' : index === 1 ? '#00ff00' : '#0000ff'; }, borderWidth: 10, fill: false }, point: { borderColor: '#ff0000', borderWidth: 10, radius: 16 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderColor/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: '#ff0000' }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { borderColor: '#0000ff', fill: false }, point: { borderColor: '#0000ff', radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderDash/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderDash: function(ctx) { return ctx.datasetIndex === 0 ? [5] : [10]; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderDash: function(ctx) { return ctx.datasetIndex === 0 ? [5] : [10]; }, fill: true, }, point: { radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderDash/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: '#ff0000', borderDash: [5] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderDash: [10], fill: false }, point: { radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderDashOffset/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3], datasets: [ { // option in dataset data: [1, 1, 1, 1], borderColor: '#ff0000', borderDash: [20], borderDashOffset: function(ctx) { return ctx.datasetIndex === 0 ? 5.0 : 0.0; } }, { // option in element (fallback) data: [0, 0, 0, 0] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderDash: [20], borderDashOffset: function(ctx) { return ctx.datasetIndex === 0 ? 5.0 : 0.0; }, fill: false }, point: { radius: 10 } }, layout: { padding: 32 }, scales: { r: { display: false, min: -1 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderDashOffset/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [1, 1, 1, 1, 1, 1], borderColor: '#ff0000', borderDash: [20], borderDashOffset: 5.0 }, { // option in element (fallback) data: [0, 0, 0, 0, 0, 0] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderDash: [20], borderDashOffset: 0.0, // default fill: false }, point: { radius: 10 } }, layout: { padding: 32 }, scales: { r: { display: false, min: -1 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderJoinStyle/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3], datasets: [ { // option in dataset data: [3, 3, null, 3], borderColor: '#ff0000', borderJoinStyle: function(ctx) { var index = ctx.datasetIndex % 3; return index === 0 ? 'round' : index === 1 ? 'miter' : 'bevel'; } }, { // option in element (fallback) data: [2, 2, null, 2], borderColor: '#0000ff' }, { // option in element (fallback) data: [1, 1, null, 1] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderJoinStyle: function(ctx) { var index = (ctx.datasetIndex % 3); return index === 0 ? 'round' : index === 1 ? 'miter' : 'bevel'; }, borderWidth: 25, fill: false, tension: 0 } }, layout: { padding: 32 }, scales: { r: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderJoinStyle/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3], datasets: [ { // option in dataset data: [3, 3, null, 3], borderColor: '#ff0000', borderJoinStyle: 'round' }, { // option in element (fallback) data: [2, 2, null, 2], borderColor: '#0000ff', borderJoinStyle: 'bevel' }, { // option in element (fallback) data: [1, 1, null, 1] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderJoinStyle: 'miter', borderWidth: 25, fill: false, tension: 0 } }, layout: { padding: 32 }, scales: { r: { display: false, beginAtZero: true } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderWidth/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: '#0000ff', borderWidth: function(ctx) { var index = ctx.index; return index % 2 ? 10 : 20; }, pointBorderColor: '#00ff00' }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { borderColor: '#ff0000', borderWidth: function(ctx) { var index = ctx.index; return index % 2 ? 10 : 20; }, fill: false }, point: { borderColor: '#00ff00', borderWidth: 5, radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderWidth/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], borderColor: '#0000ff', borderWidth: 6 }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderWidth: 3, fill: false }, point: { radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/borderWidth/zero.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: '#0000ff', borderColor: '#0000ff', borderWidth: 0, }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { borderColor: '#00ff00', borderWidth: 1, fill: false }, point: { backgroundColor: '#00ff00', radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/fill/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: '#ff0000', fill: function(ctx) { return ctx.datasetIndex === 0 ? true : false; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { backgroundColor: '#00ff00', fill: function(ctx) { return ctx.datasetIndex === 0 ? true : false; } } }, scales: { r: { display: false, min: -15 } }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/fill/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: '#ff0000', fill: false }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { backgroundColor: '#00ff00', fill: true } }, scales: { r: { display: false, min: -15 } }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/point-style.json ================================================ { "config": { "type": "radar", "data": { "labels": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "datasets": [ { "borderColor": "transparent", "data": [3, 3, 3, 3, 3, 3, 3, 3, 3, 3], "pointBackgroundColor": "#00ff00", "pointBorderColor": "transparent", "pointBorderWidth": 0, "pointRadius": 16, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] }, { "borderColor": "transparent", "data": [2, 2, 2, 2, 2, 2, 2, 2, 2, 2], "pointBackgroundColor": "transparent", "pointBorderColor": "#0000ff", "pointBorderWidth": 1, "pointRadius": 16, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] }, { "borderColor": "transparent", "data": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "pointBackgroundColor": "#00ff00", "pointBorderColor": "#0000ff", "pointBorderWidth": 1, "pointRadius": 16, "pointStyle": [ "circle", "cross", "crossRot", "dash", "line", "rect", "rectRounded", "rectRot", "star", "triangle" ] } ] }, "options": { "responsive": false, "scales": { "r": { "display": false, "min": 0, "max": 3 } }, "elements": { "line": { "fill": false } }, "layout": { "padding": { "left": 24, "right": 24 } } } }, "options": { "canvas": { "height": 512, "width": 512 } } } ================================================ FILE: test/fixtures/controller.radar/pointBackgroundColor/indexable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: [ '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#000000' ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { fill: false }, point: { backgroundColor: [ '#ff88ff', '#888888', '#ff8800', '#00ff88', '#8800ff', '#ffff88' ], radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/pointBackgroundColor/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 0 ? '#00ff00' : value > -8 ? '#0000ff' : '#ff00ff'; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { fill: false }, point: { backgroundColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff00ff' : value > 0 ? '#0000ff' : value > -8 ? '#ff0000' : '#00ff00'; }, radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/pointBackgroundColor/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: '#ff0000' }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { fill: false }, point: { backgroundColor: '#00ff00', radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/pointBorderColor/indexable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: [ '#ff0000', '#00ff00', '#0000ff', '#ffff00', '#ff00ff', '#000000' ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { fill: false }, point: { borderColor: [ '#ff88ff', '#888888', '#ff8800', '#00ff88', '#8800ff', '#ffff88' ], borderWidth: 5, radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/pointBorderColor/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff0000' : value > 0 ? '#00ff00' : value > -8 ? '#0000ff' : '#ff00ff'; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { fill: false }, point: { borderColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? '#ff00ff' : value > 0 ? '#0000ff' : value > -8 ? '#ff0000' : '#00ff00'; }, borderWidth: 5, radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/pointBorderColor/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#ff0000' }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { fill: false }, point: { borderColor: '#00ff00', borderWidth: 5, radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/pointBorderWidth/indexable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#00ff00', pointBorderWidth: [ 1, 2, 3, 4, 5, 6 ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { fill: false }, point: { borderColor: '#ff0000', borderWidth: [ 6, 5, 4, 3, 2, 1 ], radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/pointBorderWidth/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#0000ff', pointBorderWidth: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 10 : value > -4 ? 5 : 2; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { fill: false }, point: { borderColor: '#ff0000', borderWidth: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 2 : value > -4 ? 5 : 10; }, radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/pointBorderWidth/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#0000ff', pointBorderWidth: 6 }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] } ] }, options: { elements: { line: { fill: false }, point: { borderColor: '#00ff00', borderWidth: 3, radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/pointStyle/indexable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5, 6], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5, 0], pointBackgroundColor: '#ff0000', pointBorderColor: '#ff0000', pointStyle: [ 'circle', 'cross', 'crossRot', 'dash', 'line', 'rect', false ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5, -4], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#00ff00', borderColor: '#00ff00', pointStyle: [ 'line', 'rect', 'rectRounded', 'rectRot', 'star', 'triangle' ], radius: 10 } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/pointStyle/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: '#ff0000', pointBorderColor: '#ff0000', pointStyle: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? 'rect' : value > 0 ? 'star' : value > -8 ? 'cross' : 'triangle'; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#0000ff', borderColor: '#0000ff', pointStyle: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 8 ? 'triangle' : value > 0 ? 'cross' : value > -8 ? 'star' : 'rect'; }, radius: 10, } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/pointStyle/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#ff0000', pointStyle: 'star', }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#00ff00', pointStyle: 'rect', radius: 10, } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/radius/indexable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: '#00ff00', pointRadius: [ 1, 2, 3, 4, 5, 6 ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#ff0000', radius: [ 6, 5, 4, 3, 2, 1 ], } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/radius/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: '#0000ff', pointRadius: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 10 : value > -4 ? 5 : 2; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#ff0000', radius: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 2 : value > -4 ? 5 : 10; }, } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/radius/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBackgroundColor: '#0000ff', pointRadius: 6 }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { backgroundColor: '#00ff00', radius: 3, } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/rotation/indexable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#00ff00', pointRotation: [ 0, 30, 60, 90, 120, 150 ] }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: '#ff0000', borderWidth: 10, pointStyle: 'line', rotation: [ 150, 120, 90, 60, 30, 0 ], } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/rotation/scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#0000ff', pointRotation: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 120 : value > -4 ? 60 : 0; } }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: '#ff0000', rotation: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value > 4 ? 0 : value > -4 ? 60 : 120; }, pointStyle: 'line', radius: 10, } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/rotation/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], pointBorderColor: '#0000ff', pointRotation: 90 }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5], } ] }, options: { elements: { line: { fill: false, }, point: { borderColor: '#00ff00', pointStyle: 'line', radius: 10, rotation: 0, } }, scales: { r: { display: false, min: -15 } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/showLine/value.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { // option in dataset data: [0, 5, 10, null, -10, -5], backgroundColor: '#ff0000', fill: false, showLine: true }, { // option in element (fallback) data: [4, -5, -10, null, 10, 5] }, { data: [1, 1, 1, 1, 1, 1], showLine: true, backgroundColor: 'rgba(0,0,255,0.5)' } ] }, options: { showLine: false, elements: { line: { borderColor: '#ff0000', backgroundColor: 'rgba(0,255,0,0.5)', fill: true } }, scales: { r: { display: false, min: -15 } }, plugins: { legend: false, title: false, tooltip: false, filler: true } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/controller.radar/startAngle/135.js ================================================ module.exports = { config: { type: 'radar', data: { datasets: [{ data: [6, 3, 2, 3], borderWidth: 3, borderColor: 'blue' }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { min: 0, startAngle: 135, pointLabels: { display: true }, grid: { circular: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.radar/startAngle/180.js ================================================ module.exports = { config: { type: 'radar', data: { datasets: [{ data: [6, 3, 2, 3], borderWidth: 3, borderColor: 'blue' }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { min: 0, startAngle: 180, pointLabels: { display: true }, grid: { circular: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.radar/startAngle/225.js ================================================ module.exports = { config: { type: 'radar', data: { datasets: [{ data: [6, 3, 2, 3], borderWidth: 3, borderColor: 'blue' }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { min: 0, startAngle: 225, pointLabels: { display: true }, grid: { circular: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.radar/startAngle/270.js ================================================ module.exports = { config: { type: 'radar', data: { datasets: [{ data: [6, 3, 2, 3], borderWidth: 3, borderColor: 'blue' }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { min: 0, startAngle: 270, pointLabels: { display: true }, grid: { circular: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.radar/startAngle/315.js ================================================ module.exports = { config: { type: 'radar', data: { datasets: [{ data: [6, 3, 2, 3], borderWidth: 3, borderColor: 'blue' }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { min: 0, startAngle: 315, pointLabels: { display: true }, grid: { circular: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.radar/startAngle/45.js ================================================ module.exports = { config: { type: 'radar', data: { datasets: [{ data: [6, 3, 2, 3], borderWidth: 3, borderColor: 'blue' }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { min: 0, startAngle: 45, pointLabels: { display: true }, grid: { circular: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.radar/startAngle/90.js ================================================ module.exports = { config: { type: 'radar', data: { datasets: [{ data: [6, 3, 2, 3], borderWidth: 3, borderColor: 'blue' }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { min: 0, startAngle: 90, pointLabels: { display: true }, grid: { circular: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.radar/startAngle/default.js ================================================ module.exports = { config: { type: 'radar', data: { datasets: [{ data: [6, 3, 2, 3], borderWidth: 3, borderColor: 'blue' }], labels: [['label 1', 'line 2'], ['label 2', 'line 2'], ['label 3', 'line 2'], ['label 4', 'line 2']] }, options: { scales: { r: { min: 0, pointLabels: { display: true }, grid: { circular: true } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/controller.scatter/showLine/changed.js ================================================ module.exports = { description: 'showLine option should draw a line if true', config: { type: 'scatter', data: { datasets: [{ data: [{x: 10, y: 15}, {x: 15, y: 10}], pointRadius: 10, backgroundColor: 'red', label: 'dataset1' }], }, options: { scales: { x: { display: false }, y: { display: false } } } }, options: { canvas: { width: 256, height: 256 }, run(chart) { chart.options.showLine = true; chart.update(); } } }; ================================================ FILE: test/fixtures/controller.scatter/showLine/true.js ================================================ module.exports = { description: 'showLine option should draw a line if true', config: { type: 'scatter', data: { datasets: [{ data: [{x: 10, y: 15}, {x: 15, y: 10}], pointRadius: 10, backgroundColor: 'red', showLine: true, label: 'dataset1' }], }, options: { scales: { x: { display: false }, y: { display: false } } } }, options: { canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/controller.scatter/showLine/undefined.js ================================================ module.exports = { description: 'showLine option should not draw a line if undefined', config: { type: 'scatter', data: { datasets: [{ data: [{x: 10, y: 15}, {x: 15, y: 10}], pointRadius: 10, backgroundColor: 'red', label: 'dataset1' }], }, options: { scales: { x: { display: false }, y: { display: false } } } }, options: { canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/core.datasetController/stacked-initial-render.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5, 6], datasets: [ { // option in dataset data: [9, 13, 15, 25, 22, 15, 21], stack: 'construction_stack', borderWidth: 10, borderColor: 'rgb(54, 162, 235)' }, { data: [9, 13, 15, 25, 22, 15, 21], stack: 'construction_stack', borderWidth: 10, borderColor: 'rgb(255, 99, 132)' } ] }, options: { scales: { x: { ticks: { display: false } }, y: { ticks: { display: false } } }, plugins: { legend: false, title: false, tooltip: false, filler: false } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/core.interaction/drawActiveElementsOnTop-false.js ================================================ module.exports = { config: { type: 'bubble', data: { datasets: [{ data: [ {x: 1, y: 1, r: 80}, {x: 1, y: 1, r: 20} ], drawActiveElementsOnTop: false, backgroundColor: (ctx) => (ctx.dataIndex === 1 ? 'red' : 'blue'), hoverBackgroundColor: 'yellow', hoverRadius: 0, }] }, options: { scales: { x: { display: false }, y: { display: false }, }, plugins: { tooltip: false, legend: false }, } }, options: { canvas: { width: 256, height: 256 }, async run(chart) { const point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'click', {y: point.y, x: point.x + 25}); } } }; ================================================ FILE: test/fixtures/core.interaction/nearest-partial-bar.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b', 'c'], datasets: [ { data: [220, 250, 225], }, ], }, options: { events: ['click'], interaction: { mode: 'nearest' }, plugins: { tooltip: true, legend: false }, scales: { y: { beginAtZero: false } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 }, async run(chart) { const point = { x: chart.chartArea.left + chart.chartArea.width / 2, y: chart.chartArea.top + chart.chartArea.height / 2, }; await jasmine.triggerMouseEvent(chart, 'click', point); } } }; ================================================ FILE: test/fixtures/core.interaction/nearest-point-behind-scale.js ================================================ module.exports = { config: { type: 'scatter', data: { datasets: [{ data: [{x: 1, y: 1}, {x: 48, y: 1}] }] }, options: { events: ['click'], interaction: { mode: 'nearest', intersect: false }, plugins: { tooltip: true, legend: false }, scales: { x: { min: 5, max: 50 }, y: { min: 0, max: 2 } }, layout: { padding: 50 } } }, options: { spriteText: true, canvas: { width: 256, height: 256 }, async run(chart) { const point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'click', {y: point.y, x: chart.chartArea.left}); } } }; ================================================ FILE: test/fixtures/core.layouts/hidden-vertical-boxes.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', ''] }, options: { plugins: { legend: false }, scales: { x: { display: false }, y: { type: 'linear', position: 'left', ticks: { callback: function(value) { return value + ' very long unit!'; }, } }, y1: { type: 'linear', position: 'left', display: false }, y2: { type: 'linear', position: 'left', display: false }, y3: { type: 'linear', position: 'left', display: false }, y4: { type: 'linear', position: 'left', display: false }, y5: { type: 'linear', position: 'left', display: false } } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/core.layouts/long-labels.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1 is very long one', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6 is very long one'] }, options: { plugins: { legend: false }, scales: { x: { type: 'category', ticks: { maxRotation: 0, autoSkip: false } }, y: { type: 'linear', position: 'right' } } } }, options: { spriteText: true, canvas: { height: 150, width: 512 } } }; ================================================ FILE: test/fixtures/core.layouts/no-boxes-all-padding.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0], datasets: [{ data: [0], radius: 16, borderWidth: 0, backgroundColor: 'red' }], }, options: { plugins: { legend: false, tooltip: false, title: false, filler: false }, scales: { x: { display: false, offset: true }, y: { display: false } }, layout: { padding: 16 } } }, options: { canvas: { height: 32, width: 32 } } }; ================================================ FILE: test/fixtures/core.layouts/refit-vertical-boxes.js ================================================ module.exports = { tolerance: 0.002, config: { type: 'line', data: { labels: [ 'Aaron', 'Adam', 'Albert', 'Alex', 'Allan', 'Aman', 'Anthony', 'Autoenrolment', 'Avril', 'Bernard' ], datasets: [{ backgroundColor: 'rgba(252,233,79,0.5)', borderColor: 'rgba(252,233,79,1)', borderWidth: 1, data: [101, 185, 24, 311, 17, 21, 462, 340, 140, 24 ] }] }, options: { maintainAspectRatio: false, plugins: { legend: true, title: { display: true, text: 'test' } } } }, options: { spriteText: true, canvas: { height: 185, width: 185 } } }; ================================================ FILE: test/fixtures/core.layouts/scriptable.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, options: { layout: { padding: function(ctx) { // 10% padding const horizontalPadding = ctx.chart.width * 0.1; const verticalPadding = ctx.chart.height * 0.1; return { top: verticalPadding, right: horizontalPadding, bottom: verticalPadding, left: horizontalPadding }; } }, plugins: { legend: false }, scales: { x: { type: 'category', ticks: { maxRotation: 0, autoSkip: false } }, y: { type: 'linear', position: 'right' } } } }, options: { spriteText: true, canvas: { height: 150, width: 512 } } }; ================================================ FILE: test/fixtures/core.layouts/stacked-boxes-max-index-without-clip.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], borderColor: 'red'}, {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, ], labels: ['tick1', 'tick2', 'tick3'] }, options: { plugins: false, scales: { x: { type: 'linear', position: 'bottom', stack: '1', offset: true, clip: false, bounds: 'data', border: { color: 'red' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 }, max: 7 }, x1: { type: 'linear', position: 'bottom', stack: '1', offset: true, clip: false, bounds: 'data', border: { color: 'green' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 }, max: 7 }, x2: { type: 'linear', position: 'bottom', stack: '1', offset: true, clip: false, bounds: 'data', border: { color: 'blue' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 }, max: 7 }, y: { type: 'linear', position: 'left', stack: '1', offset: true, clip: false, border: { color: 'red' }, ticks: { precision: 0 } }, y1: { type: 'linear', position: 'left', stack: '1', offset: true, clip: false, border: { color: 'green' }, ticks: { precision: 0 } }, y2: { type: 'linear', position: 'left', stack: '1', offset: true, clip: false, border: { color: 'blue', }, ticks: { precision: 0 } } } } }, options: { spriteText: true, canvas: { height: 384, width: 384 } } }; ================================================ FILE: test/fixtures/core.layouts/stacked-boxes-max-index.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], borderColor: 'red'}, {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, {data: [{x: 5, y: 1}, {x: 10, y: 2}, {x: 5, y: 3}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, ], labels: ['tick1', 'tick2', 'tick3'] }, options: { plugins: false, scales: { x: { type: 'linear', position: 'bottom', stack: '1', offset: true, bounds: 'data', border: { color: 'red' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 }, max: 7 }, x1: { type: 'linear', position: 'bottom', stack: '1', offset: true, bounds: 'data', border: { color: 'green' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 }, max: 7 }, x2: { type: 'linear', position: 'bottom', stack: '1', offset: true, bounds: 'data', border: { color: 'blue' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 }, max: 7 }, y: { type: 'linear', position: 'left', stack: '1', offset: true, border: { color: 'red' }, ticks: { precision: 0 } }, y1: { type: 'linear', position: 'left', stack: '1', offset: true, border: { color: 'green' }, ticks: { precision: 0 } }, y2: { type: 'linear', position: 'left', stack: '1', offset: true, border: { color: 'blue', }, ticks: { precision: 0 } } } } }, options: { spriteText: true, canvas: { height: 384, width: 384 } } }; ================================================ FILE: test/fixtures/core.layouts/stacked-boxes-max-without-clip.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], borderColor: 'red'}, {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, ], labels: ['tick1', 'tick2', 'tick3'] }, options: { plugins: false, scales: { x: { type: 'linear', position: 'bottom', stack: '1', offset: true, clip: false, bounds: 'data', border: { color: 'red' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, x1: { type: 'linear', position: 'bottom', stack: '1', offset: true, clip: false, bounds: 'data', border: { color: 'green' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, x2: { type: 'linear', position: 'bottom', stack: '1', offset: true, clip: false, bounds: 'data', border: { color: 'blue' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, y: { type: 'linear', position: 'left', stack: '1', offset: true, clip: false, border: { color: 'red' }, ticks: { precision: 0 }, max: 7 }, y1: { type: 'linear', position: 'left', stack: '1', offset: true, clip: false, border: { color: 'green' }, ticks: { precision: 0 }, max: 7 }, y2: { type: 'linear', position: 'left', stack: '1', offset: true, clip: false, border: { color: 'blue', }, ticks: { precision: 0 }, max: 7 } } } }, options: { spriteText: true, canvas: { height: 384, width: 384 } } }; ================================================ FILE: test/fixtures/core.layouts/stacked-boxes-max.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], borderColor: 'red'}, {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, {data: [{x: 1, y: 5}, {x: 2, y: 10}, {x: 3, y: 5}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, ], labels: ['tick1', 'tick2', 'tick3'] }, options: { plugins: false, scales: { x: { type: 'linear', position: 'bottom', stack: '1', offset: true, bounds: 'data', border: { color: 'red' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, x1: { type: 'linear', position: 'bottom', stack: '1', offset: true, bounds: 'data', border: { color: 'green' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, x2: { type: 'linear', position: 'bottom', stack: '1', offset: true, bounds: 'data', border: { color: 'blue' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, y: { type: 'linear', position: 'left', stack: '1', offset: true, border: { color: 'red' }, ticks: { precision: 0 }, max: 7 }, y1: { type: 'linear', position: 'left', stack: '1', offset: true, border: { color: 'green' }, ticks: { precision: 0 }, max: 7 }, y2: { type: 'linear', position: 'left', stack: '1', offset: true, border: { color: 'blue', }, ticks: { precision: 0 }, max: 7 } } } }, options: { spriteText: true, canvas: { height: 384, width: 384 } } }; ================================================ FILE: test/fixtures/core.layouts/stacked-boxes-with-weight.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], borderColor: 'red'}, {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, ], labels: ['tick1', 'tick2', 'tick3'] }, options: { plugins: false, scales: { x: { type: 'linear', position: 'bottom', stack: '1', stackWeight: 2, offset: true, bounds: 'data', border: { color: 'red' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, x1: { type: 'linear', position: 'bottom', stack: '1', stackWeight: 2, offset: true, bounds: 'data', border: { color: 'green' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, x2: { type: 'linear', position: 'bottom', stack: '1', stackWeight: 6, offset: true, bounds: 'data', border: { color: 'blue' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, y: { type: 'linear', position: 'left', stack: '1', stackWeight: 2, offset: true, border: { color: 'red' }, ticks: { precision: 0 } }, y1: { type: 'linear', position: 'left', stack: '1', offset: true, stackWeight: 2, border: { color: 'green' }, ticks: { precision: 0 } }, y2: { type: 'linear', position: 'left', stack: '1', stackWeight: 3, offset: true, border: { color: 'blue' }, ticks: { precision: 0 } } } } }, options: { spriteText: true, canvas: { height: 384, width: 384 } } }; ================================================ FILE: test/fixtures/core.layouts/stacked-boxes.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], borderColor: 'red'}, {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], yAxisID: 'y1', xAxisID: 'x1', borderColor: 'green'}, {data: [{x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}], yAxisID: 'y2', xAxisID: 'x2', borderColor: 'blue'}, ], labels: ['tick1', 'tick2', 'tick3'] }, options: { plugins: false, scales: { x: { type: 'linear', position: 'bottom', stack: '1', offset: true, bounds: 'data', border: { color: 'red' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, x1: { type: 'linear', position: 'bottom', stack: '1', offset: true, bounds: 'data', border: { color: 'green' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, x2: { type: 'linear', position: 'bottom', stack: '1', offset: true, bounds: 'data', border: { color: 'blue' }, ticks: { autoSkip: false, maxRotation: 0, count: 3 } }, y: { type: 'linear', position: 'left', stack: '1', offset: true, border: { color: 'red' }, ticks: { precision: 0 } }, y1: { type: 'linear', position: 'left', stack: '1', offset: true, border: { color: 'green' }, ticks: { precision: 0 } }, y2: { type: 'linear', position: 'left', stack: '1', offset: true, border: { color: 'blue', }, ticks: { precision: 0 } } } } }, options: { spriteText: true, canvas: { height: 384, width: 384 } } }; ================================================ FILE: test/fixtures/core.scale/autoSkip/fit-after.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/3694', tolerance: 0.002, config: { type: 'line', data: { labels: [ 'Aaron', 'Adam', 'Albert', 'Alex', 'Allan', 'Aman', 'Anthony', 'Autoenrolment', 'Avril', 'Bernard' ], datasets: [{ backgroundColor: 'rgba(252,233,79,0.5)', borderColor: 'rgba(252,233,79,1)', borderWidth: 1, data: [101, 185, 24, 311, 17, 21, 462, 340, 140, 24 ] }], }, options: { scales: { x: { backgroundColor: '#eee' } } } }, options: { spriteText: true, canvas: { width: 185, height: 185 } } }; ================================================ FILE: test/fixtures/core.scale/autoSkip/no-offset.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8611', config: { type: 'line', data: { labels: ['Red Red Red', 'Blue Blue Blue', 'Black Black Black', 'Pink Pink Pink'], datasets: [ { label: '# of Votes', data: [12, 19, 3, 5] }, ] }, }, options: { spriteText: true, canvas: { width: 470, height: 128 } } }; ================================================ FILE: test/fixtures/core.scale/autoSkip/offset.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8611', config: { type: 'bar', data: { labels: ['Red Red Red', 'Blue Blue Blue', 'Black Black Black', 'Pink Pink Pink'], datasets: [ { label: '# of Votes', data: [12, 19, 3, 5] }, ] }, }, options: { spriteText: true, canvas: { width: 506, height: 128 } } }; ================================================ FILE: test/fixtures/core.scale/backgroundColor.js ================================================ const ticks = { display: false }; const grid = { display: false }; const title = { display: true, test: '' }; module.exports = { config: { type: 'line', options: { events: [], scales: { top: { type: 'linear', backgroundColor: 'red', position: 'top', ticks, grid, title }, left: { type: 'linear', backgroundColor: 'green', position: 'left', ticks, grid, title }, bottom: { type: 'linear', backgroundColor: 'blue', position: 'bottom', ticks, grid, title }, right: { type: 'linear', backgroundColor: 'gray', position: 'right', ticks, grid, title }, } } }, options: { canvas: { height: 256, width: 256 }, } }; ================================================ FILE: test/fixtures/core.scale/border-behind-elements.js ================================================ module.exports = { config: { type: 'bubble', data: { datasets: [ { label: '# of Votes', data: [{x: 19, y: 3, r: 3}, {x: 2, y: 2, r: 60}], radius: 100, backgroundColor: 'pink' } ] }, options: { plugins: { legend: { display: false } }, scales: { y: { ticks: { display: false }, border: { color: 'red', width: 5 } }, x: { ticks: { display: false }, border: { color: 'red', width: 5 } } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/cartesian-axis-border-settings.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [{ "data": [{ "x": -20, "y": -30 }, { "x": 0, "y": 0 }, { "x": 20, "y": 15 }] }] }, "options": { "scales": { "x": { "axis": "x", "min": -100, "max": 100, "grid": { "color": "red", "drawOnChartArea": false }, "border": { "display": true, "color": "blue", "width": 5 }, "ticks": { "display": false } }, "y": { "axis": "y", "min": -100, "max": 100, "border": { "color": "red" }, "grid": { "color": "red", "drawOnChartArea": false }, "ticks": { "display": false } } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-bottom-center.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] }, options: { scales: { x: { ticks: { crossAlign: 'center', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-bottom-far.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] }, options: { scales: { x: { ticks: { crossAlign: 'far', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-bottom-near.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] }, options: { scales: { x: { ticks: { crossAlign: 'near', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-left-center.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'left', ticks: { crossAlign: 'center', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-left-far-clipped.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long long long label 1', 'Label 2', 'Less more longer label 3'] }, options: { indexAxis: 'y', scales: { y: { position: 'left', ticks: { crossAlign: 'far', }, afterFit: axis => { axis.width = 64; }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-left-far.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'left', ticks: { crossAlign: 'far', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-left-near.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'left', ticks: { crossAlign: 'near', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-right-center.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'right', ticks: { crossAlign: 'center', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-right-far-clipped.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long long long label 1', 'Label 2', 'Less more longer label 3'] }, options: { indexAxis: 'y', scales: { y: { position: 'right', ticks: { crossAlign: 'far', }, afterFit: axis => { axis.width = 64; }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } }, tolerance: 0.1 }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-right-far.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'right', ticks: { crossAlign: 'far', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-right-near.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'right', ticks: { crossAlign: 'near', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-top-center.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] }, options: { scales: { x: { position: 'top', ticks: { crossAlign: 'center', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-top-far.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] }, options: { scales: { x: { position: 'top', ticks: { crossAlign: 'far', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/cross-align-top-near.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: [['Label1', 'line 2', 'line3'], 'Label2', 'Label3'] }, options: { scales: { x: { position: 'top', ticks: { crossAlign: 'near', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-center.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'left', ticks: { mirror: true, crossAlign: 'center', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-far.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'left', ticks: { mirror: true, crossAlign: 'far', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/mirror-cross-align-left-near.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'left', ticks: { mirror: true, crossAlign: 'near', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-center.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'right', ticks: { mirror: true, crossAlign: 'center', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-far.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'right', ticks: { mirror: true, crossAlign: 'far', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/crossAlignment/mirror-cross-align-right-near.js ================================================ module.exports = { config: { type: 'bar', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Long long label 1', 'Label2', 'Label3'] }, options: { indexAxis: 'y', scales: { y: { position: 'right', ticks: { mirror: true, crossAlign: 'near', }, }, } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/grid/border-over-grid.js ================================================ module.exports = { config: { type: 'scatter', options: { scales: { x: { position: {y: 0}, min: -10, max: 10, border: { color: 'black', width: 5 }, grid: { color: 'lightGray', lineWidth: 3, }, ticks: { display: false }, }, y: { position: {x: 0}, min: -10, max: 10, border: { color: 'black', width: 5 }, grid: { color: 'lightGray', lineWidth: 3, }, ticks: { display: false }, } } } } }; ================================================ FILE: test/fixtures/core.scale/grid/colors.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['1', '2', '3', '4', '5', '6'], datasets: [{ label: '# of Votes', data: [12, 19, 3, 5, 2, 3] }], }, options: { scales: { x: { ticks: { display: false }, border: { color: 'blue', width: 2, }, grid: { color: 'green', drawTicks: false, } }, y: { ticks: { display: false }, border: { color: 'black', width: 2, }, grid: { color: 'red', drawTicks: false, } } } } } }; ================================================ FILE: test/fixtures/core.scale/grid/scriptable-borderDash.js ================================================ module.exports = { config: { type: 'scatter', options: { scales: { x: { position: {y: 0}, min: -10, max: 10, border: { dash: (ctx) => ctx.index % 2 === 0 ? [6, 3] : [], }, grid: { color: 'lightGray', lineWidth: 3, }, ticks: { display: false }, }, y: { position: {x: 0}, min: -10, max: 10, border: { dash: (ctx) => ctx.index % 2 === 0 ? [6, 3] : [], }, grid: { color: 'lightGray', lineWidth: 3, }, ticks: { display: false }, } } } } }; ================================================ FILE: test/fixtures/core.scale/label-align-center.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Label1', 'Label2', 'Label3'] }, options: { scales: { x: { ticks: { align: 'center', }, }, y: { ticks: { align: 'center', } } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/label-align-end.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Label1', 'Label2', 'Label3'] }, options: { scales: { x: { ticks: { align: 'end', }, }, y: { ticks: { align: 'end', } } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/label-align-inner-onlyX.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Label1_long', 'Label2_long', 'Label3_long'] }, options: { scales: { x: { ticks: { align: 'inner', }, }, y: { display: false } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/label-align-inner-reverse.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Label1', 'Label2', 'Label3'] }, options: { scales: { x: { ticks: { align: 'inner' }, reverse: true } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/label-align-inner-rotate.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Label1', 'Label2', 'Label3'] }, options: { scales: { x: { ticks: { align: 'inner', maxRotation: 45, minRotation: 45 }, } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/label-align-inner.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Label1', 'Label2', 'Label3'] }, options: { scales: { x: { ticks: { align: 'inner', }, } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/label-align-start.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ data: [1, 2, 3], }], labels: ['Label1', 'Label2', 'Label3'] }, options: { scales: { x: { ticks: { align: 'start', }, }, y: { ticks: { align: 'start', } } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/label-offset-vertical-axes.json ================================================ { "config": { "type": "bar", "data": { "labels": ["\u25C0", "\u25A0", "\u25C6", "\u25CF"], "datasets": [{ "data": [12, 19, 3, 5] }] }, "options": { "indexAxis": "y", "scales": { "x": { "ticks": { "display": false }, "grid":{ "display": false }, "border": { "display": false } }, "y": { "ticks": { "labelOffset": 25 }, "border": { "display": false }, "grid":{ "display": false } } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/core.scale/tick-backdrop-alignment-inner.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [ { label: '# of Votes', data: [12, 19, 3, 5, 2, 3], }, { label: '# of Points', data: [7, 11, 5, 8, 3, 7], } ] }, options: { scales: { y: { ticks: { display: false, }, grid: { lineWidth: 0 } }, x: { position: 'top', ticks: { color: 'transparent', backdropColor: 'red', showLabelBackdrop: true, align: 'inner', }, grid: { lineWidth: 0 } } } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/tick-backdrop-rotation.js ================================================ const grid = { display: false }; const title = { display: false, }; module.exports = { tolerance: 0.0016, config: { type: 'line', options: { events: [], scales: { top: { type: 'linear', position: 'top', ticks: { display: true, showLabelBackdrop: true, minRotation: 45, backdropColor: 'blue', backdropPadding: 5, align: 'start', crossAlign: 'near', }, grid, title }, left: { type: 'linear', position: 'left', ticks: { display: true, showLabelBackdrop: true, minRotation: 90, backdropColor: 'green', backdropPadding: { x: 2, y: 5 }, crossAlign: 'center', }, grid, title }, bottom: { type: 'linear', position: 'bottom', ticks: { display: true, showLabelBackdrop: true, backdropColor: 'blue', backdropPadding: { x: 5, y: 5 }, align: 'end', crossAlign: 'far', minRotation: 60, }, grid, title }, right: { type: 'linear', position: 'right', ticks: { display: true, showLabelBackdrop: true, backdropColor: 'gray', }, grid, title }, } } }, options: { canvas: { height: 256, width: 256 }, spriteText: true, } }; ================================================ FILE: test/fixtures/core.scale/tick-backdrop.js ================================================ const grid = { display: false }; const title = { display: false, }; module.exports = { config: { type: 'line', options: { events: [], scales: { top: { type: 'linear', position: 'top', ticks: { display: true, showLabelBackdrop: true, backdropColor: 'red', backdropPadding: 5, align: 'start', crossAlign: 'near', }, grid, title }, left: { type: 'linear', position: 'left', ticks: { display: true, showLabelBackdrop: true, backdropColor: 'green', backdropPadding: 5, crossAlign: 'center', }, grid, title }, bottom: { type: 'linear', position: 'bottom', ticks: { display: true, showLabelBackdrop: true, backdropColor: 'blue', backdropPadding: 5, align: 'end', crossAlign: 'far', }, grid, title }, right: { type: 'linear', position: 'right', ticks: { display: true, showLabelBackdrop: true, backdropColor: 'gray', backdropPadding: 5, }, grid, title }, } } }, options: { canvas: { height: 256, width: 256 }, spriteText: true, } }; ================================================ FILE: test/fixtures/core.scale/tick-drawing.json ================================================ { "config": { "type": "bar", "data": { "labels": ["January", "February", "March", "April", "May", "June", "July"], "datasets": [] }, "options": { "indexAxis": "y", "scales": { "x": { "type": "category", "position": "top", "id": "x-axis-1", "ticks": { "display": false }, "border": { "display": false }, "grid":{ "drawOnChartArea": false, "color": "rgba(0, 0, 0, 1)" } }, "x2": { "type": "category", "position": "bottom", "ticks": { "display": false }, "border": { "display": false }, "grid":{ "drawOnChartArea": false, "color": "rgba(0, 0, 0, 1)" } }, "y": { "position": "left", "id": "y-axis-1", "type": "linear", "offset": false, "min": -100, "max": 100, "ticks": { "display": false }, "border": { "display": false }, "grid":{ "offset": false, "drawOnChartArea": false, "color": "rgba(0, 0, 0, 1)" } }, "y2": { "type": "linear", "position": "right", "offset": false, "min": 0, "max": 50, "ticks": { "display": false }, "border": { "display": false }, "grid":{ "offset": false, "drawOnChartArea": false, "color": "rgba(0, 0, 0, 1)" } } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/core.scale/tick-override-styles.json ================================================ { "config": { "type": "bar", "data": { "labels": ["January", "February", "March", "April", "May", "June", "July"], "datasets": [] }, "options": { "indexAxis": "y", "scales": { "x": { "type": "category", "position": "top", "id": "x-axis-1", "ticks": { "display": false }, "border": { "display": false }, "grid":{ "drawOnChartArea": false, "color": "rgba(0, 0, 0, 1)", "width": 1, "tickColor": "rgba(255, 0, 0, 1)", "tickWidth": 5 } }, "y": { "position": "left", "id": "y-axis-1", "type": "linear", "offset": false, "min": -100, "max": 100, "ticks": { "display": false }, "border": { "display": false }, "grid":{ "offset": false, "drawOnChartArea": false, "color": "rgba(0, 0, 0, 1)", "tickColor": "rgba(255, 0, 0, 1)", "tickWidth": 5 } } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/core.scale/ticks/rotated-long.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['Red Red Red Red', 'Blue Blue Blue Blue', 'Black Black Black Black', 'Green Green Green Green', 'Purple Purple Purple Purple', 'Orange Orange Orange Orange Orange Orange'], datasets: [ { data: [12, 19, 3, 5, 2, 3] }, ] }, options: { plugins: { legend: false, tooltip: false, filler: false, title: false }, scales: { x: { type: 'category', position: 'bottom' }, x2: { type: 'category', position: 'top' } } }, plugins: [{ afterDraw(chart) { const ctx = chart.ctx; ctx.save(); ctx.strokeStyle = 'red'; ctx.strokeRect(0, 0, chart.width, chart.height); ctx.restore(); } }] }, options: { spriteText: true, canvas: { width: 1024, height: 512 } } }; ================================================ FILE: test/fixtures/core.scale/ticks/rotated-multi-line.js ================================================ module.exports = { config: { type: 'line', data: { labels: [['Red', 'Red', 'Red', 'Red'], ['Blue', 'Blue', 'Blue', 'Blue'], ['Black', 'Black', 'Black', 'Black'], ['Green', 'Green', 'Green', 'Green'], ['Purple', 'Purple', 'Purple', 'Purple'], ['Orange Orange', 'Orange', 'Orange', 'Orange', 'Orange Orange']], datasets: [ { data: [12, 19, 3, 5, 2, 3] }, ] }, options: { plugins: { legend: false, tooltip: false, filler: false, title: false }, scales: { x: { type: 'category', position: 'bottom' }, x2: { type: 'category', position: 'top' } } }, plugins: [{ afterDraw(chart) { const ctx = chart.ctx; ctx.save(); ctx.strokeStyle = 'red'; ctx.strokeRect(0, 0, chart.width, chart.height); ctx.restore(); } }] }, options: { spriteText: true, canvas: { width: 610, height: 512 } } }; ================================================ FILE: test/fixtures/core.scale/ticks/skip-by-callback.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8892', config: { type: 'line', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [ { data: [12, 19, 3, 5, 2, 3], }, { data: [7, 11, 5, 8, 3, 7], } ] }, options: { scales: { x: { ticks: { callback: function(val, index) { if (index === 1) { return undefined; } if (index === 3) { return null; } return this.getLabelForValue(val); } } }, y: { ticks: { callback: function(val, index) { return index % 2 === 0 ? '' + val : null; } } } }, } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/core.scale/ticks-mirror-x.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ data: [1, -1, 3], }], labels: ['Label1', 'Label2', 'Label3'] }, options: { scales: { x: { ticks: { mirror: true } }, y: { display: false } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/ticks-mirror.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ data: [1, -1, 3], }], labels: ['Label1', 'Label2', 'Label3'] }, options: { scales: { y: { ticks: { mirror: true } } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/core.scale/title/align-end.js ================================================ module.exports = { config: { type: 'line', options: { events: [], scales: { top: { type: 'linear', position: 'top', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'end', text: 'top' } }, left: { type: 'linear', position: 'left', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'end', text: 'left' } }, bottom: { type: 'linear', position: 'bottom', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'end', text: 'bottom' } }, right: { type: 'linear', position: 'right', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'end', text: 'right' } }, } } }, options: { spriteText: true, canvas: { height: 256, width: 256 }, } }; ================================================ FILE: test/fixtures/core.scale/title/align-start.js ================================================ module.exports = { config: { type: 'line', options: { events: [], scales: { top: { type: 'linear', position: 'top', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'start', text: 'top' } }, left: { type: 'linear', position: 'left', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'start', text: 'left' } }, bottom: { type: 'linear', position: 'bottom', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'start', text: 'bottom' } }, right: { type: 'linear', position: 'right', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'start', text: 'right' } }, } } }, options: { spriteText: true, canvas: { height: 256, width: 256 }, } }; ================================================ FILE: test/fixtures/core.scale/title/default.js ================================================ module.exports = { config: { type: 'line', options: { events: [], scales: { top: { type: 'linear', position: 'top', ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'top' } }, left: { type: 'linear', position: 'left', ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'left' } }, bottom: { type: 'linear', position: 'bottom', ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'bottom' } }, right: { type: 'linear', position: 'right', ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'right' } }, } } }, options: { spriteText: true, canvas: { height: 256, width: 256 }, } }; ================================================ FILE: test/fixtures/core.scale/title/horizontal-center.js ================================================ module.exports = { config: { type: 'line', options: { events: [], scales: { y: { type: 'linear', position: 'left', min: 0, max: 100, ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'vertical' } }, x: { type: 'linear', position: 'center', min: 0, max: 100, ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'horizontal' } }, } } }, options: { spriteText: true, canvas: { height: 256, width: 256 }, } }; ================================================ FILE: test/fixtures/core.scale/title/horizontal-value.js ================================================ module.exports = { config: { type: 'line', options: { events: [], scales: { y: { type: 'linear', position: 'left', min: 0, max: 100, ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'vertical' } }, x: { type: 'linear', position: { y: 40, }, min: 0, max: 100, ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'horizontal' } }, } } }, options: { spriteText: true, canvas: { height: 256, width: 256 }, } }; ================================================ FILE: test/fixtures/core.scale/title/multi-line/align-end.js ================================================ module.exports = { config: { type: 'line', options: { events: [], scales: { top: { type: 'linear', position: 'top', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'end', text: ['top', 'line2', 'line3'] } }, left: { type: 'linear', position: 'left', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'end', text: ['left', 'line2', 'line3'] } }, bottom: { type: 'linear', position: 'bottom', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'end', text: ['bottom', 'line2', 'line3'] } }, right: { type: 'linear', position: 'right', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'end', text: ['right', 'line2', 'line3'] } }, } } }, options: { spriteText: true, canvas: { height: 256, width: 256 }, } }; ================================================ FILE: test/fixtures/core.scale/title/multi-line/align-start.js ================================================ module.exports = { config: { type: 'line', options: { events: [], scales: { top: { type: 'linear', position: 'top', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'start', text: ['top', 'line2', 'line3'] } }, left: { type: 'linear', position: 'left', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'start', text: ['left', 'line2', 'line3'] } }, bottom: { type: 'linear', position: 'bottom', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'start', text: ['bottom', 'line2', 'line3'] } }, right: { type: 'linear', position: 'right', ticks: { display: false }, grid: { display: false }, title: { display: true, align: 'start', text: ['right', 'line2', 'line3'] } }, } } }, options: { spriteText: true, canvas: { height: 256, width: 256 }, } }; ================================================ FILE: test/fixtures/core.scale/title/multi-line/default.js ================================================ module.exports = { config: { type: 'line', options: { events: [], scales: { top: { type: 'linear', position: 'top', ticks: { display: false }, grid: { display: false }, title: { display: true, text: ['top', 'line2', 'line3'] } }, left: { type: 'linear', position: 'left', ticks: { display: false }, grid: { display: false }, title: { display: true, text: ['left', 'line2', 'line3'] } }, bottom: { type: 'linear', position: 'bottom', ticks: { display: false }, grid: { display: false }, title: { display: true, text: ['bottom', 'line2', 'line3'] } }, right: { type: 'linear', position: 'right', ticks: { display: false }, grid: { display: false }, title: { display: true, text: ['right', 'line2', 'line3'] } }, } } }, options: { spriteText: true, canvas: { height: 256, width: 256 }, } }; ================================================ FILE: test/fixtures/core.scale/title/vertical-center.js ================================================ module.exports = { config: { type: 'line', options: { events: [], scales: { y: { type: 'linear', position: 'center', min: 0, max: 100, ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'vertical' } }, x: { type: 'linear', position: 'bottom', min: 0, max: 100, ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'horizontal' } }, } } }, options: { spriteText: true, canvas: { height: 256, width: 256 }, } }; ================================================ FILE: test/fixtures/core.scale/title/vertical-value.js ================================================ module.exports = { config: { type: 'line', options: { events: [], scales: { y: { type: 'linear', position: { x: 40 }, min: 0, max: 100, ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'vertical' } }, x: { type: 'linear', position: 'bottom', min: 0, max: 100, ticks: { display: false }, grid: { display: false }, title: { display: true, text: 'horizontal' } }, } } }, options: { spriteText: true, canvas: { height: 256, width: 256 }, } }; ================================================ FILE: test/fixtures/core.scale/x-axis-position-center.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [{ "data": [{ "x": -20, "y": -30 }, { "x": 0, "y": 0 }, { "x": 20, "y": 15 }] }] }, "options": { "scales": { "x": { "position": "center", "axis": "x", "min": -100, "max": 100, "border": { "color": "red" }, "grid": { "color": "red", "drawOnChartArea": false }, "ticks": { "display": true, "color": "red" } }, "y": { "position": "left", "axis": "y", "min": -100, "max": 100, "border": { "color": "red" }, "grid": { "color": "red", "drawOnChartArea": false }, "ticks": { "display": true } } } } }, "options": { "canvas": { "height": 256, "width": 512 }, "spriteText": true } } ================================================ FILE: test/fixtures/core.scale/x-axis-position-dynamic-margin.js ================================================ module.exports = { config: { type: 'line', options: { scales: { x: { labels: ['Left Label', 'Center Label', 'Right Label'], position: { y: 30 }, }, y: { display: false, min: -100, max: 100, } } } }, options: { canvas: { height: 256, width: 512 }, spriteText: true } }; ================================================ FILE: test/fixtures/core.scale/x-axis-position-dynamic.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [{ "data": [{ "x": -20, "y": -30 }, { "x": 0, "y": 0 }, { "x": 20, "y": 15 }] }] }, "options": { "scales": { "x": { "position": { "y": 30 }, "axis": "x", "min": -100, "max": 100, "border": { "color": "red" }, "grid": { "color": "red", "drawOnChartArea": false }, "ticks": { "display": true } }, "y": { "position": "left", "axis": "y", "min": -100, "max": 100, "border": { "color": "red" }, "grid": { "color": "red", "drawOnChartArea": false }, "ticks": { "display": true } } } } }, "options": { "canvas": { "height": 256, "width": 512 }, "spriteText": true } } ================================================ FILE: test/fixtures/core.scale/y-axis-position-center.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [{ "data": [{ "x": -20, "y": -30 }, { "x": 0, "y": 0 }, { "x": 20, "y": 15 }] }] }, "options": { "scales": { "x": { "position": "bottom", "axis": "x", "min": -100, "max": 100, "border": { "color": "red" }, "grid": { "color": "red", "drawOnChartArea": false }, "ticks": { "display": true } }, "y": { "position": "center", "axis": "y", "min": -100, "max": 100, "border": { "color": "red" }, "grid": { "color": "red", "drawOnChartArea": false }, "ticks": { "display": true } } } } }, "options": { "canvas": { "height": 256, "width": 512 }, "spriteText": true } } ================================================ FILE: test/fixtures/core.scale/y-axis-position-dynamic.json ================================================ { "config": { "type": "scatter", "data": { "datasets": [{ "data": [{ "x": -20, "y": -30 }, { "x": 0, "y": 0 }, { "x": 20, "y": 15 }] }] }, "options": { "scales": { "x": { "position": "bottom", "axis": "x", "min": -100, "max": 100, "border": { "color": "red" }, "grid": { "color": "red", "drawOnChartArea": false }, "ticks": { "display": true } }, "y": { "position": { "x": -50 }, "axis": "y", "min": -100, "max": 100, "border": { "color": "red" }, "grid": { "color": "red", "drawOnChartArea": false }, "ticks": { "display": true } } } } }, "options": { "canvas": { "height": 256, "width": 512 }, "spriteText": true }, "tolerance": 0.01 } ================================================ FILE: test/fixtures/element.line/cubicInterpolationMode/monotone-horizontal.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: false, cubicInterpolationMode: 'monotone' } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/cubicInterpolationMode/monotone-vertical.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 10, y: 1}, {x: 0, y: 5}, {x: -10, y: 15}, {x: -5, y: 19}], borderColor: 'red', fill: false, cubicInterpolationMode: 'monotone' } ] }, options: { indexAxis: 'y', scales: { x: {display: false, min: -15, max: 15}, y: {type: 'linear', display: false, min: 0, max: 20} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/default.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/skip/all.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 0, y: NaN}, {x: NaN, y: 0}, {x: NaN, y: -10}, {x: 19, y: NaN}], borderColor: 'red', fill: true, tension: 0 } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/skip/first-span.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: NaN, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: true, spanGaps: true, tension: 0 } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/skip/first.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: NaN, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: true, tension: 0 } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/skip/last-span.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 0, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: NaN, y: -5}], borderColor: 'red', fill: true, spanGaps: true, tension: 0 } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/skip/last.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 0, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: NaN, y: -5}], borderColor: 'red', fill: true, tension: 0 } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/skip/middle-span.js ================================================ module.exports = { config: { type: 'line', parsing: false, data: { datasets: [ { data: [{x: 0, y: 10}, {x: 5, y: 0}, {x: null, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: true, spanGaps: true, tension: 0 } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/skip/middle.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 0, y: 10}, {x: 5, y: 0}, {x: NaN, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: true, tension: 0 } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/stepped/after.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: false, tension: 0, stepped: 'after' } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/stepped/before.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: false, tension: 0, stepped: 'before' } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/stepped/default.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: false, tension: 0, stepped: true } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/stepped/middle.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: false, tension: 0, stepped: 'middle' } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/tension/default.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: false } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/tension/one.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: false, tension: 1 } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.line/tension/zero.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [{x: 1, y: 10}, {x: 5, y: 0}, {x: 15, y: -10}, {x: 19, y: -5}], borderColor: 'red', fill: false, tension: 0 } ] }, options: { scales: { x: {type: 'linear', display: false, min: 0, max: 20}, y: {display: false, min: -15, max: 15} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.point/point-style-circle.json ================================================ { "config": { "type": "bubble", "data": { "datasets": [{ "data": [ {"x": 0, "y": 3, "r": 0}, {"x": 1, "y": 3, "r": 2}, {"x": 2, "y": 3, "r": 4}, {"x": 3, "y": 3, "r": 8}, {"x": 4, "y": 3, "r": 16}, {"x": 5, "y": 3, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "transparent", "borderWidth": 0 }, { "data": [ {"x": 0, "y": 2, "r": 0}, {"x": 1, "y": 2, "r": 2}, {"x": 2, "y": 2, "r": 4}, {"x": 3, "y": 2, "r": 8}, {"x": 4, "y": 2, "r": 16}, {"x": 5, "y": 2, "r": 32} ], "backgroundColor": "transparent", "borderColor": "#0000ff", "borderWidth": 1 }, { "data": [ {"x": 0, "y": 1, "r": 0}, {"x": 1, "y": 1, "r": 2}, {"x": 2, "y": 1, "r": 4}, {"x": 3, "y": 1, "r": 8}, {"x": 4, "y": 1, "r": 16}, {"x": 5, "y": 1, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "#0000ff", "borderWidth": 2 }] }, "options": { "responsive": false, "elements": { "point": { "pointStyle": "circle" } }, "layout": { "padding": 40 }, "scales": { "x": {"display": false}, "y": {"display": false} } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/element.point/point-style-cross-rot.json ================================================ { "config": { "type": "bubble", "data": { "datasets": [{ "data": [ {"x": 0, "y": 3, "r": 0}, {"x": 1, "y": 3, "r": 2}, {"x": 2, "y": 3, "r": 4}, {"x": 3, "y": 3, "r": 8}, {"x": 4, "y": 3, "r": 16}, {"x": 5, "y": 3, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "transparent", "borderWidth": 0 }, { "data": [ {"x": 0, "y": 2, "r": 0}, {"x": 1, "y": 2, "r": 2}, {"x": 2, "y": 2, "r": 4}, {"x": 3, "y": 2, "r": 8}, {"x": 4, "y": 2, "r": 16}, {"x": 5, "y": 2, "r": 32} ], "backgroundColor": "transparent", "borderColor": "#0000ff", "borderWidth": 1 }, { "data": [ {"x": 0, "y": 1, "r": 0}, {"x": 1, "y": 1, "r": 2}, {"x": 2, "y": 1, "r": 4}, {"x": 3, "y": 1, "r": 8}, {"x": 4, "y": 1, "r": 16}, {"x": 5, "y": 1, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "#0000ff", "borderWidth": 2 }] }, "options": { "responsive": false, "elements": { "point": { "pointStyle": "crossRot" } }, "layout": { "padding": 40 }, "scales": { "x": {"display": false}, "y": {"display": false} } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/element.point/point-style-cross.json ================================================ { "config": { "type": "bubble", "data": { "datasets": [{ "data": [ {"x": 0, "y": 3, "r": 0}, {"x": 1, "y": 3, "r": 2}, {"x": 2, "y": 3, "r": 4}, {"x": 3, "y": 3, "r": 8}, {"x": 4, "y": 3, "r": 16}, {"x": 5, "y": 3, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "transparent", "borderWidth": 0 }, { "data": [ {"x": 0, "y": 2, "r": 0}, {"x": 1, "y": 2, "r": 2}, {"x": 2, "y": 2, "r": 4}, {"x": 3, "y": 2, "r": 8}, {"x": 4, "y": 2, "r": 16}, {"x": 5, "y": 2, "r": 32} ], "backgroundColor": "transparent", "borderColor": "#0000ff", "borderWidth": 1 }, { "data": [ {"x": 0, "y": 1, "r": 0}, {"x": 1, "y": 1, "r": 2}, {"x": 2, "y": 1, "r": 4}, {"x": 3, "y": 1, "r": 8}, {"x": 4, "y": 1, "r": 16}, {"x": 5, "y": 1, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "#0000ff", "borderWidth": 2 }] }, "options": { "responsive": false, "elements": { "point": { "pointStyle": "cross" } }, "layout": { "padding": 40 }, "scales": { "x": {"display": false}, "y": {"display": false} } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/element.point/point-style-dash.json ================================================ { "config": { "type": "bubble", "data": { "datasets": [{ "data": [ {"x": 0, "y": 3, "r": 0}, {"x": 1, "y": 3, "r": 2}, {"x": 2, "y": 3, "r": 4}, {"x": 3, "y": 3, "r": 8}, {"x": 4, "y": 3, "r": 16}, {"x": 5, "y": 3, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "transparent", "borderWidth": 0 }, { "data": [ {"x": 0, "y": 2, "r": 0}, {"x": 1, "y": 2, "r": 2}, {"x": 2, "y": 2, "r": 4}, {"x": 3, "y": 2, "r": 8}, {"x": 4, "y": 2, "r": 16}, {"x": 5, "y": 2, "r": 32} ], "backgroundColor": "transparent", "borderColor": "#0000ff", "borderWidth": 1 }, { "data": [ {"x": 0, "y": 1, "r": 0}, {"x": 1, "y": 1, "r": 2}, {"x": 2, "y": 1, "r": 4}, {"x": 3, "y": 1, "r": 8}, {"x": 4, "y": 1, "r": 16}, {"x": 5, "y": 1, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "#0000ff", "borderWidth": 2 }] }, "options": { "responsive": false, "elements": { "point": { "pointStyle": "dash" } }, "layout": { "padding": 40 }, "scales": { "x": {"display": false}, "y": {"display": false} } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/element.point/point-style-image.js ================================================ var imageCanvas = document.createElement('canvas'); var imageContext = imageCanvas.getContext('2d'); imageCanvas.width = 40; imageCanvas.height = 40; imageContext.fillStyle = '#f00'; imageContext.beginPath(); imageContext.moveTo(20, 0); imageContext.lineTo(10, 40); imageContext.lineTo(20, 30); imageContext.closePath(); imageContext.fill(); imageContext.fillStyle = '#a00'; imageContext.beginPath(); imageContext.moveTo(20, 0); imageContext.lineTo(30, 40); imageContext.lineTo(20, 30); imageContext.closePath(); imageContext.fill(); module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5, 6, 7], datasets: [{ data: [0, 0, 0, 0, 0, 0, 0, 0], showLine: false }] }, options: { responsive: false, elements: { point: { pointStyle: imageCanvas, rotation: [0, 45, 90, 135, 180, 225, 270, 315] } }, layout: { padding: 20 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/element.point/point-style-line.json ================================================ { "config": { "type": "bubble", "data": { "datasets": [{ "data": [ {"x": 0, "y": 3, "r": 0}, {"x": 1, "y": 3, "r": 2}, {"x": 2, "y": 3, "r": 4}, {"x": 3, "y": 3, "r": 8}, {"x": 4, "y": 3, "r": 16}, {"x": 5, "y": 3, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "transparent", "borderWidth": 0 }, { "data": [ {"x": 0, "y": 2, "r": 0}, {"x": 1, "y": 2, "r": 2}, {"x": 2, "y": 2, "r": 4}, {"x": 3, "y": 2, "r": 8}, {"x": 4, "y": 2, "r": 16}, {"x": 5, "y": 2, "r": 32} ], "backgroundColor": "transparent", "borderColor": "#0000ff", "borderWidth": 1 }, { "data": [ {"x": 0, "y": 1, "r": 0}, {"x": 1, "y": 1, "r": 2}, {"x": 2, "y": 1, "r": 4}, {"x": 3, "y": 1, "r": 8}, {"x": 4, "y": 1, "r": 16}, {"x": 5, "y": 1, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "#0000ff", "borderWidth": 2 }] }, "options": { "responsive": false, "elements": { "point": { "pointStyle": "line" } }, "layout": { "padding": 40 }, "scales": { "x": {"display": false}, "y": {"display": false} } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/element.point/point-style-rect-rot.json ================================================ { "config": { "type": "bubble", "data": { "datasets": [{ "data": [ {"x": 0, "y": 3, "r": 0}, {"x": 1, "y": 3, "r": 2}, {"x": 2, "y": 3, "r": 4}, {"x": 3, "y": 3, "r": 8}, {"x": 4, "y": 3, "r": 16}, {"x": 5, "y": 3, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "transparent", "borderWidth": 0 }, { "data": [ {"x": 0, "y": 2, "r": 0}, {"x": 1, "y": 2, "r": 2}, {"x": 2, "y": 2, "r": 4}, {"x": 3, "y": 2, "r": 8}, {"x": 4, "y": 2, "r": 16}, {"x": 5, "y": 2, "r": 32} ], "backgroundColor": "transparent", "borderColor": "#0000ff", "borderWidth": 1 }, { "data": [ {"x": 0, "y": 1, "r": 0}, {"x": 1, "y": 1, "r": 2}, {"x": 2, "y": 1, "r": 4}, {"x": 3, "y": 1, "r": 8}, {"x": 4, "y": 1, "r": 16}, {"x": 5, "y": 1, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "#0000ff", "borderWidth": 2 }] }, "options": { "responsive": false, "elements": { "point": { "pointStyle": "rectRot" } }, "layout": { "padding": 40 }, "scales": { "x": {"display": false}, "y": {"display": false} } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/element.point/point-style-rect-rounded.json ================================================ { "config": { "type": "bubble", "data": { "datasets": [{ "data": [ {"x": 0, "y": 3, "r": 0}, {"x": 1, "y": 3, "r": 2}, {"x": 2, "y": 3, "r": 4}, {"x": 3, "y": 3, "r": 8}, {"x": 4, "y": 3, "r": 16}, {"x": 5, "y": 3, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "transparent", "borderWidth": 0 }, { "data": [ {"x": 0, "y": 2, "r": 0}, {"x": 1, "y": 2, "r": 2}, {"x": 2, "y": 2, "r": 4}, {"x": 3, "y": 2, "r": 8}, {"x": 4, "y": 2, "r": 16}, {"x": 5, "y": 2, "r": 32} ], "backgroundColor": "transparent", "borderColor": "#0000ff", "borderWidth": 1 }, { "data": [ {"x": 0, "y": 1, "r": 0}, {"x": 1, "y": 1, "r": 2}, {"x": 2, "y": 1, "r": 4}, {"x": 3, "y": 1, "r": 8}, {"x": 4, "y": 1, "r": 16}, {"x": 5, "y": 1, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "#0000ff", "borderWidth": 2 }] }, "options": { "responsive": false, "elements": { "point": { "pointStyle": "rectRounded" } }, "layout": { "padding": 40 }, "scales": { "x": {"display": false}, "y": {"display": false} } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/element.point/point-style-rect.json ================================================ { "config": { "type": "bubble", "data": { "datasets": [{ "data": [ {"x": 0, "y": 3, "r": 0}, {"x": 1, "y": 3, "r": 2}, {"x": 2, "y": 3, "r": 4}, {"x": 3, "y": 3, "r": 8}, {"x": 4, "y": 3, "r": 16}, {"x": 5, "y": 3, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "transparent", "borderWidth": 0 }, { "data": [ {"x": 0, "y": 2, "r": 0}, {"x": 1, "y": 2, "r": 2}, {"x": 2, "y": 2, "r": 4}, {"x": 3, "y": 2, "r": 8}, {"x": 4, "y": 2, "r": 16}, {"x": 5, "y": 2, "r": 32} ], "backgroundColor": "transparent", "borderColor": "#0000ff", "borderWidth": 1 }, { "data": [ {"x": 0, "y": 1, "r": 0}, {"x": 1, "y": 1, "r": 2}, {"x": 2, "y": 1, "r": 4}, {"x": 3, "y": 1, "r": 8}, {"x": 4, "y": 1, "r": 16}, {"x": 5, "y": 1, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "#0000ff", "borderWidth": 2 }] }, "options": { "responsive": false, "elements": { "point": { "pointStyle": "rect" } }, "layout": { "padding": 40 }, "scales": { "x": {"display": false}, "y": {"display": false} } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/element.point/point-style-star.json ================================================ { "config": { "type": "bubble", "data": { "datasets": [{ "data": [ {"x": 0, "y": 3, "r": 0}, {"x": 1, "y": 3, "r": 2}, {"x": 2, "y": 3, "r": 4}, {"x": 3, "y": 3, "r": 8}, {"x": 4, "y": 3, "r": 16}, {"x": 5, "y": 3, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "transparent", "borderWidth": 0 }, { "data": [ {"x": 0, "y": 2, "r": 0}, {"x": 1, "y": 2, "r": 2}, {"x": 2, "y": 2, "r": 4}, {"x": 3, "y": 2, "r": 8}, {"x": 4, "y": 2, "r": 16}, {"x": 5, "y": 2, "r": 32} ], "backgroundColor": "transparent", "borderColor": "#0000ff", "borderWidth": 1 }, { "data": [ {"x": 0, "y": 1, "r": 0}, {"x": 1, "y": 1, "r": 2}, {"x": 2, "y": 1, "r": 4}, {"x": 3, "y": 1, "r": 8}, {"x": 4, "y": 1, "r": 16}, {"x": 5, "y": 1, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "#0000ff", "borderWidth": 2 }] }, "options": { "responsive": false, "elements": { "point": { "pointStyle": "star" } }, "layout": { "padding": 40 }, "scales": { "x": {"display": false}, "y": {"display": false} } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/element.point/point-style-triangle.json ================================================ { "config": { "type": "bubble", "data": { "datasets": [{ "data": [ {"x": 0, "y": 3, "r": 0}, {"x": 1, "y": 3, "r": 2}, {"x": 2, "y": 3, "r": 4}, {"x": 3, "y": 3, "r": 8}, {"x": 4, "y": 3, "r": 16}, {"x": 5, "y": 3, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "transparent", "borderWidth": 0 }, { "data": [ {"x": 0, "y": 2, "r": 0}, {"x": 1, "y": 2, "r": 2}, {"x": 2, "y": 2, "r": 4}, {"x": 3, "y": 2, "r": 8}, {"x": 4, "y": 2, "r": 16}, {"x": 5, "y": 2, "r": 32} ], "backgroundColor": "transparent", "borderColor": "#0000ff", "borderWidth": 1 }, { "data": [ {"x": 0, "y": 1, "r": 0}, {"x": 1, "y": 1, "r": 2}, {"x": 2, "y": 1, "r": 4}, {"x": 3, "y": 1, "r": 8}, {"x": 4, "y": 1, "r": 16}, {"x": 5, "y": 1, "r": 32} ], "backgroundColor": "#00ff00", "borderColor": "#0000ff", "borderWidth": 2 }] }, "options": { "responsive": false, "elements": { "point": { "pointStyle": "triangle" } }, "layout": { "padding": 40 }, "scales": { "x": {"display": false}, "y": {"display": false} } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/element.point/rotation.js ================================================ var gradient; var datasets = ['circle', 'cross', 'crossRot', 'dash', 'line', 'rect', 'rectRounded', 'rectRot', 'star', 'triangle', false].map(function(style, y) { return { pointStyle: style, data: Array.apply(null, Array(17)).map(function(v, x) { return {x: x, y: 11 - y}; }) }; }); var angles = Array.apply(null, Array(17)).map(function(v, i) { return -180 + i * 22.5; }); module.exports = { config: { type: 'bubble', data: { datasets: datasets }, options: { responsive: false, elements: { point: { rotation: angles, radius: 10, backgroundColor: function(context) { if (!gradient) { gradient = context.chart.ctx.createLinearGradient(0, 0, 512, 256); gradient.addColorStop(0, '#ff0000'); gradient.addColorStop(1, '#0000ff'); } return gradient; }, borderColor: '#cccccc' } }, layout: { padding: 20 }, scales: { x: {display: false}, y: {display: false} } } }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/mixed/bar+line-stacked.js ================================================ module.exports = { config: { data: { datasets: [ { type: 'bar', stack: 'mixed', data: [5, 20, 1, 10], backgroundColor: '#00ff00', borderColor: '#ff0000' }, { type: 'line', stack: 'mixed', data: [6, 16, 3, 19], borderColor: '#0000ff', fill: false }, ] }, options: { scales: { x: { axis: 'y', labels: ['a', 'b', 'c', 'd'] }, y: { stacked: true } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/mixed/bar+line.js ================================================ module.exports = { config: { data: { datasets: [ { type: 'line', data: [6, 16, 3, 19], borderColor: '#0000ff', fill: false }, { type: 'bar', data: [5, 20, 1, 10], backgroundColor: '#00ff00', borderColor: '#ff0000' } ] }, options: { indexAxis: 'y', scales: { x: { position: 'top' }, y: { axis: 'y', labels: ['a', 'b', 'c', 'd'] } } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/plugin.colors/bar.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [0, 5, 10, null, -10, -5], }, { data: [10, 2, 3, null, 10, 5] } ] }, options: { scales: { x: { ticks: { display: false, } }, y: { ticks: { display: false, } } }, plugins: { legend: false, colors: { enabled: true } } } } }; ================================================ FILE: test/fixtures/plugin.colors/bubble.js ================================================ module.exports = { config: { type: 'bubble', data: { datasets: [{ data: [{x: 12, y: 54, r: 22.4}] }, { data: [{x: 18, y: 38, r: 25}] }] }, options: { scales: { x: { ticks: { display: false, } }, y: { ticks: { display: false, } } }, plugins: { legend: false, colors: { enabled: true } } } } }; ================================================ FILE: test/fixtures/plugin.colors/chart-options-colors.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [0, 5, 10, null, -10, -5], }, { data: [10, 2, 3, null, 10, 5] } ] }, options: { backgroundColor: ['red', 'green'], scales: { x: { ticks: { display: false, } }, y: { ticks: { display: false, } } }, plugins: { legend: false, colors: { enabled: true } } } } }; ================================================ FILE: test/fixtures/plugin.colors/doughnut.js ================================================ module.exports = { config: { type: 'doughnut', data: { datasets: [ { data: [0, 2, 4, null, 6, 8] }, { data: [5, 1, 6, 2, null, 9] } ] }, options: { plugins: { legend: false, colors: { enabled: true } } } } }; ================================================ FILE: test/fixtures/plugin.colors/dynamic-datasets-default.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [5, 5, 5, 5, 5, 5] } ] }, options: { scales: { x: { ticks: { display: false, } }, y: { ticks: { display: false, } } }, plugins: { legend: false, colors: { enabled: true } } } }, options: { run(chart) { chart.data.datasets.push({ data: [5, 5, 5, 5, 5, 5] }); chart.update(); } } }; ================================================ FILE: test/fixtures/plugin.colors/dynamic-datasets-force-override.js ================================================ module.exports = { config: { type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [5, 5, 5, 5, 5, 5] } ] }, options: { scales: { x: { ticks: { display: false } }, y: { ticks: { display: false } } }, plugins: { legend: false, colors: { enabled: true, forceOverride: true } } } }, options: { run(chart) { chart.data.datasets.push({ data: [5, 5, 5, 5, 5, 5] }); chart.update(); } } }; ================================================ FILE: test/fixtures/plugin.colors/line.js ================================================ module.exports = { config: { type: 'line', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [0, 5, 10, null, -10, -5], }, { data: [10, 2, 3, null, 10, 5] } ] }, options: { scales: { x: { ticks: { display: false, } }, y: { ticks: { display: false, } } }, plugins: { legend: false, colors: { enabled: true } } } } }; ================================================ FILE: test/fixtures/plugin.colors/mixed.js ================================================ module.exports = { config: { data: { labels: [0, 1, 2, 3], datasets: [ { type: 'line', data: [5, 20, 1, 10], }, { type: 'bar', data: [6, 16, 3, 19] }, { type: 'pie', data: [5, 20, 1, 10], } ] }, options: { scales: { x: { ticks: { display: false, } }, y: { ticks: { display: false, } } }, plugins: { legend: false, colors: { enabled: true } } } } }; ================================================ FILE: test/fixtures/plugin.colors/pie.js ================================================ module.exports = { config: { type: 'pie', data: { datasets: [ { data: [0, 2, 4, null, 6, 8] }, { data: [5, 1, 6, 2, null, 9] } ] }, options: { plugins: { legend: false, colors: { enabled: true } } } } }; ================================================ FILE: test/fixtures/plugin.colors/polarArea.js ================================================ module.exports = { config: { type: 'polarArea', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [0, 2, 4, null, 6, 8] } ] }, options: { scales: { r: { ticks: { display: false } } }, plugins: { legend: false, colors: { enabled: true } } } } }; ================================================ FILE: test/fixtures/plugin.colors/radar.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [0, 1, 2, 3, 4, 5], datasets: [ { data: [0, 5, 10, null, -10, -5] }, { data: [4, -5, -10, null, 10, 5] } ] }, options: { scales: { r: { ticks: { display: false }, pointLabels: { display: false, } } }, plugins: { legend: false, colors: { enabled: true } } } } }; ================================================ FILE: test/fixtures/plugin.colors/scatter.js ================================================ module.exports = { config: { type: 'scatter', data: { datasets: [{ data: [{x: 10, y: 15}, {x: 15, y: 10}], pointRadius: 10, showLine: true, label: 'dataset1' }, { data: [{x: 20, y: 45}, {x: 5, y: 15}], pointRadius: 20, label: 'dataset2' }], }, options: { scales: { x: { ticks: { display: false, } }, y: { ticks: { display: false, } } }, plugins: { legend: false, colors: { enabled: true }, } } } }; ================================================ FILE: test/fixtures/plugin.filler/line/above-below-vertical-linechart.js ================================================ module.exports = { config: { type: 'line', data: { labels: [1, 2, 3, 4], datasets: [ { data: [200, 400, 200, 400], cubicInterpolationMode: 'monotone', tension: 0.4, spanGaps: true, borderColor: 'blue', pointRadius: 0, fill: { target: 1, below: 'rgba(255, 0, 0, 0.4)', above: 'rgba(53, 221, 53, 0.4)', } }, { data: [400, 200, 400, 200], cubicInterpolationMode: 'monotone', tension: 0.4, spanGaps: true, borderColor: 'orange', pointRadius: 0, }, ] }, options: { indexAxis: 'y', // maintainAspectRatio: false, plugins: { filler: { propagate: false }, datalabels: { display: false }, legend: { display: false }, } } } }; ================================================ FILE: test/fixtures/plugin.filler/line/before-dataset-draw.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['15:00', '16:00', '17:00', '18:00', '19:00', '20:00'], datasets: [ { borderColor: '#00ADEE80', backgroundColor: '#00ADEE', data: [0, 1, 1, 2, 2, 0], }, { borderColor: '#BD262880', backgroundColor: '#BD2628', data: [0, 2, 2, 1, 1, 1], } ] }, options: { borderWidth: 4, fill: true, radius: 20, pointBackgroundColor: '#ffff', cubicInterpolationMode: 'monotone', plugins: { legend: false, filler: { drawTime: 'beforeDatasetDraw' } }, scales: { x: { display: false, }, y: { display: false } } } } }; ================================================ FILE: test/fixtures/plugin.filler/line/before-datasets-draw.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['15:00', '16:00', '17:00', '18:00', '19:00', '20:00'], datasets: [ { borderColor: '#00ADEE80', backgroundColor: '#00ADEE', data: [0, 1, 1, 2, 2, 0], }, { borderColor: '#BD262880', backgroundColor: '#BD2628', data: [0, 2, 2, 1, 1, 1], } ] }, options: { borderWidth: 4, fill: true, radius: 20, pointBackgroundColor: '#ffff', cubicInterpolationMode: 'monotone', plugins: { legend: false, filler: { drawTime: 'beforeDatasetsDraw' } }, scales: { x: { display: false, }, y: { display: false } } } } }; ================================================ FILE: test/fixtures/plugin.filler/line/boundary/above-below-line-null-start.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"], "datasets": [{ "borderColor": "rgb(42, 90, 145)", "data": [null, 12, 30, 36, 45, 53, 68, 79, null, 95, 18, 18, 180], "fill": { "target": "+1", "above": "rgba(4, 142, 43, 0.5)", "below": "rgba(241, 49, 34, 0.5)" } }, { "borderColor": "#00ADEE", "data": [null, 0, 0, 0, 0, 0, 20, 108, null, 72, 72, 72, 72], "fill": false }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/above-below-line-null.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"], "datasets": [{ "borderColor": "rgb(42, 90, 145)", "data": [4, 12, 30, 36, 45, 53, 68, 79, null, 95, 18, null, 18, 180], "fill": { "target": "+1", "above": "rgba(4, 142, 43, 0.5)", "below": "rgba(241, 49, 34, 0.5)" } }, { "borderColor": "#00ADEE", "data": [0, 0, 0, 0, 0, 0, 20, 108, null, 72, 72, null, 72, 72], "fill": false }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/end-span.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [5.5, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [8, 7, 6.5, -6, -4, -6, 4, 5, 8] }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "end", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/end.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [5.5, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [8, 7, 6.5, -6, -4, -6, 4, 5, 8] }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "end", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/origin-span-dual.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [6, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 64, 192, 0.25)", "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": { "target": "origin", "below": "rgba(255, 0, 0, 0.25)" }, "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/origin-span.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [6, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 64, 192, 0.25)", "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "origin", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/origin-spline-above.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 4, 2, 1, -1, 1, 2] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] }, { "backgroundColor": "rgba(128, 0, 128, 0.25)", "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent", "fill": { "target": "origin", "below": "transparent" } } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/origin-spline-span.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 4, 2, 1, -1, 1, 2] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] }, { "backgroundColor": "rgba(128, 0, 128, 0.25)", "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent", "fill": "origin" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/origin-spline.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 4, 2, 1, -1, 1, 2] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] }, { "backgroundColor": "rgba(128, 0, 128, 0.25)", "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent", "fill": "origin" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/origin-stepped-span.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 4, 2, 1, -1, 1, 2] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] }, { "backgroundColor": "rgba(128, 0, 128, 0.25)", "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent", "stepped": true, "fill": "origin" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/origin-stepped.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 4, 2, 1, -1, 1, 2] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] }, { "backgroundColor": "rgba(128, 0, 128, 0.25)", "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent", "stepped": true, "fill": "origin" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/origin.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [6, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 64, 192, 0.25)", "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "origin", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/start-span.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [6, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "start", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/boundary/start.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [6, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "start", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/dataset/border.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": 1 }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": "+1" }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [0, 2, 0, -2, 0, 2, 0], "fill": 3 }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": "-2" }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": "-1" }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "black", "borderWidth": 5, "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/dataset/clip-bounds-x-off.js ================================================ const labels = [1, 2, 3, 4, 5, 6, 7]; const values = [65, 59, 80, 81, 56, 55, 40]; module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/12052', config: { type: 'line', data: { labels, datasets: [ { data: values.map(v => v - 10), fill: '1', borderColor: 'rgb(255, 0, 0)', backgroundColor: 'rgba(255, 0, 0, 0.25)', xAxisID: 'x1', }, { data: values, fill: false, borderColor: 'rgb(255, 0, 0)', xAxisID: 'x1', }, { data: values, fill: false, borderColor: 'rgb(0, 0, 255)', xAxisID: 'x2', }, { data: values.map(v => v + 10), fill: '-1', borderColor: 'rgb(0, 0, 255)', backgroundColor: 'rgba(0, 0, 255, 0.25)', xAxisID: 'x2', } ] }, options: { clip: false, indexAxis: 'y', animation: false, responsive: false, plugins: { legend: false, title: false, tooltip: false }, elements: { point: { radius: 0 }, line: { cubicInterpolationMode: 'monotone', borderColor: 'transparent', tension: 0 } }, scales: { x2: { axis: 'x', stack: 'stack', max: 80, display: false, }, x1: { min: 50, axis: 'x', stack: 'stack', display: false, }, y: { display: false, } } } }, }; ================================================ FILE: test/fixtures/plugin.filler/line/dataset/clip-bounds-x.js ================================================ const labels = [1, 2, 3, 4, 5, 6, 7]; const values = [65, 59, 80, 81, 56, 55, 40]; module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/12052', config: { type: 'line', data: { labels, datasets: [ { data: values.map(v => v - 10), fill: '1', borderColor: 'rgb(255, 0, 0)', backgroundColor: 'rgba(255, 0, 0, 0.25)', xAxisID: 'x1', }, { data: values, fill: false, borderColor: 'rgb(255, 0, 0)', xAxisID: 'x1', }, { data: values, fill: false, borderColor: 'rgb(0, 0, 255)', xAxisID: 'x2', }, { data: values.map(v => v + 10), fill: '-1', borderColor: 'rgb(0, 0, 255)', backgroundColor: 'rgba(0, 0, 255, 0.25)', xAxisID: 'x2', } ] }, options: { indexAxis: 'y', animation: false, responsive: false, plugins: { legend: false, title: false, tooltip: false }, elements: { point: { radius: 0 }, line: { cubicInterpolationMode: 'monotone', borderColor: 'transparent', tension: 0 } }, scales: { x2: { axis: 'x', stack: 'stack', max: 80, display: false, }, x1: { min: 50, axis: 'x', stack: 'stack', display: false, }, y: { display: false, } } } }, }; ================================================ FILE: test/fixtures/plugin.filler/line/dataset/clip-bounds-y-off.js ================================================ const labels = [1, 2, 3, 4, 5, 6, 7]; const values = [65, 59, 80, 81, 56, 55, 40]; module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/12052', config: { type: 'line', data: { labels, datasets: [ { data: values.map(v => v - 10), fill: '1', borderColor: 'rgb(255, 0, 0)', backgroundColor: 'rgba(255, 0, 0, 0.25)', yAxisID: 'y1', }, { data: values, fill: false, borderColor: 'rgb(255, 0, 0)', yAxisID: 'y1', }, { data: values, fill: false, borderColor: 'rgb(0, 0, 255)', yAxisID: 'y2', }, { data: values.map(v => v + 10), fill: '-1', borderColor: 'rgb(0, 0, 255)', backgroundColor: 'rgba(0, 0, 255, 0.25)', yAxisID: 'y2', } ] }, options: { clip: false, animation: false, responsive: false, plugins: { legend: false, title: false, tooltip: false }, elements: { point: { radius: 0 }, line: { cubicInterpolationMode: 'monotone', borderColor: 'transparent', tension: 0 } }, scales: { y2: { axis: 'y', stack: 'stack', max: 80, display: false, }, y1: { min: 50, axis: 'y', stack: 'stack', display: false, }, x: { display: false, } } } }, }; ================================================ FILE: test/fixtures/plugin.filler/line/dataset/clip-bounds-y.js ================================================ const labels = [1, 2, 3, 4, 5, 6, 7]; const values = [65, 59, 80, 81, 56, 55, 40]; module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/12052', config: { type: 'line', data: { labels, datasets: [ { data: values.map(v => v - 10), fill: '1', borderColor: 'rgb(255, 0, 0)', backgroundColor: 'rgba(255, 0, 0, 0.25)', yAxisID: 'y1', }, { data: values, fill: false, borderColor: 'rgb(255, 0, 0)', yAxisID: 'y1', }, { data: values, fill: false, borderColor: 'rgb(0, 0, 255)', yAxisID: 'y2', }, { data: values.map(v => v + 10), fill: '-1', borderColor: 'rgb(0, 0, 255)', backgroundColor: 'rgba(0, 0, 255, 0.25)', yAxisID: 'y2', } ] }, options: { animation: false, responsive: false, plugins: { legend: false, title: false, tooltip: false }, elements: { point: { radius: 0 }, line: { cubicInterpolationMode: 'monotone', borderColor: 'transparent', tension: 0 } }, scales: { y2: { axis: 'y', stack: 'stack', max: 80, display: false, }, y1: { min: 50, axis: 'y', stack: 'stack', display: false, }, x: { display: false, } } } }, }; ================================================ FILE: test/fixtures/plugin.filler/line/dataset/dual.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [0, 1, 2, -1, 0, 2, 1, -1, -2], "fill": { "target": "+1", "above": "rgba(255, 0, 0, 0.25)", "below": "rgba(0, 0, 255, 0.25)" } }, { "data": [0, 0, 0, 0, 0, 0, 0, 0, 0] }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/dataset/interpolated.js ================================================ const data1 = []; const data2 = []; const data3 = []; for (let i = 0; i < 200; i++) { const a = i / Math.PI / 10; data1.push({x: i, y: i < 86 || i > 104 && i < 178 ? Math.sin(a) : NaN}); if (i % 10 === 0) { data2.push({x: i, y: Math.cos(a)}); } if (i % 15 === 0) { data3.push({x: i, y: Math.cos(a + Math.PI / 2)}); } } module.exports = { config: { type: 'line', data: { datasets: [{ borderColor: 'rgba(255, 0, 0, 0.5)', backgroundColor: 'rgba(255, 0, 0, 0.25)', data: data1, fill: false, }, { borderColor: 'rgba(0, 0, 255, 0.5)', backgroundColor: 'rgba(0, 0, 255, 0.25)', data: data2, fill: 0, }, { borderColor: 'rgba(0, 255, 0, 0.5)', backgroundColor: 'rgba(0, 255, 0, 0.25)', data: data3, fill: 1, }] }, options: { animation: false, responsive: false, datasets: { line: { tension: 0.4, borderWidth: 1, pointRadius: 1.5, } }, plugins: { legend: false, title: false, tooltip: false }, scales: { x: { type: 'linear', display: false }, y: { type: 'linear', display: false } } } }, options: { canvas: { height: 512, width: 512 } } }; ================================================ FILE: test/fixtures/plugin.filler/line/dataset/no-border.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": 1 }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": "+1" }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [0, 2, 0, -2, 0, 2, 0], "fill": 3 }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": "-2" }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": "-1" }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/dataset/span-dual.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": { "target": 1, "above": "rgba(255, 0, 0, 0.25)", "below": "rgba(122, 0, 0, 0.25)" } }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": { "target": "+1", "above": "rgba(0, 255, 0, 0.25)", "below": "rgba(0, 255, 120, 0.25)" } }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": { "target": "-2", "above": "rgba(255, 0, 255, 0.25)", "below": "rgba(255, 0, 120, 0.25)" } }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": { "target": "-1", "above": "rgba(255, 255, 0, 0.25)", "below": "rgba(255, 120, 0, 0.25)" } }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/dataset/span.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": 1 }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": "+1" }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [0, 2, 0, -2, 0, 2, 0], "fill": 3 }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": "-2" }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": "-1" }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/dataset/spline-span-above.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": { "target": 1, "below": "transparent" } }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": { "target": "+1", "below": "transparent" } }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": { "target": "-2", "below": "transparent" } }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": { "target": "-1", "below": "transparent" } }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/dataset/spline-span-below.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": { "target": 1, "above": "transparent" } }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": { "target": "+1", "above": "transparent" } }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": { "target": "-2", "above": "transparent" } }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": { "target": "-1", "above": "transparent" } }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/dataset/spline-span.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": 1 }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": "+1" }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [0, 2, 0, -2, 0, 2, 0], "fill": 3 }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": "-2" }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": "-1" }] }, "options": { "responsive": false, "spanGaps": true, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/dataset/spline.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": 1 }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": "+1" }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [0, 2, 0, -2, 0, 2, 0], "fill": 3 }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": "-2" }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": "-1" }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "cubicInterpolationMode": "monotone", "borderColor": "transparent" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/dataset/stepped.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "stepped": true, "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": 1 }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "stepped": "after", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": "+1" }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "stepped": "before", "data": [0, 2, 0, -2, 0, 2, 0], "fill": 3 }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "stepped": "middle", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": "-2" }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "stepped": false, "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": "-1" }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "black" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetDraw.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['0', '1', '2', '3', '4', '5'], datasets: [{ backgroundColor: 'red', data: [3, -3, 0, 5, -5, 0], fill: false }] }, options: { plugins: { legend: false, title: false, filler: { drawTime: 'beforeDatasetDraw' } }, } }, }; ================================================ FILE: test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDatasetsDraw.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['0', '1', '2', '3', '4', '5'], datasets: [{ backgroundColor: 'red', data: [3, -3, 0, 5, -5, 0], fill: false }] }, options: { plugins: { legend: false, title: false, filler: { drawTime: 'beforeDatasetsDraw' } }, } }, }; ================================================ FILE: test/fixtures/plugin.filler/line/drawTimeFillFalse/beforeDraw.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['0', '1', '2', '3', '4', '5'], datasets: [{ backgroundColor: 'red', data: [3, -3, 0, 5, -5, 0], fill: false }] }, options: { plugins: { legend: false, title: false, filler: { drawTime: 'beforeDraw' } }, } }, }; ================================================ FILE: test/fixtures/plugin.filler/line/points-outside-canvas-initial.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8699', config: { type: 'line', data: { datasets: [{ backgroundColor: 'red', data: [{x: 0, y: 3}, {x: 2, y: -3}, {x: 4, y: 0}, {x: 6, y: 5}, {x: 8, y: -5}, {x: 10, y: 0}], fill: 'origin' }] }, options: { plugins: { legend: false, title: false, }, scales: { x: { display: false, type: 'linear', min: 5 }, y: { display: false } } } }, }; ================================================ FILE: test/fixtures/plugin.filler/line/points-outside-canvas-update.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8699', config: { type: 'line', data: { datasets: [{ backgroundColor: 'red', data: [{x: 0, y: 3}, {x: 2, y: -3}, {x: 4, y: 0}, {x: 6, y: 5}, {x: 8, y: -5}, {x: 10, y: 0}], fill: 'origin' }] }, options: { plugins: { legend: false, title: false, }, scales: { x: { type: 'linear', display: false }, y: { display: false } } } }, options: { run(chart) { chart.scales.x.options.min = 5; chart.update(); } } }; ================================================ FILE: test/fixtures/plugin.filler/line/segments/alignToPixels.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ { data: [ {x: 0, y: 0}, {x: 1, y: 20}, {x: 1.00001, y: 30}, {x: 2, y: 100}, {x: 2.00001, y: 100} ], backgroundColor: '#FF000070', borderColor: 'black', radius: 0, segment: { borderDash: ctx => ctx.p0.parsed.x > 1 ? [10, 5] : undefined, }, fill: true } ] }, options: { plugins: { legend: false }, scales: { x: { type: 'linear', alignToPixels: true, display: false }, y: { display: false } } } }, options: { canvas: { width: 300, height: 240 } } }; ================================================ FILE: test/fixtures/plugin.filler/line/segments/gap.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['a', 'b', 'c', 'd', 'e', 'f'], datasets: [{ data: [1, 3, NaN, NaN, 2, 1], borderColor: 'transparent', backgroundColor: 'black', fill: true, segment: { backgroundColor: ctx => ctx.p0.skip || ctx.p1.skip ? 'red' : undefined, }, spanGaps: true }] }, options: { scales: { x: {display: false}, y: {display: false} } } } }; ================================================ FILE: test/fixtures/plugin.filler/line/segments/slope.js ================================================ function slope({p0, p1}) { return (p0.y - p1.y) / (p1.x - p0.x); } module.exports = { config: { type: 'line', data: { labels: ['a', 'b', 'c', 'd', 'e', 'f'], datasets: [{ data: [1, 2, 3, 3, 2, 1], backgroundColor: 'black', borderColor: 'orange', fill: true, segment: { backgroundColor: ctx => slope(ctx) > 0 ? 'green' : slope(ctx) < 0 ? 'red' : undefined, } }] }, options: { plugins: { legend: false }, scales: { x: {display: false}, y: {display: false} } } } }; ================================================ FILE: test/fixtures/plugin.filler/line/shape.js ================================================ const data = []; for (let rad = 0; rad <= Math.PI * 2; rad += Math.PI / 45) { data.push({ x: Math.cos(rad), y: Math.sin(rad) }); } module.exports = { config: { type: 'line', data: { datasets: [{ data, fill: 'shape', backgroundColor: 'rgba(255, 0, 0, 0.5)', }] }, options: { plugins: { legend: false }, scales: { x: { type: 'linear', display: false }, y: { type: 'linear', display: false }, }, } } }; ================================================ FILE: test/fixtures/plugin.filler/line/stack-multiple-scales.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['0', '1', '2', '3'], datasets: [{ backgroundColor: 'rgba(255, 0, 0, 0.5)', data: [null, 1, 1, 1], fill: 'stack' }, { backgroundColor: 'rgba(0, 255, 0, 0.5)', data: [null, 2, 2, 2], fill: 'stack' }, { backgroundColor: 'rgba(0, 0, 255, 0.5)', data: [null, 3, 3, 3], fill: 'stack' }, { backgroundColor: 'rgba(255, 0, 255, 0.5)', data: [0.5, 0.5, 0.5, null], fill: 'stack', yAxisID: 'y2' }, { backgroundColor: 'rgba(0, 0, 0, 0.5)', data: [1.5, 1.5, 1.5, null], fill: 'stack', yAxisID: 'y2' }, { backgroundColor: 'rgba(255, 255, 0, 0.5)', data: [2.5, 2.5, 2.5, null], fill: 'stack', yAxisID: 'y2' }] }, options: { responsive: false, spanGaps: false, scales: { x: { display: false }, y: { position: 'right', stacked: true, min: 0 }, y2: { position: 'left', stacked: true, min: 0 } }, elements: { point: { radius: 0 }, line: { borderColor: 'transparent', tension: 0 } }, plugins: { legend: false, title: false, tooltip: false } } }, options: { spriteText: true, canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/plugin.filler/line/stack.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, 1, 0, 1, null, 0, 1], "fill": "stack" }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 1, null, 1, 0, null, 1, 1, 0], "fill": "stack" }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [0, 2, null, 2, 0, 2, 0], "fill": "stack" }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, null, 0, 2, 0, 2, 0, 2], "fill": "stack" }, { "backgroundColor": "rgba(0, 0, 0, 0.25)", "data": [null, null, null, 2, null, 2, 2], "fill": "stack" }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, 1, 3, 1, 1, 3, 1, 1], "fill": "stack" }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false, "stacked": true, "min": 0 } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/value.json ================================================ { "config": { "type": "line", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [{ "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [-4, 4, 0, -1, 0, 1, 0, -1, 0], "fill": { "value": 2 } }] }, "options": { "responsive": false, "spanGaps": false, "scales": { "x": { "display": false }, "y": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.filler/line/vertical.js ================================================ const data = [ {y: 1, x: 12}, {y: 3, x: 14}, {y: 4, x: 20}, {y: 6, x: 13}, {y: 9, x: 18}, ]; module.exports = { config: { type: 'line', data: { datasets: [{ data: data, borderColor: 'red', fill: false, }, { data: data.map((v) => ({y: v.y, x: 2 * v.x - 1.5 * v.y})), fill: '-1', borderColor: 'blue', backgroundColor: 'rgba(255, 200, 0, 0.5)', }] }, options: { indexAxis: 'y', radius: 0, plugins: { legend: false }, scales: { x: { display: false, type: 'linear' }, y: { display: false, type: 'linear' } } } } }; ================================================ FILE: test/fixtures/plugin.filler/radar/beforeDraw.js ================================================ module.exports = { config: { type: 'radar', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [{ label: '# of Votes', data: [9, 7, 3, 5, 2, 3], fill: 'origin', borderColor: 'red', backgroundColor: 'green', pointRadius: 12, pointBackgroundColor: 'red' }] }, options: { layout: { padding: 20 }, plugins: { legend: false, filler: { drawTime: 'beforeDraw' } }, scales: { r: { angleLines: { color: 'rgba(0,0,0,0.5)', lineWidth: 2 }, grid: { color: 'rgba(0,0,0,0.5)', lineWidth: 2 }, pointLabels: { display: false }, ticks: { beginAtZero: true, display: false }, } } } } }; ================================================ FILE: test/fixtures/plugin.filler/radar/boundary/end-circular.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [5.5, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [8, 7, 6.5, -6, -4, -6, 4, 5, 8] } ] }, "options": { "responsive": false, "spanGaps": false, "scales": { "r": { "display": false, "grid": { "circular": true } } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "end" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/boundary/end-span.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [5.5, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [8, 7, 6.5, -6, -4, -6, 4, 5, 8] } ] }, "options": { "responsive": false, "spanGaps": true, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "end" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/boundary/end.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [5.5, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [8, 7, 6.5, -6, -4, -6, 4, 5, 8] } ] }, "options": { "responsive": false, "spanGaps": false, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "end" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/boundary/origin-circular.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 4, 2, 1, -1, 1, 2] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] }, { "backgroundColor": "rgba(128, 0, 128, 0.25)", "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] } ] }, "options": { "responsive": false, "spanGaps": false, "scales": { "r": { "display": false, "grid": { "circular": true } } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "origin" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/boundary/origin-span.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [6, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 64, 192, 0.25)", "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] } ] }, "options": { "responsive": false, "spanGaps": true, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "origin" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/boundary/origin-spline-span.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 4, 2, 1, -1, 1, 2] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] }, { "backgroundColor": "rgba(128, 0, 128, 0.25)", "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] } ] }, "options": { "responsive": false, "spanGaps": true, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "tension": 0.5, "fill": "origin" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/boundary/origin-spline.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 4, 2, 1, -1, 1, 2] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [4, 2, null, 3, 2.5, null, -2, 1.5, 3] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [3.5, 2, 1, 2.5, -2, 3, -1, null, null] }, { "backgroundColor": "rgba(128, 0, 128, 0.25)", "data": [5, 6, 5, -2, -4, -3, 4, 2, 4.5] } ] }, "options": { "responsive": false, "spanGaps": false, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "tension": 0.5, "fill": "origin" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/boundary/origin.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 192, 0, 0.25)", "data": [6, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(192, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 64, 192, 0.25)", "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] } ] }, "options": { "responsive": false, "spanGaps": false, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "origin" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/boundary/start-circular.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [6, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] } ] }, "options": { "responsive": false, "spanGaps": false, "scales": { "r": { "display": false, "grid": { "circular": true } } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "start" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/boundary/start-span.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [6, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] } ] }, "options": { "responsive": false, "spanGaps": true, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "start" } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/boundary/start.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [null, null, 2, 3, 4, -4, -2, 1, 0] }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [6, 2, null, 4, 5, null, null, 2, 1] }, { "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [7, 3, 4, 5, 6, 1, 4, null, null] }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [8, 7, 6, -6, -4, -6, 4, 5, 8] } ] }, "options": { "responsive": false, "spanGaps": false, "plugins": { "legend": false, "title": false }, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": "start" } } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/dataset/border.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": 1 }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": "+1" }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [0, 2, 0, -2, 0, 2, 0], "fill": 3 }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": "-2" }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": "-1" } ] }, "options": { "responsive": false, "spanGaps": false, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "black", "borderWidth": 5, "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/dataset/default.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": 1 }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": "+1" }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [0, 2, 0, -2, 0, 2, 0], "fill": 3 }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": "-2" }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": "-1" } ] }, "options": { "responsive": false, "spanGaps": false, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/dataset/order.js ================================================ module.exports = { config: { type: 'radar', data: { labels: ['English', 'Maths', 'Physics', 'Chemistry', 'Biology', 'History'], datasets: [ { order: 1, borderColor: '#D50000', backgroundColor: 'rgba(245, 205, 121,0.5)', data: [65, 75, 70, 80, 60, 80] }, { order: 0, backgroundColor: 'rgba(0, 168, 255,1)', data: [54, 65, 60, 70, 70, 75] } ] }, options: { plugins: { legend: false, title: false, tooltip: false }, scales: { r: { display: false } } } } }; ================================================ FILE: test/fixtures/plugin.filler/radar/dataset/span.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": 1 }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": "+1" }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [0, 2, 0, -2, 0, 2, 0], "fill": 3 }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": "-2" }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": "-1" } ] }, "options": { "responsive": false, "spanGaps": true, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "tension": 0 } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/dataset/spline.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(255, 0, 0, 0.25)", "data": [null, null, 0, -1, 0, 1, 0, -1, 0], "fill": 1 }, { "backgroundColor": "rgba(0, 255, 0, 0.25)", "data": [1, 0, null, 1, 0, null, -1, 0, 1], "fill": "+1" }, { "backgroundColor": "rgba(0, 0, 255, 0.25)", "data": [0, 2, 0, -2, 0, 2, 0], "fill": 3 }, { "backgroundColor": "rgba(255, 0, 255, 0.25)", "data": [2, 0, -2, 0, 2, 0, -2, 0, 2], "fill": "-2" }, { "backgroundColor": "rgba(255, 255, 0, 0.25)", "data": [3, 1, -1, -3, -1, 1, 3, 1, -1], "fill": "-1" } ] }, "options": { "responsive": false, "spanGaps": false, "plugins": { "legend": false, "title": false }, "scales": { "r": { "display": false } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "tension": 0.5 } } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.filler/radar/value.json ================================================ { "config": { "type": "radar", "data": { "labels": ["0", "1", "2", "3", "4", "5", "6", "7", "8"], "datasets": [ { "backgroundColor": "rgba(0, 0, 192, 0.25)", "data": [0, -4, 2, 4, 2, 1, -1, 1, 2] } ] }, "options": { "responsive": false, "spanGaps": false, "scales": { "r": { "display": false, "grid": { "circular": true } } }, "elements": { "point": { "radius": 0 }, "line": { "borderColor": "transparent", "fill": { "value": 3 } } }, "plugins": { "legend": false, "title": false, "tooltip": false } } }, "options": { "canvas": { "height": 256, "width": 256 } } } ================================================ FILE: test/fixtures/plugin.legend/borderRadius/legend-border-radius.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [ { label: '# of Votes', data: [12, 19, 3, 5, 2, 3], borderWidth: 1, borderColor: '#FF0000', backgroundColor: '#00FF00', }, { label: '# of Points', data: [7, 11, 5, 8, 3, 7], borderWidth: 2, borderColor: '#FF00FF', backgroundColor: '#0000FF', } ] }, options: { scales: { x: {display: false}, y: {display: false} }, plugins: { title: false, tooltip: false, filler: false, legend: { labels: { generateLabels: (chart) => { const items = Chart.defaults.plugins.legend.labels.generateLabels(chart); for (const item of items) { item.borderRadius = 5; } return items; } } } } } }, options: { spriteText: true, canvas: { width: 512, height: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/horizontal-rtl-hitbox.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9278', config: { type: 'pie', data: { labels: ['aaa', 'bb', 'c'], datasets: [{ data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], backgroundColor: 'red' }] }, options: { plugins: { legend: { position: 'top', rtl: 'true', } }, layout: { padding: { top: 50, left: 30, right: 30, bottom: 50 } } }, plugins: [{ id: 'legend-hit-box', afterDraw(chart) { const ctx = chart.ctx; ctx.save(); ctx.strokeStyle = 'green'; ctx.lineWidth = 1; const legend = chart.legend; legend.legendHitBoxes.forEach(box => { ctx.strokeRect(box.left, box.top, box.width, box.height); }); ctx.restore(); } }] }, options: { spriteText: true, canvas: { width: 400, height: 300 }, } }; ================================================ FILE: test/fixtures/plugin.legend/label-textAlign/center.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['aaaa', 'bb', 'c'], datasets: [ { data: [1, 2, 3] } ] }, options: { plugins: { legend: { position: 'right', labels: { textAlign: 'center' } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/label-textAlign/horizontal-left.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['aaaa', 'bb', 'c'], datasets: [ { data: [1, 2, 3] } ] }, options: { plugins: { legend: { position: 'top', labels: { textAlign: 'left' } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/label-textAlign/horizontal-right.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['aaaa', 'bb', 'c'], datasets: [ { data: [1, 2, 3] } ] }, options: { plugins: { legend: { position: 'top', labels: { textAlign: 'right' } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-left.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['aaaa', 'bb', 'c'], datasets: [ { data: [1, 2, 3] } ] }, options: { plugins: { legend: { position: 'top', rtl: true, labels: { textAlign: 'left' } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/label-textAlign/horizontal-rtl-right.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['aaaa', 'bb', 'c'], datasets: [ { data: [1, 2, 3] } ] }, options: { plugins: { legend: { rtl: true, position: 'top', labels: { textAlign: 'right' } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/label-textAlign/left.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['aaaa', 'bb', 'c'], datasets: [ { data: [1, 2, 3] } ] }, options: { plugins: { legend: { position: 'right', labels: { textAlign: 'left' } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/label-textAlign/right.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['aaaa', 'bb', 'c'], datasets: [ { data: [1, 2, 3] } ] }, options: { plugins: { legend: { position: 'right', labels: { textAlign: 'right' } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/label-textAlign/rtl-center.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['aaaa', 'bb', 'c'], datasets: [ { data: [1, 2, 3] } ] }, options: { plugins: { legend: { position: 'right', rtl: true, labels: { textAlign: 'center' } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/label-textAlign/rtl-left.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['aaaa', 'bb', 'c'], datasets: [ { data: [1, 2, 3] } ] }, options: { plugins: { legend: { position: 'right', rtl: true, labels: { textAlign: 'left' } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/label-textAlign/rtl-right.js ================================================ module.exports = { config: { type: 'pie', data: { labels: ['aaaa', 'bb', 'c'], datasets: [ { data: [1, 2, 3] } ] }, options: { plugins: { legend: { rtl: true, position: 'right', labels: { textAlign: 'right' } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-bottom-center-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 20, 10], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "bottom", "align": "center" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-bottom-center-single.json ================================================ { "config": { "type": "doughnut", "data": { "labels": [""], "datasets": [{ "data": [10], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "bottom", "align": "center" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-bottom-end-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "bottom", "align": "end" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-bottom-start-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "bottom", "align": "start" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-left-center-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "left", "align": "center" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-left-center-single.json ================================================ { "config": { "type": "doughnut", "data": { "labels": [""], "datasets": [{ "data": [10], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "left", "align": "center" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-left-default-center.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "left" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-left-end-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "left", "align": "end" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-left-start-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "left", "align": "start" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-point-style.json ================================================ { "config": { "type": "doughnut", "data": { "labels": [""], "datasets": [{ "data": [10], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "labels": { "pointStyle": "triangle", "usePointStyle": true } } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline-labels.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["Example Label", ["I like these colors", "Red", "Green", "Blue", "Yellow"], "Example Label", "Example Label", "Example Label"], "datasets": [{ "data": [10, 20, 30, 40, 50], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "right", "align": "center" } } } }, "options": { "spriteText": true, "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-right-center-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "right", "align": "center" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-right-center-single.json ================================================ { "config": { "type": "doughnut", "data": { "labels": [""], "datasets": [{ "data": [10], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "right", "align": "center" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-right-default-center.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "right" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-right-end-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "right", "align": "end" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-right-start-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "right", "align": "start" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-top-center-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 20, 10], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "top", "align": "center" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-top-center-single.json ================================================ { "config": { "type": "doughnut", "data": { "labels": [""], "datasets": [{ "data": [10], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "top", "align": "center" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-top-end-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "top", "align": "end" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-doughnut-top-start-mulitiline.json ================================================ { "config": { "type": "doughnut", "data": { "labels": ["", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""], "datasets": [{ "data": [10, 20, 30, 40, 50, 60, 70, 10, 20, 30, 40, 50, 60, 70, 10], "backgroundColor": "#00ff00", "borderWidth": 0 }] }, "options": { "plugins": { "legend": { "position": "top", "align": "start" } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/legend-line-chart-area.json ================================================ { "config": { "type": "line", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [{ "data": [10, 20, 30, 40, 50], "backgroundColor": "#00ff00", "borderWidth": 0, "label": "" }] }, "options": { "plugins": { "legend": { "position": "chartArea" } }, "scales": { "x": { "display": false }, "y": { "display": false } } } }, "options": { "canvas": { "height": 256, "width": 512 } } } ================================================ FILE: test/fixtures/plugin.legend/maxWidth/infinity.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [ { label: '# of Votes', data: [12, 19, 3, 5, 2, 3], borderWidth: 1 }, { label: '# of Points', data: [7, 11, 5, 8, 3, 7], borderWidth: 1 } ] }, options: { scales: { x: {display: false}, y: {display: false} }, plugins: { title: false, tooltip: false, filler: false, legend: { position: 'left', maxWidth: Infinity } } } }, options: { spriteText: true, canvas: { width: 150, height: 75 } } }; ================================================ FILE: test/fixtures/plugin.legend/maxWidth/undefined.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [ { label: '# of Votes', data: [12, 19, 3, 5, 2, 3], borderWidth: 1 }, { label: '# of Points', data: [7, 11, 5, 8, 3, 7], borderWidth: 1 } ] }, options: { scales: { x: {display: false}, y: {display: false} }, plugins: { title: false, tooltip: false, filler: false, legend: { position: 'left', } } } }, options: { spriteText: true, canvas: { width: 150, height: 75 } } }; ================================================ FILE: test/fixtures/plugin.legend/maxWidth/value.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [ { label: '# of Votes', data: [12, 19, 3, 5, 2, 3], borderWidth: 1 }, { label: '# of Points', data: [7, 11, 5, 8, 3, 7], borderWidth: 1 } ] }, options: { scales: { x: {display: false}, y: {display: false} }, plugins: { title: false, tooltip: false, filler: false, legend: { position: 'left', maxWidth: 100 } } } }, options: { spriteText: true, canvas: { width: 150, height: 75 } } }; ================================================ FILE: test/fixtures/plugin.legend/padding/2cols-with-padding.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9278', config: { type: 'pie', data: { labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k'], datasets: [{ data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], backgroundColor: 'red' }] }, options: { plugins: { legend: { position: 'left' } }, layout: { padding: { top: 50, left: 30, right: 30, bottom: 50 } } } }, options: { spriteText: true, canvas: { width: 400, height: 300 }, } }; ================================================ FILE: test/fixtures/plugin.legend/padding/add-column.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9278', config: { type: 'pie', data: { labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'], datasets: [{ data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], backgroundColor: 'red' }] }, options: { plugins: { legend: { position: 'left' } }, layout: { padding: { top: 55, left: 30, right: 30 } } } }, options: { spriteText: true, canvas: { width: 400, height: 300 }, run(chart) { chart.data.labels.push('k'); chart.data.datasets[0].data.push(11); chart.update(); } } }; ================================================ FILE: test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width-default.json ================================================ { "config": { "type": "line", "data": { "labels": ["A", "B", "C"], "datasets": [{ "data": [10, 10, 10], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "line" }, { "data": [15, 15, 15], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "triangle" }, { "data": [20, 20, 20], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "rectRounded" }, { "data": [30, 30, 30], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "" }, { "data": [40, 40, 40], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "rect" }, { "data": [25, 25, 25], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "rectRot" }, { "data": [35, 35, 35], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "crossRot" }, { "data": [45, 45, 45], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "cross" }, { "data": [50, 50, 50], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "star" }, { "data": [55, 55, 55], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "dash" }] }, "options": { "plugins": { "legend": { "display": true, "labels": { "usePointStyle": true } } }, "scales": { "x": { "display": false }, "y": { "display": false } } } }, "options": { "canvas": { "height": 512, "width": 1024 } } } ================================================ FILE: test/fixtures/plugin.legend/pointStyle-width/legend-pointStyle-width.json ================================================ { "config": { "type": "line", "data": { "labels": ["A", "B", "C"], "datasets": [{ "data": [10, 10, 10], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "line" }, { "data": [15, 15, 15], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "triangle" }, { "data": [20, 20, 20], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "rectRounded" }, { "data": [30, 30, 30], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "" }, { "data": [40, 40, 40], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "rect" }, { "data": [25, 25, 25], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "rectRot" }, { "data": [35, 35, 35], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "crossRot" }, { "data": [45, 45, 45], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "cross" }, { "data": [50, 50, 50], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "star" }, { "data": [55, 55, 55], "backgroundColor": "#00ff00", "borderColor": "#ff0000", "borderWidth": 1, "label": "", "pointStyle": "dash" }] }, "options": { "plugins": { "legend": { "display": true, "labels": { "usePointStyle": true, "pointStyleWidth": 75 } } }, "scales": { "x": { "display": false }, "y": { "display": false } } } }, "options": { "canvas": { "height": 512, "width": 1024 } } } ================================================ FILE: test/fixtures/plugin.legend/title/bottom-center-center.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'bottom', align: 'center', title: { display: true, position: 'center', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/title/bottom-end-end.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'bottom', align: 'end', title: { display: true, position: 'end', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/title/bottom-start-start.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'bottom', align: 'start', title: { display: true, position: 'start', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/title/left-center-center.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'left', align: 'center', title: { display: true, position: 'center', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/title/left-end-end.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'left', align: 'end', title: { display: true, position: 'end', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/title/left-start-start.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'left', align: 'start', title: { display: true, position: 'start', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/title/right-center-center.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'right', align: 'center', title: { display: true, position: 'center', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/title/right-end-end.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'right', align: 'end', title: { display: true, position: 'end', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/title/right-start-start.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'right', align: 'start', title: { display: true, position: 'start', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/title/top-center-center.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'top', align: 'center', title: { display: true, position: 'center', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/title/top-end-end.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'top', align: 'end', title: { display: true, position: 'end', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.legend/title/top-start-start.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [ {label: 'a', data: []}, {label: 'b', data: []}, {label: 'c', data: []} ] }, options: { plugins: { legend: { position: 'top', align: 'start', title: { display: true, position: 'start', text: 'title' } } }, scales: { x: {display: false}, y: {display: false} } } }, options: { spriteText: true, canvas: { height: 256, width: 256 } } }; ================================================ FILE: test/fixtures/plugin.subtitle/basic.js ================================================ module.exports = { config: { type: 'scatter', data: { datasets: [{ data: [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}], backgroundColor: 'red', radius: 1, hoverRadius: 0 }], }, options: { scales: { x: {display: false}, y: {display: false} }, plugins: { legend: false, title: { display: true, text: 'Title Text', }, subtitle: { display: true, text: 'SubTitle Text', }, filler: false, tooltip: false }, }, }, options: { spriteText: true, canvas: { height: 400, width: 400 } } }; ================================================ FILE: test/fixtures/plugin.title/scriptable-options.js ================================================ const data = []; for (let x = 0; x < 3; x++) { for (let y = 0; y < 3; y++) { data.push({x, y}); } } module.exports = { config: { type: 'scatter', data: { datasets: [{ data, backgroundColor: 'red', radius: 1, hoverRadius: 0 }], }, options: { scales: { x: {display: false}, y: {display: false} }, plugins: { legend: false, title: { display: true, text: () => 'Title Text', }, filler: false, tooltip: false }, }, }, options: { spriteText: true, canvas: { height: 400, width: 400 } } }; ================================================ FILE: test/fixtures/plugin.tooltip/box-padding.js ================================================ const data = []; for (let x = 0; x < 3; x++) { for (let y = 0; y < 3; y++) { data.push({x, y}); } } module.exports = { config: { type: 'scatter', data: { datasets: [{ data, backgroundColor: 'red', radius: 1, hoverRadius: 0 }], }, options: { scales: { x: {display: false}, y: {display: false} }, plugins: { legend: false, title: false, filler: false, tooltip: { mode: 'point', intersect: true, // spriteText: use white background to hide any gaps between fonts backgroundColor: 'white', borderColor: 'black', borderWidth: 1, callbacks: { label: () => 'label', }, boxPadding: 30 }, }, }, plugins: [{ afterDraw: function(chart) { const canvas = chart.canvas; const rect = canvas.getBoundingClientRect(); const meta = chart.getDatasetMeta(0); let point, event; for (let i = 0; i < data.length; i++) { point = meta.data[i]; event = { type: 'mousemove', target: canvas, clientX: rect.left + point.x, clientY: rect.top + point.y }; chart._handleEvent(event); chart.tooltip.handleEvent(event); chart.tooltip.draw(chart.ctx); } } }] }, options: { spriteText: true, canvas: { height: 400, width: 500 } } }; ================================================ FILE: test/fixtures/plugin.tooltip/caret-position.js ================================================ const data = []; for (let x = 1; x < 4; x++) { for (let y = 1; y < 4; y++) { data.push({x, y}); } } module.exports = { config: { type: 'scatter', data: { datasets: [{ data, backgroundColor: 'red', radius: 8, hoverRadius: 0 }], }, options: { scales: { x: {display: false, min: 0.96, max: 3.04}, y: {display: false, min: 1, max: 3} }, plugins: { legend: false, title: false, filler: false, tooltip: { mode: 'point', intersect: true, // spriteText: use white background to hide any gaps between fonts backgroundColor: 'white', borderColor: 'black', borderWidth: 1, callbacks: { label: () => 'label', } }, }, }, plugins: [{ afterDraw: function(chart) { const canvas = chart.canvas; const rect = canvas.getBoundingClientRect(); const meta = chart.getDatasetMeta(0); let point, event; for (let i = 0; i < data.length; i++) { point = meta.data[i]; event = { type: 'mousemove', target: canvas, clientX: rect.left + point.x, clientY: rect.top + point.y }; chart._handleEvent(event); chart.tooltip.handleEvent(event); chart.tooltip.draw(chart.ctx); } } }] }, options: { spriteText: true, canvas: { height: 240, width: 320 } } }; ================================================ FILE: test/fixtures/plugin.tooltip/color-box-border-dash.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ data: [8, 7, 6, 5], pointBorderColor: '#ff0000', pointBackgroundColor: '#00ff00', showLine: false }], labels: ['', '', '', ''] }, options: { scales: { x: {display: false}, y: {display: false} }, elements: { line: { fill: false } }, plugins: { legend: false, title: false, filler: false, tooltip: { mode: 'nearest', intersect: false, callbacks: { label: function() { return '\u200b'; }, labelColor: function(tooltipItem) { const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); const options = meta.controller.getStyle(tooltipItem.dataIndex); return { borderColor: options.borderColor, backgroundColor: options.backgroundColor, borderWidth: 2, borderDash: [2, 2] }; }, } }, }, layout: { padding: 15 } }, plugins: [{ afterDraw: function(chart) { const canvas = chart.canvas; const rect = canvas.getBoundingClientRect(); const point = chart.getDatasetMeta(0).data[1]; const event = { type: 'mousemove', target: canvas, clientX: rect.left + point.x, clientY: rect.top + point.y }; chart._handleEvent(event); chart.tooltip.handleEvent(event); chart.tooltip.draw(chart.ctx); } }] }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/plugin.tooltip/color-box-border-radius.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ data: [8, 7, 6, 5], pointBorderColor: '#ff0000', pointBackgroundColor: '#00ff00', showLine: false }], labels: ['', '', '', ''] }, options: { scales: { x: {display: false}, y: {display: false} }, elements: { line: { fill: false } }, plugins: { legend: false, title: false, filler: false, tooltip: { mode: 'nearest', intersect: false, callbacks: { label: function() { return '\u200b'; }, labelColor: function(tooltipItem) { const meta = tooltipItem.chart.getDatasetMeta(tooltipItem.datasetIndex); const options = meta.controller.getStyle(tooltipItem.dataIndex); return { borderColor: options.borderColor, backgroundColor: options.backgroundColor, borderWidth: 2, borderRadius: { topRight: 5, bottomRight: 5, }, }; }, } }, }, layout: { padding: 15 } }, plugins: [{ afterDraw: function(chart) { const canvas = chart.canvas; const rect = canvas.getBoundingClientRect(); const point = chart.getDatasetMeta(0).data[1]; const event = { type: 'mousemove', target: canvas, clientX: rect.left + point.x, clientY: rect.top + point.y }; chart._handleEvent(event); chart.tooltip.handleEvent(event); chart.tooltip.draw(chart.ctx); } }] }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/plugin.tooltip/corner-radius.js ================================================ const data = []; for (let x = 0; x < 3; x++) { for (let y = 0; y < 3; y++) { data.push({x, y}); } } module.exports = { config: { type: 'scatter', data: { datasets: [{ data, backgroundColor: 'red', radius: 1, hoverRadius: 0 }], }, options: { scales: { x: {display: false}, y: {display: false} }, plugins: { legend: false, title: false, filler: false, tooltip: { mode: 'point', intersect: true, // spriteText: use white background to hide any gaps between fonts backgroundColor: 'white', borderColor: 'black', borderWidth: 1, callbacks: { beforeLabel: () => 'before label', label: () => 'label', afterLabel: () => 'after1\nafter2\nafter3\nafter4\nafter5' }, cornerRadius: { topLeft: 10, topRight: 20, bottomRight: 5, bottomLeft: 0, } }, }, }, plugins: [{ afterDraw: function(chart) { const canvas = chart.canvas; const rect = canvas.getBoundingClientRect(); const meta = chart.getDatasetMeta(0); let point, event; for (let i = 0; i < data.length; i++) { point = meta.data[i]; event = { type: 'mousemove', target: canvas, clientX: rect.left + point.x, clientY: rect.top + point.y }; chart._handleEvent(event); chart.tooltip.handleEvent(event); chart.tooltip.draw(chart.ctx); } } }] }, options: { spriteText: true, canvas: { height: 400, width: 500 } } }; ================================================ FILE: test/fixtures/plugin.tooltip/opacity.js ================================================ var patternCanvas = document.createElement('canvas'); var patternContext = patternCanvas.getContext('2d'); patternCanvas.width = 6; patternCanvas.height = 6; patternContext.fillStyle = '#ff0000'; patternContext.fillRect(0, 0, 6, 6); patternContext.fillStyle = '#ffff00'; patternContext.fillRect(0, 0, 4, 4); var pattern = patternContext.createPattern(patternCanvas, 'repeat'); var gradient; module.exports = { config: { type: 'line', data: { datasets: [{ data: [8, 8, 8, 8, 8, 8, 7, 8, 8, 8, 8], pointBorderColor: '#ff0000', pointBackgroundColor: '#00ff00', showLine: false }, { label: '', data: [4, 4, 4, 4, 4, 5, 3, 4, 4, 4, 4], pointBorderColor: pattern, pointBackgroundColor: pattern, showLine: false }, { label: '', data: [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], showLine: false }], labels: ['', '', '', '', '', '', '', '', '', '', ''] }, options: { scales: { x: {display: false}, y: {display: false} }, elements: { line: { fill: false } }, plugins: { legend: false, title: false, filler: false, tooltip: { mode: 'nearest', intersect: false, callbacks: { label: function() { return '\u200b'; }, } }, }, layout: { padding: 15 } }, plugins: [{ beforeDatasetsUpdate: function(chart) { if (!gradient) { gradient = chart.ctx.createLinearGradient(0, 0, 512, 256); gradient.addColorStop(0, '#ff0000'); gradient.addColorStop(1, '#0000ff'); } chart.config.data.datasets[2].pointBorderColor = gradient; chart.config.data.datasets[2].pointBackgroundColor = gradient; return true; }, afterDraw: function(chart) { var canvas = chart.canvas; var rect = canvas.getBoundingClientRect(); var point, event; for (var i = 0; i < 3; ++i) { for (var j = 0; j < 11; ++j) { point = chart.getDatasetMeta(i).data[j]; event = { type: 'mousemove', target: canvas, clientX: rect.left + point.x, clientY: rect.top + point.y }; chart._handleEvent(event); chart.tooltip.handleEvent(event); chart.tooltip.opacity = j / 10; chart.tooltip.draw(chart.ctx); } } } }] }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/plugin.tooltip/point-style.js ================================================ const pointStyles = ['circle', 'cross', 'crossRot', 'dash', 'line', 'rect', 'rectRounded', 'rectRot', 'star', 'triangle', false]; function newDataset(pointStyle, i) { return { label: '', data: pointStyles.map(() => i), pointStyle: pointStyle, pointBackgroundColor: '#0000ff', pointBorderColor: '#00ff00', showLine: false }; } module.exports = { config: { type: 'line', data: { datasets: pointStyles.map((pointStyle, i) => newDataset(pointStyle, i)), labels: pointStyles.map(() => '') }, options: { scales: { x: {display: false}, y: {display: false} }, elements: { line: { fill: false } }, plugins: { legend: false, title: false, filler: false, tooltip: { mode: 'nearest', intersect: false, padding: 5, usePointStyle: true, callbacks: { label: function() { return '\u200b'; } } }, }, layout: { padding: 15 } }, plugins: [{ afterDraw: function(chart) { var canvas = chart.canvas; var rect = canvas.getBoundingClientRect(); var point, event; for (var i = 0; i < pointStyles.length; ++i) { point = chart.getDatasetMeta(i).data[i]; event = { type: 'mousemove', target: canvas, clientX: rect.left + point.x, clientY: rect.top + point.y }; chart._handleEvent(event); chart.tooltip.handleEvent(event); chart.tooltip.draw(chart.ctx); } } }] }, options: { canvas: { height: 256, width: 512 } } }; ================================================ FILE: test/fixtures/plugin.tooltip/positioning.js ================================================ const data = []; for (let x = 0; x < 3; x++) { for (let y = 0; y < 3; y++) { data.push({x, y}); } } module.exports = { config: { type: 'scatter', data: { datasets: [{ data, backgroundColor: 'red', radius: 1, hoverRadius: 0 }], }, options: { scales: { x: {display: false}, y: {display: false} }, plugins: { legend: false, title: false, filler: false, tooltip: { mode: 'point', intersect: true, // spriteText: use white background to hide any gaps between fonts backgroundColor: 'white', borderColor: 'black', borderWidth: 1, callbacks: { beforeLabel: () => 'before label', label: () => 'label', afterLabel: () => 'after1\nafter2\nafter3\nafter4\nafter5' } } }, }, plugins: [{ afterDraw: function(chart) { const canvas = chart.canvas; const rect = canvas.getBoundingClientRect(); const meta = chart.getDatasetMeta(0); let point, event; for (let i = 0; i < data.length; i++) { point = meta.data[i]; event = { type: 'mousemove', target: canvas, clientX: rect.left + point.x, clientY: rect.top + point.y }; chart._handleEvent(event); chart.tooltip.handleEvent(event); chart.tooltip.draw(chart.ctx); } } }] }, options: { spriteText: true, canvas: { height: 400, width: 500 } } }; ================================================ FILE: test/fixtures/scale.category/invalid-data.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g'], datasets: [{ data: [ {x: 'a', y: 1}, {x: null, y: 1}, {x: 2, y: 1}, {x: undefined, y: 1}, {x: 4, y: 1}, {x: NaN, y: 1}, {x: 6, y: 1} ], backgroundColor: 'red', borderColor: 'red', borderWidth: 5 }] }, options: { scales: { y: { display: false }, x: { grid: { display: false } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/scale.category/max-ticks-limit-a.js ================================================ const data = Array.from({length: 42}, (_, i) => i + 1); const labels = data.map(v => 'tick' + v); module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/7302', config: { type: 'bar', data: { datasets: [{ data }], labels }, options: { scales: { x: { ticks: { display: false, maxTicksLimit: 7 }, grid: { color: 'red' } }, y: {display: false} }, layout: { padding: { right: 2 } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.category/max-ticks-limit-b.js ================================================ const data = Array.from({length: 42}, (_, i) => i + 1); const labels = data.map(v => 'tick' + v); module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/7302', config: { type: 'bar', data: { datasets: [{ data }], labels }, options: { scales: { x: { ticks: { display: false, maxTicksLimit: 6 }, grid: { color: 'red' } }, y: {display: false} }, layout: { padding: { right: 2 } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.category/max-ticks-limit-norotation.js ================================================ const data = Array.from({length: 42}, (_, i) => i + 1); const labels = data.map(v => 'tick' + v); module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/10856', config: { type: 'bar', data: { datasets: [{ data }], labels }, options: { scales: { x: { ticks: { display: true, maxTicksLimit: 6 }, grid: { color: 'red' } }, y: {display: false} }, layout: { padding: { right: 2 } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.category/ticks-from-data.js ================================================ module.exports = { threshold: 0.01, config: { type: 'bar', data: { datasets: [{ data: [10, 5, 0, 25, 78], backgroundColor: 'transparent' }], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'] }, options: { indexAxis: 'y', scales: { x: {display: false}, y: {display: true} } } }, options: { spriteText: true, canvas: { width: 128, height: 256 } } }; ================================================ FILE: test/fixtures/scale.linear/grace/grace-10%.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b'], datasets: [{ data: [90, -10], }], }, options: { indexAxis: 'y', scales: { y: { display: false }, x: { grace: '10%' } } } }, options: { spriteText: true, canvas: { width: 512, height: 128 } } }; ================================================ FILE: test/fixtures/scale.linear/grace/grace-beginAtZero.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b'], datasets: [{ data: [100, 0], backgroundColor: 'blue' }, { xAxisID: 'x2', data: [0, 100], backgroundColor: 'red' }], }, options: { indexAxis: 'y', scales: { y: { display: false }, x: { position: 'top', beginAtZero: true, grace: '10%', }, x2: { position: 'bottom', type: 'linear', beginAtZero: false, grace: '10%', } } } }, options: { spriteText: true, canvas: { width: 512, height: 128 } } }; ================================================ FILE: test/fixtures/scale.linear/grace/grace-neg.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a'], datasets: [{ data: [-0.18], }], }, options: { indexAxis: 'y', scales: { y: { display: false }, x: { grace: '5%' } } } }, options: { spriteText: true, canvas: { width: 512, height: 128 } } }; ================================================ FILE: test/fixtures/scale.linear/grace/grace-pos.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a'], datasets: [{ data: [0.18], }], }, options: { indexAxis: 'y', scales: { y: { display: false }, x: { grace: '5%' } } } }, options: { spriteText: true, canvas: { width: 512, height: 128 } } }; ================================================ FILE: test/fixtures/scale.linear/grace/grace.js ================================================ module.exports = { config: { type: 'bar', data: { labels: ['a', 'b'], datasets: [{ data: [1.2, -0.2], }], }, options: { indexAxis: 'y', scales: { y: { display: false }, x: { grace: 0.3 } } } }, options: { spriteText: true, canvas: { width: 512, height: 128 } } }; ================================================ FILE: test/fixtures/scale.linear/grace/issue-8912.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8912', config: { type: 'bar', data: { labels: ['Red', 'Blue'], datasets: [{ data: [10, -10] }] }, options: { plugins: false, scales: { x: { display: false, }, y: { grace: '100%' } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.linear/issue-8806.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8806', config: { type: 'bar', data: { labels: ['0', '1', '2', '3', '4', '5', '6'], datasets: [{ label: '# of Votes', data: [32, 46, 28, 21, 20, 13, 27] }] }, options: { scales: { x: {display: false}, y: {ticks: {maxTicksLimit: 4}, min: 0} } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/scale.linear/min-max-skip/edge-case-1.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8982', config: { type: 'scatter', options: { scales: { y: { max: 1069, min: 230, ticks: { autoSkip: false } }, x: { max: 1069, min: 230, ticks: { autoSkip: false } } } } }, options: { spriteText: true, canvas: { height: 196, width: 407 } } }; ================================================ FILE: test/fixtures/scale.linear/min-max-skip/edge-case-2.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8982', config: { type: 'scatter', options: { scales: { y: { max: 1069, min: 230, ticks: { autoSkip: false } }, x: { max: 1069, min: 230, ticks: { autoSkip: false } } } } }, options: { spriteText: true, canvas: { height: 197, width: 420 } } }; ================================================ FILE: test/fixtures/scale.linear/min-max-skip/edge-case-3.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8982', config: { type: 'scatter', options: { scales: { y: { max: 1069, min: 230, ticks: { autoSkip: false } }, x: { max: 1069, min: 230, ticks: { autoSkip: false } } } } }, options: { spriteText: true, canvas: { height: 199, width: 556 } } }; ================================================ FILE: test/fixtures/scale.linear/min-max-skip/edge-case-4.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8982', config: { type: 'scatter', options: { scales: { y: { max: 1069, min: 230, ticks: { autoSkip: false } }, x: { max: 1069, min: 230, ticks: { autoSkip: false } } } } }, options: { spriteText: true, canvas: { height: 200, width: 557 } } }; ================================================ FILE: test/fixtures/scale.linear/min-max-skip/includeBounds.js ================================================ module.exports = { config: { type: 'scatter', options: { scales: { y: { max: 1225.2, min: 369.5, ticks: { includeBounds: false } }, x: { min: 20, max: 100, ticks: { includeBounds: false } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.linear/min-max-skip/min-max-skip.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/7734', config: { type: 'line', options: { scales: { y: { max: 1225.2, min: 369.5, }, x: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.linear/min-max-skip/no-collision.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9025', threshold: 0.2, config: { type: 'line', data: { datasets: [{ data: [ {x: 10000000, y: 65}, {x: 20000000, y: 12}, {x: 30000000, y: 23}, {x: 40000000, y: 51}, {x: 50000000, y: 17}, {x: 60000000, y: 23} ] }] }, options: { scales: { x: { type: 'linear', min: 10000000, max: 60000000, ticks: { minRotation: 45, maxRotation: 45, count: 6 } } } } }, options: { canvas: { width: 200, height: 200 }, spriteText: true } }; ================================================ FILE: test/fixtures/scale.linear/min-max-skip/rotated-case-1.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9025', threshold: 0.2, config: { type: 'scatter', options: { scales: { y: { max: 1069, min: 230, ticks: { autoSkip: false, minRotation: 22.5 } }, x: { max: 1069, min: 230, ticks: { autoSkip: false, minRotation: 67.5 } } } } }, options: { spriteText: true, canvas: { height: 231, width: 221 } } }; ================================================ FILE: test/fixtures/scale.linear/min-max-skip/rotated-case-2.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9025', threshold: 0.2, config: { type: 'scatter', options: { scales: { y: { max: 1069, min: 230, ticks: { autoSkip: false, minRotation: 22.5 } }, x: { max: 1069, min: 230, ticks: { autoSkip: false, minRotation: 67.5 } } } } }, options: { spriteText: true, canvas: { height: 232, width: 222 } } }; ================================================ FILE: test/fixtures/scale.linear/min-max-skip/rotated-case-3.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9025', threshold: 0.2, config: { type: 'scatter', options: { scales: { y: { max: 1069, min: 230, ticks: { autoSkip: false, minRotation: 22.5 } }, x: { max: 1069, min: 230, ticks: { autoSkip: false, minRotation: 67.5 } } } } }, options: { spriteText: true, canvas: { height: 234, width: 224 } } }; ================================================ FILE: test/fixtures/scale.linear/min-max-skip/rotated-case-4.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9025', threshold: 0.2, config: { type: 'scatter', options: { scales: { y: { max: 1069, min: 230, ticks: { autoSkip: false, minRotation: 22.5 } }, x: { max: 1069, min: 230, ticks: { autoSkip: false, minRotation: 67.5 } } } } }, options: { spriteText: true, canvas: { height: 235, width: 225 } } }; ================================================ FILE: test/fixtures/scale.linear/rotated-45.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9025', threshold: 0.2, config: { type: 'scatter', options: { scales: { y: { min: 1612781975085.5466, max: 1620287255085.5466, ticks: { autoSkip: false, minRotation: 45, maxRotation: 45, count: 13 } }, x: { min: 1612781975085.5466, max: 1620287255085.5466, ticks: { autoSkip: false, minRotation: 45, maxRotation: 45, count: 13 } } } } }, options: { spriteText: true, canvas: { height: 350, width: 350 } } }; ================================================ FILE: test/fixtures/scale.linear/rotated-5.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9025', threshold: 0.2, config: { type: 'scatter', options: { scales: { y: { min: 0, max: 500000, ticks: { minRotation: 5, maxRotation: 5, } }, x: { min: 0, max: 500000, ticks: { minRotation: 5, maxRotation: 5, } } } } }, options: { spriteText: true, canvas: { height: 350, width: 350 } } }; ================================================ FILE: test/fixtures/scale.linear/rotated-85.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9025', threshold: 0.2, config: { type: 'scatter', options: { scales: { y: { min: 0, max: 500000, ticks: { minRotation: 85, maxRotation: 85, } }, x: { min: 0, max: 500000, ticks: { minRotation: 85, maxRotation: 85, } } } } }, options: { spriteText: true, canvas: { height: 350, width: 350 } } }; ================================================ FILE: test/fixtures/scale.linear/tick-count-data-limits.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/4234', config: { type: 'line', data: { datasets: [{ data: [0, 2, 45, 30] }], labels: ['A', 'B', 'C', 'D'] }, options: { scales: { y: { ticks: { count: 21, callback: (v) => v.toString(), } }, x: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.linear/tick-count-min-max-not-aligned.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/4234', config: { type: 'line', options: { scales: { y: { max: 27, min: -3, ticks: { count: 11, } }, x: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.linear/tick-count-min-max-not-int.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9078', config: { type: 'bar', data: { datasets: [{ data: [ {x: 1, y: 3.5}, {x: 2, y: 4.7}, {x: 3, y: 7.3}, {x: 4, y: 6.7} ] }] }, options: { scales: { x: { type: 'linear', display: false, }, y: { min: 3.5, max: 8.5, ticks: { count: 6, } } } } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/scale.linear/tick-count-min-max.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/4234', config: { type: 'line', options: { scales: { y: { max: 50, min: 0, ticks: { count: 21, } }, x: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.linear/tick-step-min-max-step-fp.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/9334', config: { type: 'line', options: { scales: { y: { display: false, }, x: { type: 'linear', min: 7.2, max: 21.6, ticks: { stepSize: 1.8 } }, } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.linear/tick-step-min-max.js ================================================ module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/4234', config: { type: 'line', options: { scales: { y: { max: 27, min: -3, ticks: { stepSize: 3, } }, x: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.linear/tiny-numbers.js ================================================ // Should generate max and min that are not equal when data contains values that are very close to each other module.exports = { config: { type: 'scatter', data: { datasets: [{ data: [ {x: 1, y: 1.8548483304974972}, {x: 2, y: 1.8548483304974974}, ] }], }, }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.logarithmic/large-range.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ backgroundColor: 'red', borderColor: 'red', fill: false, data: [23, 21, 34, 52, 115, 3333, 5116] }] }, options: { responsive: true, scales: { x: { display: false, }, y: { type: 'logarithmic', ticks: { autoSkip: false } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.logarithmic/large-values-small-range.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ backgroundColor: 'red', borderColor: 'red', fill: false, data: [5000.002, 5000.012, 5000.01, 5000.03, 5000.04, 5000.004, 5000.032] }] }, options: { responsive: true, scales: { x: { display: false, }, y: { type: 'logarithmic', ticks: { autoSkip: false } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.logarithmic/med-range.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ backgroundColor: 'red', borderColor: 'red', fill: false, data: [25, 24, 27, 32, 45, 30, 28] }] }, options: { responsive: true, scales: { x: { display: false, }, y: { type: 'logarithmic', ticks: { autoSkip: false } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.logarithmic/min-max.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ backgroundColor: 'red', borderColor: 'red', fill: false, data: [250, 240, 270, 320, 450, 300, 280] }] }, options: { responsive: true, scales: { x: { display: false, }, y: { type: 'logarithmic', min: 233, max: 471, ticks: { autoSkip: false } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.logarithmic/null-values.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ backgroundColor: 'red', borderColor: 'red', fill: false, data: [ 150, null, 1500, 200, 9000, 3000, 8888 ], spanGaps: true }, { backgroundColor: 'blue', borderColor: 'blue', fill: false, data: [ 1000, 5500, 800, 7777, null, 6666, 5555 ], spanGaps: false }] }, options: { responsive: true, scales: { x: { display: false, }, y: { display: false, type: 'logarithmic', } } } } }; ================================================ FILE: test/fixtures/scale.logarithmic/small-range.js ================================================ module.exports = { config: { type: 'line', data: { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ backgroundColor: 'red', borderColor: 'red', fill: false, data: [3, 1, 4, 2, 5, 3, 16] }] }, options: { responsive: true, scales: { x: { display: false, }, y: { type: 'logarithmic', ticks: { autoSkip: false } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.radialLinear/anglelines-disable.json ================================================ { "config": { "type": "radar", "data": { "labels": ["A", "B", "C", "D", "E"] }, "options": { "responsive": false, "scales": { "r": { "grid": { "color": "rgb(0, 0, 0)", "lineWidth": 1 }, "angleLines": { "display": false }, "pointLabels": { "display": false }, "ticks": { "display": false } } } } } } ================================================ FILE: test/fixtures/scale.radialLinear/anglelines-indexable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: ['A', 'B', 'C', 'D', 'E'] }, options: { responsive: false, scales: { r: { grid: { display: true, }, angleLines: { color: ['red', 'green'], lineWidth: [1, 5] }, pointLabels: { display: false }, ticks: { display: false } } } } } }; ================================================ FILE: test/fixtures/scale.radialLinear/anglelines-reverse-scale.js ================================================ module.exports = { config: { type: 'radar', data: { labels: ['A', 'B', 'C', 'D', 'E'], datasets: [{ data: [1, 1, 2, 3, 5] }] }, options: { responsive: false, scales: { r: { reverse: true, grid: { display: true, }, angleLines: { color: 'red', lineWidth: 5, }, pointLabels: { display: false }, ticks: { display: true, } } } } }, options: { spriteText: true, } }; ================================================ FILE: test/fixtures/scale.radialLinear/anglelines-scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: ['A', 'B', 'C', 'D', 'E'] }, options: { responsive: false, scales: { r: { grid: { display: true, }, angleLines: { color: function(context) { return context.index % 2 === 0 ? 'red' : 'green'; }, lineWidth: function(context) { return context.index % 2 === 0 ? 1 : 5; }, }, pointLabels: { display: false }, ticks: { display: false } } } } } }; ================================================ FILE: test/fixtures/scale.radialLinear/backgroundColor.js ================================================ module.exports = { threshold: 0.01, config: { type: 'radar', data: { labels: [1, 2, 3, 4, 5, 6], datasets: [ { data: [3, 2, 2, 1, 3, 1] } ] }, options: { plugins: { legend: false, tooltip: false, filler: false }, scales: { r: { backgroundColor: '#00FF00', min: 0, max: 3, pointLabels: { display: false }, ticks: { display: false, stepSize: 1, } } }, responsive: true, maintainAspectRatio: false } }, }; ================================================ FILE: test/fixtures/scale.radialLinear/border-dash.json ================================================ { "config": { "type": "radar", "data": { "labels": ["A", "B", "C", "D", "E"] }, "options": { "responsive": false, "scales": { "r": { "grid": { "color": "rgba(0, 0, 255, 0.5)", "lineWidth": 1 }, "border": { "dash": [4, 2], "dashOffset": 2 }, "angleLines": { "color": "rgba(0, 0, 255, 0.5)", "lineWidth": 1, "borderDash": [4, 2], "borderDashOffset": 2 }, "pointLabels": { "display": false }, "ticks": { "display": false } } } } } } ================================================ FILE: test/fixtures/scale.radialLinear/circular-backgroundColor.js ================================================ module.exports = { threshold: 0.05, config: { type: 'radar', data: { labels: [1, 2, 3, 4, 5, 6], datasets: [ { data: [3, 2, 2, 1, 3, 1] } ] }, options: { plugins: { legend: false, tooltip: false, filler: false }, scales: { r: { backgroundColor: '#00FF00', min: 0, max: 3, grid: { circular: true }, pointLabels: { display: false }, ticks: { display: false, stepSize: 1, } } }, responsive: true, maintainAspectRatio: false } }, }; ================================================ FILE: test/fixtures/scale.radialLinear/circular-border-dash.json ================================================ { "config": { "type": "radar", "data": { "labels": ["A", "B", "C", "D", "E"] }, "options": { "responsive": false, "scales": { "r": { "border": { "dash": [4, 2], "dashOffset": 2 }, "grid": { "circular": true, "color": "rgba(0, 0, 255, 0.5)", "lineWidth": 1 }, "angleLines": { "color": "rgba(0, 0, 255, 0.5)", "lineWidth": 1, "borderDash": [4, 2], "borderDashOffset": 2 }, "pointLabels": { "display": false }, "ticks": { "display": false } } } } } } ================================================ FILE: test/fixtures/scale.radialLinear/gridlines-disable.json ================================================ { "config": { "type": "radar", "data": { "labels": ["A", "B", "C", "D", "E"] }, "options": { "responsive": false, "scales": { "r": { "grid": { "display": false }, "angleLines": { "color": "rgb(0, 0, 0)", "lineWidth": 1 }, "pointLabels": { "display": false }, "ticks": { "display": false } } } } } } ================================================ FILE: test/fixtures/scale.radialLinear/gridlines-no-z.json ================================================ { "config": { "type": "radar", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [ { "backgroundColor": "rgba(255, 0, 0, 1)", "data": [1, 2, 3, 3, 3] } ] }, "options": { "responsive": false, "scales": { "r": { "grid": { "color": "rgba(0, 0, 0, 1)", "lineWidth": 1 }, "angleLines": { "color": "rgba(0, 0, 255, 1)", "lineWidth": 1 }, "pointLabels": { "display": false }, "ticks": { "display": false } } }, "plugins": { "legend": false, "title": false, "tooltip": false, "filler": true } } } } ================================================ FILE: test/fixtures/scale.radialLinear/gridlines-scriptable.js ================================================ module.exports = { config: { type: 'radar', data: { labels: ['A', 'B', 'C', 'D', 'E'] }, options: { responsive: false, scales: { r: { grid: { display: true, color: function(context) { return context.index % 2 === 0 ? 'green' : 'red'; }, lineWidth: function(context) { return context.index % 2 === 0 ? 5 : 1; }, }, angleLines: { color: 'rgba(255, 255, 255, 0.5)', lineWidth: 2 }, pointLabels: { display: false }, ticks: { display: false } } } } } }; ================================================ FILE: test/fixtures/scale.radialLinear/gridlines-z.json ================================================ { "config": { "type": "radar", "data": { "labels": ["A", "B", "C", "D", "E"], "datasets": [ { "backgroundColor": "rgba(255, 0, 0, 1)", "data": [1, 2, 3, 3, 3] } ] }, "options": { "responsive": false, "scales": { "r": { "grid": { "color": "rgba(0, 0, 0, 1)", "lineWidth": 1, "z": 1 }, "angleLines": { "color": "rgba(0, 0, 255, 1)", "lineWidth": 1 }, "pointLabels": { "display": false }, "ticks": { "display": false } } }, "plugins": { "legend": false, "title": false, "tooltip": false, "filler": true } } } } ================================================ FILE: test/fixtures/scale.radialLinear/indexable-gridlines.json ================================================ { "config": { "type": "radar", "data": { "labels": ["A", "B", "C", "D", "E"] }, "options": { "responsive": false, "scales": { "r": { "grid": { "display": true, "color": [ "rgba(0, 0, 0, 0.5)", "rgba(255, 255, 255, 0.5)", false, "", "rgba(255, 0, 0, 0.5)", "rgba(0, 255, 0, 0.5)", "rgba(0, 0, 255, 0.5)", "rgba(255, 255, 0, 0.5)", "rgba(255, 0, 255, 0.5)", "rgba(0, 255, 255, 0.5)" ], "lineWidth": [false, 0, 1, 2, 1, 2, 1, 2, 1, 2] }, "angleLines": { "color": "rgba(255, 255, 255, 0.5)", "lineWidth": 2 }, "pointLabels": { "display": false }, "ticks": { "display": false } } } } } } ================================================ FILE: test/fixtures/scale.radialLinear/pointLabels/background.js ================================================ module.exports = { tolerance: 0.01, config: { type: 'radar', data: { labels: [ ['VENTE ET', 'COMMERCIALISATION'], ['GESTION', 'FINANCIÈRE'], 'NUMÉRIQUE', ['ADMINISTRATION', 'ET OPÉRATION'], ['RESSOURCES', 'HUMAINES'], 'INNOVATION' ], datasets: [ { backgroundColor: '#E43E51', label: 'Compétences entrepreunariales', data: [3, 2, 2, 1, 3, 1] } ] }, options: { plugins: { legend: false, tooltip: false, filler: false }, scales: { r: { min: 0, max: 3, pointLabels: { backdropColor: 'blue', backdropPadding: {left: 5, right: 5, top: 2, bottom: 2}, }, ticks: { display: false, stepSize: 1, maxTicksLimit: 1 } } }, responsive: true, maintainAspectRatio: false } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.radialLinear/pointLabels/border-radius.js ================================================ module.exports = { tolerance: 0.01, config: { type: 'radar', data: { labels: [ ['VENTE ET', 'COMMERCIALISATION'], ['GESTION', 'FINANCIÈRE'], 'NUMÉRIQUE', ['ADMINISTRATION', 'ET OPÉRATION'], ['RESSOURCES', 'HUMAINES'], 'INNOVATION' ], datasets: [ { backgroundColor: '#E43E51', label: 'Compétences entrepreunariales', data: [3, 2, 2, 1, 3, 1] } ] }, options: { plugins: { legend: false, tooltip: false, filler: false }, scales: { r: { min: 0, max: 3, pointLabels: { backdropColor: 'blue', backdropPadding: {left: 5, right: 5, top: 2, bottom: 2}, borderRadius: 10, }, ticks: { display: false, stepSize: 1, maxTicksLimit: 1 } } }, responsive: true, maintainAspectRatio: false } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.radialLinear/pointLabels/no-more-than-half-radius.js ================================================ module.exports = { config: { type: 'radar', data: { labels: ['Too long label 1', 'Too long label 2', 'Too long label 3', 'Too long label 4'], datasets: [ { backgroundColor: '#E43E51', data: [1, 1, 1, 1] } ] }, options: { scales: { r: { max: 1, ticks: { display: false, }, grid: { display: false } } }, } }, options: { spriteText: true, canvas: { width: 256, height: 256 } } }; ================================================ FILE: test/fixtures/scale.radialLinear/pointLabels/padding.js ================================================ module.exports = { config: { type: 'radar', data: { labels: [ ['VENTE ET', 'COMMERCIALISATION'], ['GESTION', 'FINANCIÈRE'], 'NUMÉRIQUE', ['ADMINISTRATION', 'ET OPÉRATION'], ['RESSOURCES', 'HUMAINES'], 'INNOVATION' ], datasets: [ { radius: 12, backgroundColor: '#E43E51', label: 'Compétences entrepreunariales', data: [3, 2, 2, 1, 3, 1] } ] }, options: { plugins: { legend: false, tooltip: false, filler: false }, scales: { r: { min: 0, max: 3, pointLabels: { padding: 30 }, ticks: { display: false, stepSize: 1, maxTicksLimit: 1 } } }, responsive: true, maintainAspectRatio: false } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.radialLinear/pointLabels/scriptable-color-small.js ================================================ module.exports = { config: { type: 'radar', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange', 'Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange', 'Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange', 'Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [{ label: '# of Votes', data: [12, 19, 3, 5, 1, 3, 12, 19, 3, 5, 1, 3, 12, 19, 3, 5, 1, 3, 12, 19, 3, 5, 1, 3] }] }, options: { scales: { r: { ticks: { display: false, }, angleLines: { color: (ctx) => { return ctx.index % 2 === 0 ? 'green' : 'red'; } }, pointLabels: { display: false, } } }, } }, options: { spriteText: true, width: 300, } }; ================================================ FILE: test/fixtures/scale.radialLinear/ticks-below-zero.js ================================================ module.exports = { config: { type: 'radar', data: { labels: ['A', 'B', 'C', 'D', 'E'] }, options: { responsive: false, scales: { r: { min: -1, max: 1, grid: { display: true, color: 'blue', lineWidth: 2 }, angleLines: { color: 'rgba(255, 255, 255, 0.5)', lineWidth: 2 }, pointLabels: { display: false }, ticks: { display: true, autoSkip: false, stepSize: 0.2, callback: function(value) { if (value === 0.8) { return 'Strong'; } if (value === 0.4) { return 'Weak'; } if (value === 0) { return 'No'; } } } } } } } }; ================================================ FILE: test/fixtures/scale.time/autoskip-major.js ================================================ var date = moment('Jan 01 1990', 'MMM DD YYYY'); var data = []; for (var i = 0; i < 60; i++) { data.push({x: date.valueOf(), y: i}); date = date.clone().add(1, 'month'); } module.exports = { threshold: 0.05, config: { type: 'line', data: { datasets: [{ xAxisID: 'x', data: data, fill: false }], }, options: { scales: { x: { type: 'time', ticks: { major: { enabled: true }, source: 'data', autoSkip: true, maxRotation: 0 } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/bar-large-gap-between-data.js ================================================ var date = moment('May 24 2020', 'MMM DD YYYY'); module.exports = { threshold: 0.05, config: { type: 'bar', data: { datasets: [{ backgroundColor: 'rgba(255, 0, 0, 0.5)', data: [ { x: date.clone().add(-2, 'day'), y: 20, }, { x: date.clone().add(-1, 'day'), y: 30, }, { x: date, y: 40, }, { x: date.clone().add(1, 'day'), y: 50, }, { x: date.clone().add(7, 'day'), y: 10, } ] }] }, options: { scales: { x: { display: false, type: 'time', ticks: { source: 'auto' }, time: { unit: 'day' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/custom-parser.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0025, config: { type: 'line', data: { labels: ['foo', 'bar'], datasets: [{ data: [0, 1], fill: false }], }, options: { scales: { x: { type: 'time', position: 'bottom', time: { unit: 'day', round: true, parser: function(label) { return label === 'foo' ? moment('2000/01/02', 'YYYY/MM/DD') : moment('2016/05/08', 'YYYY/MM/DD'); } }, ticks: { source: 'labels' } }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 256, height: 128} } }; ================================================ FILE: test/fixtures/scale.time/data-ty.js ================================================ function newDateFromRef(days) { return moment('01/01/2015 12:00', 'DD/MM/YYYY HH:mm').add(days, 'd').toDate(); } module.exports = { threshold: 0.01, tolerance: 0.003, config: { type: 'line', data: { datasets: [{ data: [{ t: newDateFromRef(0), y: 1 }, { t: newDateFromRef(1), y: 10 }, { t: newDateFromRef(2), y: 0 }, { t: newDateFromRef(4), y: 5 }, { t: newDateFromRef(6), y: 77 }, { t: newDateFromRef(7), y: 9 }, { t: newDateFromRef(9), y: 5 }], fill: false, parsing: { xAxisKey: 't' } }], }, options: { scales: { x: { type: 'time', position: 'bottom', ticks: { maxRotation: 0 } }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 800, height: 200} } }; ================================================ FILE: test/fixtures/scale.time/data-xy.js ================================================ function newDateFromRef(days) { return moment('01/01/2015 12:00', 'DD/MM/YYYY HH:mm').add(days, 'd').toDate(); } module.exports = { threshold: 0.01, tolerance: 0.003, config: { type: 'line', data: { datasets: [{ data: [{ x: newDateFromRef(0), y: 1 }, { x: newDateFromRef(1), y: 10 }, { x: newDateFromRef(2), y: 0 }, { x: newDateFromRef(4), y: 5 }, { x: newDateFromRef(6), y: 77 }, { x: newDateFromRef(7), y: 9 }, { x: newDateFromRef(9), y: 5 }], fill: false }], }, options: { scales: { x: { type: 'time', position: 'bottom', ticks: { maxRotation: 0 } }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 800, height: 200} } }; ================================================ FILE: test/fixtures/scale.time/invalid-data.js ================================================ module.exports = { description: 'Invalid data, https://github.com/chartjs/Chart.js/issues/5563', config: { type: 'line', data: { datasets: [{ data: [{ x: '14:45:00', y: 20, }, { x: '20:30:00', y: 10, }, { x: '25:15:00', y: 15, }, { x: null, y: 15, }, { x: undefined, y: 15, }, { x: NaN, y: 15, }, { x: 'monday', y: 15, }], }] }, options: { scales: { x: { type: 'time', time: { parser: 'HH:mm:ss', unit: 'hour' }, }, }, layout: { padding: 16 } } }, options: { spriteText: true, canvas: {width: 1000, height: 200} } }; ================================================ FILE: test/fixtures/scale.time/labels-date.js ================================================ function newDateFromRef(days) { return moment('01/01/2015 12:00', 'DD/MM/YYYY HH:mm').add(days, 'd').toDate(); } module.exports = { threshold: 0.1, tolerance: 0.002, config: { type: 'line', data: { labels: [newDateFromRef(0), newDateFromRef(1), newDateFromRef(2), newDateFromRef(4), newDateFromRef(6), newDateFromRef(7), newDateFromRef(9)], fill: false }, options: { scales: { x: { type: 'time', }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 1000, height: 200} } }; ================================================ FILE: test/fixtures/scale.time/labels-strings.js ================================================ module.exports = { threshold: 0.05, tolerance: 0.002, config: { type: 'line', data: { labels: ['2015-01-01T12:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'] }, options: { scales: { x: { type: 'time', }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 1000, height: 200} } }; ================================================ FILE: test/fixtures/scale.time/labels.js ================================================ var timeOpts = { parser: 'YYYY', unit: 'year', displayFormats: { year: 'YYYY' } }; module.exports = { threshold: 0.01, tolerance: 0.002, config: { type: 'line', data: { labels: ['1975', '1976', '1977'], xLabels: ['1985', '1986', '1987'], yLabels: ['1995', '1996', '1997'] }, options: { scales: { x: { type: 'time', labels: ['2015', '2016', '2017'], time: timeOpts }, x2: { type: 'time', position: 'bottom', time: timeOpts }, y: { type: 'time', time: timeOpts }, y2: { position: 'left', type: 'time', labels: ['2005', '2006', '2007'], time: timeOpts } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/negative-times.js ================================================ module.exports = { config: { type: 'line', data: { datasets: [{ fill: true, backgroundColor: 'red', data: [ {x: -1000000, y: 1}, {x: 1000000000, y: 2} ] }] }, options: { scales: { x: { type: 'time', time: { unit: 'day' }, ticks: { display: false } }, y: { ticks: { display: false } } }, plugins: { legend: false, title: false, tooltip: false } } }, options: { canvas: {width: 1000, height: 200} } }; ================================================ FILE: test/fixtures/scale.time/offset-auto-skip-ticks.js ================================================ const data = { labels: [], datasets: [{ label: 'Dataset', borderColor: '#2f54eb', data: [{ y: 3, x: 1646345700000 }, { y: 7, x: 1646346600000 }, { y: 9, x: 1646347500000 }, { y: 5, x: 1646348400000 }, { y: 5, x: 1646349300000 }], }] }; module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/10215', config: { type: 'bar', data, options: { maintainAspectRatio: false, scales: { x: { type: 'time', offset: true, offsetAfterAutoskip: true, axis: 'x', grid: { offset: true }, }, y: { display: false, } } } }, options: { spriteText: true, canvas: {width: 600, height: 400} } }; ================================================ FILE: test/fixtures/scale.time/offset-with-1-tick.js ================================================ const data = { datasets: [ { label: 6, backgroundColor: 'red', data: [ { x: '2021-03-24', y: 464 } ] }, { label: 1, backgroundColor: 'red', data: [ { x: '2021-03-24', y: 464 } ] }, { label: 17, backgroundColor: 'blue', data: [ { x: '2021-03-24', y: 390 } ] } ] }; module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8718', config: { type: 'bar', data, options: { scales: { x: { type: 'time', time: { unit: 'day', }, }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 256, height: 128} } }; ================================================ FILE: test/fixtures/scale.time/offset-with-2-ticks.js ================================================ const data = { datasets: [ { label: 1, backgroundColor: 'orange', data: [ { x: '2021-03-24', y: 464 } ] }, { label: 2, backgroundColor: 'red', data: [ { x: '2021-03-24', y: 464 } ] }, { label: 3, backgroundColor: 'blue', data: [ { x: '2021-03-24', y: 390 } ] }, { label: 4, backgroundColor: 'purple', data: [ { x: '2021-03-25', y: 464 } ] }, { label: 5, backgroundColor: 'black', data: [ { x: '2021-03-25', y: 464 } ] }, { label: 6, backgroundColor: 'cyan', data: [ { x: '2021-03-25', y: 390 } ] } ] }; module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/8718', config: { type: 'bar', data, options: { scales: { x: { type: 'time', time: { unit: 'day', }, }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 256, height: 128} } }; ================================================ FILE: test/fixtures/scale.time/offset-with-no-ticks.js ================================================ const data = { datasets: [ { data: [ { x: moment('15/10/2020', 'DD/MM/YYYY').valueOf(), y: 55 }, { x: moment('18/10/2020', 'DD/MM/YYYY').valueOf(), y: 10 }, { x: moment('19/10/2020', 'DD/MM/YYYY').valueOf(), y: 15 } ], backgroundColor: 'blue' }, { data: [ { x: moment('15/10/2020', 'DD/MM/YYYY').valueOf(), y: 6 }, { x: moment('18/10/2020', 'DD/MM/YYYY').valueOf(), y: 11 }, { x: moment('19/10/2020', 'DD/MM/YYYY').valueOf(), y: 16 } ], backgroundColor: 'green', }, { data: [ { x: moment('15/10/2020', 'DD/MM/YYYY').valueOf(), y: 7 }, { x: moment('18/10/2020', 'DD/MM/YYYY').valueOf(), y: 12 }, { x: moment('19/10/2020', 'DD/MM/YYYY').valueOf(), y: 17 } ], backgroundColor: 'red', } ] }; module.exports = { description: 'https://github.com/chartjs/Chart.js/issues/7991', config: { type: 'bar', data, options: { scales: { x: { type: 'time', time: { unit: 'month', }, }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 256, height: 128} } }; ================================================ FILE: test/fixtures/scale.time/skip-null-gridlines.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0025, config: { type: 'line', data: { labels: ['2017', '2018', '2019', '2020', '2025'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'time', time: { parser: 'YYYY', unit: 'year' }, ticks: { source: 'auto', callback: (tick, index) => index % 2 === 0 ? null : tick, } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/skip-undefined-gridlines.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0025, config: { type: 'line', data: { labels: ['2017', '2018', '2019', '2020', '2025'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'time', time: { parser: 'YYYY', unit: 'year' }, ticks: { source: 'auto', callback: (tick, index) => index % 2 === 0 ? undefined : tick, } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/source-auto-linear.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0025, config: { type: 'line', data: { labels: ['2017', '2018', '2019', '2020', '2025'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'time', time: { parser: 'YYYY', unit: 'year' }, ticks: { source: 'auto' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/source-data-linear.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0015, config: { type: 'line', data: { labels: ['2017', '2018', '2019', '2020', '2025'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'time', time: { parser: 'YYYY', unit: 'year' }, ticks: { source: 'data' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/source-labels-linear-offset-min-max.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0015, config: { type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4, 5], fill: false}] }, options: { scales: { x: { type: 'time', min: '2012', max: '2051', offset: true, time: { parser: 'YYYY', }, ticks: { source: 'labels' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/source-labels-linear.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0015, config: { type: 'line', data: { labels: ['2017', '2018', '2019', '2020', '2025'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'time', time: { parser: 'YYYY', unit: 'year' }, ticks: { source: 'labels' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/ticks-capacity.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.002, config: { type: 'line', data: { labels: [ '2012-01-01', '2013-01-01', '2014-01-01', '2015-01-01', '2016-01-01', '2017-01-01', '2018-01-01', '2019-01-01' ] }, options: { scales: { x: { type: 'time', time: { unit: 'year' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/ticks-minunit.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.002, config: { type: 'line', data: { labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], }, options: { scales: { x: { type: 'time', bounds: 'ticks', time: { minUnit: 'day' } }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 256, height: 128} } }; ================================================ FILE: test/fixtures/scale.time/ticks-reverse-linear-min-max.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0015, config: { type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4, 5], fill: false}] }, options: { scales: { x: { type: 'time', min: '2012', max: '2050', time: { parser: 'YYYY' }, reverse: true, ticks: { source: 'labels' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/ticks-reverse-linear.js ================================================ module.exports = { threshold: 0.01, config: { type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'time', time: { parser: 'YYYY' }, reverse: true, ticks: { source: 'labels' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/ticks-reverse-offset.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.002, config: { type: 'line', data: { labels: ['2017', '2018', '2019', '2020', '2021'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'time', reverse: true, offset: true, time: { parser: 'YYYY', unit: 'year' }, ticks: { source: 'labels', }, }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/ticks-reverse.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0015, config: { type: 'line', data: { labels: ['2017', '2018', '2019', '2020', '2021'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'time', reverse: true, time: { parser: 'YYYY', unit: 'year' }, ticks: { source: 'labels', }, }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.time/ticks-round.js ================================================ module.exports = { threshold: 0.05, config: { type: 'line', data: { labels: ['2015-01-01T20:00:00', '2015-02-02T21:00:00', '2015-02-21T01:00:00'] }, options: { scales: { x: { type: 'time', bounds: 'ticks', time: { unit: 'week', round: 'week' } }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 512, height: 256} } }; ================================================ FILE: test/fixtures/scale.time/ticks-stepsize.js ================================================ module.exports = { threshold: 0.01, config: { type: 'line', data: { labels: ['2015-01-01T20:00:00', '2015-01-01T21:00:00'] }, options: { scales: { x: { type: 'time', bounds: 'ticks', time: { unit: 'hour', }, ticks: { stepSize: 2 } }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 512, height: 128} } }; ================================================ FILE: test/fixtures/scale.time/ticks-unit.js ================================================ module.exports = { threshold: 0.05, tolerance: 0.002, config: { type: 'line', data: { labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00'], }, options: { scales: { x: { type: 'time', time: { unit: 'hour', } }, y: { display: false } } } }, options: { spriteText: true, canvas: {width: 1200, height: 200} } }; ================================================ FILE: test/fixtures/scale.timeseries/data-timestamps.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0015, config: { type: 'line', data: { datasets: [{data: [ {x: 1687849697000, y: 904}, {x: 1687817063000, y: 905}, {x: 1687694268000, y: 913}, {x: 1687609438000, y: 914}, {x: 1687561387000, y: 916}, {x: 1686875127000, y: 918}, {x: 1686873138000, y: 920}, {x: 1686872777000, y: 928}, {x: 1686081641000, y: 915} ], fill: false}, {data: [ {x: 1687816803000, y: 1105}, {x: 1686869490000, y: 1114}, {x: 1686869397000, y: 1103}, {x: 1686869225000, y: 1091}, {x: 1686556516000, y: 1078} ]}] }, options: { scales: { x: { type: 'timeseries', bounds: 'data', time: { unit: 'day' }, ticks: { source: 'auto' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.timeseries/financial-daily.js ================================================ const data = [{x: 631180800000, y: 31.80}, {x: 631267200000, y: 30.20}, {x: 631353600000, y: 29.84}, {x: 631440000000, y: 29.72}, {x: 631526400000, y: 28.91}, {x: 631785600000, y: 29.55}, {x: 631872000000, y: 30.39}, {x: 631958400000, y: 29.54}, {x: 632044800000, y: 28.86}, {x: 632131200000, y: 30.75}, {x: 632390400000, y: 31.86}, {x: 632476800000, y: 33.59}, {x: 632563200000, y: 31.22}, {x: 632649600000, y: 30.12}, {x: 632736000000, y: 30.68}, {x: 632995200000, y: 31.46}, {x: 633081600000, y: 30.77}, {x: 633168000000, y: 30.27}, {x: 633254400000, y: 29.64}, {x: 633340800000, y: 30.53}, {x: 633600000000, y: 30.79}, {x: 633686400000, y: 30.27}, {x: 633772800000, y: 30.18}, {x: 633859200000, y: 27.72}, {x: 633945600000, y: 27.83}, {x: 634204800000, y: 27.82}, {x: 634291200000, y: 29.10}, {x: 634377600000, y: 28.34}, {x: 634464000000, y: 29.52}, {x: 634550400000, y: 28.69}, {x: 634809600000, y: 28.23}, {x: 634896000000, y: 27.45}, {x: 634982400000, y: 27.40}, {x: 635068800000, y: 28.39}, {x: 635155200000, y: 30.03}, {x: 635414400000, y: 31.19}, {x: 635500800000, y: 32.30}, {x: 635587200000, y: 33.84}, {x: 635673600000, y: 32.34}, {x: 635760000000, y: 31.96}, {x: 636019200000, y: 31.95}, {x: 636105600000, y: 32.84}, {x: 636192000000, y: 30.80}, {x: 636278400000, y: 31.54}, {x: 636364800000, y: 30.81}, {x: 636624000000, y: 32.99}, {x: 636710400000, y: 32.25}, {x: 636796800000, y: 33.87}, {x: 636883200000, y: 35.75}, {x: 636969600000, y: 35.71}, {x: 637228800000, y: 36.60}, {x: 637315200000, y: 35.65}, {x: 637401600000, y: 34.36}, {x: 637488000000, y: 33.61}, {x: 637574400000, y: 34.24}, {x: 637833600000, y: 32.79}, {x: 637920000000, y: 34.41}, {x: 638006400000, y: 34.11}, {x: 638092800000, y: 33.91}, {x: 638179200000, y: 33.33}, {x: 638438400000, y: 32.99}, {x: 638524800000, y: 34.17}, {x: 638611200000, y: 33.50}, {x: 638697600000, y: 35.64}, {x: 638784000000, y: 35.50}, {x: 639039600000, y: 33.11}, {x: 639126000000, y: 34.08}, {x: 639212400000, y: 35.69}, {x: 639298800000, y: 38.24}, {x: 639385200000, y: 40.86}, {x: 639644400000, y: 41.99}, {x: 639730800000, y: 44.45}, {x: 639817200000, y: 45.06}, {x: 639903600000, y: 44.32}, {x: 639990000000, y: 43.70}, {x: 640249200000, y: 44.97}, {x: 640335600000, y: 44.92}, {x: 640422000000, y: 44.11}, {x: 640508400000, y: 44.42}, {x: 640594800000, y: 43.90}, {x: 640854000000, y: 41.91}, {x: 640940400000, y: 41.60}, {x: 641026800000, y: 41.84}, {x: 641113200000, y: 42.55}, {x: 641199600000, y: 40.56}, {x: 641458800000, y: 39.99}, {x: 641545200000, y: 43.51}, {x: 641631600000, y: 43.17}, {x: 641718000000, y: 40.52}, {x: 641804400000, y: 41.06}, {x: 642063600000, y: 40.15}, {x: 642150000000, y: 43.82}, {x: 642236400000, y: 43.19}, {x: 642322800000, y: 40.99}, {x: 642409200000, y: 41.16}, {x: 642668400000, y: 41.02}, {x: 642754800000, y: 40.03}, {x: 642841200000, y: 36.46}, {x: 642927600000, y: 39.11}, {x: 643014000000, y: 41.10}, {x: 643273200000, y: 41.15}, {x: 643359600000, y: 39.01}, {x: 643446000000, y: 39.48}, {x: 643532400000, y: 41.89}, {x: 643618800000, y: 40.74}, {x: 643878000000, y: 38.88}, {x: 643964400000, y: 38.11}, {x: 644050800000, y: 40.39}, {x: 644137200000, y: 38.28}, {x: 644223600000, y: 39.96}, {x: 644482800000, y: 39.37}, {x: 644569200000, y: 39.39}, {x: 644655600000, y: 39.62}, {x: 644742000000, y: 38.99}, {x: 644828400000, y: 40.25}, {x: 645087600000, y: 42.85}, {x: 645174000000, y: 45.91}, {x: 645260400000, y: 46.66}, {x: 645346800000, y: 48.08}, {x: 645433200000, y: 51.00}, {x: 645692400000, y: 50.61}, {x: 645778800000, y: 54.55}, {x: 645865200000, y: 53.59}, {x: 645951600000, y: 53.39}, {x: 646038000000, y: 54.61}, {x: 646297200000, y: 55.02}, {x: 646383600000, y: 57.35}, {x: 646470000000, y: 56.95}, {x: 646556400000, y: 60.08}, {x: 646642800000, y: 59.80}, {x: 646902000000, y: 61.29}, {x: 646988400000, y: 63.45}, {x: 647074800000, y: 62.07}, {x: 647161200000, y: 59.01}, {x: 647247600000, y: 59.76}, {x: 647506800000, y: 60.08}, {x: 647593200000, y: 60.96}, {x: 647679600000, y: 60.56}, {x: 647766000000, y: 58.60}, {x: 647852400000, y: 57.40}, {x: 648111600000, y: 59.86}, {x: 648198000000, y: 58.76}, {x: 648284400000, y: 57.54}, {x: 648370800000, y: 57.78}, {x: 648457200000, y: 54.33}, {x: 648716400000, y: 54.57}, {x: 648802800000, y: 53.69}, {x: 648889200000, y: 57.02}, {x: 648975600000, y: 52.30}, {x: 649062000000, y: 49.79}, {x: 649321200000, y: 47.40}, {x: 649407600000, y: 45.44}, {x: 649494000000, y: 46.75}, {x: 649580400000, y: 44.19}, {x: 649666800000, y: 43.05}, {x: 649926000000, y: 43.99}, {x: 650012400000, y: 45.99}, {x: 650098800000, y: 42.15}, {x: 650185200000, y: 41.84}, {x: 650271600000, y: 43.30}, {x: 650530800000, y: 41.57}, {x: 650617200000, y: 42.13}, {x: 650703600000, y: 43.29}, {x: 650790000000, y: 43.98}, {x: 650876400000, y: 44.51}, {x: 651135600000, y: 45.50}, {x: 651222000000, y: 43.63}, {x: 651308400000, y: 41.93}, {x: 651394800000, y: 38.41}, {x: 651481200000, y: 41.01}, {x: 651740400000, y: 38.17}, {x: 651826800000, y: 38.32}, {x: 651913200000, y: 38.27}, {x: 651999600000, y: 36.10}, {x: 652086000000, y: 34.62}, {x: 652345200000, y: 33.91}, {x: 652431600000, y: 34.25}, {x: 652518000000, y: 33.97}, {x: 652604400000, y: 35.11}, {x: 652690800000, y: 35.05}, {x: 652950000000, y: 36.37}, {x: 653036400000, y: 35.54}, {x: 653122800000, y: 35.80}, {x: 653209200000, y: 36.75}, {x: 653295600000, y: 35.48}, {x: 653554800000, y: 36.78}, {x: 653641200000, y: 34.35}, {x: 653727600000, y: 32.62}, {x: 653814000000, y: 32.66}, {x: 653900400000, y: 31.45}, {x: 654159600000, y: 29.29}, {x: 654246000000, y: 31.18}, {x: 654332400000, y: 29.47}, {x: 654418800000, y: 28.40}, {x: 654505200000, y: 28.21}, {x: 654764400000, y: 27.73}, {x: 654850800000, y: 27.08}, {x: 654937200000, y: 25.32}, {x: 655023600000, y: 25.69}, {x: 655110000000, y: 27.28}, {x: 655369200000, y: 28.53}, {x: 655455600000, y: 27.88}, {x: 655542000000, y: 28.17}, {x: 655628400000, y: 26.22}, {x: 655714800000, y: 26.07}, {x: 655974000000, y: 28.42}, {x: 656060400000, y: 28.27}, {x: 656146800000, y: 29.76}, {x: 656233200000, y: 29.58}, {x: 656319600000, y: 29.41}, {x: 656578800000, y: 29.34}, {x: 656665200000, y: 29.45}, {x: 656751600000, y: 27.93}, {x: 656838000000, y: 27.68}, {x: 656924400000, y: 27.42}, {x: 657187200000, y: 25.79}, {x: 657273600000, y: 25.84}, {x: 657360000000, y: 26.00}, {x: 657446400000, y: 26.57}, {x: 657532800000, y: 26.66}, {x: 657792000000, y: 26.40}, {x: 657878400000, y: 28.06}, {x: 657964800000, y: 27.58}, {x: 658051200000, y: 27.18}, {x: 658137600000, y: 27.71}, {x: 658396800000, y: 26.37}, {x: 658483200000, y: 26.53}, {x: 658569600000, y: 26.19}, {x: 658656000000, y: 25.29}, {x: 658742400000, y: 27.33}, {x: 659001600000, y: 26.08}, {x: 659088000000, y: 26.26}, {x: 659174400000, y: 26.35}, {x: 659260800000, y: 24.88}, {x: 659347200000, y: 23.71}, {x: 659606400000, y: 25.77}, {x: 659692800000, y: 26.03}, {x: 659779200000, y: 27.38}, {x: 659865600000, y: 27.82}, {x: 659952000000, y: 27.61}, {x: 660211200000, y: 26.15}, {x: 660297600000, y: 26.79}, {x: 660384000000, y: 26.78}, {x: 660470400000, y: 28.69}, {x: 660556800000, y: 29.38}, {x: 660816000000, y: 30.16}, {x: 660902400000, y: 29.42}, {x: 660988800000, y: 29.06}, {x: 661075200000, y: 28.05}, {x: 661161600000, y: 29.48}, {x: 661420800000, y: 28.48}, {x: 661507200000, y: 28.67}, {x: 661593600000, y: 28.27}, {x: 661680000000, y: 27.29}, {x: 661766400000, y: 26.88}, {x: 662025600000, y: 27.12}, {x: 662112000000, y: 27.02}, {x: 662198400000, y: 27.08}, {x: 662284800000, y: 24.53}, {x: 662371200000, y: 25.19}, {x: 662630400000, y: 26.70}, {x: 662716800000, y: 27.23}, {x: 662803200000, y: 26.26}, {x: 662889600000, y: 26.46}, {x: 662976000000, y: 25.38}, {x: 663235200000, y: 25.23}, {x: 663321600000, y: 25.53}, {x: 663408000000, y: 25.71}, {x: 663494400000, y: 25.39}, {x: 663580800000, y: 24.35}, {x: 663840000000, y: 23.64}, {x: 663926400000, y: 22.98}, {x: 664012800000, y: 22.75}, {x: 664099200000, y: 22.70}, {x: 664185600000, y: 21.56}, {x: 664444800000, y: 22.65}, {x: 664531200000, y: 21.54}, {x: 664617600000, y: 20.68}, {x: 664704000000, y: 21.37}, {x: 664790400000, y: 22.44}, {x: 665049600000, y: 23.89}, {x: 665136000000, y: 25.02}, {x: 665222400000, y: 26.84}, {x: 665308800000, y: 26.11}, {x: 665395200000, y: 25.91}, {x: 665654400000, y: 27.21}, {x: 665740800000, y: 26.37}, {x: 665827200000, y: 26.81}, {x: 665913600000, y: 26.42}, {x: 666000000000, y: 26.73}, {x: 666259200000, y: 27.25}, {x: 666345600000, y: 25.01}, {x: 666432000000, y: 24.55}, {x: 666518400000, y: 25.34}, {x: 666604800000, y: 25.37}, {x: 666864000000, y: 27.51}, {x: 666950400000, y: 27.51}, {x: 667036800000, y: 28.65}, {x: 667123200000, y: 28.90}, {x: 667209600000, y: 29.22}, {x: 667468800000, y: 29.77}, {x: 667555200000, y: 29.21}, {x: 667641600000, y: 29.81}, {x: 667728000000, y: 27.75}, {x: 667814400000, y: 28.56}, {x: 668073600000, y: 28.06}, {x: 668160000000, y: 26.70}, {x: 668246400000, y: 26.39}, {x: 668332800000, y: 26.42}, {x: 668419200000, y: 29.05}, {x: 668678400000, y: 27.84}, {x: 668764800000, y: 27.67}, {x: 668851200000, y: 26.75}, {x: 668937600000, y: 26.20}, {x: 669024000000, y: 27.33}, {x: 669283200000, y: 27.55}, {x: 669369600000, y: 26.79}, {x: 669456000000, y: 25.29}, {x: 669542400000, y: 25.17}, {x: 669628800000, y: 25.55}, {x: 669888000000, y: 23.87}, {x: 669974400000, y: 22.92}, {x: 670060800000, y: 23.80}, {x: 670147200000, y: 24.18}, {x: 670233600000, y: 22.56}, {x: 670492800000, y: 21.93}, {x: 670579200000, y: 20.96}, {x: 670665600000, y: 21.94}, {x: 670752000000, y: 21.48}, {x: 670838400000, y: 22.17}, {x: 671094000000, y: 22.68}, {x: 671180400000, y: 20.56}, {x: 671266800000, y: 18.98}, {x: 671353200000, y: 19.93}, {x: 671439600000, y: 19.53}, {x: 671698800000, y: 18.93}, {x: 671785200000, y: 19.41}, {x: 671871600000, y: 18.61}, {x: 671958000000, y: 18.88}, {x: 672044400000, y: 18.70}, {x: 672303600000, y: 18.80}, {x: 672390000000, y: 17.72}, {x: 672476400000, y: 17.65}, {x: 672562800000, y: 17.99}, {x: 672649200000, y: 17.01}, {x: 672908400000, y: 17.05}, {x: 672994800000, y: 16.39}, {x: 673081200000, y: 15.96}, {x: 673167600000, y: 15.82}, {x: 673254000000, y: 16.26}, {x: 673513200000, y: 16.33}, {x: 673599600000, y: 15.73}, {x: 673686000000, y: 15.02}, {x: 673772400000, y: 14.51}, {x: 673858800000, y: 14.71}, {x: 674118000000, y: 15.29}, {x: 674204400000, y: 15.46}, {x: 674290800000, y: 15.30}, {x: 674377200000, y: 14.14}, {x: 674463600000, y: 13.94}, {x: 674722800000, y: 13.01}, {x: 674809200000, y: 13.59}, {x: 674895600000, y: 13.67}, {x: 674982000000, y: 13.28}, {x: 675068400000, y: 13.11}, {x: 675327600000, y: 13.52}, {x: 675414000000, y: 14.02}, {x: 675500400000, y: 14.53}, {x: 675586800000, y: 14.61}, {x: 675673200000, y: 14.53}, {x: 675932400000, y: 14.29}, {x: 676018800000, y: 14.46}, {x: 676105200000, y: 14.07}, {x: 676191600000, y: 13.91}, {x: 676278000000, y: 14.08}, {x: 676537200000, y: 13.63}, {x: 676623600000, y: 14.38}, {x: 676710000000, y: 14.86}, {x: 676796400000, y: 14.82}, {x: 676882800000, y: 14.04}, {x: 677142000000, y: 14.63}, {x: 677228400000, y: 14.83}, {x: 677314800000, y: 15.33}, {x: 677401200000, y: 14.67}, {x: 677487600000, y: 14.18}, {x: 677746800000, y: 14.40}, {x: 677833200000, y: 14.45}, {x: 677919600000, y: 14.78}, {x: 678006000000, y: 14.93}, {x: 678092400000, y: 14.09}, {x: 678351600000, y: 13.56}, {x: 678438000000, y: 14.26}, {x: 678524400000, y: 14.36}, {x: 678610800000, y: 14.82}, {x: 678697200000, y: 15.96}, {x: 678956400000, y: 15.83}, {x: 679042800000, y: 15.92}, {x: 679129200000, y: 15.29}, {x: 679215600000, y: 16.29}, {x: 679302000000, y: 15.31}, {x: 679561200000, y: 15.13}, {x: 679647600000, y: 15.59}, {x: 679734000000, y: 14.97}, {x: 679820400000, y: 15.81}, {x: 679906800000, y: 15.59}, {x: 680166000000, y: 14.83}, {x: 680252400000, y: 14.57}, {x: 680338800000, y: 14.24}, {x: 680425200000, y: 14.49}, {x: 680511600000, y: 13.80}, {x: 680770800000, y: 14.17}, {x: 680857200000, y: 14.40}, {x: 680943600000, y: 14.31}, {x: 681030000000, y: 13.89}, {x: 681116400000, y: 13.59}, {x: 681375600000, y: 13.36}, {x: 681462000000, y: 13.33}, {x: 681548400000, y: 13.26}, {x: 681634800000, y: 13.71}, {x: 681721200000, y: 13.67}, {x: 681980400000, y: 12.87}, {x: 682066800000, y: 14.03}, {x: 682153200000, y: 13.95}, {x: 682239600000, y: 13.11}, {x: 682326000000, y: 14.05}, {x: 682585200000, y: 14.47}, {x: 682671600000, y: 14.45}, {x: 682758000000, y: 15.14}, {x: 682844400000, y: 15.65}, {x: 682930800000, y: 15.15}, {x: 683190000000, y: 15.22}, {x: 683276400000, y: 15.38}, {x: 683362800000, y: 16.42}, {x: 683449200000, y: 16.26}, {x: 683535600000, y: 16.51}, {x: 683794800000, y: 15.66}, {x: 683881200000, y: 15.88}, {x: 683967600000, y: 16.36}, {x: 684054000000, y: 15.87}, {x: 684140400000, y: 15.61}, {x: 684399600000, y: 16.63}, {x: 684486000000, y: 15.88}, {x: 684572400000, y: 17.21}, {x: 684658800000, y: 18.46}, {x: 684745200000, y: 18.76}, {x: 685004400000, y: 18.39}, {x: 685090800000, y: 18.14}, {x: 685177200000, y: 17.31}, {x: 685263600000, y: 17.21}, {x: 685350000000, y: 17.17}, {x: 685609200000, y: 17.21}, {x: 685695600000, y: 16.86}, {x: 685782000000, y: 17.17}, {x: 685868400000, y: 16.20}, {x: 685954800000, y: 15.14}, {x: 686214000000, y: 15.05}, {x: 686300400000, y: 16.09}, {x: 686386800000, y: 16.40}, {x: 686473200000, y: 15.83}, {x: 686559600000, y: 16.53}, {x: 686818800000, y: 16.32}, {x: 686905200000, y: 16.47}, {x: 686991600000, y: 16.59}, {x: 687078000000, y: 16.51}, {x: 687164400000, y: 17.41}, {x: 687423600000, y: 18.17}, {x: 687510000000, y: 17.63}, {x: 687596400000, y: 17.62}, {x: 687682800000, y: 17.69}, {x: 687769200000, y: 17.54}, {x: 688028400000, y: 16.56}, {x: 688114800000, y: 16.83}, {x: 688201200000, y: 15.98}, {x: 688287600000, y: 16.52}, {x: 688374000000, y: 17.08}, {x: 688636800000, y: 17.27}, {x: 688723200000, y: 18.18}, {x: 688809600000, y: 18.67}, {x: 688896000000, y: 18.97}, {x: 688982400000, y: 20.31}, {x: 689241600000, y: 21.30}, {x: 689328000000, y: 20.96}, {x: 689414400000, y: 20.01}, {x: 689500800000, y: 21.13}, {x: 689587200000, y: 21.52}, {x: 689846400000, y: 22.08}, {x: 689932800000, y: 21.88}, {x: 690019200000, y: 21.18}, {x: 690105600000, y: 22.79}, {x: 690192000000, y: 22.51}, {x: 690451200000, y: 23.66}, {x: 690537600000, y: 23.43}, {x: 690624000000, y: 24.08}, {x: 690710400000, y: 24.83}, {x: 690796800000, y: 23.49}, {x: 691056000000, y: 23.43}, {x: 691142400000, y: 23.98}, {x: 691228800000, y: 24.52}, {x: 691315200000, y: 23.32}, {x: 691401600000, y: 23.63}, {x: 691660800000, y: 21.74}, {x: 691747200000, y: 20.03}, {x: 691833600000, y: 20.37}, {x: 691920000000, y: 21.09}, {x: 692006400000, y: 21.33}, {x: 692265600000, y: 20.48}, {x: 692352000000, y: 20.15}, {x: 692438400000, y: 20.33}, {x: 692524800000, y: 19.53}, {x: 692611200000, y: 19.34}, {x: 692870400000, y: 18.63}, {x: 692956800000, y: 18.42}, {x: 693043200000, y: 19.49}, {x: 693129600000, y: 18.75}, {x: 693216000000, y: 18.11}, {x: 693475200000, y: 17.40}, {x: 693561600000, y: 17.40}, {x: 693648000000, y: 17.73}, {x: 693734400000, y: 18.36}, {x: 693820800000, y: 18.14}, {x: 694080000000, y: 18.71}, {x: 694166400000, y: 17.97}, {x: 694252800000, y: 18.90}, {x: 694339200000, y: 18.31}, {x: 694425600000, y: 18.67}, {x: 694684800000, y: 18.78}, {x: 694771200000, y: 19.53}, {x: 694857600000, y: 19.41}, {x: 694944000000, y: 19.42}, {x: 695030400000, y: 20.29}, {x: 695289600000, y: 21.08}, {x: 695376000000, y: 20.69}, {x: 695462400000, y: 21.37}, {x: 695548800000, y: 20.67}, {x: 695635200000, y: 20.79}, {x: 695894400000, y: 20.39}, {x: 695980800000, y: 19.98}, {x: 696067200000, y: 19.35}, {x: 696153600000, y: 18.60}, {x: 696240000000, y: 18.67}, {x: 696499200000, y: 19.41}, {x: 696585600000, y: 20.62}, {x: 696672000000, y: 21.09}, {x: 696758400000, y: 21.43}, {x: 696844800000, y: 20.31}, {x: 697104000000, y: 19.40}, {x: 697190400000, y: 19.82}, {x: 697276800000, y: 19.55}, {x: 697363200000, y: 19.77}, {x: 697449600000, y: 19.33}, {x: 697708800000, y: 18.75}, {x: 697795200000, y: 18.50}, {x: 697881600000, y: 18.39}, {x: 697968000000, y: 19.19}, {x: 698054400000, y: 19.93}, {x: 698313600000, y: 20.15}, {x: 698400000000, y: 22.09}, {x: 698486400000, y: 20.29}, {x: 698572800000, y: 20.37}, {x: 698659200000, y: 19.06}, {x: 698918400000, y: 20.51}, {x: 699004800000, y: 20.06}, {x: 699091200000, y: 19.54}, {x: 699177600000, y: 17.89}, {x: 699264000000, y: 17.57}, {x: 699523200000, y: 16.88}, {x: 699609600000, y: 17.26}, {x: 699696000000, y: 17.15}, {x: 699782400000, y: 15.73}, {x: 699868800000, y: 15.08}, {x: 700128000000, y: 14.73}, {x: 700214400000, y: 14.58}, {x: 700300800000, y: 14.33}, {x: 700387200000, y: 14.76}, {x: 700473600000, y: 15.44}, {x: 700732800000, y: 16.63}, {x: 700819200000, y: 15.63}, {x: 700905600000, y: 15.61}, {x: 700992000000, y: 16.88}, {x: 701078400000, y: 16.26}, {x: 701337600000, y: 15.95}, {x: 701424000000, y: 15.41}, {x: 701510400000, y: 16.14}, {x: 701596800000, y: 15.77}, {x: 701683200000, y: 15.84}, {x: 701942400000, y: 14.41}, {x: 702028800000, y: 15.62}, {x: 702115200000, y: 15.62}, {x: 702201600000, y: 15.85}, {x: 702288000000, y: 17.18}, {x: 702543600000, y: 17.58}, {x: 702630000000, y: 19.25}, {x: 702716400000, y: 19.77}, {x: 702802800000, y: 20.66}, {x: 702889200000, y: 19.70}, {x: 703148400000, y: 20.01}, {x: 703234800000, y: 19.93}, {x: 703321200000, y: 19.94}, {x: 703407600000, y: 19.77}, {x: 703494000000, y: 19.83}]; module.exports = { threshold: 0.01, tolerance: 0.0015, config: { data: { datasets: [{ data, type: 'line', pointRadius: 0, fill: false, tension: 0, borderWidth: 2 }] }, options: { animation: { duration: 0 }, scales: { x: { type: 'timeseries', offset: true, ticks: { major: { enabled: true, }, font: function(context) { return context.tick && context.tick.major ? {weight: 'bold'} : undefined; }, source: 'data', autoSkip: true, autoSkipPadding: 75, maxRotation: 0, sampleSize: 100, maxTicksLimit: 3 }, // manually set major ticks so that test passes in all time zones with moment adapter afterBuildTicks: function(scale) { const ticks = scale.ticks; const major = [0, 264, 522]; for (let i = 0; i < ticks.length; i++) { ticks[i].major = major.indexOf(i) >= 0; } } }, y: { type: 'linear', border: { display: false } } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.timeseries/normalize.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.002, config: { type: 'line', data: { datasets: [{ data: [ {x: '2017', y: null}, {x: '2018', y: 1}, {x: '2019', y: 2}, {x: '2020', y: 3}, {x: '2021', y: 4} ], fill: false }] }, options: { normalized: true, scales: { x: { type: 'timeseries', time: { parser: 'YYYY' }, ticks: { source: 'data' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.timeseries/source-auto.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0025, config: { type: 'line', data: { labels: ['2017', '2018', '2019', '2020', '2025'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'timeseries', time: { parser: 'YYYY', unit: 'year' }, ticks: { source: 'auto' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.timeseries/source-data-offset-min-max.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.002, config: { type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'timeseries', min: '2012', max: '2051', offset: true, time: { parser: 'YYYY', }, ticks: { source: 'data' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.timeseries/source-data.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0015, config: { type: 'line', data: { labels: ['2017', '2018', '2019', '2020', '2025'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'timeseries', time: { parser: 'YYYY', unit: 'year' }, ticks: { source: 'data' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.timeseries/source-labels-offset-min-max.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0015, config: { type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'timeseries', min: '2012', max: '2051', offset: true, time: { parser: 'YYYY', }, ticks: { source: 'labels' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.timeseries/source-labels.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0015, config: { type: 'line', data: { labels: ['2017', '2018', '2019', '2020', '2025'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'timeseries', time: { parser: 'YYYY', unit: 'year' }, ticks: { source: 'labels' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.timeseries/ticks-reverse-max.js ================================================ module.exports = { threshold: 0.01, config: { type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'timeseries', max: '2050', time: { parser: 'YYYY' }, reverse: true, ticks: { source: 'labels' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.timeseries/ticks-reverse-min-max.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.002, config: { type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'timeseries', min: '2012', max: '2050', time: { parser: 'YYYY' }, reverse: true, ticks: { source: 'labels' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.timeseries/ticks-reverse-min.js ================================================ module.exports = { threshold: 0.01, config: { type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'timeseries', min: '2012', time: { parser: 'YYYY' }, reverse: true, ticks: { source: 'labels' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/fixtures/scale.timeseries/ticks-reverse.js ================================================ module.exports = { threshold: 0.01, tolerance: 0.0015, config: { type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4], fill: false}] }, options: { scales: { x: { type: 'timeseries', time: { parser: 'YYYY' }, reverse: true, ticks: { source: 'labels' } }, y: { display: false } } } }, options: { spriteText: true } }; ================================================ FILE: test/index.js ================================================ import {acquireChart, releaseChart, createMockContext, afterEvent, waitForResize, injectWrapperCSS, specsFromFixtures, triggerMouseEvent, addMatchers, releaseCharts} from 'chartjs-test-utils'; // force ratio=1 for tests on high-res/retina devices // fixes https://github.com/chartjs/Chart.js/issues/4515 window.devicePixelRatio = 1; window.acquireChart = acquireChart; window.afterEvent = afterEvent; window.releaseChart = releaseChart; window.waitForResize = waitForResize; window.createMockContext = createMockContext; injectWrapperCSS(); jasmine.fixture = { specs: specsFromFixtures }; jasmine.triggerMouseEvent = triggerMouseEvent; // Set a fixed time zone (and, in particular, disable Daylight Saving Time) for // more stable test results. window.moment.tz.setDefault('Etc/UTC'); beforeAll(() => { // Disable colors plugin for tests. window.Chart.defaults.plugins.colors.enabled = false; }); beforeEach(() => { addMatchers(); }); afterEach(() => { releaseCharts(); }); ================================================ FILE: test/integration/node/package.json ================================================ { "private": true, "description": "chart.js should work in Node", "type": "module", "scripts": { "test": "npm run test-mjs && npm run test-cjs", "test-mjs": "node test.js", "test-cjs": "node test.cjs" }, "dependencies": { "chart.js": "workspace:*" } } ================================================ FILE: test/integration/node/test.cjs ================================================ const {Chart} = require('chart.js'); const {valueOrDefault} = require('chart.js/helpers'); Chart.register({ id: 'TEST_PLUGIN', dummyValue: valueOrDefault(0, 1) }); ================================================ FILE: test/integration/node/test.js ================================================ import {Chart} from 'chart.js'; import {valueOrDefault} from 'chart.js/helpers'; Chart.register({ id: 'TEST_PLUGIN', dummyValue: valueOrDefault(0, 1) }); ================================================ FILE: test/integration/node-commonjs/package.json ================================================ { "private": true, "description": "chart.js should work in Node", "scripts": { "test": "npm run test-index && npm run test-auto", "test-index": "node test.js", "test-auto": "node test-auto.js" }, "dependencies": { "chart.js": "workspace:*" } } ================================================ FILE: test/integration/node-commonjs/test-auto.js ================================================ const Chart = require('chart.js/auto'); const {valueOrDefault} = require('chart.js/helpers'); Chart.register({ id: 'TEST_PLUGIN', dummyValue: valueOrDefault(0, 1) }); ================================================ FILE: test/integration/node-commonjs/test.js ================================================ const {Chart} = require('chart.js'); const {valueOrDefault} = require('chart.js/helpers'); Chart.register({ id: 'TEST_PLUGIN', dummyValue: valueOrDefault(0, 1) }); ================================================ FILE: test/integration/react-browser/package.json ================================================ { "private": true, "description": "chart.js should work in react-browser (Web)", "dependencies": { "@babel/core": "^7.0.0", "@babel/plugin-syntax-flow": "^7.14.5", "@babel/plugin-transform-react-jsx": "^7.14.9", "@types/node": "^18.7.6", "@types/react": "^18.0.17", "@types/react-dom": "^18.0.6", "chart.js": "workspace:*", "react": "^18.2.0", "react-dom": "^18.2.0", "react-scripts": "5.0.1", "typescript": "^4.7.4", "web-vitals": "^2.1.4" }, "scripts": { "test": "react-scripts build" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: test/integration/react-browser/public/index.html ================================================ Chartjs test React App

================================================ FILE: test/integration/react-browser/src/App.tsx ================================================ import React, {useEffect} from 'react'; import {Chart, DoughnutController, ArcElement} from 'chart.js'; import {merge} from 'chart.js/helpers'; Chart.register(DoughnutController, ArcElement); function App() { useEffect(() => { const c = Chart.getChart('myChart'); if (c) { c.destroy(); } merge({a: 1}, {b: 2}); // eslint-disable-next-line no-new new Chart('myChart', { type: 'doughnut', data: { labels: ['Chart', 'JS'], datasets: [{ data: [2, 3] }] } }); }, []); return (
); } export default App; ================================================ FILE: test/integration/react-browser/src/AppAuto.tsx ================================================ import React, {useEffect} from 'react'; import Chart from 'chart.js/auto'; import {merge} from 'chart.js/helpers'; function AppAuto() { useEffect(() => { const c = Chart.getChart('myChart'); if (c) { c.destroy(); } merge({a: 1}, {b: 2}); // eslint-disable-next-line no-new new Chart('myChart', { type: 'doughnut', data: { labels: ['Chart', 'JS'], datasets: [{ data: [2, 3] }] } }); }, []); return (
); } export default AppAuto; ================================================ FILE: test/integration/react-browser/src/index.tsx ================================================ import React from 'react'; import {render} from 'react-dom'; import App from './App'; import AppAuto from './AppAuto'; render( , document.getElementById('root') ); ================================================ FILE: test/integration/react-browser/tsconfig.json ================================================ { "compilerOptions": { "jsx": "react", "target": "ES6", "moduleResolution": "Node", "allowSyntheticDefaultImports": true, "alwaysStrict": true, "strict": true, "noEmit": true }, "include": [ "./**/*.tsx", ] } ================================================ FILE: test/integration/typescript-node/package.json ================================================ { "private": true, "type": "module", "description": "chart.js should work in node typescript project", "dependencies": { "chart.js": "workspace:*", "typescript": "^4.7.4" }, "scripts": { "test": "tsc" }, "devDependencies": { "ts-expect": "^1.3.0" } } ================================================ FILE: test/integration/typescript-node/src/index.ts ================================================ /* eslint-disable @typescript-eslint/no-explicit-any, no-console */ import {Chart} from 'chart.js'; import AutoChart from 'chart.js/auto'; import {debounce} from 'chart.js/helpers'; import {TypeOf, expectType} from 'ts-expect'; expectType>(false); expectType>(false); expectType>(false); ================================================ FILE: test/integration/typescript-node/tsconfig.json ================================================ { "compilerOptions": { "target": "ES6", "moduleResolution": "Node", "noEmit": true, "lib": ["es2018", "DOM"] }, "include": [ "./src/**/*.ts", ] } ================================================ FILE: test/integration/typescript-node-next/package.json ================================================ { "private": true, "type": "module", "description": "chart.js should work in node next typescript project", "dependencies": { "chart.js": "workspace:*", "typescript": "^4.7.4" }, "scripts": { "test": "tsc" }, "devDependencies": { "ts-expect": "^1.3.0" } } ================================================ FILE: test/integration/typescript-node-next/src/index.ts ================================================ /* eslint-disable @typescript-eslint/no-explicit-any, no-console */ import {Chart} from 'chart.js'; import AutoChart from 'chart.js/auto'; import {debounce} from 'chart.js/helpers'; import {TypeOf, expectType} from 'ts-expect'; expectType>(false); expectType>(false); expectType>(false); ================================================ FILE: test/integration/typescript-node-next/tsconfig.json ================================================ { "compilerOptions": { "target": "ES6", "moduleResolution": "NodeNext", "noEmit": true, "lib": ["es2018", "DOM"] }, "include": [ "./src/**/*.ts", ] } ================================================ FILE: test/seed-reporter.cjs ================================================ const SeedReporter = function(baseReporterDecorator) { baseReporterDecorator(this); this.onBrowserComplete = function(browser, result) { if (result.order && result.order.random && result.order.seed) { this.write('%s: Randomized with seed %s\n', browser, result.order.seed); } }; }; module.exports = { 'reporter:jasmine-seed': ['type', SeedReporter] }; ================================================ FILE: test/specs/controller.bar.tests.js ================================================ describe('Chart.controllers.bar', function() { describe('auto', jasmine.fixture.specs('controller.bar')); it('should be registered as dataset controller', function() { expect(typeof Chart.controllers.bar).toBe('function'); }); it('should be constructed', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: []}, {data: []} ], labels: [] } }); var meta = chart.getDatasetMeta(1); expect(meta.type).toEqual('bar'); expect(meta.data).toEqual([]); expect(meta.hidden).toBe(null); expect(meta.controller).not.toBe(undefined); expect(meta.controller.index).toBe(1); expect(meta.xAxisID).not.toBe(null); expect(meta.yAxisID).not.toBe(null); meta.controller.updateIndex(0); expect(meta.controller.index).toBe(0); }); it('should set null bars to the reset state', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [10, null, 0, -4], label: 'dataset1', }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(0); var bar = meta.data[1]; var {x, y, base} = bar.getProps(['x', 'y', 'base'], true); expect(isNaN(x)).toBe(false); expect(isNaN(y)).toBe(false); expect(isNaN(base)).toBe(false); }); it('should use the first scale IDs if the dataset does not specify them', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: []}, {data: []} ], labels: [] }, }); var meta = chart.getDatasetMeta(1); expect(meta.xAxisID).toBe('x'); expect(meta.yAxisID).toBe('y'); }); it('should correctly count the number of stacks ignoring datasets of other types and hidden datasets', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], type: 'line'}, {data: [], hidden: true}, {data: []}, {data: []} ], labels: [] } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackCount()).toBe(2); }); it('should correctly count the number of stacks when a group is not specified', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: []}, {data: []}, {data: []}, {data: []} ], labels: [] } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackCount()).toBe(4); }); it('should correctly count the number of stacks when a group is not specified and the scale is stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: []}, {data: []}, {data: []}, {data: []} ], labels: [] }, options: { scales: { x: { stacked: true }, y: { stacked: true } } } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackCount()).toBe(1); }); it('should correctly count the number of stacks when a group is not specified and the scale is not stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: []}, {data: []}, {data: []}, {data: []} ], labels: [] }, options: { scales: { x: { stacked: false }, y: { stacked: false } } } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackCount()).toBe(4); }); it('should correctly count the number of stacks when a group is specified for some', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: []}, {data: []} ], labels: [] } }); var meta = chart.getDatasetMeta(3); expect(meta.controller._getStackCount()).toBe(3); }); it('should correctly count the number of stacks when a group is specified for some and the scale is stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: []}, {data: []} ], labels: [] }, options: { scales: { x: { stacked: true }, y: { stacked: true } } } }); var meta = chart.getDatasetMeta(3); expect(meta.controller._getStackCount()).toBe(2); }); it('should correctly count the number of stacks when a group is specified for some and the scale is not stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: []}, {data: []} ], labels: [] }, options: { scales: { x: { stacked: false }, y: { stacked: false } } } }); var meta = chart.getDatasetMeta(3); expect(meta.controller._getStackCount()).toBe(4); }); it('should correctly count the number of stacks when a group is specified for all', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: [], stack: 'stack2'}, {data: [], stack: 'stack2'} ], labels: [] } }); var meta = chart.getDatasetMeta(3); expect(meta.controller._getStackCount()).toBe(2); }); it('should correctly count the number of stacks when a group is specified for all and the scale is stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: [], stack: 'stack2'}, {data: [], stack: 'stack2'} ], labels: [] }, options: { scales: { x: { stacked: true }, y: { stacked: true } } } }); var meta = chart.getDatasetMeta(3); expect(meta.controller._getStackCount()).toBe(2); }); it('should correctly count the number of stacks when a group is specified for all and the scale is not stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: [], stack: 'stack2'}, {data: [], stack: 'stack2'} ], labels: [] }, options: { scales: { x: { stacked: false }, y: { stacked: false } } } }); var meta = chart.getDatasetMeta(3); expect(meta.controller._getStackCount()).toBe(4); }); it('should correctly get the stack index accounting for datasets of other types and hidden datasets', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: []}, {data: [], hidden: true}, {data: [], type: 'line'}, {data: []} ], labels: [] } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackIndex(0)).toBe(0); expect(meta.controller._getStackIndex(3)).toBe(1); }); it('should correctly get the stack index when a group is not specified', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: []}, {data: []}, {data: []}, {data: []} ], labels: [] } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackIndex(0)).toBe(0); expect(meta.controller._getStackIndex(1)).toBe(1); expect(meta.controller._getStackIndex(2)).toBe(2); expect(meta.controller._getStackIndex(3)).toBe(3); }); it('should correctly get the stack index when a group is not specified and the scale is stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: []}, {data: []}, {data: []}, {data: []} ], labels: [] }, options: { scales: { x: { stacked: true }, y: { stacked: true } } } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackIndex(0)).toBe(0); expect(meta.controller._getStackIndex(1)).toBe(0); expect(meta.controller._getStackIndex(2)).toBe(0); expect(meta.controller._getStackIndex(3)).toBe(0); }); it('should correctly get the stack index when a group is not specified and the scale is not stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: []}, {data: []}, {data: []}, {data: []} ], labels: [] }, options: { scales: { x: { stacked: false }, y: { stacked: false } } } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackIndex(0)).toBe(0); expect(meta.controller._getStackIndex(1)).toBe(1); expect(meta.controller._getStackIndex(2)).toBe(2); expect(meta.controller._getStackIndex(3)).toBe(3); }); it('should correctly get the stack index when a group is specified for some', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: []}, {data: []} ], labels: [] } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackIndex(0)).toBe(0); expect(meta.controller._getStackIndex(1)).toBe(0); expect(meta.controller._getStackIndex(2)).toBe(1); expect(meta.controller._getStackIndex(3)).toBe(2); }); it('should correctly get the stack index when a group is specified for some and the scale is stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: []}, {data: []} ], labels: [] }, options: { scales: { x: { stacked: true }, y: { stacked: true } } } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackIndex(0)).toBe(0); expect(meta.controller._getStackIndex(1)).toBe(0); expect(meta.controller._getStackIndex(2)).toBe(1); expect(meta.controller._getStackIndex(3)).toBe(1); }); it('should correctly get the stack index when a group is specified for some and the scale is not stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: []}, {data: []} ], labels: [] }, options: { scales: { x: { stacked: false }, y: { stacked: false } } } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackIndex(0)).toBe(0); expect(meta.controller._getStackIndex(1)).toBe(1); expect(meta.controller._getStackIndex(2)).toBe(2); expect(meta.controller._getStackIndex(3)).toBe(3); }); it('should correctly get the stack index when a group is specified for all', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: [], stack: 'stack2'}, {data: [], stack: 'stack2'} ], labels: [] } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackIndex(0)).toBe(0); expect(meta.controller._getStackIndex(1)).toBe(0); expect(meta.controller._getStackIndex(2)).toBe(1); expect(meta.controller._getStackIndex(3)).toBe(1); }); it('should correctly get the stack index when a group is specified for all and the scale is stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: [], stack: 'stack2'}, {data: [], stack: 'stack2'} ], labels: [] }, options: { scales: { x: { stacked: true }, y: { stacked: true } } } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackIndex(0)).toBe(0); expect(meta.controller._getStackIndex(1)).toBe(0); expect(meta.controller._getStackIndex(2)).toBe(1); expect(meta.controller._getStackIndex(3)).toBe(1); }); it('should correctly get the stack index when a group is specified for all and the scale is not stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [], stack: 'stack1'}, {data: [], stack: 'stack1'}, {data: [], stack: 'stack2'}, {data: [], stack: 'stack2'} ], labels: [] }, options: { scales: { x: { stacked: false }, y: { stacked: false } } } }); var meta = chart.getDatasetMeta(1); expect(meta.controller._getStackIndex(0)).toBe(0); expect(meta.controller._getStackIndex(1)).toBe(1); expect(meta.controller._getStackIndex(2)).toBe(2); expect(meta.controller._getStackIndex(3)).toBe(3); }); it('should create bar elements for each data item during initialization', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: []}, {data: [10, 15, 0, -4]} ], labels: [] } }); var meta = chart.getDatasetMeta(1); expect(meta.data.length).toBe(4); // 4 bars created expect(meta.data[0] instanceof Chart.elements.BarElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.BarElement).toBe(true); expect(meta.data[2] instanceof Chart.elements.BarElement).toBe(true); expect(meta.data[3] instanceof Chart.elements.BarElement).toBe(true); }); it('should update elements when modifying data', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 2], label: 'dataset1' }, { data: [10, 15, 0, -4], label: 'dataset2', borderColor: 'blue' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, elements: { bar: { backgroundColor: 'red', borderSkipped: 'top', borderColor: 'green', borderWidth: 2, } }, scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false, beginAtZero: false } } } }); var meta = chart.getDatasetMeta(1); expect(meta.data.length).toBe(4); chart.data.datasets[1].data = [1, 2]; // remove 2 items chart.data.datasets[1].borderWidth = 1; chart.update(); expect(meta.data.length).toBe(2); expect(meta._parsed.length).toBe(2); [ {x: 89, y: 512}, {x: 217, y: 0} ].forEach(function(expected, i) { expect(meta.data[i].x).toBeCloseToPixel(expected.x); expect(meta.data[i].y).toBeCloseToPixel(expected.y); expect(meta.data[i].base).toBeCloseToPixel(1024); expect(meta.data[i].width).toBeCloseToPixel(46); expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'red', borderSkipped: 'top', borderColor: 'blue', borderWidth: 1 })); }); chart.data.datasets[1].data = [1, 2, 3]; // add 1 items chart.update(); expect(meta.data.length).toBe(3); // should add a new meta data item }); it('should get the correct bar points when datasets of different types exist', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 2], label: 'dataset1' }, { type: 'line', data: [4, 6], label: 'dataset2' }, { data: [8, 10], label: 'dataset3' }], labels: ['label1', 'label2'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false, beginAtZero: false } } } }); var meta = chart.getDatasetMeta(2); expect(meta.data.length).toBe(2); var bar1 = meta.data[0]; var bar2 = meta.data[1]; expect(bar1.x).toBeCloseToPixel(179); expect(bar1.y).toBeCloseToPixel(117); expect(bar2.x).toBeCloseToPixel(431); expect(bar2.y).toBeCloseToPixel(4); }); it('should get the bar points for hidden dataset', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 2], label: 'dataset1', hidden: true }], labels: ['label1', 'label2'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false }, y: { type: 'linear', min: 0, max: 2, display: false } } } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(2); var bar1 = meta.data[0]; var bar2 = meta.data[1]; expect(bar1.x).toBeCloseToPixel(128); expect(bar1.y).toBeCloseToPixel(256); expect(bar2.x).toBeCloseToPixel(384); expect(bar2.y).toBeCloseToPixel(0); }); it('should update elements when the scales are stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [10, -10, 10, -10], label: 'dataset1' }, { data: [10, 15, 0, -4], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false, stacked: true } } } }); var meta0 = chart.getDatasetMeta(0); [ {b: 293, w: 92 / 2, x: 38, y: 146}, {b: 293, w: 92 / 2, x: 166, y: 439}, {b: 293, w: 92 / 2, x: 295, y: 146}, {b: 293, w: 92 / 2, x: 422, y: 439} ].forEach(function(values, i) { expect(meta0.data[i].base).toBeCloseToPixel(values.b); expect(meta0.data[i].width).toBeCloseToPixel(values.w); expect(meta0.data[i].x).toBeCloseToPixel(values.x); expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); [ {b: 146, w: 92 / 2, x: 89, y: 0}, {b: 293, w: 92 / 2, x: 217, y: 73}, {b: 146, w: 92 / 2, x: 345, y: 146}, {b: 439, w: 92 / 2, x: 473, y: 497} ].forEach(function(values, i) { expect(meta1.data[i].base).toBeCloseToPixel(values.b); expect(meta1.data[i].width).toBeCloseToPixel(values.w); expect(meta1.data[i].x).toBeCloseToPixel(values.x); expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); it('should update elements when the scales are stacked and the y axis has a user defined minimum', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [50, 20, 10, 100], label: 'dataset1' }, { data: [50, 80, 90, 0], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false, stacked: true, min: 50, max: 100 } } } }); var meta0 = chart.getDatasetMeta(0); [ {b: 1024, w: 92 / 2, x: 38, y: 512}, {b: 1024, w: 92 / 2, x: 166, y: 819}, {b: 1024, w: 92 / 2, x: 294, y: 922}, {b: 1024, w: 92 / 2, x: 422.5, y: 0} ].forEach(function(values, i) { expect(meta0.data[i].base).toBeCloseToPixel(values.b); expect(meta0.data[i].width).toBeCloseToPixel(values.w); expect(meta0.data[i].x).toBeCloseToPixel(values.x); expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); [ {b: 512, w: 92 / 2, x: 89, y: 0}, {b: 819.2, w: 92 / 2, x: 217, y: 0}, {b: 921.6, w: 92 / 2, x: 345, y: 0}, {b: 0, w: 92 / 2, x: 473.5, y: 0} ].forEach(function(values, i) { expect(meta1.data[i].base).toBeCloseToPixel(values.b); expect(meta1.data[i].width).toBeCloseToPixel(values.w); expect(meta1.data[i].x).toBeCloseToPixel(values.x); expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); it('should update elements when only the category scale is stacked', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [20, -10, 10, -10], label: 'dataset1' }, { data: [10, 15, 0, -14], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false, stacked: true }, y: { type: 'linear', display: false } } } }); var meta0 = chart.getDatasetMeta(0); [ {b: 293, w: 92, x: 64, y: 0}, {b: 293, w: 92, x: 192, y: 439}, {b: 293, w: 92, x: 320, y: 146}, {b: 293, w: 92, x: 448, y: 439} ].forEach(function(values, i) { expect(meta0.data[i].base).toBeCloseToPixel(values.b); expect(meta0.data[i].width).toBeCloseToPixel(values.w); expect(meta0.data[i].x).toBeCloseToPixel(values.x); expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); [ {b: 293, w: 92, x: 64, y: 146}, {b: 293, w: 92, x: 192, y: 73}, {b: 293, w: 92, x: 320, y: 293}, {b: 293, w: 92, x: 448, y: 497} ].forEach(function(values, i) { expect(meta1.data[i].base).toBeCloseToPixel(values.b); expect(meta1.data[i].width).toBeCloseToPixel(values.w); expect(meta1.data[i].x).toBeCloseToPixel(values.x); expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); it('should update elements when the scales are stacked and data is strings', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: ['10', '-10', '10', '-10'], label: 'dataset1' }, { data: ['10', '15', '0', '-4'], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false, stacked: true } } } }); var meta0 = chart.getDatasetMeta(0); [ {b: 293, w: 92 / 2, x: 38, y: 146}, {b: 293, w: 92 / 2, x: 166, y: 439}, {b: 293, w: 92 / 2, x: 295, y: 146}, {b: 293, w: 92 / 2, x: 422, y: 439} ].forEach(function(values, i) { expect(meta0.data[i].base).toBeCloseToPixel(values.b); expect(meta0.data[i].width).toBeCloseToPixel(values.w); expect(meta0.data[i].x).toBeCloseToPixel(values.x); expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); [ {b: 146, w: 92 / 2, x: 89, y: 0}, {b: 293, w: 92 / 2, x: 217, y: 73}, {b: 146, w: 92 / 2, x: 345, y: 146}, {b: 439, w: 92 / 2, x: 473, y: 497} ].forEach(function(values, i) { expect(meta1.data[i].base).toBeCloseToPixel(values.b); expect(meta1.data[i].width).toBeCloseToPixel(values.w); expect(meta1.data[i].x).toBeCloseToPixel(values.x); expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); it('should get the correct bar points for grouped stacked chart if the group name is same', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [10, -10, 10, -10], label: 'dataset1', stack: 'stack1' }, { data: [10, 15, 0, -4], label: 'dataset2', stack: 'stack1' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false, stacked: true } } } }); var meta0 = chart.getDatasetMeta(0); [ {b: 293, w: 92, x: 64, y: 146}, {b: 293, w: 92, x: 192, y: 439}, {b: 293, w: 92, x: 320, y: 146}, {b: 293, w: 92, x: 448, y: 439} ].forEach(function(values, i) { expect(meta0.data[i].base).toBeCloseToPixel(values.b); expect(meta0.data[i].width).toBeCloseToPixel(values.w); expect(meta0.data[i].x).toBeCloseToPixel(values.x); expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta = chart.getDatasetMeta(1); [ {b: 146, w: 92, x: 64, y: 0}, {b: 293, w: 92, x: 192, y: 73}, {b: 146, w: 92, x: 320, y: 146}, {b: 439, w: 92, x: 448, y: 497} ].forEach(function(values, i) { expect(meta.data[i].base).toBeCloseToPixel(values.b); expect(meta.data[i].width).toBeCloseToPixel(values.w); expect(meta.data[i].x).toBeCloseToPixel(values.x); expect(meta.data[i].y).toBeCloseToPixel(values.y); }); }); it('should get the correct bar points for grouped stacked chart if the group name is different', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 2], stack: 'stack1' }, { data: [1, 2], stack: 'stack2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false, stacked: true, } } } }); var meta = chart.getDatasetMeta(1); [ {x: 89, y: 256}, {x: 217, y: 0} ].forEach(function(values, i) { expect(meta.data[i].base).toBeCloseToPixel(512); expect(meta.data[i].width).toBeCloseToPixel(46); expect(meta.data[i].x).toBeCloseToPixel(values.x); expect(meta.data[i].y).toBeCloseToPixel(values.y); }); }); it('should get the correct bar points for grouped stacked chart', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 2], stack: 'stack1' }, { data: [0.5, 1], stack: 'stack2' }, { data: [0.5, 1], stack: 'stack2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false, stacked: true } } } }); var meta = chart.getDatasetMeta(2); [ {b: 384, x: 89, y: 256}, {b: 256, x: 217, y: 0} ].forEach(function(values, i) { expect(meta.data[i].base).toBeCloseToPixel(values.b); expect(meta.data[i].width).toBeCloseToPixel(46); expect(meta.data[i].x).toBeCloseToPixel(values.x); expect(meta.data[i].y).toBeCloseToPixel(values.y); }); }); it('should draw all bars', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [], }, { data: [10, 15, 0, -4], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(1); spyOn(meta.data[0], 'draw'); spyOn(meta.data[1], 'draw'); spyOn(meta.data[2], 'draw'); spyOn(meta.data[3], 'draw'); chart.update(); expect(meta.data[0].draw.calls.count()).toBe(1); expect(meta.data[1].draw.calls.count()).toBe(1); expect(meta.data[2].draw.calls.count()).toBe(1); expect(meta.data[3].draw.calls.count()).toBe(1); }); it('should set hover styles on bars', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [], }, { data: [10, 15, 0, -4], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { elements: { bar: { backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 0, 255)', borderWidth: 2, } } } }); var meta = chart.getDatasetMeta(1); var bar = meta.data[0]; meta.controller.setHoverStyle(bar, 1, 0); expect(bar.options.backgroundColor).toBe('#E60000'); expect(bar.options.borderColor).toBe('#0000E6'); expect(bar.options.borderWidth).toBe(2); // Set a dataset style chart.data.datasets[1].hoverBackgroundColor = 'rgb(128, 128, 128)'; chart.data.datasets[1].hoverBorderColor = 'rgb(0, 0, 0)'; chart.data.datasets[1].hoverBorderWidth = 5; chart.update(); meta.controller.setHoverStyle(bar, 1, 0); expect(bar.options.backgroundColor).toBe('rgb(128, 128, 128)'); expect(bar.options.borderColor).toBe('rgb(0, 0, 0)'); expect(bar.options.borderWidth).toBe(5); // Should work with array styles so that we can set per bar chart.data.datasets[1].hoverBackgroundColor = ['rgb(255, 255, 255)', 'rgb(128, 128, 128)']; chart.data.datasets[1].hoverBorderColor = ['rgb(9, 9, 9)', 'rgb(0, 0, 0)']; chart.data.datasets[1].hoverBorderWidth = [2.5, 5]; chart.update(); meta.controller.setHoverStyle(bar, 1, 0); expect(bar.options.backgroundColor).toBe('rgb(255, 255, 255)'); expect(bar.options.borderColor).toBe('rgb(9, 9, 9)'); expect(bar.options.borderWidth).toBe(2.5); }); it('should remove a hover style from a bar', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [], }, { data: [10, 15, 0, -4], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { elements: { bar: { backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 0, 255)', borderWidth: 2, } } } }); var meta = chart.getDatasetMeta(1); var bar = meta.data[0]; var helpers = window.Chart.helpers; // Change default chart.options.elements.bar.backgroundColor = 'rgb(128, 128, 128)'; chart.options.elements.bar.borderColor = 'rgb(15, 15, 15)'; chart.options.elements.bar.borderWidth = 3.14; chart.update(); expect(bar.options.backgroundColor).toBe('rgb(128, 128, 128)'); expect(bar.options.borderColor).toBe('rgb(15, 15, 15)'); expect(bar.options.borderWidth).toBe(3.14); meta.controller.setHoverStyle(bar, 1, 0); expect(bar.options.backgroundColor).toBe(helpers.getHoverColor('rgb(128, 128, 128)')); expect(bar.options.borderColor).toBe(helpers.getHoverColor('rgb(15, 15, 15)')); expect(bar.options.borderWidth).toBe(3.14); meta.controller.removeHoverStyle(bar); expect(bar.options.backgroundColor).toBe('rgb(128, 128, 128)'); expect(bar.options.borderColor).toBe('rgb(15, 15, 15)'); expect(bar.options.borderWidth).toBe(3.14); // Should work with array styles so that we can set per bar chart.data.datasets[1].backgroundColor = ['rgb(255, 255, 255)', 'rgb(128, 128, 128)']; chart.data.datasets[1].borderColor = ['rgb(9, 9, 9)', 'rgb(0, 0, 0)']; chart.data.datasets[1].borderWidth = [2.5, 5]; chart.update(); expect(bar.options.backgroundColor).toBe('rgb(255, 255, 255)'); expect(bar.options.borderColor).toBe('rgb(9, 9, 9)'); expect(bar.options.borderWidth).toBe(2.5); meta.controller.setHoverStyle(bar, 1, 0); expect(bar.options.backgroundColor).toBe(helpers.getHoverColor('rgb(255, 255, 255)')); expect(bar.options.borderColor).toBe(helpers.getHoverColor('rgb(9, 9, 9)')); expect(bar.options.borderWidth).toBe(2.5); meta.controller.removeHoverStyle(bar); expect(bar.options.backgroundColor).toBe('rgb(255, 255, 255)'); expect(bar.options.borderColor).toBe('rgb(9, 9, 9)'); expect(bar.options.borderWidth).toBe(2.5); }); describe('Bar width', function() { beforeEach(function() { // 2 datasets this.data = { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ data: [10, 20, 30, 40, 50, 60, 70], }, { data: [10, 20, 30, 40, 50, 60, 70], }] }; }); afterEach(function() { var chart = window.acquireChart(this.config); var meta = chart.getDatasetMeta(0); var xScale = chart.scales[meta.xAxisID]; var options = Chart.defaults.datasets.bar; var categoryPercentage = options.categoryPercentage; var barPercentage = options.barPercentage; var stacked = xScale.options.stacked; var totalBarWidth = 0; for (var i = 0; i < chart.data.datasets.length; i++) { var bars = chart.getDatasetMeta(i).data; for (var j = xScale.min; j <= xScale.max; j++) { totalBarWidth += bars[j].width; } if (stacked) { break; } } var actualValue = totalBarWidth; var expectedValue = xScale.width * categoryPercentage * barPercentage; expect(actualValue).toBeCloseToPixel(expectedValue); }); it('should correctly set bar width when min and max option is set.', function() { this.config = { type: 'bar', data: this.data, options: { scales: { x: { min: 'March', max: 'May', } } } }; }); it('should correctly set bar width when scale are stacked with min and max options.', function() { this.config = { type: 'bar', data: this.data, options: { scales: { x: { min: 'March', max: 'May', }, y: { stacked: true } } } }; }); }); describe('Bar height (horizontal type)', function() { beforeEach(function() { // 2 datasets this.data = { labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], datasets: [{ data: [10, 20, 30, 40, 50, 60, 70], }, { data: [10, 20, 30, 40, 50, 60, 70], }] }; }); afterEach(function() { var chart = window.acquireChart(this.config); var meta = chart.getDatasetMeta(0); var yScale = chart.scales[meta.yAxisID]; var config = meta.controller.options; var categoryPercentage = config.categoryPercentage; var barPercentage = config.barPercentage; var stacked = yScale.options.stacked; var totalBarHeight = 0; for (var i = 0; i < chart.data.datasets.length; i++) { var bars = chart.getDatasetMeta(i).data; for (var j = yScale.min; j <= yScale.max; j++) { totalBarHeight += bars[j].height; } if (stacked) { break; } } var actualValue = totalBarHeight; var expectedValue = yScale.height * categoryPercentage * barPercentage; expect(actualValue).toBeCloseToPixel(expectedValue); }); it('should correctly set bar height when min and max option is set.', function() { this.config = { type: 'bar', data: this.data, options: { indexAxis: 'y', scales: { y: { min: 'March', max: 'May', } } } }; }); it('should correctly set bar height when scale are stacked with min and max options.', function() { this.config = { type: 'bar', data: this.data, options: { indexAxis: 'y', scales: { x: { stacked: true }, y: { min: 'March', max: 'May', } } } }; }); }); describe('Bar thickness with a category scale', function() { [undefined, 20].forEach(function(barThickness) { describe('When barThickness is ' + barThickness, function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 2] }, { data: [1, 2] }], labels: ['label1', 'label2', 'label3'] }, options: { legend: false, title: false, datasets: { bar: { barThickness: barThickness } }, scales: { x: { id: 'x', type: 'category', }, y: { type: 'linear', } } } }); }); it('should correctly set bar width', function() { var chart = this.chart; var expected, i, ilen, meta; if (barThickness) { expected = barThickness; } else { var scale = chart.scales.x; var options = Chart.defaults.datasets.bar; var categoryPercentage = options.categoryPercentage; var barPercentage = options.barPercentage; var tickInterval = scale.getPixelForTick(1) - scale.getPixelForTick(0); expected = tickInterval * categoryPercentage / 2 * barPercentage; } for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { meta = chart.getDatasetMeta(i); expect(meta.data[0].width).toBeCloseToPixel(expected); expect(meta.data[1].width).toBeCloseToPixel(expected); } }); it('should correctly set bar width if maxBarThickness is specified', function() { var chart = this.chart; var i, ilen, meta; chart.data.datasets[0].maxBarThickness = 10; chart.data.datasets[1].maxBarThickness = 10; chart.update(); for (i = 0, ilen = chart.data.datasets.length; i < ilen; ++i) { meta = chart.getDatasetMeta(i); expect(meta.data[0].width).toBeCloseToPixel(10); expect(meta.data[1].width).toBeCloseToPixel(10); } }); }); }); }); it('minBarLength settings should be used on Y axis on bar chart', function() { var minBarLength = 4; var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ minBarLength: minBarLength, data: [0.05, -0.05, 10, 15, 20, 25, 30, 35] }] } }); var data = chart.getDatasetMeta(0).data; var halfBaseLine = chart.scales.y.getLineWidthForValue(0) / 2; expect(data[0].base - minBarLength + halfBaseLine).toEqual(data[0].y); expect(data[1].base + minBarLength - halfBaseLine).toEqual(data[1].y); }); it('minBarLength settings should be used on X axis on horizontal bar chart', function() { var minBarLength = 4; var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ indexAxis: 'y', minBarLength: minBarLength, data: [0.05, -0.05, 10, 15, 20, 25, 30, 35] }] } }); var data = chart.getDatasetMeta(0).data; var halfBaseLine = chart.scales.x.getLineWidthForValue(0) / 2; expect(data[0].base + minBarLength - halfBaseLine).toEqual(data[0].x); expect(data[1].base - minBarLength + halfBaseLine).toEqual(data[1].x); }); it('should respect the data visibility settings', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 2, 3, 4] }], labels: ['A', 'B', 'C', 'D'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false, } } } }); var data = chart.getDatasetMeta(0).data; expect(data[0].base).toBeCloseToPixel(512); expect(data[0].y).toBeCloseToPixel(384); chart.toggleDataVisibility(0); chart.update(); data = chart.getDatasetMeta(0).data; expect(data[0].base).toBeCloseToPixel(512); expect(data[0].y).toBeCloseToPixel(512); }); it('should hide bar dataset beneath the chart for correct animations', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 2, 3, 4] }, { data: [1, 2, 3, 4] }], labels: ['A', 'B', 'C', 'D'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false, stacked: true, }, y: { type: 'linear', display: false, stacked: true, } } } }); var data = chart.getDatasetMeta(0).data; expect(data[0].base).toBeCloseToPixel(512); expect(data[0].y).toBeCloseToPixel(448); chart.setDatasetVisibility(0, false); chart.update(); data = chart.getDatasetMeta(0).data; expect(data[0].base).toBeCloseToPixel(640); expect(data[0].y).toBeCloseToPixel(512); }); describe('Float bar', function() { it('Should return correct values from getMinMax', function() { var chart = window.acquireChart({ type: 'bar', data: { labels: ['a'], datasets: [{ data: [[10, -10]] }] } }); expect(chart.scales.y.getMinMax()).toEqual({min: -10, max: 10}); }); }); describe('clip', function() { it('Should not use ctx.clip when clip=false', function() { var ctx = window.createMockContext(); ctx.resetTransform = function() {}; var chart = window.acquireChart({ type: 'bar', data: { labels: ['a', 'b', 'c'], datasets: [{ data: [1, 2, 3], clip: false }] } }); var orig = chart.ctx; // Draw on mock context chart.ctx = ctx; chart.draw(); chart.ctx = orig; expect(ctx.getCalls().filter(x => x.name === 'clip').length).toEqual(0); }); }); it('should not crash with skipNull and uneven datasets', function() { function unevenChart() { window.acquireChart({ type: 'bar', data: { labels: [1, 2], datasets: [ {data: [1, 2]}, {data: [1, 2, 3]}, ] }, options: { skipNull: true, } }); } expect(unevenChart).not.toThrow(); }); it('should correctly count the number of stacks when skipNull and different order datasets', function() { const chart = window.acquireChart({ type: 'bar', data: { datasets: [ { id: '1', label: 'USA', data: [ { xScale: 'First', Country: 'USA', yScale: 524 }, { xScale: 'Second', Country: 'USA', yScale: 325 } ], yAxisID: 'yScale', xAxisID: 'xScale', parsing: { yAxisKey: 'yScale', xAxisKey: 'xScale' } }, { id: '2', label: 'BRA', data: [ { xScale: 'Second', Country: 'BRA', yScale: 183 }, { xScale: 'First', Country: 'BRA', yScale: 177 } ], yAxisID: 'yScale', xAxisID: 'xScale', parsing: { yAxisKey: 'yScale', xAxisKey: 'xScale' } }, { id: '3', label: 'DEU', data: [ { xScale: 'First', Country: 'DEU', yScale: 162 } ], yAxisID: 'yScale', xAxisID: 'xScale', parsing: { yAxisKey: 'yScale', xAxisKey: 'xScale' } } ] }, options: { skipNull: true } }); var meta = chart.getDatasetMeta(0); expect(meta.controller._getStackCount(0)).toBe(3); expect(meta.controller._getStackCount(1)).toBe(2); }); it('should not override tooltip title and label callbacks', async() => { const chart = window.acquireChart({ type: 'bar', data: { labels: ['Label 1', 'Label 2'], datasets: [{ data: [21, 79], label: 'Dataset 1' }, { data: [33, 67], label: 'Dataset 2' }] }, options: { responsive: true, maintainAspectRatio: true, } }); const {tooltip} = chart; const point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Label 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Dataset 1: 21'], after: [] }]); chart.options.plugins.tooltip = {mode: 'dataset'}; chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Dataset 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Label 1: 21'], after: [] }, { before: [], lines: ['Label 2: 79'], after: [] }]); }); }); ================================================ FILE: test/specs/controller.bubble.tests.js ================================================ describe('Chart.controllers.bubble', function() { describe('auto', jasmine.fixture.specs('controller.bubble')); it('should be registered as dataset controller', function() { expect(typeof Chart.controllers.bubble).toBe('function'); }); it('should be constructed', function() { var chart = window.acquireChart({ type: 'bubble', data: { datasets: [{ data: [] }] } }); var meta = chart.getDatasetMeta(0); expect(meta.type).toBe('bubble'); expect(meta.controller).not.toBe(undefined); expect(meta.controller.index).toBe(0); expect(meta.data).toEqual([]); meta.controller.updateIndex(1); expect(meta.controller.index).toBe(1); }); it('should use the first scale IDs if the dataset does not specify them', function() { var chart = window.acquireChart({ type: 'bubble', data: { datasets: [{ data: [] }] }, }); var meta = chart.getDatasetMeta(0); expect(meta.xAxisID).toBe('x'); expect(meta.yAxisID).toBe('y'); }); it('should create point elements for each data item during initialization', function() { var chart = window.acquireChart({ type: 'bubble', data: { datasets: [{ data: [10, 15, 0, -4] }] } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); // 4 points created expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[2] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[3] instanceof Chart.elements.PointElement).toBe(true); }); it('should draw all elements', function() { var chart = window.acquireChart({ type: 'bubble', data: { datasets: [{ data: [10, 15, 0, -4] }] }, options: { animation: false, showLine: true } }); var meta = chart.getDatasetMeta(0); spyOn(meta.data[0], 'draw'); spyOn(meta.data[1], 'draw'); spyOn(meta.data[2], 'draw'); spyOn(meta.data[3], 'draw'); chart.update(); expect(meta.data[0].draw.calls.count()).toBe(1); expect(meta.data[1].draw.calls.count()).toBe(1); expect(meta.data[2].draw.calls.count()).toBe(1); expect(meta.data[3].draw.calls.count()).toBe(1); }); it('should update elements when modifying style', function() { var chart = window.acquireChart({ type: 'bubble', data: { datasets: [{ data: [{ x: 10, y: 10, r: 5 }, { x: -15, y: -10, r: 1 }, { x: 0, y: -9, r: 2 }, { x: -4, y: 10, r: 1 }] }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false } } } }); var meta = chart.getDatasetMeta(0); [ {r: 5, x: 5, y: 5}, {r: 1, x: 171, y: 507}, {r: 2, x: 341, y: 482}, {r: 1, x: 507, y: 5} ].forEach(function(expected, i) { expect(meta.data[i].x).toBeCloseToPixel(expected.x); expect(meta.data[i].y).toBeCloseToPixel(expected.y); expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: Chart.defaults.backgroundColor, borderColor: Chart.defaults.borderColor, borderWidth: 1, hitRadius: 1, radius: expected.r })); }); // Use dataset level styles for lines & points chart.data.datasets[0].backgroundColor = 'rgb(98, 98, 98)'; chart.data.datasets[0].borderColor = 'rgb(8, 8, 8)'; chart.data.datasets[0].borderWidth = 0.55; // point styles chart.data.datasets[0].radius = 22; chart.data.datasets[0].hitRadius = 3.3; chart.update(); for (var i = 0; i < 4; ++i) { expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(98, 98, 98)', borderColor: 'rgb(8, 8, 8)', borderWidth: 0.55, hitRadius: 3.3 })); } }); it('should handle number of data point changes in update', function() { var chart = window.acquireChart({ type: 'bubble', data: { datasets: [{ data: [{ x: 10, y: 10, r: 5 }, { x: -15, y: -10, r: 1 }, { x: 0, y: -9, r: 2 }, { x: -4, y: 10, r: 1 }] }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); chart.data.datasets[0].data = [{ x: 1, y: 1, r: 10 }, { x: 10, y: 5, r: 2 }]; // remove 2 items chart.update(); expect(meta.data.length).toBe(2); expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); chart.data.datasets[0].data = [{ x: 10, y: 10, r: 5 }, { x: -15, y: -10, r: 1 }, { x: 0, y: -9, r: 2 }, { x: -4, y: 10, r: 1 }, { x: -5, y: 0, r: 3 }]; // add 3 items chart.update(); expect(meta.data.length).toBe(5); expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[2] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[3] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[4] instanceof Chart.elements.PointElement).toBe(true); }); describe('Interactions', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'bubble', data: { labels: ['label1', 'label2', 'label3', 'label4'], datasets: [{ data: [{ x: 5, y: 5, r: 20 }, { x: -15, y: -10, r: 15 }, { x: 15, y: 10, r: 10 }, { x: -15, y: 10, r: 5 }] }] }, options: { elements: { point: { backgroundColor: 'rgb(100, 150, 200)', borderColor: 'rgb(50, 100, 150)', borderWidth: 2, radius: 3 } } } }); }); it ('should handle default hover styles', async function() { var chart = this.chart; var point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point.options.backgroundColor).toBe('#3187DD'); expect(point.options.borderColor).toBe('#175A9D'); expect(point.options.borderWidth).toBe(1); expect(point.options.radius).toBe(20 + 4); await jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); expect(point.options.borderWidth).toBe(2); expect(point.options.radius).toBe(20); }); it ('should handle hover styles defined via dataset properties', async function() { var chart = this.chart; var point = chart.getDatasetMeta(0).data[0]; Chart.helpers.merge(chart.data.datasets[0], { hoverBackgroundColor: 'rgb(200, 100, 150)', hoverBorderColor: 'rgb(150, 50, 100)', hoverBorderWidth: 8.4, hoverRadius: 4.2 }); chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); expect(point.options.borderWidth).toBe(8.4); expect(point.options.radius).toBe(20 + 4.2); await jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); expect(point.options.borderWidth).toBe(2); expect(point.options.radius).toBe(20); }); it ('should handle hover styles defined via element options', async function() { var chart = this.chart; var point = chart.getDatasetMeta(0).data[0]; Chart.helpers.merge(chart.options.elements.point, { hoverBackgroundColor: 'rgb(200, 100, 150)', hoverBorderColor: 'rgb(150, 50, 100)', hoverBorderWidth: 8.4, hoverRadius: 4.2 }); chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); expect(point.options.borderWidth).toBe(8.4); expect(point.options.radius).toBe(20 + 4.2); await jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); expect(point.options.borderWidth).toBe(2); expect(point.options.radius).toBe(20); }); }); it('should not override tooltip title and label callbacks', async() => { const chart = window.acquireChart({ type: 'bubble', data: { labels: ['Label 1', 'Label 2'], datasets: [{ data: [{ x: 10, y: 15, r: 15 }, { x: 12, y: 10, r: 10 }], label: 'Dataset 1' }, { data: [{ x: 20, y: 10, r: 5 }, { x: 4, y: 8, r: 30 }], label: 'Dataset 2' }] }, options: { responsive: true, maintainAspectRatio: true, } }); const {tooltip} = chart; const point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Label 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Dataset 1: (10, 15, 15)'], after: [] }]); chart.options.plugins.tooltip = {mode: 'dataset'}; chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Dataset 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Label 1: (10, 15, 15)'], after: [] }, { before: [], lines: ['Label 2: (12, 10, 10)'], after: [] }]); }); }); ================================================ FILE: test/specs/controller.doughnut.tests.js ================================================ describe('Chart.controllers.doughnut', function() { describe('auto', jasmine.fixture.specs('controller.doughnut')); it('should be registered as dataset controller', function() { expect(typeof Chart.controllers.doughnut).toBe('function'); expect(typeof Chart.controllers.pie).toBe('function'); }); it('should be constructed', function() { var chart = window.acquireChart({ type: 'doughnut', data: { datasets: [{ data: [] }], labels: [] } }); var meta = chart.getDatasetMeta(0); expect(meta.type).toBe('doughnut'); expect(meta.controller).not.toBe(undefined); expect(meta.controller.index).toBe(0); expect(meta.data).toEqual([]); meta.controller.updateIndex(1); expect(meta.controller.index).toBe(1); }); it('should create arc elements for each data item during initialization', function() { var chart = window.acquireChart({ type: 'doughnut', data: { datasets: [{ data: [10, 15, 0, 4] }], labels: [] } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); // 4 arcs created expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[2] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[3] instanceof Chart.elements.ArcElement).toBe(true); }); it ('should reset and update elements', function() { var chart = window.acquireChart({ type: 'doughnut', data: { datasets: [{ data: [1, 2, 3, 4], hidden: true }, { data: [5, 6, 0, 7] }, { data: [8, 9, 10, 11] }], labels: ['label0', 'label1', 'label2', 'label3'] }, options: { plugins: { legend: false, title: false, }, animation: { duration: 0, animateRotate: true, animateScale: false }, cutout: '50%', rotation: 0, circumference: 360, elements: { arc: { backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 0, 255)', borderWidth: 2 } } } }); var meta = chart.getDatasetMeta(1); meta.controller.reset(); // reset first expect(meta.data.length).toBe(4); [ {c: 0}, {c: 0}, {c: 0}, {c: 0} ].forEach(function(expected, i) { expect(meta.data[i].x).toBeCloseToPixel(256); expect(meta.data[i].y).toBeCloseToPixel(256); expect(meta.data[i].outerRadius).toBeCloseToPixel(256); expect(meta.data[i].innerRadius).toBeCloseToPixel(192); expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); expect(meta.data[i].startAngle).toBeCloseToPixel(Math.PI * -0.5); expect(meta.data[i].endAngle).toBeCloseToPixel(Math.PI * -0.5); expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 0, 255)', borderWidth: 2 })); }); chart.update(); [ {c: 1.7453292519, s: -1.5707963267, e: 0.1745329251}, {c: 2.0943951023, s: 0.1745329251, e: 2.2689280275}, {c: 0, s: 2.2689280275, e: 2.2689280275}, {c: 2.4434609527, s: 2.2689280275, e: 4.7123889803} ].forEach(function(expected, i) { expect(meta.data[i].x).toBeCloseToPixel(256); expect(meta.data[i].y).toBeCloseToPixel(256); expect(meta.data[i].outerRadius).toBeCloseToPixel(256); expect(meta.data[i].innerRadius).toBeCloseToPixel(192); expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); expect(meta.data[i].startAngle).toBeCloseTo(expected.s, 8); expect(meta.data[i].endAngle).toBeCloseTo(expected.e, 8); expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 0, 255)', borderWidth: 2 })); }); // Change the amount of data and ensure that arcs are updated accordingly chart.data.datasets[1].data = [1, 2]; // remove 2 elements from dataset 0 chart.update(); expect(meta.data.length).toBe(2); expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); // Add data chart.data.datasets[1].data = [1, 2, 3, 4]; chart.update(); expect(meta.data.length).toBe(4); expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[2] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[3] instanceof Chart.elements.ArcElement).toBe(true); }); it ('should rotate and limit circumference', function() { var chart = window.acquireChart({ type: 'doughnut', data: { datasets: [{ data: [2, 4], hidden: true }, { data: [1, 3] }, { data: [1, 0] }], labels: ['label0', 'label1', 'label2'] }, options: { plugins: { legend: false, title: false, }, cutout: '50%', rotation: 270, circumference: 90, elements: { arc: { backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 0, 255)', borderWidth: 2 } } } }); var meta = chart.getDatasetMeta(1); expect(meta.data.length).toBe(2); // Only startAngle, endAngle and circumference should be different. [ {c: Math.PI / 8, s: Math.PI, e: Math.PI + Math.PI / 8}, {c: 3 * Math.PI / 8, s: Math.PI + Math.PI / 8, e: Math.PI + Math.PI / 2} ].forEach(function(expected, i) { expect(meta.data[i].x).toBeCloseToPixel(512); expect(meta.data[i].y).toBeCloseToPixel(512); expect(meta.data[i].outerRadius).toBeCloseToPixel(512); expect(meta.data[i].innerRadius).toBeCloseToPixel(384); expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); expect(meta.data[i].startAngle).toBeCloseTo(expected.s, 8); expect(meta.data[i].endAngle).toBeCloseTo(expected.e, 8); }); }); it('should treat negative values as positive', function() { var chart = window.acquireChart({ type: 'doughnut', data: { datasets: [{ data: [-1, -3] }], labels: ['label0', 'label1'] }, options: { plugins: { legend: false, title: false }, cutout: '50%', rotation: 270, circumference: 90, elements: { arc: { backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 0, 255)', borderWidth: 2 } } } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(2); // Only startAngle, endAngle and circumference should be different. [ {c: Math.PI / 8, s: Math.PI, e: Math.PI + Math.PI / 8}, {c: 3 * Math.PI / 8, s: Math.PI + Math.PI / 8, e: Math.PI + Math.PI / 2} ].forEach(function(expected, i) { expect(meta.data[i].circumference).toBeCloseTo(expected.c, 8); expect(meta.data[i].startAngle).toBeCloseTo(expected.s, 8); expect(meta.data[i].endAngle).toBeCloseTo(expected.e, 8); }); }); it ('should draw all arcs', function() { var chart = window.acquireChart({ type: 'doughnut', data: { datasets: [{ data: [10, 15, 0, 4] }], labels: ['label0', 'label1', 'label2', 'label3'] } }); var meta = chart.getDatasetMeta(0); spyOn(meta.data[0], 'draw'); spyOn(meta.data[1], 'draw'); spyOn(meta.data[2], 'draw'); spyOn(meta.data[3], 'draw'); chart.update(); expect(meta.data[0].draw.calls.count()).toBe(1); expect(meta.data[1].draw.calls.count()).toBe(1); expect(meta.data[2].draw.calls.count()).toBe(1); expect(meta.data[3].draw.calls.count()).toBe(1); }); it ('should calculate radiuses based on the border widths of the visible outermost dataset', function() { var chart = window.acquireChart({ type: 'doughnut', data: { datasets: [{ data: [2, 4], borderWidth: 4, hidden: true }, { data: [1, 3], borderWidth: 8 }, { data: [1, 0], borderWidth: 12 }], labels: ['label0', 'label1'] }, options: { plugins: { legend: false, title: false } } }); chart.update(); var controller = chart.getDatasetMeta(0).controller; expect(chart.chartArea.bottom - chart.chartArea.top).toBe(512); expect(controller.getMaxBorderWidth()).toBe(8); expect(controller.outerRadius).toBe(252); expect(controller.innerRadius).toBe(189); controller = chart.getDatasetMeta(1).controller; expect(controller.getMaxBorderWidth()).toBe(8); expect(controller.outerRadius).toBe(252); expect(controller.innerRadius).toBe(189); controller = chart.getDatasetMeta(2).controller; expect(controller.getMaxBorderWidth()).toBe(8); expect(controller.outerRadius).toBe(189); expect(controller.innerRadius).toBe(126); }); describe('Interactions', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'doughnut', data: { labels: ['label1', 'label2', 'label3', 'label4'], datasets: [{ data: [10, 15, 0, 4] }] }, options: { cutout: '50%', elements: { arc: { backgroundColor: 'rgb(100, 150, 200)', borderColor: 'rgb(50, 100, 150)', borderWidth: 2, } } } }); }); it ('should handle default hover styles', async function() { var chart = this.chart; var arc = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', arc); expect(arc.options.backgroundColor).toBe('#3187DD'); expect(arc.options.borderColor).toBe('#175A9D'); expect(arc.options.borderWidth).toBe(2); await jasmine.triggerMouseEvent(chart, 'mouseout', arc); expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); expect(arc.options.borderWidth).toBe(2); }); it ('should handle hover styles defined via dataset properties', async function() { var chart = this.chart; var arc = chart.getDatasetMeta(0).data[0]; Chart.helpers.merge(chart.data.datasets[0], { hoverBackgroundColor: 'rgb(200, 100, 150)', hoverBorderColor: 'rgb(150, 50, 100)', hoverBorderWidth: 8.4, }); chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', arc); expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); expect(arc.options.borderWidth).toBe(8.4); await jasmine.triggerMouseEvent(chart, 'mouseout', arc); expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); expect(arc.options.borderWidth).toBe(2); }); it ('should handle hover styles defined via element options', async function() { var chart = this.chart; var arc = chart.getDatasetMeta(0).data[0]; Chart.helpers.merge(chart.options.elements.arc, { hoverBackgroundColor: 'rgb(200, 100, 150)', hoverBorderColor: 'rgb(150, 50, 100)', hoverBorderWidth: 8.4, }); chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', arc); expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); expect(arc.options.borderWidth).toBe(8.4); await jasmine.triggerMouseEvent(chart, 'mouseout', arc); expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); expect(arc.options.borderWidth).toBe(2); }); }); it('should not override tooltip title and label callbacks', async() => { const chart = window.acquireChart({ type: 'doughnut', data: { labels: ['Label 1', 'Label 2'], datasets: [{ data: [21, 79], label: 'Dataset 1' }, { data: [33, 67], label: 'Dataset 2' }] }, options: { responsive: true, maintainAspectRatio: true, } }); const {tooltip} = chart; const point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Label 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Dataset 1: 21'], after: [] }]); chart.options.plugins.tooltip = {mode: 'dataset'}; chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Dataset 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Label 1: 21'], after: [] }, { before: [], lines: ['Label 2: 79'], after: [] }]); }); }); ================================================ FILE: test/specs/controller.line.tests.js ================================================ describe('Chart.controllers.line', function() { describe('auto', jasmine.fixture.specs('controller.line')); it('should be registered as dataset controller', function() { expect(typeof Chart.controllers.line).toBe('function'); }); it('should be constructed', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [] }], labels: [] } }); var meta = chart.getDatasetMeta(0); expect(meta.type).toBe('line'); expect(meta.controller).not.toBe(undefined); expect(meta.controller.index).toBe(0); expect(meta.data).toEqual([]); meta.controller.updateIndex(1); expect(meta.controller.index).toBe(1); }); it('Should use the first scale IDs if the dataset does not specify them', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [] }], labels: [] }, }); var meta = chart.getDatasetMeta(0); expect(meta.xAxisID).toBe('x'); expect(meta.yAxisID).toBe('y'); }); it('Should not throw with empty dataset when tension is non-zero', function() { // https://github.com/chartjs/Chart.js/issues/8676 function createChart() { return window.acquireChart({ type: 'line', data: { datasets: [{ data: [], tension: 0.5 }], labels: [] }, }); } expect(createChart).not.toThrow(); }); it('should find min and max for stacked chart', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, 11, 12, 13] }, { data: [1, 2, 3, 4] }], labels: ['a', 'b', 'c', 'd'] }, options: { scales: { y: { stacked: true } } } }); expect(chart.getDatasetMeta(0).controller.getMinMax(chart.scales.y, true)).toEqual({min: 10, max: 13}); expect(chart.getDatasetMeta(1).controller.getMinMax(chart.scales.y, true)).toEqual({min: 11, max: 17}); chart.hide(0); expect(chart.getDatasetMeta(0).controller.getMinMax(chart.scales.y, true)).toEqual({min: 10, max: 13}); expect(chart.getDatasetMeta(1).controller.getMinMax(chart.scales.y, true)).toEqual({min: 1, max: 4}); }); it('Should create line elements and point elements for each data item during initialization', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset1' }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); // 4 points created expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[2] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[3] instanceof Chart.elements.PointElement).toBe(true); expect(meta.dataset instanceof Chart.elements.LineElement).toBe(true); // 1 line element }); it('should draw all elements', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset1' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { showLine: true } }); var meta = chart.getDatasetMeta(0); spyOn(meta.dataset, 'updateControlPoints'); spyOn(meta.dataset, 'draw'); spyOn(meta.data[0], 'draw'); spyOn(meta.data[1], 'draw'); spyOn(meta.data[2], 'draw'); spyOn(meta.data[3], 'draw'); chart.update(); expect(meta.dataset.updateControlPoints.calls.count()).toBeGreaterThanOrEqual(1); expect(meta.dataset.draw.calls.count()).toBe(1); expect(meta.data[0].draw.calls.count()).toBe(1); expect(meta.data[1].draw.calls.count()).toBe(1); expect(meta.data[2].draw.calls.count()).toBe(1); expect(meta.data[3].draw.calls.count()).toBe(1); }); it('should update elements when modifying data', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset', xAxisID: 'x', yAxisID: 'y' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { showLine: true, plugins: { legend: false, title: false }, elements: { point: { backgroundColor: 'red', borderColor: 'blue', } }, scales: { x: { display: false }, y: { display: false } } }, }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); chart.data.datasets[0].data = [1, 2]; // remove 2 items chart.data.datasets[0].borderWidth = 1; chart.update(); expect(meta.data.length).toBe(2); expect(meta._parsed.length).toBe(2); [ {x: 5, y: 507}, {x: 171, y: 5} ].forEach(function(expected, i) { expect(meta.data[i].x).toBeCloseToPixel(expected.x); expect(meta.data[i].y).toBeCloseToPixel(expected.y); expect(meta.data[i].options).toEqual(jasmine.objectContaining({ backgroundColor: 'red', borderColor: 'blue', })); }); chart.data.datasets[0].data = [1, 2, 3]; // add 1 items chart.update(); expect(meta.data.length).toBe(3); // should add a new meta data item }); it('should correctly calculate x scale for label and point', function() { var chart = window.acquireChart({ type: 'line', data: { labels: ['One'], datasets: [{ data: [1], }] }, options: { plugins: { legend: false, title: false }, hover: { mode: 'nearest', intersect: true }, scales: { x: { display: false, }, y: { display: false, beginAtZero: true } } } }); var meta = chart.getDatasetMeta(0); // 1 point var point = meta.data[0]; expect(point.x).toBeCloseToPixel(5); // 2 points chart.data.labels = ['One', 'Two']; chart.data.datasets[0].data = [1, 2]; chart.update(); var points = meta.data; expect(points[0].x).toBeCloseToPixel(5); expect(points[1].x).toBeCloseToPixel(507); // 3 points chart.data.labels = ['One', 'Two', 'Three']; chart.data.datasets[0].data = [1, 2, 3]; chart.update(); points = meta.data; expect(points[0].x).toBeCloseToPixel(5); expect(points[1].x).toBeCloseToPixel(256); expect(points[2].x).toBeCloseToPixel(507); // 4 points chart.data.labels = ['One', 'Two', 'Three', 'Four']; chart.data.datasets[0].data = [1, 2, 3, 4]; chart.update(); points = meta.data; expect(points[0].x).toBeCloseToPixel(5); expect(points[1].x).toBeCloseToPixel(171); expect(points[2].x).toBeCloseToPixel(340); expect(points[3].x).toBeCloseToPixel(507); }); it('should update elements when the y scale is stacked', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, -10, 10, -10], label: 'dataset1' }, { data: [10, 15, 0, -4], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, scales: { x: { display: false, }, y: { display: false, stacked: true } } } }); var meta0 = chart.getDatasetMeta(0); [ {x: 5, y: 148}, {x: 171, y: 435}, {x: 341, y: 148}, {x: 507, y: 435} ].forEach(function(values, i) { expect(meta0.data[i].x).toBeCloseToPixel(values.x); expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); [ {x: 5, y: 5}, {x: 171, y: 76}, {x: 341, y: 148}, {x: 507, y: 492} ].forEach(function(values, i) { expect(meta1.data[i].x).toBeCloseToPixel(values.x); expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); it('should update elements when the y scale is stacked with multiple axes', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, -10, 10, -10], label: 'dataset1' }, { data: [10, 15, 0, -4], label: 'dataset2' }, { data: [10, 10, -10, -10], label: 'dataset3', yAxisID: 'y2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false, }, scales: { x: { display: false, }, y: { display: false, stacked: true }, y2: { type: 'linear', position: 'right', display: false } } } }); var meta0 = chart.getDatasetMeta(0); [ {x: 5, y: 148}, {x: 171, y: 435}, {x: 341, y: 148}, {x: 507, y: 435} ].forEach(function(values, i) { expect(meta0.data[i].x).toBeCloseToPixel(values.x); expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); [ {x: 5, y: 5}, {x: 171, y: 76}, {x: 341, y: 148}, {x: 507, y: 492} ].forEach(function(values, i) { expect(meta1.data[i].x).toBeCloseToPixel(values.x); expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); it('should update elements when the y scale is stacked and datasets is scatter data', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [{ x: 0, y: 10 }, { x: 1, y: -10 }, { x: 2, y: 10 }, { x: 3, y: -10 }], label: 'dataset1' }, { data: [{ x: 0, y: 10 }, { x: 1, y: 15 }, { x: 2, y: 0 }, { x: 3, y: -4 }], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, scales: { x: { display: false, }, y: { display: false, stacked: true } } } }); var meta0 = chart.getDatasetMeta(0); [ {x: 5, y: 148}, {x: 171, y: 435}, {x: 341, y: 148}, {x: 507, y: 435} ].forEach(function(values, i) { expect(meta0.data[i].x).toBeCloseToPixel(values.x); expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); [ {x: 5, y: 5}, {x: 171, y: 76}, {x: 341, y: 148}, {x: 507, y: 492} ].forEach(function(values, i) { expect(meta1.data[i].x).toBeCloseToPixel(values.x); expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); it('should update elements when the y scale is stacked and data is strings', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: ['10', '-10', '10', '-10'], label: 'dataset1' }, { data: ['10', '15', '0', '-4'], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { plugins: { legend: false, title: false }, scales: { x: { display: false, }, y: { display: false, stacked: true } } } }); var meta0 = chart.getDatasetMeta(0); [ {x: 5, y: 148}, {x: 171, y: 435}, {x: 341, y: 148}, {x: 507, y: 435} ].forEach(function(values, i) { expect(meta0.data[i].x).toBeCloseToPixel(values.x); expect(meta0.data[i].y).toBeCloseToPixel(values.y); }); var meta1 = chart.getDatasetMeta(1); [ {x: 5, y: 5}, {x: 171, y: 76}, {x: 341, y: 148}, {x: 507, y: 492} ].forEach(function(values, i) { expect(meta1.data[i].x).toBeCloseToPixel(values.x); expect(meta1.data[i].y).toBeCloseToPixel(values.y); }); }); it('should fall back to the line styles for points', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [0, 0], label: 'dataset1', // line styles backgroundColor: 'rgb(98, 98, 98)', borderColor: 'rgb(8, 8, 8)', borderWidth: 0.55, }], labels: ['label1', 'label2'] } }); var meta = chart.getDatasetMeta(0); expect(meta.dataset.options.backgroundColor).toBe('rgb(98, 98, 98)'); expect(meta.dataset.options.borderColor).toBe('rgb(8, 8, 8)'); expect(meta.dataset.options.borderWidth).toBe(0.55); }); describe('dataset global defaults', function() { beforeEach(function() { this._defaults = Chart.helpers.clone(Chart.defaults.datasets.line); }); afterEach(function() { Chart.defaults.datasets.line = this._defaults; delete this._defaults; }); it('should utilize the dataset global default options', function() { Chart.helpers.merge(Chart.defaults.datasets.line, { spanGaps: true, tension: 0.231, backgroundColor: '#add', borderWidth: '#daa', borderColor: '#dad', borderCapStyle: 'round', borderDash: [0], borderDashOffset: 0.871, borderJoinStyle: 'miter', fill: 'start', cubicInterpolationMode: 'monotone' }); var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [0, 0], label: 'dataset1' }], labels: ['label1', 'label2'] } }); var options = chart.getDatasetMeta(0).dataset.options; expect(options.spanGaps).toBe(true); expect(options.tension).toBe(0.231); expect(options.backgroundColor).toBe('#add'); expect(options.borderWidth).toBe('#daa'); expect(options.borderColor).toBe('#dad'); expect(options.borderCapStyle).toBe('round'); expect(options.borderDash).toEqual([0]); expect(options.borderDashOffset).toBe(0.871); expect(options.borderJoinStyle).toBe('miter'); expect(options.fill).toBe('start'); expect(options.cubicInterpolationMode).toBe('monotone'); }); it('should be overridden by user-supplied values', function() { Chart.helpers.merge(Chart.defaults.datasets.line, { spanGaps: true, tension: 0.231 }); var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [0, 0], label: 'dataset1', spanGaps: true, backgroundColor: '#dad' }], labels: ['label1', 'label2'] }, options: { datasets: { line: { tension: 0.345, backgroundColor: '#add' } } } }); var options = chart.getDatasetMeta(0).dataset.options; // dataset-level option overrides global default expect(options.spanGaps).toBe(true); // chart-level default overrides global default expect(options.tension).toBe(0.345); // dataset-level option overrides chart-level default expect(options.backgroundColor).toBe('#dad'); }); }); it('should obey the chart-level dataset options', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [0, 0], label: 'dataset1' }], labels: ['label1', 'label2'] }, options: { datasets: { line: { spanGaps: true, tension: 0.231, backgroundColor: '#add', borderWidth: '#daa', borderColor: '#dad', borderCapStyle: 'round', borderDash: [0], borderDashOffset: 0.871, borderJoinStyle: 'miter', fill: 'start', cubicInterpolationMode: 'monotone' } } } }); var options = chart.getDatasetMeta(0).dataset.options; expect(options.spanGaps).toBe(true); expect(options.tension).toBe(0.231); expect(options.backgroundColor).toBe('#add'); expect(options.borderWidth).toBe('#daa'); expect(options.borderColor).toBe('#dad'); expect(options.borderCapStyle).toBe('round'); expect(options.borderDash).toEqual([0]); expect(options.borderDashOffset).toBe(0.871); expect(options.borderJoinStyle).toBe('miter'); expect(options.fill).toBe('start'); expect(options.cubicInterpolationMode).toBe('monotone'); }); it('should obey the dataset options', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [0, 0], label: 'dataset1', spanGaps: true, tension: 0.231, backgroundColor: '#add', borderWidth: '#daa', borderColor: '#dad', borderCapStyle: 'round', borderDash: [0], borderDashOffset: 0.871, borderJoinStyle: 'miter', fill: 'start', cubicInterpolationMode: 'monotone' }], labels: ['label1', 'label2'] } }); var options = chart.getDatasetMeta(0).dataset.options; expect(options.spanGaps).toBe(true); expect(options.tension).toBe(0.231); expect(options.backgroundColor).toBe('#add'); expect(options.borderWidth).toBe('#daa'); expect(options.borderColor).toBe('#dad'); expect(options.borderCapStyle).toBe('round'); expect(options.borderDash).toEqual([0]); expect(options.borderDashOffset).toBe(0.871); expect(options.borderJoinStyle).toBe('miter'); expect(options.fill).toBe('start'); expect(options.cubicInterpolationMode).toBe('monotone'); }); it('should handle number of data point changes in update', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset1', }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(0); chart.data.datasets[0].data = [1, 2]; // remove 2 items chart.update(); expect(meta.data.length).toBe(2); expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); chart.data.datasets[0].data = [1, 2, 3, 4, 5]; // add 3 items chart.update(); expect(meta.data.length).toBe(5); expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[2] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[3] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[4] instanceof Chart.elements.PointElement).toBe(true); }); describe('Interactions', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { labels: ['label1', 'label2', 'label3', 'label4'], datasets: [{ data: [10, 15, 0, -4] }] }, options: { scales: { x: { offset: true } }, elements: { point: { backgroundColor: 'rgb(100, 150, 200)', borderColor: 'rgb(50, 100, 150)', borderWidth: 2, radius: 3 } } } }); }); it ('should handle default hover styles', async function() { var chart = this.chart; var point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point.options.backgroundColor).toBe('#3187DD'); expect(point.options.borderColor).toBe('#175A9D'); expect(point.options.borderWidth).toBe(1); expect(point.options.radius).toBe(4); await jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); expect(point.options.borderWidth).toBe(2); expect(point.options.radius).toBe(3); }); it ('should handle hover styles defined via dataset properties', async function() { var chart = this.chart; var point = chart.getDatasetMeta(0).data[0]; Chart.helpers.merge(chart.data.datasets[0], { hoverBackgroundColor: 'rgb(200, 100, 150)', hoverBorderColor: 'rgb(150, 50, 100)', hoverBorderWidth: 8.4, hoverRadius: 4.2 }); chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); expect(point.options.borderWidth).toBe(8.4); expect(point.options.radius).toBe(4.2); await jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); expect(point.options.borderWidth).toBe(2); expect(point.options.radius).toBe(3); }); it('should handle hover styles defined via element options', async function() { var chart = this.chart; var point = chart.getDatasetMeta(0).data[0]; Chart.helpers.merge(chart.options.elements.point, { hoverBackgroundColor: 'rgb(200, 100, 150)', hoverBorderColor: 'rgb(150, 50, 100)', hoverBorderWidth: 8.4, hoverRadius: 4.2 }); chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); expect(point.options.borderWidth).toBe(8.4); expect(point.options.radius).toBe(4.2); await jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); expect(point.options.borderWidth).toBe(2); expect(point.options.radius).toBe(3); }); it('should handle dataset hover styles defined via dataset properties', async function() { var chart = this.chart; var point = chart.getDatasetMeta(0).data[0]; var dataset = chart.getDatasetMeta(0).dataset; Chart.helpers.merge(chart.data.datasets[0], { backgroundColor: '#AAA', borderColor: '#BBB', borderWidth: 6, hoverBackgroundColor: '#000', hoverBorderColor: '#111', hoverBorderWidth: 12 }); chart.options.hover = {mode: 'dataset'}; chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(dataset.options.backgroundColor).toBe('#000'); expect(dataset.options.borderColor).toBe('#111'); expect(dataset.options.borderWidth).toBe(12); await jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(dataset.options.backgroundColor).toBe('#AAA'); expect(dataset.options.borderColor).toBe('#BBB'); expect(dataset.options.borderWidth).toBe(6); }); }); it('should allow 0 as a point border width', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset1', pointBorderWidth: 0 }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(0); var point = meta.data[0]; expect(point.options.borderWidth).toBe(0); }); it('should allow an array as the point border width setting', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset1', pointBorderWidth: [1, 2, 3, 4] }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(0); expect(meta.data[0].options.borderWidth).toBe(1); expect(meta.data[1].options.borderWidth).toBe(2); expect(meta.data[2].options.borderWidth).toBe(3); expect(meta.data[3].options.borderWidth).toBe(4); }); it('should render a million points', function() { var data = []; for (let x = 0; x < 1e6; x++) { data.push({x, y: Math.sin(x / 10000)}); } function createChart() { window.acquireChart({ type: 'line', data: { datasets: [{ data, borderWidth: 1, radius: 0 }], }, options: { scales: { x: {type: 'linear'}, y: {type: 'linear'} } } }); } expect(createChart).not.toThrow(); }); it('should set skipped points to the reset state', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, null, 0, -4], label: 'dataset1', pointBorderWidth: [1, 2, 3, 4] }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(0); var point = meta.data[1]; var {x, y} = point.getProps(['x', 'y'], true); expect(point.skip).toBe(true); expect(isNaN(x)).toBe(false); expect(isNaN(y)).toBe(false); }); it('should honor spangap interval forwards', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ spanGaps: 10, data: [{x: 10, y: 123}, {x: 15, y: 124}, {x: 26, y: 125}, {x: 30, y: 126}, {x: 35, y: 127}], label: 'dataset1', }], }, options: { scales: { x: { type: 'linear', } } } }); var meta = chart.getDatasetMeta(0); for (var i = 0; i < meta.data.length; ++i) { var point = meta.data[i]; expect(point.stop).toBe(i === 2); } }); it('should honor spangap interval backwards', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ spanGaps: 10, data: [{x: 35, y: 123}, {x: 30, y: 124}, {x: 26, y: 125}, {x: 15, y: 126}, {x: 10, y: 127}], label: 'dataset1', }], }, options: { scales: { x: { type: 'linear', } } } }); var meta = chart.getDatasetMeta(0); for (var i = 0; i < meta.data.length; ++i) { var point = meta.data[i]; expect(point.stop).toBe(i === 3); } }); it('should correctly calc visible points on update', async() => { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [ {x: 10, y: 20}, {x: 15, y: 19}, ] }], }, options: { scales: { y: { type: 'linear', min: 0, max: 25, }, x: { type: 'linear', min: 0, max: 50 }, } } }); chart.data.datasets[0].data = [ {x: 10, y: 20}, {x: 15, y: 19}, {x: 17, y: 12}, {x: 50, y: 9}, {x: 50, y: 9}, {x: 50, y: 9}, {x: 51, y: 9}, {x: 52, y: 9}, {x: 52, y: 9}, ]; chart.update(); var point = chart.getDatasetMeta(0).data[0]; var event = { type: 'mousemove', native: true, ...point }; chart._handleEvent(event, false, true); const visiblePoints = chart.getSortedVisibleDatasetMetas()[0].data.filter(_ => !_.skip); expect(visiblePoints.length).toBe(6); }, 500); it('should correctly calc _drawStart and _drawCount when first points beyond scale limits are null and spanGaps=true', async() => { var chart = window.acquireChart({ type: 'line', data: { labels: [0, 10, 20, 30, 40, 50], datasets: [{ data: [3, null, 2, 3, null, 1.5], spanGaps: true, tension: 0.4 }] }, options: { scales: { x: { type: 'linear', min: 11, max: 40, } } } }); chart.update(); var controller = chart.getDatasetMeta(0).controller; expect(controller._drawStart).toBe(0); expect(controller._drawCount).toBe(6); }, 500); it('should correctly calc _drawStart and _drawCount when all points beyond scale limits are null and spanGaps=true', async() => { var chart = window.acquireChart({ type: 'line', data: { labels: [0, 10, 20, 30, 40, 50], datasets: [{ data: [null, null, 2, 3, null, null], spanGaps: true, tension: 0.4 }] }, options: { scales: { x: { type: 'linear', min: 11, max: 40, } } } }); chart.update(); var controller = chart.getDatasetMeta(0).controller; expect(controller._drawStart).toBe(1); expect(controller._drawCount).toBe(4); }, 500); it('should correctly calc _drawStart and _drawCount when spanGaps=false', async() => { var chart = window.acquireChart({ type: 'line', data: { labels: [0, 10, 20, 30, 40, 50], datasets: [{ data: [3, null, 2, 3, null, 1.5], spanGaps: false, tension: 0.4 }] }, options: { scales: { x: { type: 'linear', min: 11, max: 40, } } } }); chart.update(); var controller = chart.getDatasetMeta(0).controller; expect(controller._drawStart).toBe(1); expect(controller._drawCount).toBe(4); }, 500); it('should not override tooltip title and label callbacks', async() => { const chart = window.acquireChart({ type: 'line', data: { labels: ['Label 1', 'Label 2'], datasets: [{ data: [21, 79], label: 'Dataset 1' }, { data: [33, 67], label: 'Dataset 2' }] }, options: { responsive: true, maintainAspectRatio: true, } }); const {tooltip} = chart; const point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Label 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Dataset 1: 21'], after: [] }]); chart.options.plugins.tooltip = {mode: 'dataset'}; chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Dataset 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Label 1: 21'], after: [] }, { before: [], lines: ['Label 2: 79'], after: [] }]); }); }); ================================================ FILE: test/specs/controller.polarArea.tests.js ================================================ describe('Chart.controllers.polarArea', function() { describe('auto', jasmine.fixture.specs('controller.polarArea')); it('should update the scale correctly when data visibility is changed', function() { var expectedScaleMax = 1; var chart = window.acquireChart({ type: 'polarArea', data: { datasets: [ {data: [100]} ], labels: ['x'] } }); chart.toggleDataVisibility(0); chart.update(); expect(chart.scales.r.max).toBe(expectedScaleMax); }); it('should be registered as dataset controller', function() { expect(typeof Chart.controllers.polarArea).toBe('function'); }); it('should be constructed', function() { var chart = window.acquireChart({ type: 'polarArea', data: { datasets: [ {data: []}, {data: []} ], labels: [] } }); var meta = chart.getDatasetMeta(1); expect(meta.type).toEqual('polarArea'); expect(meta.data).toEqual([]); expect(meta.hidden).toBe(null); expect(meta.controller).not.toBe(undefined); expect(meta.controller.index).toBe(1); meta.controller.updateIndex(0); expect(meta.controller.index).toBe(0); }); it('should create arc elements for each data item during initialization', function() { var chart = window.acquireChart({ type: 'polarArea', data: { datasets: [ {data: []}, {data: [10, 15, 0, -4]} ], labels: [] } }); var meta = chart.getDatasetMeta(1); expect(meta.data.length).toBe(4); // 4 arcs created expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[2] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[3] instanceof Chart.elements.ArcElement).toBe(true); }); it('should draw all elements', function() { var chart = window.acquireChart({ type: 'polarArea', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(0); spyOn(meta.data[0], 'draw'); spyOn(meta.data[1], 'draw'); spyOn(meta.data[2], 'draw'); spyOn(meta.data[3], 'draw'); chart.update(); expect(meta.data[0].draw.calls.count()).toBe(1); expect(meta.data[1].draw.calls.count()).toBe(1); expect(meta.data[2].draw.calls.count()).toBe(1); expect(meta.data[3].draw.calls.count()).toBe(1); }); it('should update elements when modifying data', function() { var chart = window.acquireChart({ type: 'polarArea', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { showLine: true, plugins: { legend: false, title: false }, elements: { arc: { backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 255, 0)', borderWidth: 1.2 } } } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); [ {o: 174, s: -0.5 * Math.PI, e: 0}, {o: 236, s: 0, e: 0.5 * Math.PI}, {o: 51, s: 0.5 * Math.PI, e: Math.PI}, {o: 0, s: Math.PI, e: 1.5 * Math.PI} ].forEach(function(expected, i) { expect(meta.data[i].x).withContext(i).toBeCloseToPixel(256); expect(meta.data[i].y).withContext(i).toBeCloseToPixel(256); expect(meta.data[i].innerRadius).withContext(i).toBeCloseToPixel(0); expect(meta.data[i].outerRadius).withContext(i).toBeCloseToPixel(expected.o); expect(meta.data[i].startAngle).withContext(i).toBe(expected.s); expect(meta.data[i].endAngle).withContext(i).toBe(expected.e); expect(meta.data[i].options).withContext(i).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 255, 0)', borderWidth: 1.2 })); }); // arc styles chart.data.datasets[0].backgroundColor = 'rgb(128, 129, 130)'; chart.data.datasets[0].borderColor = 'rgb(56, 57, 58)'; chart.data.datasets[0].borderWidth = 1.123; chart.update(); for (var i = 0; i < 4; ++i) { expect(meta.data[i].options.backgroundColor).toBe('rgb(128, 129, 130)'); expect(meta.data[i].options.borderColor).toBe('rgb(56, 57, 58)'); expect(meta.data[i].options.borderWidth).toBe(1.123); } chart.update(); expect(meta.data[0].x).toBeCloseToPixel(256); expect(meta.data[0].y).toBeCloseToPixel(256); expect(meta.data[0].innerRadius).toBeCloseToPixel(0); expect(meta.data[0].outerRadius).toBeCloseToPixel(174); }); it('should update elements with start angle from options', function() { var chart = window.acquireChart({ type: 'polarArea', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { showLine: true, plugins: { legend: false, title: false, }, scales: { r: { startAngle: 90, // default is 0 } }, elements: { arc: { backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 255, 0)', borderWidth: 1.2 } } } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); [ {o: 174, s: 0, e: 0.5 * Math.PI}, {o: 236, s: 0.5 * Math.PI, e: Math.PI}, {o: 51, s: Math.PI, e: 1.5 * Math.PI}, {o: 0, s: 1.5 * Math.PI, e: 2.0 * Math.PI} ].forEach(function(expected, i) { expect(meta.data[i].x).withContext(i).toBeCloseToPixel(256); expect(meta.data[i].y).withContext(i).toBeCloseToPixel(256); expect(meta.data[i].innerRadius).withContext(i).toBeCloseToPixel(0); expect(meta.data[i].outerRadius).withContext(i).toBeCloseToPixel(expected.o); expect(meta.data[i].startAngle).withContext(i).toBe(expected.s); expect(meta.data[i].endAngle).withContext(i).toBe(expected.e); expect(meta.data[i].options).withContext(i).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 255, 0)', borderWidth: 1.2 })); }); }); it('should handle number of data point changes in update', function() { var chart = window.acquireChart({ type: 'polarArea', data: { datasets: [{ data: [10, 15, 0, -4], label: 'dataset2' }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { showLine: true, elements: { arc: { backgroundColor: 'rgb(255, 0, 0)', borderColor: 'rgb(0, 255, 0)', borderWidth: 1.2 } } } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(4); // remove 2 items chart.data.labels = ['label1', 'label2']; chart.data.datasets[0].data = [1, 2]; chart.update(); expect(meta.data.length).toBe(2); expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); // add 3 items chart.data.labels = ['label1', 'label2', 'label3', 'label4', 'label5']; chart.data.datasets[0].data = [1, 2, 3, 4, 5]; chart.update(); expect(meta.data.length).toBe(5); expect(meta.data[0] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[2] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[3] instanceof Chart.elements.ArcElement).toBe(true); expect(meta.data[4] instanceof Chart.elements.ArcElement).toBe(true); }); describe('Interactions', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'polarArea', data: { labels: ['label1', 'label2', 'label3', 'label4'], datasets: [{ data: [10, 15, 0, 4] }] }, options: { cutoutPercentage: 0, elements: { arc: { backgroundColor: 'rgb(100, 150, 200)', borderColor: 'rgb(50, 100, 150)', borderWidth: 2, } } } }); }); it('should handle default hover styles', async function() { var chart = this.chart; var arc = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', arc); expect(arc.options.backgroundColor).toBe('#3187DD'); expect(arc.options.borderColor).toBe('#175A9D'); expect(arc.options.borderWidth).toBe(2); await jasmine.triggerMouseEvent(chart, 'mouseout', arc); expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); expect(arc.options.borderWidth).toBe(2); }); it('should handle hover styles defined via dataset properties', async function() { var chart = this.chart; var arc = chart.getDatasetMeta(0).data[0]; Chart.helpers.merge(chart.data.datasets[0], { hoverBackgroundColor: 'rgb(200, 100, 150)', hoverBorderColor: 'rgb(150, 50, 100)', hoverBorderWidth: 8.4, }); chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', arc); expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); expect(arc.options.borderWidth).toBe(8.4); await jasmine.triggerMouseEvent(chart, 'mouseout', arc); expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); expect(arc.options.borderWidth).toBe(2); }); it('should handle hover styles defined via element options', async function() { var chart = this.chart; var arc = chart.getDatasetMeta(0).data[0]; Chart.helpers.merge(chart.options.elements.arc, { hoverBackgroundColor: 'rgb(200, 100, 150)', hoverBorderColor: 'rgb(150, 50, 100)', hoverBorderWidth: 8.4, }); chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', arc); expect(arc.options.backgroundColor).toBe('rgb(200, 100, 150)'); expect(arc.options.borderColor).toBe('rgb(150, 50, 100)'); expect(arc.options.borderWidth).toBe(8.4); await jasmine.triggerMouseEvent(chart, 'mouseout', arc); expect(arc.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(arc.options.borderColor).toBe('rgb(50, 100, 150)'); expect(arc.options.borderWidth).toBe(2); }); }); it('should not override tooltip title and label callbacks', async() => { const chart = window.acquireChart({ type: 'polarArea', data: { labels: ['Label 1', 'Label 2'], datasets: [{ data: [21, 79], label: 'Dataset 1' }, { data: [33, 67], label: 'Dataset 2' }] }, options: { responsive: true, maintainAspectRatio: true, } }); const {tooltip} = chart; const point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Label 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Dataset 1: 21'], after: [] }]); chart.options.plugins.tooltip = {mode: 'dataset'}; chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Dataset 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Label 1: 21'], after: [] }, { before: [], lines: ['Label 2: 79'], after: [] }]); }); }); ================================================ FILE: test/specs/controller.radar.tests.js ================================================ describe('Chart.controllers.radar', function() { describe('auto', jasmine.fixture.specs('controller.radar')); it('should be registered as dataset controller', function() { expect(typeof Chart.controllers.radar).toBe('function'); }); it('Should be constructed', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [] }], labels: [] } }); var meta = chart.getDatasetMeta(0); expect(meta.type).toBe('radar'); expect(meta.controller).not.toBe(undefined); expect(meta.controller.index).toBe(0); expect(meta.data).toEqual([]); meta.controller.updateIndex(1); expect(meta.controller.index).toBe(1); }); it('Should create arc elements for each data item during initialization', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 15, 0, 4] }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(0); expect(meta.dataset instanceof Chart.elements.LineElement).toBe(true); // line element expect(meta.data.length).toBe(4); // 4 points created expect(meta.data[0] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[1] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[2] instanceof Chart.elements.PointElement).toBe(true); expect(meta.data[3] instanceof Chart.elements.PointElement).toBe(true); }); it('should draw all elements', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 15, 0, 4] }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(0); spyOn(meta.dataset, 'draw'); spyOn(meta.data[0], 'draw'); spyOn(meta.data[1], 'draw'); spyOn(meta.data[2], 'draw'); spyOn(meta.data[3], 'draw'); chart.update(); expect(meta.dataset.draw.calls.count()).toBe(1); expect(meta.data[0].draw.calls.count()).toBe(1); expect(meta.data[1].draw.calls.count()).toBe(1); expect(meta.data[2].draw.calls.count()).toBe(1); expect(meta.data[3].draw.calls.count()).toBe(1); }); it('should draw all elements with object notation and default key', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [{r: 10}, {r: 20}, {r: 15}] }], labels: ['label1', 'label2', 'label3'] } }); var meta = chart.getDatasetMeta(0); spyOn(meta.dataset, 'draw'); spyOn(meta.data[0], 'draw'); spyOn(meta.data[1], 'draw'); spyOn(meta.data[2], 'draw'); chart.update(); expect(meta.dataset.draw.calls.count()).toBe(1); expect(meta.data[0].draw.calls.count()).toBe(1); expect(meta.data[1].draw.calls.count()).toBe(1); expect(meta.data[2].draw.calls.count()).toBe(1); }); it('should update elements', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 15, 0, 4] }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { showLine: true, plugins: { legend: false, title: false, }, elements: { line: { backgroundColor: 'rgb(255, 0, 0)', borderCapStyle: 'round', borderColor: 'rgb(0, 255, 0)', borderDash: [], borderDashOffset: 0.1, borderJoinStyle: 'bevel', borderWidth: 1.2, fill: true, tension: 0.1, }, point: { backgroundColor: Chart.defaults.backgroundColor, borderWidth: 1, borderColor: Chart.defaults.borderColor, hitRadius: 1, hoverRadius: 4, hoverBorderWidth: 1, radius: 3, pointStyle: 'circle' } } } }); var meta = chart.getDatasetMeta(0); chart.reset(); // reset first // Line element expect(meta.dataset.options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(255, 0, 0)', borderCapStyle: 'round', borderColor: 'rgb(0, 255, 0)', borderDash: [], borderDashOffset: 0.1, borderJoinStyle: 'bevel', borderWidth: 1.2, fill: true, tension: 0.1, })); [ {x: 256, y: 256}, {x: 256, y: 256}, {x: 256, y: 256}, {x: 256, y: 256}, ].forEach(function(expected, i) { expect(meta.data[i].x).withContext(i).toBeCloseToPixel(expected.x); expect(meta.data[i].y).withContext(i).toBeCloseToPixel(expected.y); expect(meta.data[i].options).withContext(i).toEqual(jasmine.objectContaining({ backgroundColor: Chart.defaults.backgroundColor, borderWidth: 1, borderColor: Chart.defaults.borderColor, hitRadius: 1, radius: 3, pointStyle: 'circle', })); }); chart.update(); [ {x: 256, y: 122, cppx: 246, cppy: 122, cpnx: 272, cpny: 122}, {x: 457, y: 256, cppx: 457, cppy: 249, cpnx: 457, cpny: 262}, {x: 256, y: 256, cppx: 277, cppy: 256, cpnx: 250, cpny: 256}, {x: 202, y: 256, cppx: 202, cppy: 260, cpnx: 202, cpny: 246}, ].forEach(function(expected, i) { expect(meta.data[i].x).withContext(i).toBeCloseToPixel(expected.x); expect(meta.data[i].y).withContext(i).toBeCloseToPixel(expected.y); expect(meta.data[i].cp1x).withContext(i).toBeCloseToPixel(expected.cppx); expect(meta.data[i].cp1y).withContext(i).toBeCloseToPixel(expected.cppy); expect(meta.data[i].cp2x).withContext(i).toBeCloseToPixel(expected.cpnx); expect(meta.data[i].cp2y).withContext(i).toBeCloseToPixel(expected.cpny); expect(meta.data[i].options).withContext(i).toEqual(jasmine.objectContaining({ backgroundColor: Chart.defaults.backgroundColor, borderWidth: 1, borderColor: Chart.defaults.borderColor, hitRadius: 1, radius: 3, pointStyle: 'circle', })); }); // Use dataset level styles for lines & points chart.data.datasets[0].tension = 0; chart.data.datasets[0].backgroundColor = 'rgb(98, 98, 98)'; chart.data.datasets[0].borderColor = 'rgb(8, 8, 8)'; chart.data.datasets[0].borderWidth = 0.55; chart.data.datasets[0].borderCapStyle = 'butt'; chart.data.datasets[0].borderDash = [2, 3]; chart.data.datasets[0].borderDashOffset = 7; chart.data.datasets[0].borderJoinStyle = 'miter'; chart.data.datasets[0].fill = false; // point styles chart.data.datasets[0].pointRadius = 22; chart.data.datasets[0].hitRadius = 3.3; chart.data.datasets[0].pointBackgroundColor = 'rgb(128, 129, 130)'; chart.data.datasets[0].pointBorderColor = 'rgb(56, 57, 58)'; chart.data.datasets[0].pointBorderWidth = 1.123; chart.update(); expect(meta.dataset.options).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(98, 98, 98)', borderCapStyle: 'butt', borderColor: 'rgb(8, 8, 8)', borderDash: [2, 3], borderDashOffset: 7, borderJoinStyle: 'miter', borderWidth: 0.55, fill: false, tension: 0, })); // Since tension is now 0, we don't care about the control points [ {x: 256, y: 122}, {x: 457, y: 256}, {x: 256, y: 256}, {x: 202, y: 256}, ].forEach(function(expected, i) { expect(meta.data[i].x).withContext(i).toBeCloseToPixel(expected.x); expect(meta.data[i].y).withContext(i).toBeCloseToPixel(expected.y); expect(meta.data[i].options).withContext(i).toEqual(jasmine.objectContaining({ backgroundColor: 'rgb(128, 129, 130)', borderWidth: 1.123, borderColor: 'rgb(56, 57, 58)', hitRadius: 3.3, radius: 22, pointStyle: 'circle' })); }); }); describe('Interactions', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'radar', data: { labels: ['label1', 'label2', 'label3', 'label4'], datasets: [{ data: [10, 15, 0, 4] }] }, options: { elements: { point: { backgroundColor: 'rgb(100, 150, 200)', borderColor: 'rgb(50, 100, 150)', borderWidth: 2, radius: 3 } } } }); }); it('should handle default hover styles', async function() { var chart = this.chart; var point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point.options.backgroundColor).toBe('#3187DD'); expect(point.options.borderColor).toBe('#175A9D'); expect(point.options.borderWidth).toBe(1); expect(point.options.radius).toBe(4); await jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); expect(point.options.borderWidth).toBe(2); expect(point.options.radius).toBe(3); }); it('should handle hover styles defined via dataset properties', async function() { var chart = this.chart; var point = chart.getDatasetMeta(0).data[0]; Chart.helpers.merge(chart.data.datasets[0], { hoverBackgroundColor: 'rgb(200, 100, 150)', hoverBorderColor: 'rgb(150, 50, 100)', hoverBorderWidth: 8.4, hoverRadius: 4.2 }); chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); expect(point.options.borderWidth).toBe(8.4); expect(point.options.radius).toBe(4.2); await jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); expect(point.options.borderWidth).toBe(2); expect(point.options.radius).toBe(3); }); it('should handle hover styles defined via element options', async function() { var chart = this.chart; var point = chart.getDatasetMeta(0).data[0]; Chart.helpers.merge(chart.options.elements.point, { hoverBackgroundColor: 'rgb(200, 100, 150)', hoverBorderColor: 'rgb(150, 50, 100)', hoverBorderWidth: 8.4, hoverRadius: 4.2 }); chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(point.options.backgroundColor).toBe('rgb(200, 100, 150)'); expect(point.options.borderColor).toBe('rgb(150, 50, 100)'); expect(point.options.borderWidth).toBe(8.4); expect(point.options.radius).toBe(4.2); await jasmine.triggerMouseEvent(chart, 'mouseout', point); expect(point.options.backgroundColor).toBe('rgb(100, 150, 200)'); expect(point.options.borderColor).toBe('rgb(50, 100, 150)'); expect(point.options.borderWidth).toBe(2); expect(point.options.radius).toBe(3); }); }); it('should allow pointBorderWidth to be set to 0', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 15, 0, 4], pointBorderWidth: 0 }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta = chart.getDatasetMeta(0); var point = meta.data[0]; expect(point.options.borderWidth).toBe(0); }); it('should use the pointRadius setting over the radius setting', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 15, 0, 4], pointRadius: 10, radius: 15, }, { data: [20, 20, 20, 20], radius: 20 }], labels: ['label1', 'label2', 'label3', 'label4'] } }); var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); expect(meta0.data[0].options.radius).toBe(10); expect(meta1.data[0].options.radius).toBe(20); }); it('should return id for value scale', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 15, 0, 4], pointBorderWidth: 0 }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { scales: { test: { axis: 'r' } } } }); var meta = chart.getDatasetMeta(0); expect(meta.vScale.id).toBe('test'); }); it('should not override tooltip title and label callbacks', async() => { const chart = window.acquireChart({ type: 'radar', data: { labels: ['Label 1', 'Label 2'], datasets: [{ data: [21, 79], label: 'Dataset 1' }, { data: [33, 67], label: 'Dataset 2' }] }, options: { responsive: true, maintainAspectRatio: true, } }); const {tooltip} = chart; const point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Label 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Dataset 1: 21'], after: [] }]); chart.options.plugins.tooltip = {mode: 'dataset'}; chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Dataset 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Label 1: 21'], after: [] }, { before: [], lines: ['Label 2: 79'], after: [] }]); }); }); ================================================ FILE: test/specs/controller.scatter.tests.js ================================================ describe('Chart.controllers.scatter', function() { describe('auto', jasmine.fixture.specs('controller.scatter')); it('should be registered as dataset controller', function() { expect(typeof Chart.controllers.scatter).toBe('function'); }); it('should only show a single point in the tooltip on multiple datasets', async function() { var chart = window.acquireChart({ type: 'scatter', data: { datasets: [{ data: [{ x: 10, y: 15 }, { x: 12, y: 10 }], label: 'dataset1' }, { data: [{ x: 20, y: 10 }, { x: 4, y: 8 }], label: 'dataset2' }] }, options: {} }); var point = chart.getDatasetMeta(0).data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(chart.tooltip.body.length).toEqual(1); }); it('should not create line element by default', function() { var chart = window.acquireChart({ type: 'scatter', data: { datasets: [{ data: [{ x: 10, y: 15 }, { x: 12, y: 10 }], label: 'dataset1' }, { data: [{ x: 20, y: 10 }, { x: 4, y: 8 }], label: 'dataset2' }] }, }); var meta = chart.getDatasetMeta(0); expect(meta.dataset instanceof Chart.elements.LineElement).toBe(false); }); it('should create line element if showline is true at datasets options', function() { var chart = window.acquireChart({ type: 'scatter', data: { datasets: [{ showLine: true, data: [{ x: 10, y: 15 }, { x: 12, y: 10 }], label: 'dataset1' }, { data: [{ x: 20, y: 10 }, { x: 4, y: 8 }], label: 'dataset2' }] }, }); var meta = chart.getDatasetMeta(0); expect(meta.dataset instanceof Chart.elements.LineElement).toBe(true); }); it('should create line element if showline is true at root options', function() { var chart = window.acquireChart({ type: 'scatter', data: { datasets: [{ data: [{ x: 10, y: 15 }, { x: 12, y: 10 }], label: 'dataset1' }, { data: [{ x: 20, y: 10 }, { x: 4, y: 8 }], label: 'dataset2' }] }, options: { showLine: true } }); var meta = chart.getDatasetMeta(0); expect(meta.dataset instanceof Chart.elements.LineElement).toBe(true); }); it('should not override tooltip title and label callbacks', async() => { const chart = window.acquireChart({ type: 'scatter', data: { labels: ['Label 1', 'Label 2'], datasets: [{ data: [{ x: 10, y: 15 }, { x: 12, y: 10 }], label: 'Dataset 1' }, { data: [{ x: 20, y: 10 }, { x: 4, y: 8 }], label: 'Dataset 2' }] }, options: { responsive: true, maintainAspectRatio: true, } }); const {tooltip} = chart; const point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Label 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Dataset 1: (10, 15)'], after: [] }]); chart.options.plugins.tooltip = {mode: 'dataset'}; chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip.title).toEqual(['Dataset 1']); expect(tooltip.body).toEqual([{ before: [], lines: ['Label 1: (10, 15)'], after: [] }, { before: [], lines: ['Label 2: (12, 10)'], after: [] }]); }); }); ================================================ FILE: test/specs/core.animation.tests.js ================================================ describe('Chart.Animation', function() { it('should animate boolean', function() { const target = {prop: false}; const anim = new Chart.Animation({duration: 1000}, target, 'prop', true); expect(anim.active()).toBeTrue(); anim.tick(anim._start + 500); expect(anim.active()).toBeTrue(); expect(target.prop).toBeFalse(); anim.tick(anim._start + 501); expect(anim.active()).toBeTrue(); expect(target.prop).toBeTrue(); anim.tick(anim._start - 100); expect(anim.active()).toBeTrue(); expect(target.prop).toBeFalse(); anim.tick(anim._start + 1000); expect(anim.active()).toBeFalse(); expect(target.prop).toBeTrue(); }); describe('color', function() { it('should fall back to transparent', function() { const target = {}; const anim = new Chart.Animation({duration: 1000, type: 'color'}, target, 'color', 'red'); anim._from = undefined; anim.tick(anim._start + 500); expect(target.color).toEqual('#FF000080'); anim._from = 'blue'; anim._to = undefined; anim.tick(anim._start + 500); expect(target.color).toEqual('#0000FF80'); }); it('should not try to mix invalid color', function() { const target = {color: 'blue'}; const anim = new Chart.Animation({duration: 1000, type: 'color'}, target, 'color', 'invalid'); anim.tick(anim._start + 500); expect(target.color).toEqual('invalid'); }); }); it('should loop', function() { const target = {value: 0}; const anim = new Chart.Animation({duration: 100, loop: true}, target, 'value', 10); anim.tick(anim._start + 50); expect(target.value).toEqual(5); anim.tick(anim._start + 100); expect(target.value).toEqual(10); anim.tick(anim._start + 150); expect(target.value).toEqual(5); anim.tick(anim._start + 400); expect(target.value).toEqual(0); }); it('should update', function() { const target = {testColor: 'transparent'}; const anim = new Chart.Animation({duration: 100, type: 'color'}, target, 'testColor', 'red'); anim.tick(anim._start + 50); expect(target.testColor).toEqual('#FF000080'); anim.update({duration: 500}, 'blue', Date.now()); anim.tick(anim._start + 250); expect(target.testColor).toEqual('#4000BFBF'); anim.tick(anim._start + 500); expect(target.testColor).toEqual('blue'); }); it('should not update when finished', function() { const target = {testColor: 'transparent'}; const anim = new Chart.Animation({duration: 100, type: 'color'}, target, 'testColor', 'red'); anim.tick(anim._start + 100); expect(target.testColor).toEqual('red'); expect(anim.active()).toBeFalse(); anim.update({duration: 500}, 'blue', Date.now()); expect(anim._duration).toEqual(100); expect(anim._to).toEqual('red'); }); }); ================================================ FILE: test/specs/core.animations.tests.js ================================================ describe('Chart.animations', function() { it('should override property collection with property', function() { const chart = {}; const anims = new Chart.Animations(chart, { collection1: { properties: ['property1', 'property2'], duration: 1000 }, property2: { duration: 2000 } }); expect(anims._properties.get('property1')).toEqual(jasmine.objectContaining({duration: 1000})); expect(anims._properties.get('property2')).toEqual(jasmine.objectContaining({duration: 2000})); }); it('should ignore duplicate definitions from collections', function() { const chart = {}; const anims = new Chart.Animations(chart, { collection1: { properties: ['property1'], duration: 1000 }, collection2: { properties: ['property1', 'property2'], duration: 2000 } }); expect(anims._properties.get('property1')).toEqual(jasmine.objectContaining({duration: 1000})); expect(anims._properties.get('property2')).toEqual(jasmine.objectContaining({duration: 2000})); }); it('should not animate undefined options key', function() { const chart = {}; const anims = new Chart.Animations(chart, {value: {duration: 100}, option: {duration: 200}}); const target = { value: 1, options: { option: 2 } }; expect(anims.update(target, { options: undefined })).toBeUndefined(); }); it('should assign options directly, if target does not have previous options', function() { const chart = {}; const anims = new Chart.Animations(chart, {option: {duration: 200}}); const target = {}; expect(anims.update(target, {options: {option: 1}})).toBeUndefined(); }); it('should clone the target options, if those are shared and new options are not', function() { const chart = {options: {}}; const anims = new Chart.Animations(chart, {option: {duration: 200}}); const options = {option: 0, $shared: true}; const target = {options}; expect(anims.update(target, {options: {option: 1}})).toBeTrue(); expect(target.options.$shared).not.toBeTrue(); expect(target.options !== options).toBeTrue(); }); it('should assign shared options to target after animations complete', function(done) { const chart = { draw: function() {}, options: {} }; const anims = new Chart.Animations(chart, {value: {duration: 100}, option: {duration: 200}}); const target = { value: 1, options: { option: 2 } }; const sharedOpts = {option: 10, $shared: true}; expect(anims.update(target, { options: sharedOpts })).toBeTrue(); expect(target.options !== sharedOpts).toBeTrue(); Chart.animator.start(chart); setTimeout(function() { expect(Chart.animator.running(chart)).toBeFalse(); expect(target.options === sharedOpts).toBeTrue(); Chart.animator.remove(chart); done(); }, 300); }); it('should not assign shared options to target when animations are cancelled', function(done) { const chart = { draw: function() {}, options: {} }; const anims = new Chart.Animations(chart, {value: {duration: 100}, option: {duration: 200}}); const target = { value: 1, options: { option: 2 } }; const sharedOpts = {option: 10, $shared: true}; expect(anims.update(target, { options: sharedOpts })).toBeTrue(); expect(target.options !== sharedOpts).toBeTrue(); Chart.animator.start(chart); setTimeout(function() { expect(Chart.animator.running(chart)).toBeTrue(); Chart.animator.stop(chart); expect(Chart.animator.running(chart)).toBeFalse(); setTimeout(function() { expect(target.options === sharedOpts).toBeFalse(); Chart.animator.remove(chart); done(); }, 250); }, 50); }); it('should assign final shared options to target after animations complete', function(done) { const chart = { draw: function() {}, options: {} }; const anims = new Chart.Animations(chart, {value: {duration: 100}, option: {duration: 200}}); const origOpts = {option: 2}; const target = { value: 1, options: origOpts }; const sharedOpts = {option: 10, $shared: true}; const sharedOpts2 = {option: 20, $shared: true}; expect(anims.update(target, { options: sharedOpts })).toBeTrue(); expect(target.options !== sharedOpts).toBeTrue(); Chart.animator.start(chart); setTimeout(function() { expect(Chart.animator.running(chart)).toBeTrue(); expect(target.options === origOpts).toBeTrue(); expect(anims.update(target, { options: sharedOpts2 })).toBeUndefined(); expect(target.options === origOpts).toBeTrue(); setTimeout(function() { expect(target.options === sharedOpts2).toBeTrue(); Chart.animator.remove(chart); done(); }, 250); }, 50); }); }); ================================================ FILE: test/specs/core.animator.tests.js ================================================ describe('Chart.animator', function() { it('should fire onProgress for each draw', function(done) { let count = 0; let drawCount = 0; const progress = (animation) => { count++; expect(animation.numSteps).toEqual(250); expect(animation.currentStep <= 250).toBeTrue(); }; acquireChart({ type: 'bar', data: { datasets: [ {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, options: { animation: { duration: 250, onProgress: progress, onComplete: function() { expect(count).toEqual(drawCount); done(); } } }, plugins: [{ afterDraw() { drawCount++; } }] }, { canvas: { height: 150, width: 250 }, }); }); it('should not fail when adding no items', function() { const chart = {}; Chart.animator.add(chart, undefined); Chart.animator.add(chart, []); Chart.animator.start(chart); expect(Chart.animator.running(chart)).toBeFalse(); }); }); ================================================ FILE: test/specs/core.controller.tests.js ================================================ describe('Chart', function() { const overrides = Chart.overrides; // https://github.com/chartjs/Chart.js/issues/2481 // See global.deprecations.tests.js for backward compatibility it('should be defined and prototype of chart instances', function() { var chart = acquireChart({}); expect(Chart).toBeDefined(); expect(Chart instanceof Object).toBeTruthy(); expect(chart.constructor).toBe(Chart); expect(chart instanceof Chart).toBeTruthy(); }); it('should throw an error if the canvas is already in use', function() { var config = { type: 'line', data: { datasets: [{ data: [1, 2, 3, 4] }], labels: ['A', 'B', 'C', 'D'] } }; var chart = acquireChart(config); var canvas = chart.canvas; function createChart() { return new Chart(canvas, config); } expect(createChart).toThrow(new Error( 'Canvas is already in use. ' + 'Chart with ID \'' + chart.id + '\'' + ' must be destroyed before the canvas with ID \'' + chart.canvas.id + '\' can be reused.' )); chart.destroy(); expect(createChart).not.toThrow(); }); describe('config initialization', function() { it('should create missing config.data properties', function() { var chart = acquireChart({}); var data = chart.data; expect(data instanceof Object).toBeTruthy(); expect(data.labels instanceof Array).toBeTruthy(); expect(data.labels.length).toBe(0); expect(data.datasets instanceof Array).toBeTruthy(); expect(data.datasets.length).toBe(0); }); it('should not alter config.data references', function() { var ds0 = {data: [10, 11, 12, 13]}; var ds1 = {data: [20, 21, 22, 23]}; var datasets = [ds0, ds1]; var labels = [0, 1, 2, 3]; var data = {labels: labels, datasets: datasets}; var chart = acquireChart({ type: 'line', data: data }); expect(chart.data).toBe(data); expect(chart.data.labels).toBe(labels); expect(chart.data.datasets).toBe(datasets); expect(chart.data.datasets[0]).toBe(ds0); expect(chart.data.datasets[1]).toBe(ds1); expect(chart.data.datasets[0].data).toBe(ds0.data); expect(chart.data.datasets[1].data).toBe(ds1.data); }); it('should define chart.data as an alias for config.data', function() { var config = {data: {labels: [], datasets: []}}; var chart = acquireChart(config); expect(chart.data).toBe(config.data); chart.data = {labels: [1, 2, 3], datasets: [{data: [4, 5, 6]}]}; expect(config.data).toBe(chart.data); expect(config.data.labels).toEqual([1, 2, 3]); expect(config.data.datasets[0].data).toEqual([4, 5, 6]); config.data = {labels: [7, 8, 9], datasets: [{data: [10, 11, 12]}]}; expect(chart.data).toBe(config.data); expect(chart.data.labels).toEqual([7, 8, 9]); expect(chart.data.datasets[0].data).toEqual([10, 11, 12]); }); it('should initialize config with default interaction options', function() { var callback = function() {}; var defaults = Chart.defaults; defaults.onHover = callback; overrides.line.interaction = { mode: 'test' }; var chart = acquireChart({ type: 'line' }); var options = chart.options; expect(options.font.size).toBe(defaults.font.size); expect(options.onHover).toBe(callback); expect(options.hover.mode).toBe('test'); defaults.onHover = null; delete overrides.line.interaction; }); it('should initialize config with default hover options', function() { var callback = function() {}; var defaults = Chart.defaults; defaults.onHover = callback; overrides.line.hover = { mode: 'test' }; var chart = acquireChart({ type: 'line' }); var options = chart.options; expect(options.font.size).toBe(defaults.font.size); expect(options.onHover).toBe(callback); expect(options.hover.mode).toBe('test'); defaults.onHover = null; delete overrides.line.hover; }); it('should override default options', function() { var callback = function() {}; var defaults = Chart.defaults; var defaultSpanGaps = defaults.datasets.line.spanGaps; defaults.onHover = callback; overrides.line.hover = { mode: 'x-axis' }; defaults.datasets.line.spanGaps = true; var chart = acquireChart({ type: 'line', options: { spanGaps: false, hover: { mode: 'dataset', }, plugins: { title: { position: 'bottom' } } } }); var options = chart.options; expect(options.spanGaps).toBe(false); expect(options.hover.mode).toBe('dataset'); expect(options.plugins.title.position).toBe('bottom'); defaults.onHover = null; delete overrides.line.hover; defaults.datasets.line.spanGaps = defaultSpanGaps; }); it('should initialize config with default dataset options', function() { var defaults = Chart.defaults.datasets.pie; var chart = acquireChart({ type: 'pie' }); var options = chart.options; expect(options.circumference).toBe(defaults.circumference); }); it('should override axis positions that are incorrect', function() { var chart = acquireChart({ type: 'line', options: { scales: { x: { position: 'left', }, y: { position: 'bottom' } } } }); var scaleOptions = chart.options.scales; expect(scaleOptions.x.position).toBe('bottom'); expect(scaleOptions.y.position).toBe('left'); }); it('should throw an error if the chart type is incorrect', function() { function createChart() { acquireChart({ type: 'area', data: { datasets: [{ label: 'first', data: [10, 20] }], labels: ['0', '1'], }, options: { scales: { x: { type: 'linear', position: 'left', }, y: { type: 'category', position: 'bottom' } } } }); } expect(createChart).toThrow(new Error('"area" is not a registered controller.')); }); it('should initialize the data object', function() { const chart = acquireChart({type: 'bar'}); expect(chart.data).toEqual(jasmine.objectContaining({labels: [], datasets: []})); chart.data = {}; expect(chart.data).toEqual(jasmine.objectContaining({labels: [], datasets: []})); chart.data = null; expect(chart.data).toEqual(jasmine.objectContaining({labels: [], datasets: []})); chart.data = undefined; expect(chart.data).toEqual(jasmine.objectContaining({labels: [], datasets: []})); }); describe('should disable hover', function() { it('when options.hover=false', function() { var chart = acquireChart({ type: 'line', options: { hover: false } }); expect(chart.options.hover).toBeFalse(); }); it('when options.interaction=false and options.hover is not defined', function() { var chart = acquireChart({ type: 'line', options: { interaction: false } }); expect(chart.options.hover).toBeFalse(); }); it('when options.interaction=false and options.hover is defined', function() { var chart = acquireChart({ type: 'line', options: { interaction: false, hover: {mode: 'nearest'} } }); expect(chart.options.hover).toBeFalse(); }); }); it('should activate element on hover', async function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] } }); var point = chart.getDatasetMeta(0).data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 1, element: point}]); }); it('should handle changing the events at runtime', async function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { events: ['click'] } }); var point1 = chart.getDatasetMeta(0).data[1]; var point2 = chart.getDatasetMeta(0).data[2]; await jasmine.triggerMouseEvent(chart, 'click', point1); expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 1, element: point1}]); chart.options.events = ['mousemove']; chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', point2); expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 2, element: point2}]); }); it('should activate element on hover when minPadding pixels outside chart area', async function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100], hoverRadius: 0 }], }, options: { scales: { x: {display: false}, y: {display: false} } } }); var point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 1, y: point.y}); expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point}]); }); it('should not activate elements when hover is disabled', async function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { hover: false } }); var point = chart.getDatasetMeta(0).data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(chart.getActiveElements()).toEqual([]); }); it('should not change the active elements when outside chartArea, except for mouseout', async function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100], hoverRadius: 0 }], }, options: { scales: { x: {display: false}, y: {display: false} }, layout: { padding: 5 } } }); var point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', {x: point.x, y: point.y}); expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point}]); await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 1, y: 1}); expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point}]); await jasmine.triggerMouseEvent(chart, 'mouseout', {x: 1, y: 1}); expect(chart.tooltip.getActiveElements()).toEqual([]); }); }); describe('when merging scale options', function() { beforeEach(function() { Chart.helpers.merge(Chart.defaults.scale, { _jasmineCheckA: 'a0', _jasmineCheckB: 'b0', _jasmineCheckC: 'c0' }); Chart.helpers.merge(Chart.defaults.scales.logarithmic, { _jasmineCheckB: 'b1', _jasmineCheckC: 'c1', }); }); afterEach(function() { delete Chart.defaults.scale._jasmineCheckA; delete Chart.defaults.scale._jasmineCheckB; delete Chart.defaults.scale._jasmineCheckC; delete Chart.defaults.scales.logarithmic._jasmineCheckB; delete Chart.defaults.scales.logarithmic._jasmineCheckC; }); it('should default to "category" for x scales and "linear" for y scales', function() { var chart = acquireChart({ type: 'line', options: { scales: { xFoo0: {}, xFoo1: {}, yBar0: {}, yBar1: {}, } } }); expect(chart.scales.xFoo0.type).toBe('category'); expect(chart.scales.xFoo1.type).toBe('category'); expect(chart.scales.yBar0.type).toBe('linear'); expect(chart.scales.yBar1.type).toBe('linear'); }); it('should correctly apply defaults on central scale', function() { var chart = acquireChart({ type: 'line', options: { scales: { foo: { axis: 'x', type: 'logarithmic', _jasmineCheckC: 'c2', _jasmineCheckD: 'd2' } } } }); // let's check a few values from the user options and defaults expect(chart.scales.foo.type).toBe('logarithmic'); expect(chart.scales.foo.options).toEqual(chart.options.scales.foo); expect(chart.scales.foo.options).toEqual( jasmine.objectContaining({ _jasmineCheckA: 'a0', _jasmineCheckB: 'b1', _jasmineCheckC: 'c2', _jasmineCheckD: 'd2' })); }); it('should correctly apply defaults on xy scales', function() { var chart = acquireChart({ type: 'line', options: { scales: { x: { type: 'logarithmic', _jasmineCheckC: 'c2', _jasmineCheckD: 'd2' }, y: { type: 'time', _jasmineCheckC: 'c2', _jasmineCheckE: 'e2' } } } }); expect(chart.scales.x.type).toBe('logarithmic'); expect(chart.scales.x.options).toEqual(chart.options.scales.x); expect(chart.scales.x.options).toEqual( jasmine.objectContaining({ _jasmineCheckA: 'a0', _jasmineCheckB: 'b1', _jasmineCheckC: 'c2', _jasmineCheckD: 'd2' })); expect(chart.scales.y.type).toBe('time'); expect(chart.scales.y.options).toEqual(chart.options.scales.y); expect(chart.scales.y.options).toEqual( jasmine.objectContaining({ _jasmineCheckA: 'a0', _jasmineCheckB: 'b0', _jasmineCheckC: 'c2', _jasmineCheckE: 'e2' })); }); it('should not alter defaults when merging config', function() { var chart = acquireChart({ type: 'line', options: { _jasmineCheck: 42, scales: { x: { type: 'linear', _jasmineCheck: 42, }, y: { type: 'category', _jasmineCheck: 42, } } } }); expect(chart.options._jasmineCheck).toBeDefined(); expect(chart.scales.x.options._jasmineCheck).toBeDefined(); expect(chart.scales.y.options._jasmineCheck).toBeDefined(); expect(Chart.overrides.line._jasmineCheck).not.toBeDefined(); expect(Chart.defaults._jasmineCheck).not.toBeDefined(); expect(Chart.defaults.scales.linear._jasmineCheck).not.toBeDefined(); expect(Chart.defaults.scales.category._jasmineCheck).not.toBeDefined(); }); it('should ignore proxy passed as scale options', function() { let failure = false; const chart = acquireChart({ type: 'line', data: [], options: { scales: { x: { grid: { color: ctx => { if (!ctx.tick) { failure = true; } } } } } } }); chart.options.scales = { x: chart.options.scales.x, y: { type: 'linear', position: 'right' } }; chart.update(); expect(failure).toEqual(false); }); it('should ignore array passed as scale options', function() { const chart = acquireChart({ type: 'line', data: [], options: { scales: { xAxes: [{id: 'xAxes', type: 'category'}] } } }); expect(chart.scales.xAxes).not.toBeDefined(); }); }); describe('Updating options', function() { it('update should result to same set of options as construct', function() { var chart = acquireChart({ type: 'line', data: [], options: { animation: false, locale: 'en-US', responsive: false } }); const options = chart.options; chart.options = { animation: false, locale: 'en-US', responsive: false }; chart.update(); expect(chart.options).toEqualOptions(options); }); }); describe('config.options.responsive: true (maintainAspectRatio: false)', function() { it('should fill parent width and height', function() { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: 'width: 150px; height: 245px' }, wrapper: { style: 'width: 300px; height: 350px' } }); expect(chart).toBeChartOfSize({ dw: 300, dh: 350, rw: 300, rh: 350, }); }); it('should call onResize with correct arguments and context', function() { let count = 0; let correctThis = false; let size = { width: 0, height: 0 }; acquireChart({ options: { responsive: true, maintainAspectRatio: false, onResize(chart, newSize) { count++; correctThis = this === chart; size.width = newSize.width; size.height = newSize.height; } } }, { canvas: { style: 'width: 150px; height: 245px' }, wrapper: { style: 'width: 300px; height: 350px' } }); expect(count).toEqual(1); expect(correctThis).toBeTrue(); expect(size).toEqual({width: 300, height: 350}); }); it('should resize the canvas when parent width changes', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: '' }, wrapper: { style: 'width: 300px; height: 350px; position: relative' } }); expect(chart).toBeChartOfSize({ dw: 300, dh: 350, rw: 300, rh: 350, }); var wrapper = chart.canvas.parentNode; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 455, dh: 350, rw: 455, rh: 350, }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 150, dh: 350, rw: 150, rh: 350, }); done(); }); wrapper.style.width = '150px'; }); wrapper.style.width = '455px'; }); it('should restore the original size when parent became invisible', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: '' }, wrapper: { style: 'width: 300px; height: 350px; position: relative' } }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 300, dh: 350, rw: 300, rh: 350, }); var original = chart.resize; chart.resize = function() { fail('resize should not have been called'); }; var wrapper = chart.canvas.parentNode; wrapper.style.display = 'none'; setTimeout(function() { expect(wrapper.clientWidth).toEqual(0); expect(wrapper.clientHeight).toEqual(0); expect(chart).toBeChartOfSize({ dw: 300, dh: 350, rw: 300, rh: 350, }); chart.resize = original; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 300, dh: 350, rw: 300, rh: 350, }); done(); }); wrapper.style.display = 'block'; }, 200); }); }); it('should resize the canvas when parent is RTL and width changes', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: '' }, wrapper: { style: 'width: 300px; height: 350px; position: relative; direction: rtl' } }); expect(chart).toBeChartOfSize({ dw: 300, dh: 350, rw: 300, rh: 350, }); var wrapper = chart.canvas.parentNode; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 455, dh: 350, rw: 455, rh: 350, }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 150, dh: 350, rw: 150, rh: 350, }); done(); }); wrapper.style.width = '150px'; }); wrapper.style.width = '455px'; }); it('should resize the canvas when parent height changes', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: '' }, wrapper: { style: 'width: 300px; height: 350px; position: relative' } }); expect(chart).toBeChartOfSize({ dw: 300, dh: 350, rw: 300, rh: 350, }); var wrapper = chart.canvas.parentNode; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 300, dh: 455, rw: 300, rh: 455, }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 300, dh: 150, rw: 300, rh: 150, }); done(); }); wrapper.style.height = '150px'; }); wrapper.style.height = '455px'; }); it('should not include parent padding when resizing the canvas', function(done) { var chart = acquireChart({ type: 'line', options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: '' }, wrapper: { style: 'padding: 50px; width: 320px; height: 350px; position: relative' } }); expect(chart).toBeChartOfSize({ dw: 320, dh: 350, rw: 320, rh: 350, }); var wrapper = chart.canvas.parentNode; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 455, dh: 355, rw: 455, rh: 355, }); done(); }); wrapper.style.height = '355px'; wrapper.style.width = '455px'; }); it('should resize the canvas when the canvas display style changes from "none" to "block"', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: 'display: none;' }, wrapper: { style: 'width: 320px; height: 350px' } }); var canvas = chart.canvas; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 320, dh: 350, rw: 320, rh: 350, }); done(); }); canvas.style.display = 'block'; }); it('should resize the canvas when the wrapper display style changes from "none" to "block"', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: '' }, wrapper: { style: 'display: none; width: 460px; height: 380px' } }); var wrapper = chart.canvas.parentNode; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 460, dh: 380, rw: 460, rh: 380, }); done(); }); wrapper.style.display = 'block'; }); it('should resize the canvas when the wrapper has display style changes from "none" to "block"', function(done) { // https://github.com/chartjs/Chart.js/issues/4659 var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: '' }, wrapper: { style: 'display: none; max-width: 600px; max-height: 400px;' } }); var wrapper = chart.canvas.parentNode; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 600, dh: 300, rw: 600, rh: 300, }); done(); }); wrapper.style.display = 'block'; }); // https://github.com/chartjs/Chart.js/issues/5485 it('should resize the canvas when the devicePixelRatio changes', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false, devicePixelRatio: 1 } }, { canvas: { style: '' }, wrapper: { style: 'width: 400px; height: 200px; position: relative' } }); expect(chart).toBeChartOfSize({ dw: 400, dh: 200, rw: 400, rh: 200, }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 400, dh: 200, rw: 800, rh: 400, }); done(); }); chart.options.devicePixelRatio = 2; chart.resize(); }); // https://github.com/chartjs/Chart.js/issues/3790 it('should resize the canvas if attached to the DOM after construction', function(done) { var canvas = document.createElement('canvas'); var wrapper = document.createElement('div'); var body = window.document.body; var chart = new Chart(canvas, { type: 'line', options: { responsive: true, maintainAspectRatio: false } }); expect(chart).toBeChartOfSize({ dw: 0, dh: 0, rw: 0, rh: 0, }); expect(chart.chartArea).toBeUndefined(); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 455, dh: 355, rw: 455, rh: 355, }); expect(chart.chartArea).not.toBeUndefined(); body.removeChild(wrapper); chart.destroy(); done(); }); wrapper.style.cssText = 'width: 455px; height: 355px'; wrapper.appendChild(canvas); body.appendChild(wrapper); }); it('should resize the canvas when attached to a different parent', function(done) { var canvas = document.createElement('canvas'); var wrapper = document.createElement('div'); var body = window.document.body; var chart = new Chart(canvas, { type: 'line', options: { responsive: true, maintainAspectRatio: false } }); expect(chart).toBeChartOfSize({ dw: 0, dh: 0, rw: 0, rh: 0, }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 455, dh: 355, rw: 455, rh: 355, }); var target = document.createElement('div'); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 640, dh: 480, rw: 640, rh: 480, }); body.removeChild(wrapper); body.removeChild(target); chart.destroy(); done(); }); target.style.cssText = 'width: 640px; height: 480px'; target.appendChild(canvas); body.appendChild(target); }); wrapper.style.cssText = 'width: 455px; height: 355px'; wrapper.appendChild(canvas); body.appendChild(wrapper); }); // https://github.com/chartjs/Chart.js/issues/3521 it('should resize the canvas after the wrapper has been re-attached to the DOM', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: '' }, wrapper: { style: 'width: 320px; height: 350px' } }); expect(chart).toBeChartOfSize({ dw: 320, dh: 350, rw: 320, rh: 350, }); var wrapper = chart.canvas.parentNode; var parent = wrapper.parentNode; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 320, dh: 355, rw: 320, rh: 355, }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 455, dh: 355, rw: 455, rh: 355, }); done(); }); parent.removeChild(wrapper); wrapper.style.width = '455px'; parent.appendChild(wrapper); }); parent.removeChild(wrapper); setTimeout(() => { parent.appendChild(wrapper); wrapper.style.height = '355px'; }, 0); }); // https://github.com/chartjs/Chart.js/issues/9875 it('should detect detach/attach in series', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: '' }, wrapper: { style: 'width: 320px; height: 350px' } }); var wrapper = chart.canvas.parentNode; var parent = wrapper.parentNode; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 320, dh: 350, rw: 320, rh: 350, }); done(); }); parent.removeChild(wrapper); parent.appendChild(wrapper); }); it('should detect detach/attach/detach in series', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: '' }, wrapper: { style: 'width: 320px; height: 350px' } }); var wrapper = chart.canvas.parentNode; var parent = wrapper.parentNode; waitForResize(chart, function() { fail(); }); parent.removeChild(wrapper); parent.appendChild(wrapper); parent.removeChild(wrapper); setTimeout(function() { expect(chart.attached).toBeFalse(); done(); }, 100); }); it('should detect attach/detach in series', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: false } }, { canvas: { style: '' }, wrapper: { style: 'width: 320px; height: 350px' } }); var wrapper = chart.canvas.parentNode; var parent = wrapper.parentNode; parent.removeChild(wrapper); setTimeout(function() { expect(chart.attached).toBeFalse(); waitForResize(chart, function() { fail(); }); parent.appendChild(wrapper); parent.removeChild(wrapper); setTimeout(function() { expect(chart.attached).toBeFalse(); done(); }, 100); }, 100); }); // https://github.com/chartjs/Chart.js/issues/4737 it('should resize the canvas when re-creating the chart', function(done) { var chart = acquireChart({ options: { responsive: true } }, { wrapper: { style: 'width: 320px' } }); var wrapper = chart.canvas.parentNode; waitForResize(chart, function() { var canvas = chart.canvas; expect(chart).toBeChartOfSize({ dw: 320, dh: 320, rw: 320, rh: 320, }); chart.destroy(); chart = new Chart(canvas, { type: 'line', options: { responsive: true } }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 455, dh: 455, rw: 455, rh: 455, }); chart.destroy(); window.document.body.removeChild(wrapper); done(); }); canvas.parentNode.style.width = '455px'; canvas.parentNode.style.height = '455px'; }); }); it('should resize the canvas if attached to the DOM after construction with multiple parents', function(done) { var canvas = document.createElement('canvas'); var wrapper = document.createElement('div'); var wrapper2 = document.createElement('div'); var wrapper3 = document.createElement('div'); var body = window.document.body; var chart = new Chart(canvas, { type: 'line', options: { responsive: true, maintainAspectRatio: false } }); expect(chart).toBeChartOfSize({ dw: 0, dh: 0, rw: 0, rh: 0, }); expect(chart.chartArea).toBeUndefined(); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 455, dh: 355, rw: 455, rh: 355, }); expect(chart.chartArea).not.toBeUndefined(); body.removeChild(wrapper3); chart.destroy(); done(); }); wrapper3.appendChild(wrapper2); wrapper2.appendChild(wrapper); wrapper.style.cssText = 'width: 455px; height: 355px'; wrapper.appendChild(canvas); body.appendChild(wrapper3); }); }); describe('config.options.responsive: true (maintainAspectRatio: true)', function() { it('should resize the canvas with correct aspect ratio when parent width changes', function(done) { var chart = acquireChart({ type: 'line', // AR == 2 options: { responsive: true, maintainAspectRatio: true } }, { canvas: { style: '' }, wrapper: { style: 'width: 300px; height: 350px; position: relative' } }); expect(chart).toBeChartOfSize({ dw: 300, dh: 150, rw: 300, rh: 150, }); var wrapper = chart.canvas.parentNode; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 450, dh: 225, rw: 450, rh: 225, }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 150, dh: 75, rw: 150, rh: 75, }); done(); }); wrapper.style.width = '150px'; }); wrapper.style.width = '450px'; }); it('should maintain aspect ratio when parent height changes', function(done) { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: true } }, { canvas: { style: '' }, wrapper: { style: 'width: 320px; height: 350px; position: relative' } }); expect(chart).toBeChartOfSize({ dw: 320, dh: 160, rw: 320, rh: 160, }); var wrapper = chart.canvas.parentNode; waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 320, dh: 160, rw: 320, rh: 160, }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 300, dh: 150, rw: 300, rh: 150, }); done(); }); wrapper.style.height = '150px'; }); wrapper.style.height = '455px'; }); }); describe('Retina scale (a.k.a. device pixel ratio)', function() { beforeEach(function() { this.devicePixelRatio = window.devicePixelRatio; window.devicePixelRatio = 3; }); afterEach(function() { window.devicePixelRatio = this.devicePixelRatio; }); // see https://github.com/chartjs/Chart.js/issues/3575 it ('should scale the render size but not the "implicit" display size', function() { var chart = acquireChart({ options: { responsive: false } }, { canvas: { width: 320, height: 240, } }); expect(chart).toBeChartOfSize({ dw: 320, dh: 240, rw: 960, rh: 720, }); }); it ('should scale the render size but not the "explicit" display size', function() { var chart = acquireChart({ options: { responsive: false } }, { canvas: { style: 'width: 320px; height: 240px' } }); expect(chart).toBeChartOfSize({ dw: 320, dh: 240, rw: 960, rh: 720, }); }); }); describe('config.options.devicePixelRatio', function() { beforeEach(function() { this.devicePixelRatio = window.devicePixelRatio; window.devicePixelRatio = 1; }); afterEach(function() { window.devicePixelRatio = this.devicePixelRatio; }); // see https://github.com/chartjs/Chart.js/issues/3575 it ('should scale the render size but not the "implicit" display size', function() { var chart = acquireChart({ options: { responsive: false, devicePixelRatio: 3 } }, { canvas: { width: 320, height: 240, } }); expect(chart).toBeChartOfSize({ dw: 320, dh: 240, rw: 960, rh: 720, }); }); it ('should scale the render size but not the "explicit" display size', function() { var chart = acquireChart({ options: { responsive: false, devicePixelRatio: 3 } }, { canvas: { style: 'width: 320px; height: 240px' } }); expect(chart).toBeChartOfSize({ dw: 320, dh: 240, rw: 960, rh: 720, }); }); }); describe('config.options.aspectRatio', function() { it('should resize the canvas when the aspectRatio option changes', function(done) { var chart = acquireChart({ options: { responsive: true, aspectRatio: 1, } }, { canvas: { style: '', width: 400, }, }); expect(chart).toBeChartOfSize({ dw: 400, dh: 400, rw: 400, rh: 400, }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 400, dh: 200, rw: 400, rh: 200, }); done(); }); chart.options.aspectRatio = 2; chart.resize(); }); }); describe('controller.reset', function() { it('should reset the chart elements', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 0] }] }, options: { responsive: true } }); var meta = chart.getDatasetMeta(0); // Verify that points are at their initial correct location, // then we will reset and see that they moved expect(meta.data[0].y).toBeCloseToPixel(333); expect(meta.data[1].y).toBeCloseToPixel(183); expect(meta.data[2].y).toBeCloseToPixel(32); expect(meta.data[3].y).toBeCloseToPixel(482); chart.reset(); // For a line chart, the animation state is the bottom expect(meta.data[0].y).toBeCloseToPixel(482); expect(meta.data[1].y).toBeCloseToPixel(482); expect(meta.data[2].y).toBeCloseToPixel(482); expect(meta.data[3].y).toBeCloseToPixel(482); }); }); describe('config update', function() { it ('should update options', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { responsive: true } }); chart.options = { responsive: false, scales: { y: { min: 0, max: 10 } } }; chart.update(); var yScale = chart.scales.y; expect(yScale.options.min).toBe(0); expect(yScale.options.max).toBe(10); }); it ('should update scales options', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { responsive: true } }); chart.options.scales.y.min = 0; chart.options.scales.y.max = 10; chart.update(); var yScale = chart.scales.y; expect(yScale.options.min).toBe(0); expect(yScale.options.max).toBe(10); }); it ('should update scales options from new object', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { responsive: true } }); var newScalesConfig = { y: { min: 0, max: 10 } }; chart.options.scales = newScalesConfig; chart.update(); var yScale = chart.scales.y; expect(yScale.options.min).toBe(0); expect(yScale.options.max).toBe(10); }); it ('should remove discarded scale', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { responsive: true, scales: { yAxis0: { min: 0, max: 10 } } } }); var newScalesConfig = { y: { min: 0, max: 10 } }; chart.options.scales = newScalesConfig; chart.update(); var yScale = chart.scales.yAxis0; expect(yScale).toBeUndefined(); var newyScale = chart.scales.y; expect(newyScale.options.min).toBe(0); expect(newyScale.options.max).toBe(10); }); it ('should update tooltip options', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { responsive: true } }); var newTooltipConfig = { mode: 'dataset', intersect: false }; chart.options.plugins.tooltip = newTooltipConfig; chart.update(); expect(chart.tooltip.options).toEqualOptions(newTooltipConfig); }); it ('should update the tooltip on update', async function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { responsive: true, tooltip: { mode: 'nearest' } } }); // Trigger an event over top of a point to // put an item into the tooltip var meta = chart.getDatasetMeta(0); var point = meta.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); // Check and see if tooltip was displayed var tooltip = chart.tooltip; expect(chart._active[0].element).toEqual(point); expect(tooltip._active[0].element).toEqual(point); // Update and confirm tooltip is updated chart.update(); expect(chart._active[0].element).toEqual(point); expect(tooltip._active[0].element).toEqual(point); }); it ('should update the metadata', function() { var cfg = { data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ type: 'line', data: [10, 20, 30, 0] }] }, options: { responsive: true, scales: { x: { type: 'category' }, y: { type: 'linear', title: { display: true, text: 'Value' } } } } }; var chart = acquireChart(cfg); var meta = chart.getDatasetMeta(0); expect(meta.type).toBe('line'); // change the dataset to bar and check that meta was updated chart.config.data.datasets[0].type = 'bar'; chart.update(); meta = chart.getDatasetMeta(0); expect(meta.type).toBe('bar'); }); }); describe('plugin.extensions', function() { var hooks = { install: ['install'], uninstall: ['uninstall'], init: [ 'beforeInit', 'resize', 'afterInit' ], start: ['start'], stop: ['stop'], update: [ 'beforeUpdate', 'beforeLayout', 'beforeDataLimits', // y-axis fit 'afterDataLimits', 'beforeBuildTicks', 'afterBuildTicks', 'beforeDataLimits', // x-axis fit 'afterDataLimits', 'beforeBuildTicks', 'afterBuildTicks', // 'beforeBuildTicks', // y-axis re-fit // 'afterBuildTicks', 'afterLayout', 'beforeDatasetsUpdate', 'beforeDatasetUpdate', 'afterDatasetUpdate', 'afterDatasetsUpdate', 'afterUpdate', ], render: [ 'beforeRender', 'beforeDraw', 'beforeDatasetsDraw', 'beforeDatasetDraw', 'afterDatasetDraw', 'afterDatasetsDraw', // 'beforeTooltipDraw', // 'afterTooltipDraw', 'afterDraw', 'afterRender', ], resize: [ 'resize' ], destroy: [ 'beforeDestroy', 'afterDestroy' ] }; it ('should notify plugin in correct order', function(done) { var plugin = this.plugin = {}; var sequence = []; Object.keys(hooks).forEach(function(group) { hooks[group].forEach(function(name) { plugin[name] = function() { sequence.push(name); }; }); }); var chart = window.acquireChart({ type: 'line', data: {datasets: [{}]}, plugins: [plugin], options: { responsive: true } }, { wrapper: { style: 'width: 300px' } }); waitForResize(chart, function() { chart.destroy(); expect(sequence).toEqual([].concat( hooks.install, hooks.start, hooks.init, hooks.update, hooks.render, hooks.resize, hooks.update, hooks.render, hooks.destroy, hooks.stop, hooks.uninstall )); done(); }); chart.canvas.parentNode.style.width = '400px'; chart.canvas.parentNode.style.height = '400px'; }); it ('should notify initially disabled plugin in correct order', function() { var plugin = this.plugin = {id: 'plugin'}; var sequence = []; Object.keys(hooks).forEach(function(group) { hooks[group].forEach(function(name) { plugin[name] = function() { sequence.push(name); }; }); }); var chart = window.acquireChart({ type: 'line', data: {datasets: [{}]}, plugins: [plugin], options: { plugins: { plugin: false } } }); expect(sequence).toEqual([].concat( hooks.install )); sequence = []; chart.options.plugins.plugin = true; chart.update(); expect(sequence).toEqual([].concat( hooks.start, hooks.update, hooks.render )); sequence = []; chart.options.plugins.plugin = false; chart.update(); expect(sequence).toEqual(hooks.stop); sequence = []; chart.destroy(); expect(sequence).toEqual(hooks.uninstall); }); it('should not notify before/afterDatasetDraw if dataset is hidden', function() { var sequence = []; var plugin = this.plugin = { beforeDatasetDraw: function(chart, args) { sequence.push('before-' + args.index); }, afterDatasetDraw: function(chart, args) { sequence.push('after-' + args.index); } }; window.acquireChart({ type: 'line', data: {datasets: [{}, {hidden: true}, {}]}, plugins: [plugin] }); expect(sequence).toEqual([ 'before-2', 'after-2', 'before-0', 'after-0' ]); }); it('should not crash when accessing options of a blank inline plugin', function() { var chart = window.acquireChart({ type: 'line', data: {datasets: [{}]}, plugins: [{}], }); function iterateOptions() { for (const plugin of chart._plugins._init) { // triggering bug https://github.com/chartjs/Chart.js/issues/9368 expect(Object.getPrototypeOf(plugin.options)).toBeNull(); } } expect(iterateOptions).not.toThrow(); }); }); describe('metasets', function() { beforeEach(function() { this.chart = acquireChart({ type: 'line', data: { datasets: [ {label: '1', order: 2}, {label: '2', order: 1}, {label: '3', order: 4}, {label: '4', order: 3}, ] } }); }); afterEach(function() { const metasets = this.chart._metasets; expect(metasets.length).toEqual(this.chart.data.datasets.length); for (let i = 0; i < metasets.length; i++) { expect(metasets[i].index).toEqual(i); expect(metasets[i]._dataset).toEqual(this.chart.data.datasets[i]); } }); it('should build metasets array in order', function() { const metasets = this.chart._metasets; expect(metasets[0].order).toEqual(2); expect(metasets[1].order).toEqual(1); expect(metasets[2].order).toEqual(4); expect(metasets[3].order).toEqual(3); }); it('should build sorted metasets array in correct order', function() { const metasets = this.chart._sortedMetasets; expect(metasets[0].order).toEqual(1); expect(metasets[1].order).toEqual(2); expect(metasets[2].order).toEqual(3); expect(metasets[3].order).toEqual(4); }); it('should be moved when datasets are removed from beginning', function() { this.chart.data.datasets.splice(0, 2); this.chart.update(); const metasets = this.chart._metasets; expect(metasets[0].order).toEqual(4); expect(metasets[1].order).toEqual(3); }); it('should be moved when datasets are removed from middle', function() { this.chart.data.datasets.splice(1, 2); this.chart.update(); const metasets = this.chart._metasets; expect(metasets[0].order).toEqual(2); expect(metasets[1].order).toEqual(3); }); it('should be moved when datasets are inserted', function() { this.chart.data.datasets.splice(1, 0, {label: '1.5', order: 5}); this.chart.update(); const metasets = this.chart._metasets; expect(metasets[0].order).toEqual(2); expect(metasets[1].order).toEqual(5); expect(metasets[2].order).toEqual(1); expect(metasets[3].order).toEqual(4); expect(metasets[4].order).toEqual(3); }); it('should be replaced when dataset is replaced', function() { this.chart.data.datasets.splice(1, 1, {label: '1.5', order: 5}); this.chart.update(); const metasets = this.chart._metasets; expect(metasets[0].order).toEqual(2); expect(metasets[1].order).toEqual(5); expect(metasets[2].order).toEqual(4); expect(metasets[3].order).toEqual(3); }); it('should update properly when dataset locations are swapped', function() { const orig = this.chart.data.datasets; this.chart.data.datasets = [orig[0], orig[2], orig[1], orig[3]]; this.chart.update(); let metasets = this.chart._metasets; expect(metasets[0].label).toEqual('1'); expect(metasets[1].label).toEqual('3'); expect(metasets[2].label).toEqual('2'); expect(metasets[3].label).toEqual('4'); this.chart.data.datasets = [{label: 'new', order: 10}, orig[3], orig[2], orig[1], orig[0]]; this.chart.update(); metasets = this.chart._metasets; expect(metasets[0].label).toEqual('new'); expect(metasets[1].label).toEqual('4'); expect(metasets[2].label).toEqual('3'); expect(metasets[3].label).toEqual('2'); expect(metasets[4].label).toEqual('1'); this.chart.data.datasets = [orig[3], orig[2], orig[1], {label: 'new', order: 10}]; this.chart.update(); metasets = this.chart._metasets; expect(metasets[0].label).toEqual('4'); expect(metasets[1].label).toEqual('3'); expect(metasets[2].label).toEqual('2'); expect(metasets[3].label).toEqual('new'); }); }); describe('_destroyDatasetMeta', function() { beforeEach(function() { this.chart = acquireChart({ type: 'line', data: { datasets: [ {label: '1', order: 2}, {label: '2', order: 1}, {label: '3', order: 4}, {label: '4', order: 3}, ] } }); }); it('cleans up metasets when the chart is destroyed', function() { this.chart.destroy(); expect(this.chart._metasets).toEqual([undefined, undefined, undefined, undefined]); }); }); describe('data visibility', function() { it('should hide a dataset', function() { var chart = acquireChart({ type: 'line', data: { datasets: [{ data: [0, 1, 2] }], labels: ['a', 'b', 'c'] } }); chart.setDatasetVisibility(0, false); var meta = chart.getDatasetMeta(0); expect(meta.hidden).toBe(true); }); it('should toggle data visibility by index', function() { var chart = acquireChart({ type: 'pie', data: { datasets: [{ data: [1, 2, 3] }] } }); expect(chart.getDataVisibility(1)).toBe(true); chart.toggleDataVisibility(1); expect(chart.getDataVisibility(1)).toBe(false); chart.update(); expect(chart.getDataVisibility(1)).toBe(false); }); it('should maintain data visibility indices when data changes', function() { var chart = acquireChart({ type: 'pie', data: { labels: ['0', '1', '2', '3'], datasets: [{ data: [0, 1, 2, 3] }, { data: [0, 1, 2, 3] }] } }); chart.toggleDataVisibility(3); chart.data.labels.splice(1, 1); chart.data.datasets[0].data.splice(1, 1); chart.data.datasets[1].data.splice(1, 1); chart.update(); expect(chart.getDataVisibility(0)).toBe(true); expect(chart.getDataVisibility(1)).toBe(true); expect(chart.getDataVisibility(2)).toBe(false); chart.data.labels.unshift('-1', '-2'); chart.data.datasets[0].data.unshift(-1, -2); chart.data.datasets[1].data.unshift(-1, -2); chart.update(); expect(chart.getDataVisibility(0)).toBe(true); expect(chart.getDataVisibility(1)).toBe(true); expect(chart.getDataVisibility(2)).toBe(true); expect(chart.getDataVisibility(3)).toBe(true); expect(chart.getDataVisibility(4)).toBe(false); chart.data.labels.shift(); chart.data.datasets[0].data.shift(); chart.data.datasets[1].data.shift(); chart.update(); expect(chart.getDataVisibility(0)).toBe(true); expect(chart.getDataVisibility(1)).toBe(true); expect(chart.getDataVisibility(2)).toBe(true); expect(chart.getDataVisibility(3)).toBe(false); chart.data.labels.pop(); chart.data.datasets[0].data.pop(); chart.data.datasets[1].data.pop(); chart.update(); expect(chart.getDataVisibility(0)).toBe(true); expect(chart.getDataVisibility(1)).toBe(true); expect(chart.getDataVisibility(2)).toBe(true); expect(chart.getDataVisibility(3)).toBe(true); chart.toggleDataVisibility(1); chart.data.labels.splice(1, 0, 'b'); chart.data.datasets[0].data.splice(1, 0, 1); chart.data.datasets[1].data.splice(1, 0, 1); chart.update(); expect(chart.getDataVisibility(0)).toBe(true); expect(chart.getDataVisibility(1)).toBe(true); expect(chart.getDataVisibility(2)).toBe(false); expect(chart.getDataVisibility(3)).toBe(true); }); it('should leave data visibility indices intact when data changes in non-uniform way', function() { var chart = acquireChart({ type: 'pie', data: { labels: ['0', '1', '2', '3'], datasets: [{ data: [0, 1, 2, 3] }, { data: [0, 1, 2, 3] }] } }); chart.toggleDataVisibility(0); chart.data.labels.push('a'); chart.data.datasets[0].data.pop(); chart.data.datasets[1].data.push(5); chart.update(); expect(chart.getDataVisibility(0)).toBe(false); expect(chart.getDataVisibility(1)).toBe(true); expect(chart.getDataVisibility(2)).toBe(true); expect(chart.getDataVisibility(3)).toBe(true); }); }); describe('isDatasetVisible', function() { it('should return false if index is out of bounds', function() { var chart = acquireChart({ type: 'line', data: { datasets: [{ data: [0, 1, 2] }], labels: ['a', 'b', 'c'] } }); expect(chart.isDatasetVisible(1)).toBe(false); }); }); describe('getChart', function() { it('should get the chart from the canvas ID', function() { var chart = acquireChart({ type: 'pie', data: { datasets: [{ data: [1, 2, 3] }] } }); chart.canvas.id = 'myID'; expect(Chart.getChart('myID')).toBe(chart); }); it('should get the chart from an HTMLCanvasElement', function() { var chart = acquireChart({ type: 'pie', data: { datasets: [{ data: [1, 2, 3] }] } }); expect(Chart.getChart(chart.canvas)).toBe(chart); }); it('should get the chart from an CanvasRenderingContext2D', function() { var chart = acquireChart({ type: 'pie', data: { datasets: [{ data: [1, 2, 3] }] } }); expect(Chart.getChart(chart.ctx)).toBe(chart); }); it('should return undefined when a chart is not found or bad data is provided', function() { expect(Chart.getChart(1)).toBeUndefined(); }); }); describe('active elements', function() { it('should set the active elements', function() { var chart = acquireChart({ type: 'pie', data: { datasets: [{ data: [1, 2, 3], borderColor: 'red', hoverBorderColor: 'blue', }] } }); const meta = chart.getDatasetMeta(0); let props = meta.data[0].getProps(['borderColor']); expect(props.options.borderColor).toEqual('red'); chart.setActiveElements([{ datasetIndex: 0, index: 0, }]); props = meta.data[0].getProps(['borderColor']); expect(props.options.borderColor).toEqual('blue'); const active = chart.getActiveElements(); expect(active.length).toEqual(1); expect(active[0].element).toBe(meta.data[0]); }); }); it('should not replace the user set active elements by event replay', async function() { var chart = acquireChart({ type: 'line', data: { labels: [1, 2, 3], datasets: [{ data: [1, 2, 3], borderColor: 'red', hoverBorderColor: 'blue', }] } }); const meta = chart.getDatasetMeta(0); const point0 = meta.data[0]; const point1 = meta.data[1]; let props = meta.data[0].getProps(['borderColor']); expect(props.options.borderColor).toEqual('red'); await jasmine.triggerMouseEvent(chart, 'mousemove', {x: point0.x, y: point0.y}); expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point0}]); expect(point0.options.borderColor).toEqual('blue'); expect(point1.options.borderColor).toEqual('red'); chart.setActiveElements([{datasetIndex: 0, index: 1}]); expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 1, element: point1}]); expect(point0.options.borderColor).toEqual('red'); expect(point1.options.borderColor).toEqual('blue'); chart.update(); expect(chart.getActiveElements()).toEqual([{datasetIndex: 0, index: 1, element: point1}]); expect(point0.options.borderColor).toEqual('red'); expect(point1.options.borderColor).toEqual('blue'); }); describe('platform', function() { it('should use the platform constructor provided in config', function() { const chart = acquireChart({ platform: Chart.platforms.BasicPlatform, type: 'line', }); expect(chart.platform).toBeInstanceOf(Chart.platforms.BasicPlatform); }); }); }); ================================================ FILE: test/specs/core.datasetController.tests.js ================================================ describe('Chart.DatasetController', function() { describe('auto', jasmine.fixture.specs('core.datasetController')); it('should listen for dataset data insertions or removals', function() { var data = [0, 1, 2, 3, 4, 5]; var chart = acquireChart({ type: 'line', data: { datasets: [{ data: data }] } }); var controller = chart.getDatasetMeta(0).controller; var methods = [ '_onDataPush', '_onDataPop', '_onDataShift', '_onDataSplice', '_onDataUnshift' ]; methods.forEach(function(method) { spyOn(controller, method); }); data.push(6, 7, 8); data.push(9); data.pop(); data.shift(); data.shift(); data.shift(); data.splice(1, 4, 10, 11); data.unshift(12, 13, 14, 15); data.unshift(16, 17); [2, 1, 3, 1, 2].forEach(function(expected, index) { expect(controller[methods[index]].calls.count()).toBe(expected); }); }); it('should not try to delete non existent stacks', function() { function createAndUpdateChart() { var chart = acquireChart({ data: { labels: ['q'], datasets: [ { id: 'dismissed', label: 'Test before', yAxisID: 'count', data: [816], type: 'bar', stack: 'stack' } ] }, options: { scales: { count: { axis: 'y', type: 'linear' } } } }); chart.data = { datasets: [ { id: 'tests', yAxisID: 'count', label: 'Test after', data: [38300], type: 'bar' } ], labels: ['q'] }; chart.update(); } expect(createAndUpdateChart).not.toThrow(); }); describe('inextensible data', function() { it('should handle a frozen data object', function() { function createChart() { var data = Object.freeze([0, 1, 2, 3, 4, 5]); expect(Object.isExtensible(data)).toBeFalsy(); var chart = acquireChart({ type: 'line', data: { datasets: [{ data: data }] } }); var dataset = chart.data.datasets[0]; dataset.data = Object.freeze([5, 4, 3, 2, 1, 0]); expect(Object.isExtensible(dataset.data)).toBeFalsy(); chart.update(); // Tests that the unlisten path also works for frozen objects chart.destroy(); } expect(createChart).not.toThrow(); }); it('should handle a sealed data object', function() { function createChart() { var data = Object.seal([0, 1, 2, 3, 4, 5]); expect(Object.isExtensible(data)).toBeFalsy(); var chart = acquireChart({ type: 'line', data: { datasets: [{ data: data }] } }); var dataset = chart.data.datasets[0]; dataset.data = Object.seal([5, 4, 3, 2, 1, 0]); expect(Object.isExtensible(dataset.data)).toBeFalsy(); chart.update(); // Tests that the unlisten path also works for frozen objects chart.destroy(); } expect(createChart).not.toThrow(); }); it('should handle an unextendable data object', function() { function createChart() { var data = Object.preventExtensions([0, 1, 2, 3, 4, 5]); expect(Object.isExtensible(data)).toBeFalsy(); var chart = acquireChart({ type: 'line', data: { datasets: [{ data: data }] } }); var dataset = chart.data.datasets[0]; dataset.data = Object.preventExtensions([5, 4, 3, 2, 1, 0]); expect(Object.isExtensible(dataset.data)).toBeFalsy(); chart.update(); // Tests that the unlisten path also works for frozen objects chart.destroy(); } expect(createChart).not.toThrow(); }); }); it('should parse data using correct scales', function() { const data1 = [0, 1, 2, 3, 4, 5]; const data2 = ['a', 'b', 'c', 'd', 'a']; const chart = acquireChart({ type: 'line', data: { datasets: [ {data: data1}, {data: data2, xAxisID: 'x2', yAxisID: 'y2'} ] }, options: { scales: { x: { type: 'category', labels: ['one', 'two', 'three', 'four', 'five', 'six'] }, x2: { type: 'logarithmic', labels: ['1', '10', '100', '1000', '2000'] }, y: { type: 'linear' }, y2: { type: 'category', labels: ['a', 'b', 'c', 'd', 'e'] } } } }); const meta1 = chart.getDatasetMeta(0); const parsedXValues1 = meta1._parsed.map(p => p.x); const parsedYValues1 = meta1._parsed.map(p => p.y); expect(meta1.data.length).toBe(6); expect(parsedXValues1).toEqual([0, 1, 2, 3, 4, 5]); // label indices expect(parsedYValues1).toEqual(data1); const meta2 = chart.getDatasetMeta(1); const parsedXValues2 = meta2._parsed.map(p => p.x); const parsedYValues2 = meta2._parsed.map(p => p.y); expect(meta2.data.length).toBe(5); expect(parsedXValues2).toEqual([1, 10, 100, 1000, 2000]); // logarithmic scale labels expect(parsedYValues2).toEqual([0, 1, 2, 3, 0]); // label indices }); it('should parse using provided keys', function() { const chart = acquireChart({ type: 'line', data: { datasets: [{ data: [ {x: 1, data: {key: 'one', value: 20}}, {data: {key: 'two', value: 30}} ] }] }, options: { parsing: { xAxisKey: 'data.key', yAxisKey: 'data.value' }, scales: { x: { type: 'category', labels: ['one', 'two'] }, y: { type: 'linear' }, } } }); const meta = chart.getDatasetMeta(0); const parsedXValues = meta._parsed.map(p => p.x); const parsedYValues = meta._parsed.map(p => p.y); expect(meta.data.length).toBe(2); expect(parsedXValues).toEqual([0, 1]); // label indices expect(parsedYValues).toEqual([20, 30]); }); describe('labels array synchronization', function() { const data1 = [ {x: 'One', name: 'One', y: 1, value: 1}, {x: 'Two', name: 'Two', y: 2, value: 2} ]; const data2 = [ {x: 'Three', name: 'Three', y: 3, value: 3}, {x: 'Four', name: 'Four', y: 4, value: 4}, {x: 'Five', name: 'Five', y: 5, value: 5} ]; [ true, false, { xAxisKey: 'name', yAxisKey: 'value' } ].forEach(function(parsing) { describe('when parsing is ' + JSON.stringify(parsing), function() { it('should remove old labels when data is updated', function() { const chart = acquireChart({ type: 'line', data: { datasets: [{ data: data1 }] }, options: { parsing } }); chart.data.datasets[0].data = data2; chart.update(); const meta = chart.getDatasetMeta(0); const labels = meta.iScale.getLabels(); expect(labels).toEqual(data2.map(n => n.x)); }); it('should not remove any user added labels', function() { const chart = acquireChart({ type: 'line', data: { datasets: [{ data: data1 }] }, options: { parsing } }); chart.data.labels.push('user-added'); chart.data.datasets[0].data = []; chart.update(); const meta = chart.getDatasetMeta(0); const labels = meta.iScale.getLabels(); expect(labels).toEqual(['user-added']); }); it('should not remove any user defined labels', function() { const chart = acquireChart({ type: 'line', data: { datasets: [{ data: data1 }], labels: ['user1', 'user2'] }, options: { parsing } }); const meta = chart.getDatasetMeta(0); expect(meta.iScale.getLabels()).toEqual(['user1', 'user2'].concat(data1.map(n => n.x))); chart.data.datasets[0].data = data2; chart.update(); expect(meta.iScale.getLabels()).toEqual(['user1', 'user2'].concat(data2.map(n => n.x))); }); it('should keep up with multiple datasets', function() { const chart = acquireChart({ type: 'line', data: { datasets: [{ data: data1 }, { data: data2 }], labels: ['One', 'Three'] }, options: { parsing } }); const scale = chart.scales.x; expect(scale.getLabels()).toEqual(['One', 'Three', 'Two', 'Four', 'Five']); chart.data.datasets[0].data = data2; chart.data.datasets[1].data = data1; chart.update(); expect(scale.getLabels()).toEqual(['One', 'Three', 'Four', 'Five', 'Two']); }); }); }); }); it('should synchronize metadata when data are inserted or removed and parsing is on', function() { const data = [0, 1, 2, 3, 4, 5]; const chart = acquireChart({ type: 'line', data: { datasets: [{ data: data }] } }); const meta = chart.getDatasetMeta(0); const parsedYValues = () => meta._parsed.map(p => p.y); let first, second, last; first = meta.data[0]; last = meta.data[5]; data.push(6, 7, 8); data.push(9); chart.update(); expect(meta.data.length).toBe(10); expect(meta.data[0]).toBe(first); expect(meta.data[5]).toBe(last); expect(parsedYValues()).toEqual(data); last = meta.data[9]; data.pop(); chart.update(); expect(meta.data.length).toBe(9); expect(meta.data[0]).toBe(first); expect(meta.data.indexOf(last)).toBe(-1); expect(parsedYValues()).toEqual(data); last = meta.data[8]; data.shift(); data.shift(); data.shift(); chart.update(); expect(meta.data.length).toBe(6); expect(meta.data.indexOf(first)).toBe(-1); expect(meta.data[5]).toBe(last); expect(parsedYValues()).toEqual(data); first = meta.data[0]; second = meta.data[1]; last = meta.data[5]; data.splice(1, 4, 10, 11); chart.update(); expect(meta.data.length).toBe(4); expect(meta.data[0]).toBe(first); expect(meta.data[3]).toBe(last); expect(meta.data.indexOf(second)).toBe(-1); expect(parsedYValues()).toEqual(data); data.unshift(12, 13, 14, 15); data.unshift(16, 17); chart.update(); expect(meta.data.length).toBe(10); expect(meta.data[6]).toBe(first); expect(meta.data[9]).toBe(last); expect(parsedYValues()).toEqual(data); }); it('should synchronize metadata when data are inserted or removed and parsing is off', function() { var data = [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}, {x: 4, y: 4}, {x: 5, y: 5}]; var chart = acquireChart({ type: 'line', data: { datasets: [{ data: data }] }, options: { parsing: false, scales: { x: {type: 'linear'}, y: {type: 'linear'} } } }); var meta = chart.getDatasetMeta(0); var controller = meta.controller; var first, last; first = controller.getParsed(0); last = controller.getParsed(5); data.push({x: 6, y: 6}, {x: 7, y: 7}, {x: 8, y: 8}); data.push({x: 9, y: 9}); chart.update(); expect(meta.data.length).toBe(10); expect(controller.getParsed(0)).toBe(first); expect(controller.getParsed(5)).toBe(last); last = controller.getParsed(9); data.pop(); chart.update(); expect(meta.data.length).toBe(9); expect(controller.getParsed(0)).toBe(first); expect(controller.getParsed(9)).toBe(undefined); expect(controller.getParsed(8)).toEqual({x: 8, y: 8}); last = controller.getParsed(8); data.shift(); data.shift(); data.shift(); chart.update(); expect(meta.data.length).toBe(6); expect(controller.getParsed(5)).toBe(last); first = controller.getParsed(0); last = controller.getParsed(5); data.splice(1, 4, {x: 10, y: 10}, {x: 11, y: 11}); chart.update(); expect(meta.data.length).toBe(4); expect(controller.getParsed(0)).toBe(first); expect(controller.getParsed(3)).toBe(last); expect(controller.getParsed(1)).toEqual({x: 10, y: 10}); data.unshift({x: 12, y: 12}, {x: 13, y: 13}, {x: 14, y: 14}, {x: 15, y: 15}); data.unshift({x: 16, y: 16}, {x: 17, y: 17}); chart.update(); expect(meta.data.length).toBe(10); expect(controller.getParsed(6)).toBe(first); expect(controller.getParsed(9)).toBe(last); }); it('should synchronize insert before removal when parsing is off', function() { // https://github.com/chartjs/Chart.js/issues/9511 const data = [{x: 0, y: 1}, {x: 2, y: 7}, {x: 3, y: 5}]; var chart = acquireChart({ type: 'scatter', data: { datasets: [{ data: data, }], }, options: { parsing: false, scales: { x: { type: 'linear', min: 0, max: 10, }, y: { type: 'linear', min: 0, max: 10, }, }, }, }); var meta = chart.getDatasetMeta(0); var controller = meta.controller; data.push({ x: 10, y: 6 }); data.splice(0, 1); chart.update(); expect(meta.data.length).toBe(3); expect(controller.getParsed(0)).toBe(data[0]); expect(controller.getParsed(2)).toBe(data[2]); }); it('should re-synchronize metadata when the data object reference changes', function() { var data0 = [0, 1, 2, 3, 4, 5]; var data1 = [6, 7, 8]; var data2 = [1, 2, 3, 4, 5, 6, 7, 8]; var chart = acquireChart({ type: 'line', data: { datasets: [{ data: data0 }] } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(6); expect(meta._parsed.map(p => p.y)).toEqual(data0); const point0 = meta.data[0]; chart.data.datasets[0].data = data1; chart.update(); expect(meta.data.length).toBe(3); expect(meta._parsed.map(p => p.y)).toEqual(data1); expect(meta.data[0]).toEqual(point0); data1.push(9); chart.update(); expect(meta.data.length).toBe(4); chart.data.datasets[0].data = data0; chart.update(); expect(meta.data.length).toBe(6); expect(meta._parsed.map(p => p.y)).toEqual(data0); chart.data.datasets[0].data = data2; chart.update(); expect(meta.data.length).toBe(8); expect(meta._parsed.map(p => p.y)).toEqual(data2); }); it('should re-synchronize metadata when the data object reference changes, with animation', function() { var data0 = [0, 1, 2, 3, 4, 5]; var data1 = [6, 7, 8]; var data2 = [1, 2, 3, 4, 5, 6, 7, 8]; var chart = acquireChart({ type: 'line', data: { datasets: [{ data: data0 }] }, options: { animation: true } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(6); expect(meta._parsed.map(p => p.y)).toEqual(data0); const point0 = meta.data[0]; chart.data.datasets[0].data = data1; chart.update(); expect(meta.data.length).toBe(3); expect(meta._parsed.map(p => p.y)).toEqual(data1); expect(meta.data[0]).toEqual(point0); data1.push(9); chart.update(); expect(meta.data.length).toBe(4); chart.data.datasets[0].data = data0; chart.update(); expect(meta.data.length).toBe(6); expect(meta._parsed.map(p => p.y)).toEqual(data0); chart.data.datasets[0].data = data2; chart.update(); expect(meta.data.length).toBe(8); expect(meta._parsed.map(p => p.y)).toEqual(data2); }); it('should re-synchronize metadata when data are unusually altered', function() { var data = [0, 1, 2, 3, 4, 5]; var chart = acquireChart({ type: 'line', data: { datasets: [{ data: data }] } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(6); data.length = 2; chart.update(); expect(meta.data.length).toBe(2); data.length = 42; chart.update(); expect(meta.data.length).toBe(42); }); // https://github.com/chartjs/Chart.js/issues/7243 it('should re-synchronize metadata when data is moved and values are equal', function() { var data = [10, 10, 10, 10, 10, 10]; var chart = acquireChart({ type: 'line', data: { labels: ['a', 'b', 'c', 'd', 'e', 'f'], datasets: [{ data, fill: true }] } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(6); const firstX = meta.data[0].x; data.push(data.shift()); chart.update(); expect(meta.data.length).toBe(6); expect(meta.data[0].x).toEqual(firstX); }); // https://github.com/chartjs/Chart.js/issues/7445 it('should re-synchronize metadata when data is objects and directly altered', function() { var data = [{x: 'a', y: 1}, {x: 'b', y: 2}, {x: 'c', y: 3}]; var chart = acquireChart({ type: 'line', data: { labels: ['a', 'b', 'c'], datasets: [{ data, fill: true }] } }); var meta = chart.getDatasetMeta(0); expect(meta.data.length).toBe(3); const y3 = meta.data[2].y; data[0].y = 3; chart.update(); expect(meta.data[0].y).toEqual(y3); }); it('should re-synchronize metadata when scaleID changes', function() { var chart = acquireChart({ type: 'line', data: { datasets: [{ data: [], xAxisID: 'firstXScaleID', yAxisID: 'firstYScaleID', }] }, options: { scales: { firstXScaleID: { type: 'category', position: 'bottom' }, secondXScaleID: { type: 'category', position: 'bottom' }, firstYScaleID: { type: 'linear', position: 'left' }, secondYScaleID: { type: 'linear', position: 'left' }, } } }); var meta = chart.getDatasetMeta(0); expect(meta.xAxisID).toBe('firstXScaleID'); expect(meta.yAxisID).toBe('firstYScaleID'); chart.data.datasets[0].xAxisID = 'secondXScaleID'; chart.data.datasets[0].yAxisID = 'secondYScaleID'; chart.update(); expect(meta.xAxisID).toBe('secondXScaleID'); expect(meta.yAxisID).toBe('secondYScaleID'); }); it('should re-synchronize stacks when stack is changed', function() { var chart = acquireChart({ type: 'bar', data: { labels: ['a', 'b'], datasets: [{ data: [1, 10], stack: '1' }, { data: [2, 20], stack: '2' }, { data: [3, 30], stack: '1' }] } }); expect(chart._stacks).toEqual({ 'x.y.1': { 0: {0: 1, 2: 3, _top: 2, _bottom: null, _visualValues: {0: 1, 2: 3}}, 1: {0: 10, 2: 30, _top: 2, _bottom: null, _visualValues: {0: 10, 2: 30}} }, 'x.y.2': { 0: {1: 2, _top: 1, _bottom: null, _visualValues: {1: 2}}, 1: {1: 20, _top: 1, _bottom: null, _visualValues: {1: 20}} } }); chart.data.datasets[2].stack = '2'; chart.update(); expect(chart._stacks).toEqual({ 'x.y.1': { 0: {0: 1, _top: 2, _bottom: null, _visualValues: {0: 1}}, 1: {0: 10, _top: 2, _bottom: null, _visualValues: {0: 10}} }, 'x.y.2': { 0: {1: 2, 2: 3, _top: 2, _bottom: null, _visualValues: {1: 2, 2: 3}}, 1: {1: 20, 2: 30, _top: 2, _bottom: null, _visualValues: {1: 20, 2: 30}} } }); }); it('should re-synchronize stacks when data is removed', function() { var chart = acquireChart({ type: 'bar', data: { labels: ['a', 'b'], datasets: [{ data: [1, 10], stack: '1' }, { data: [2, 20], stack: '2' }, { data: [3, 30], stack: '1' }] } }); expect(chart._stacks).toEqual({ 'x.y.1': { 0: {0: 1, 2: 3, _top: 2, _bottom: null, _visualValues: {0: 1, 2: 3}}, 1: {0: 10, 2: 30, _top: 2, _bottom: null, _visualValues: {0: 10, 2: 30}} }, 'x.y.2': { 0: {1: 2, _top: 1, _bottom: null, _visualValues: {1: 2}}, 1: {1: 20, _top: 1, _bottom: null, _visualValues: {1: 20}} } }); chart.data.datasets[2].data = [4]; chart.update(); expect(chart._stacks).toEqual({ 'x.y.1': { 0: {0: 1, 2: 4, _top: 2, _bottom: null, _visualValues: {0: 1, 2: 4}}, 1: {0: 10, _top: 2, _bottom: null, _visualValues: {0: 10}} }, 'x.y.2': { 0: {1: 2, _top: 1, _bottom: null, _visualValues: {1: 2}}, 1: {1: 20, _top: 1, _bottom: null, _visualValues: {1: 20}} } }); }); it('should cleanup attached properties when the reference changes or when the chart is destroyed', function() { var data0 = [0, 1, 2, 3, 4, 5]; var data1 = [6, 7, 8]; var chart = acquireChart({ type: 'line', data: { datasets: [{ data: data0 }] } }); var hooks = ['push', 'pop', 'shift', 'splice', 'unshift']; expect(data0._chartjs).toBeDefined(); hooks.forEach(function(hook) { expect(data0[hook]).not.toBe(Array.prototype[hook]); }); expect(data1._chartjs).not.toBeDefined(); hooks.forEach(function(hook) { expect(data1[hook]).toBe(Array.prototype[hook]); }); chart.data.datasets[0].data = data1; chart.update(); expect(data0._chartjs).not.toBeDefined(); hooks.forEach(function(hook) { expect(data0[hook]).toBe(Array.prototype[hook]); }); expect(data1._chartjs).toBeDefined(); hooks.forEach(function(hook) { expect(data1[hook]).not.toBe(Array.prototype[hook]); }); chart.destroy(); expect(data1._chartjs).not.toBeDefined(); hooks.forEach(function(hook) { expect(data1[hook]).toBe(Array.prototype[hook]); }); }); it('should resolve data element options to the default color', function() { var data0 = [0, 1, 2, 3, 4, 5]; var oldColor = Chart.defaults.borderColor; Chart.defaults.borderColor = 'red'; var chart = acquireChart({ type: 'line', data: { datasets: [{ data: data0 }] } }); var meta = chart.getDatasetMeta(0); expect(meta.dataset.options.borderColor).toBe('red'); expect(meta.data[0].options.borderColor).toBe('red'); // Reset old shared state Chart.defaults.borderColor = oldColor; }); it('should read parsing from options when default is false', function() { const originalDefault = Chart.defaults.parsing; Chart.defaults.parsing = false; var chart = acquireChart({ type: 'line', data: { datasets: [{ data: [{t: 1, y: 0}] }] }, options: { parsing: { xAxisKey: 't' } } }); var meta = chart.getDatasetMeta(0); expect(meta.data[0].x).not.toBeNaN(); // Reset old shared state Chart.defaults.parsing = originalDefault; }); it('should not fail to produce stacks when parsing is off', function() { var chart = acquireChart({ type: 'line', data: { datasets: [{ data: [{x: 1, y: 10}] }, { data: [{x: 1, y: 20}] }] }, options: { parsing: false, scales: { x: {stacked: true}, y: {stacked: true} } } }); var meta = chart.getDatasetMeta(0); expect(meta._parsed[0]._stacks).toEqual(jasmine.objectContaining({y: {0: 10, 1: 20, _top: 1, _bottom: null, _visualValues: {0: 10, 1: 20}}})); }); describe('resolveDataElementOptions', function() { it('should cache options when possible', function() { const chart = acquireChart({ type: 'line', data: { datasets: [{ data: [1, 2, 3], }] }, }); const controller = chart.getDatasetMeta(0).controller; expect(controller.enableOptionSharing).toBeTrue(); const opts0 = controller.resolveDataElementOptions(0); const opts1 = controller.resolveDataElementOptions(1); expect(opts0 === opts1).toBeTrue(); expect(opts0.$shared).toBeTrue(); expect(Object.isFrozen(opts0)).toBeTrue(); }); it('should not cache options when option sharing is disabled', function() { const chart = acquireChart({ type: 'radar', data: { datasets: [{ data: [1, 2, 3], }] }, }); const controller = chart.getDatasetMeta(0).controller; expect(controller.enableOptionSharing).toBeFalse(); const opts0 = controller.resolveDataElementOptions(0); const opts1 = controller.resolveDataElementOptions(1); expect(opts0 === opts1).toBeFalse(); expect(opts0.$shared).not.toBeTrue(); expect(Object.isFrozen(opts0)).toBeFalse(); }); it('should not cache options when functions are used', function() { const chart = acquireChart({ type: 'line', data: { datasets: [{ data: [1, 2, 3], backgroundColor: () => 'red' }] }, }); const controller = chart.getDatasetMeta(0).controller; const opts0 = controller.resolveDataElementOptions(0); const opts1 = controller.resolveDataElementOptions(1); expect(opts0 === opts1).toBeFalse(); expect(opts0.$shared).not.toBeTrue(); expect(Object.isFrozen(opts0)).toBeFalse(); }); it('should support nested scriptable options', function() { const chart = acquireChart({ type: 'line', data: { datasets: [{ data: [100, 120, 130], fill: { value: (ctx) => ctx.type === 'dataset' ? 75 : 0 } }] }, }); const controller = chart.getDatasetMeta(0).controller; const opts = controller.resolveDatasetElementOptions(); expect(opts).toEqualOptions({ fill: { value: 75 } }); }); it('should support nested scriptable defaults', function() { Chart.defaults.datasets.line.fill = { value: (ctx) => ctx.type === 'dataset' ? 75 : 0 }; const chart = acquireChart({ type: 'line', data: { datasets: [{ data: [100, 120, 130], }] }, }); const controller = chart.getDatasetMeta(0).controller; const opts = controller.resolveDatasetElementOptions(); expect(opts).toEqualOptions({ fill: { value: 75 } }); delete Chart.defaults.datasets.line.fill; }); }); describe('_resolveAnimations', function() { function animationsExpectations(anims, props) { for (const [prop, opts] of Object.entries(props)) { const anim = anims._properties.get(prop); expect(anim).withContext(prop).toBeInstanceOf(Object); if (anim) { for (const [name, value] of Object.entries(opts)) { expect(anim[name]).withContext('"' + name + '" of ' + JSON.stringify(anim)).toEqual(value); } } } } it('should resolve to empty Animations when globally disabled', function() { const chart = acquireChart({ type: 'line', data: { datasets: [{ data: [1], animation: { test: {duration: 10} } }] }, options: { animation: false } }); const controller = chart.getDatasetMeta(0).controller; expect(controller._resolveAnimations(0)._properties.size).toEqual(0); }); it('should resolve to empty Animations when disabled at dataset level', function() { const chart = acquireChart({ type: 'line', data: { datasets: [{ data: [1], animation: false }] } }); const controller = chart.getDatasetMeta(0).controller; expect(controller._resolveAnimations(0)._properties.size).toEqual(0); }); it('should fallback properly', function() { const chart = acquireChart({ type: 'line', data: { datasets: [{ data: [1], animation: { duration: 200 } }, { type: 'bar', data: [2] }] }, options: { animation: { delay: 100 }, animations: { x: { delay: 200 } }, transitions: { show: { x: { delay: 300 } } }, datasets: { bar: { animation: { duration: 500 } } } } }); const controller = chart.getDatasetMeta(0).controller; expect(Chart.defaults.animation.duration).toEqual(1000); const def0 = controller._resolveAnimations(0, 'default', false); animationsExpectations(def0, { x: { delay: 200, duration: 200 }, y: { delay: 100, duration: 200 } }); const controller2 = chart.getDatasetMeta(1).controller; const def1 = controller2._resolveAnimations(0, 'default', false); animationsExpectations(def1, { x: { delay: 200, duration: 500 } }); }); }); describe('getContext', function() { it('should reflect updated data', function() { var chart = acquireChart({ type: 'scatter', data: { datasets: [{ data: [{x: 1, y: 0}, {x: 2, y: '1'}] }] }, }); let meta = chart.getDatasetMeta(0); expect(meta.controller.getContext(undefined, true, 'test')).toEqual(jasmine.objectContaining({ active: true, datasetIndex: 0, dataset: chart.data.datasets[0], index: 0, mode: 'test' })); expect(meta.controller.getContext(1, false, 'datatest')).toEqual(jasmine.objectContaining({ active: false, datasetIndex: 0, dataset: chart.data.datasets[0], dataIndex: 1, element: meta.data[1], index: 1, parsed: {x: 2, y: 1}, raw: {x: 2, y: '1'}, mode: 'datatest' })); chart.data.datasets[0].data[1].y = 5; chart.update(); expect(meta.controller.getContext(1, false, 'datatest')).toEqual(jasmine.objectContaining({ active: false, datasetIndex: 0, dataset: chart.data.datasets[0], dataIndex: 1, element: meta.data[1], index: 1, parsed: {x: 2, y: 5}, raw: {x: 2, y: 5}, mode: 'datatest' })); chart.data.datasets = [{ data: [{x: 0, y: 0}, {x: 1, y: 1}] }]; chart.update(); // meta is re-created when dataset is replaced meta = chart.getDatasetMeta(0); expect(meta.controller.getContext(undefined, false, 'test2')).toEqual(jasmine.objectContaining({ active: false, datasetIndex: 0, dataset: chart.data.datasets[0], index: 0, mode: 'test2' })); expect(meta.controller.getContext(1, true, 'datatest2')).toEqual(jasmine.objectContaining({ active: true, datasetIndex: 0, dataset: chart.data.datasets[0], dataIndex: 1, element: meta.data[1], index: 1, parsed: {x: 1, y: 1}, raw: {x: 1, y: 1}, mode: 'datatest2' })); chart.data.datasets[0].data.unshift({x: -1, y: -1}); chart.update(); expect(meta.controller.getContext(0, true, 'unshift')).toEqual(jasmine.objectContaining({ active: true, datasetIndex: 0, dataset: chart.data.datasets[0], dataIndex: 0, element: meta.data[0], index: 0, parsed: {x: -1, y: -1}, raw: {x: -1, y: -1}, mode: 'unshift' })); expect(meta.controller.getContext(2, true, 'unshift2')).toEqual(jasmine.objectContaining({ active: true, datasetIndex: 0, dataset: chart.data.datasets[0], dataIndex: 2, element: meta.data[2], index: 2, parsed: {x: 1, y: 1}, raw: {x: 1, y: 1}, mode: 'unshift2' })); chart.data.datasets.unshift({data: [{x: 10, y: 20}]}); chart.update(); meta = chart.getDatasetMeta(0); expect(meta.controller.getContext(0, true, 'unshift3')).toEqual(jasmine.objectContaining({ active: true, datasetIndex: 0, dataset: chart.data.datasets[0], dataIndex: 0, element: meta.data[0], index: 0, parsed: {x: 10, y: 20}, raw: {x: 10, y: 20}, mode: 'unshift3' })); meta = chart.getDatasetMeta(1); expect(meta.controller.getContext(2, true, 'unshift4')).toEqual(jasmine.objectContaining({ active: true, datasetIndex: 1, dataset: chart.data.datasets[1], dataIndex: 2, element: meta.data[2], index: 2, parsed: {x: 1, y: 1}, raw: {x: 1, y: 1}, mode: 'unshift4' })); }); }); }); ================================================ FILE: test/specs/core.defaults.tests.js ================================================ describe('Chart.defaults', function() { describe('.set', function() { it('Should set defaults directly to root when scope is not provided', function() { expect(Chart.defaults.test).toBeUndefined(); Chart.defaults.set({test: true}); expect(Chart.defaults.test).toEqual(true); delete Chart.defaults.test; }); it('Should create scope when it does not exist', function() { expect(Chart.defaults.test).toBeUndefined(); Chart.defaults.set('test', {value: true}); expect(Chart.defaults.test.value).toEqual(true); delete Chart.defaults.test; }); }); describe('.route', function() { it('Should read the source, but not change it', function() { expect(Chart.defaults.testscope).toBeUndefined(); Chart.defaults.set('testscope', {test: true}); Chart.defaults.route('testscope', 'test2', 'testscope', 'test'); expect(Chart.defaults.testscope.test).toEqual(true); expect(Chart.defaults.testscope.test2).toEqual(true); Chart.defaults.set('testscope', {test2: false}); expect(Chart.defaults.testscope.test).toEqual(true); expect(Chart.defaults.testscope.test2).toEqual(false); Chart.defaults.set('testscope', {test2: undefined}); expect(Chart.defaults.testscope.test2).toEqual(true); delete Chart.defaults.testscope; }); }); }); ================================================ FILE: test/specs/core.element.tests.js ================================================ describe('Chart.element', function() { describe('getProps', function() { it('should return requested properties', function() { const elem = new Chart.Element(); elem.x = 10; elem.y = 1.5; expect(elem.getProps(['x', 'y'])).toEqual(jasmine.objectContaining({x: 10, y: 1.5})); expect(elem.getProps(['x', 'y'], true)).toEqual(jasmine.objectContaining({x: 10, y: 1.5})); elem.$animations = {x: {active: () => true, _to: 20}}; expect(elem.getProps(['x', 'y'])).toEqual(jasmine.objectContaining({x: 10, y: 1.5})); expect(elem.getProps(['x', 'y'], true)).toEqual(jasmine.objectContaining({x: 20, y: 1.5})); }); }); }); ================================================ FILE: test/specs/core.helpers.tests.js ================================================ describe('Core helper tests', function() { var helpers; beforeAll(function() { helpers = window.Chart.helpers; }); it('should generate integer ids', function() { var uid = helpers.uid(); expect(uid).toEqual(jasmine.any(Number)); expect(helpers.uid()).toBe(uid + 1); expect(helpers.uid()).toBe(uid + 2); expect(helpers.uid()).toBe(uid + 3); }); describe('clone', function() { it('should not allow prototype pollution', function() { const test = helpers.clone(JSON.parse('{"__proto__":{"polluted": true}}')); expect(test.prototype).toBeUndefined(); expect(Object.prototype.polluted).toBeUndefined(); }); }); }); ================================================ FILE: test/specs/core.interaction.tests.js ================================================ describe('Core.Interaction', function() { describe('auto', jasmine.fixture.specs('core.interaction')); describe('point mode', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 20, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] } }); }); it ('should return all items under the point', function() { var chart = this.chart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); var point = meta0.data[1]; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: point.x, y: point.y, }; var elements = Chart.Interaction.modes.point(chart, evt, {}).map(item => item.element); expect(elements).toEqual([point, meta1.data[1]]); }); it ('should return an empty array when no items are found', function() { var chart = this.chart; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: 0, y: 0 }; var elements = Chart.Interaction.modes.point(chart, evt, {}).map(item => item.element); expect(elements).toEqual([]); }); }); describe('index mode', function() { describe('intersect: true', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] } }); }); it ('gets correct items', function() { var chart = this.chart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); var point = meta0.data[1]; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: point.x, y: point.y, }; var elements = Chart.Interaction.modes.index(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual([point, meta1.data[1]]); }); it ('returns empty array when nothing found', function() { var chart = this.chart; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: 0, y: 0, }; var elements = Chart.Interaction.modes.index(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual([]); }); }); describe ('intersect: false', function() { var data = { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }; beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: data }); }); it ('axis: x gets correct items', function() { var chart = this.chart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: chart.chartArea.left, y: chart.chartArea.top }; var elements = Chart.Interaction.modes.index(chart, evt, {intersect: false}).map(item => item.element); expect(elements).toEqual([meta0.data[0], meta1.data[0]]); }); it ('axis: y gets correct items', function() { var chart = window.acquireChart({ type: 'bar', data: data, options: { indexAxis: 'y', } }); var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); var center = meta0.data[0].getCenterPoint(); var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: center.x, y: center.y + 30, }; var elements = Chart.Interaction.modes.index(chart, evt, {axis: 'y', intersect: false}).map(item => item.element); expect(elements).toEqual([meta0.data[0], meta1.data[0]]); }); it ('axis: xy gets correct items', function() { var chart = this.chart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: chart.chartArea.left, y: chart.chartArea.top }; var elements = Chart.Interaction.modes.index(chart, evt, {axis: 'xy', intersect: false}).map(item => item.element); expect(elements).toEqual([meta0.data[0], meta1.data[0]]); }); }); }); describe('dataset mode', function() { describe('intersect: true', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] } }); }); it ('should return all items in the dataset of the first item found', function() { var chart = this.chart; var meta = chart.getDatasetMeta(0); var point = meta.data[1]; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: point.x, y: point.y }; var elements = Chart.Interaction.modes.dataset(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual(meta.data); }); it ('should return an empty array if nothing found', function() { var chart = this.chart; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: 0, y: 0 }; var elements = Chart.Interaction.modes.dataset(chart, evt, {intersect: true}); expect(elements).toEqual([]); }); }); describe('intersect: false', function() { var data = { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }; beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: data }); }); it ('axis: x gets correct items', function() { var chart = window.acquireChart({ type: 'bar', data: data, options: { indexAxis: 'y', } }); var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: chart.chartArea.left, y: chart.chartArea.top }; var elements = Chart.Interaction.modes.dataset(chart, evt, {axis: 'x', intersect: false}).map(item => item.element); expect(elements).toEqual(chart.getDatasetMeta(0).data); }); it ('axis: y gets correct items', function() { var chart = this.chart; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: chart.chartArea.left, y: chart.chartArea.top }; var elements = Chart.Interaction.modes.dataset(chart, evt, {axis: 'y', intersect: false}).map(item => item.element); expect(elements).toEqual(chart.getDatasetMeta(1).data); }); it ('axis: xy gets correct items', function() { var chart = this.chart; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: chart.chartArea.left, y: chart.chartArea.top }; var elements = Chart.Interaction.modes.dataset(chart, evt, {intersect: false}).map(item => item.element); expect(elements).toEqual(chart.getDatasetMeta(1).data); }); }); }); describe('nearest mode', function() { describe('intersect: false', function() { beforeEach(function() { this.lineChart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 40, 30], pointRadius: [5, 5, 5], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointRadius: [10, 10, 10], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] } }); this.polarChart = window.acquireChart({ type: 'polarArea', data: { datasets: [{ data: [1, 9, 5] }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { legend: { display: false }, }, } }); }); describe('axis: xy', function() { it ('should return the nearest item', function() { var chart = this.lineChart; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: chart.chartArea.left, y: chart.chartArea.top }; // Nearest to 0,0 (top left) will be first point of dataset 2 var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: false}).map(item => item.element); var meta = chart.getDatasetMeta(1); expect(elements).toEqual([meta.data[0]]); }); it ('should return all items at the same nearest distance', function() { var chart = this.lineChart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { x: meta0.data[1].x, y: (meta0.data[1].y + meta1.data[1].y) / 2 }; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: pt.x, y: pt.y }; // Both points are nearest var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: false}).map(item => item.element); expect(elements).toEqual([meta0.data[1], meta1.data[1]]); }); }); describe('axis: x', function() { it ('should return all items at current x', function() { var chart = this.lineChart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); // At 'Point 2', 10 var pt = { x: meta0.data[1].x, y: meta0.data[0].y }; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: pt.x, y: pt.y }; // Middle point from both series are nearest var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'x', intersect: false}).map(item => item.element); expect(elements).toEqual([meta0.data[1], meta1.data[1]]); }); it ('should return all items at nearest x-distance', function() { var chart = this.lineChart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); // Haflway between 'Point 1' and 'Point 2', y=10 var pt = { x: (meta0.data[0].x + meta0.data[1].x) / 2, y: meta0.data[0].y }; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: pt.x, y: pt.y }; // Should return all (4) points from 'Point 1' and 'Point 2' var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'x', intersect: false}).map(item => item.element); expect(elements).toEqual([meta0.data[0], meta0.data[1], meta1.data[0], meta1.data[1]]); }); }); describe('axis: y', function() { it ('should return item with value 30', function() { var chart = this.lineChart; var meta0 = chart.getDatasetMeta(0); // 'Point 1', y = 30 var pt = { x: meta0.data[0].x, y: meta0.data[2].y }; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: pt.x, y: pt.y }; // Middle point from both series are nearest var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'y', intersect: false}).map(item => item.element); expect(elements).toEqual([meta0.data[2]]); }); it ('should return all items at value 40', function() { var chart = this.lineChart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); // 'Point 1', y = 40 var pt = { x: meta0.data[0].x, y: meta0.data[1].y }; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: pt.x, y: pt.y }; // Should return points with value 40 var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'y', intersect: false}).map(item => item.element); expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]); }); }); describe('axis: r', function() { it ('should return item with value 9', function() { var chart = this.polarChart; var meta0 = chart.getDatasetMeta(0); var evt = { type: 'click', chart: chart, native: true, // Needed, otherwise assumed to be a DOM event x: chart.width / 2, y: chart.height / 2 + 5, }; var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'r'}).map(item => item.element); expect(elements).toEqual([meta0.data[1]]); }); it ('should return item with value 1 when clicked outside of it', function() { var chart = this.polarChart; var meta0 = chart.getDatasetMeta(0); var evt = { type: 'click', chart: chart, native: true, // Needed, otherwise assumed to be a DOM event x: chart.width, y: 0, }; var elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'r', intersect: false}).map(item => item.element); expect(elements).toEqual([meta0.data[0]]); }); }); }); describe('intersect: true', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] } }); }); describe('axis=xy', function() { it ('should return the nearest item', function() { var chart = this.chart; var meta = chart.getDatasetMeta(1); var point = meta.data[1]; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: point.x + 15, y: point.y }; // Nothing intersects so find nothing var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual([]); evt = { type: 'click', chart: chart, native: true, x: point.x, y: point.y }; elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual([point]); }); it ('should return the nearest item even if 2 intersect', function() { var chart = this.chart; chart.data.datasets[0].pointRadius = [5, 30, 5]; chart.data.datasets[0].data[1] = 39; chart.data.datasets[1].pointRadius = [10, 10, 10]; chart.update(); // Trigger an event over top of the var meta0 = chart.getDatasetMeta(0); // Halfway between 2 mid points var pt = { x: meta0.data[1].x, y: meta0.data[1].y }; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: pt.x, y: pt.y }; var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual([meta0.data[1]]); }); it ('should return the all items if more than 1 are at the same distance', function() { var chart = this.chart; chart.data.datasets[0].pointRadius = [5, 5, 5]; chart.data.datasets[0].data[1] = 40; chart.data.datasets[1].pointRadius = [10, 10, 10]; chart.update(); var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { x: meta0.data[1].x, y: meta0.data[1].y }; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: pt.x, y: pt.y }; var elements = Chart.Interaction.modes.nearest(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual([meta0.data[1], meta1.data[1]]); }); }); }); }); describe('x mode', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 40, 30], pointRadius: [5, 10, 5], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointRadius: [10, 10, 10], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] } }); }); it('should return items at the same x value when intersect is false', function() { var chart = this.chart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { x: meta0.data[1].x, y: meta0.data[1].y }; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: pt.x, y: 0 }; var elements = Chart.Interaction.modes.x(chart, evt, {intersect: false}).map(item => item.element); expect(elements).toEqual([meta0.data[1], meta1.data[1]]); evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: pt.x + 20, y: 0 }; elements = Chart.Interaction.modes.x(chart, evt, {intersect: false}).map(item => item.element); expect(elements).toEqual([]); }); it('should return items at the same x value when intersect is true', function() { var chart = this.chart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { x: meta0.data[1].x, y: meta0.data[1].y }; var evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: pt.x, y: 0 }; var elements = Chart.Interaction.modes.x(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual([]); // we don't intersect anything evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: pt.x, y: pt.y }; elements = Chart.Interaction.modes.x(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual([meta0.data[1], meta1.data[1]]); }); }); describe('y mode', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 40, 30], pointRadius: [5, 10, 5], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointRadius: [10, 10, 10], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] } }); }); it('should return items at the same y value when intersect is false', function() { var chart = this.chart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { x: meta0.data[1].x, y: meta0.data[1].y }; var evt = { type: 'click', chart: chart, native: true, x: 0, y: pt.y, }; var elements = Chart.Interaction.modes.y(chart, evt, {intersect: false}).map(item => item.element); expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]); evt = { type: 'click', chart: chart, native: true, x: pt.x, y: pt.y + 20, // out of range }; elements = Chart.Interaction.modes.y(chart, evt, {intersect: false}).map(item => item.element); expect(elements).toEqual([]); }); it('should return items at the same y value when intersect is true', function() { var chart = this.chart; var meta0 = chart.getDatasetMeta(0); var meta1 = chart.getDatasetMeta(1); // Halfway between 2 mid points var pt = { x: meta0.data[1].x, y: meta0.data[1].y }; var evt = { type: 'click', chart: chart, native: true, x: 0, y: pt.y }; var elements = Chart.Interaction.modes.y(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual([]); // we don't intersect anything evt = { type: 'click', chart: chart, native: true, x: pt.x, y: pt.y, }; elements = Chart.Interaction.modes.y(chart, evt, {intersect: true}).map(item => item.element); expect(elements).toEqual([meta0.data[1], meta1.data[0], meta1.data[1], meta1.data[2]]); }); }); describe('tooltip element of scatter chart', function() { it ('out-of-range datapoints are not shown in tooltip', function() { let data = []; for (let i = 0; i < 1000; i++) { data.push({x: i, y: i}); } const chart = window.acquireChart({ type: 'scatter', data: { datasets: [{data}] }, options: { scales: { x: { min: 2 } } } }); const meta0 = chart.getDatasetMeta(0); const firstElement = meta0.data[0]; const evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: firstElement.x, y: firstElement.y }; const elements = Chart.Interaction.modes.point(chart, evt, {intersect: true}).map(item => item.element); expect(elements).not.toContain(firstElement); }); it ('out-of-range datapoints are shown in tooltip if included', function() { let data = []; for (let i = 0; i < 1000; i++) { data.push({x: i, y: i}); } const chart = window.acquireChart({ type: 'scatter', data: { datasets: [{data}] }, options: { scales: { x: { min: 2 } } } }); const meta0 = chart.getDatasetMeta(0); const firstElement = meta0.data[0]; const evt = { type: 'click', chart: chart, native: true, // needed otherwise it thinks its a DOM event x: firstElement.x, y: firstElement.y }; const elements = Chart.Interaction.modes.point( chart, evt, { intersect: true, includeInvisible: true }).map(item => item.element); expect(elements).toContain(firstElement); }); }); const testCases = [ { data: [12, 19, null, null, null, null, 5, 2], clickPointIndex: 0, expectedNearestPointIndex: 0 }, { data: [12, 19, null, null, null, null, 5, 2], clickPointIndex: 1, expectedNearestPointIndex: 1}, { data: [12, 19, null, null, null, null, 5, 2], clickPointIndex: 2, expectedNearestPointIndex: 1 }, { data: [12, 19, null, null, null, null, 5, 2], clickPointIndex: 3, expectedNearestPointIndex: 1 }, { data: [12, 19, null, null, null, null, 5, 2], clickPointIndex: 4, expectedNearestPointIndex: 6 }, { data: [12, 19, null, null, null, null, 5, 2], clickPointIndex: 5, expectedNearestPointIndex: 6 }, { data: [12, 19, null, null, null, null, 5, 2], clickPointIndex: 6, expectedNearestPointIndex: 6 }, { data: [12, 19, null, null, null, null, 5, 2], clickPointIndex: 7, expectedNearestPointIndex: 7 }, { data: [12, 0, null, null, null, null, 0, 2], clickPointIndex: 3, expectedNearestPointIndex: 1 }, { data: [12, 0, null, null, null, null, 0, 2], clickPointIndex: 4, expectedNearestPointIndex: 6 }, { data: [12, -1, null, null, null, null, -1, 2], clickPointIndex: 3, expectedNearestPointIndex: 1 }, { data: [12, -1, null, null, null, null, -1, 2], clickPointIndex: 4, expectedNearestPointIndex: 6 }, { data: [null, 2], clickPointIndex: 0, expectedNearestPointIndex: 1 }, { data: [2, null], clickPointIndex: 1, expectedNearestPointIndex: 0 }, { data: [null, null, 2], clickPointIndex: 0, expectedNearestPointIndex: 2 }, { data: [2, null, null], clickPointIndex: 2, expectedNearestPointIndex: 0 } ]; testCases.forEach(({data, clickPointIndex, expectedNearestPointIndex}, i) => { it(`should select nearest non-null element with index ${expectedNearestPointIndex} when clicking on element with index ${clickPointIndex} in test case ${i + 1} if spanGaps=true`, function() { const chart = window.acquireChart({ type: 'line', data: { labels: [1, 2, 3, 4, 5, 6, 7, 8, 9], datasets: [{ data: data, spanGaps: true, }] } }); chart.update(); const meta = chart.getDatasetMeta(0); const point = meta.data[clickPointIndex]; const evt = { type: 'click', chart: chart, native: true, // needed otherwise things its a DOM event x: point.x, y: point.y, }; const elements = Chart.Interaction.modes.nearest(chart, evt, {axis: 'x', intersect: false}).map(item => item.element); expect(elements).toEqual([meta.data[expectedNearestPointIndex]]); }); }); }); ================================================ FILE: test/specs/core.layouts.tests.js ================================================ function getLabels(scale) { return scale.ticks.map(t => t.label); } describe('Chart.layouts', function() { describe('auto', jasmine.fixture.specs('core.layouts')); it('should be exposed through Chart.layouts', function() { expect(Chart.layouts).toBeDefined(); expect(typeof Chart.layouts).toBe('object'); expect(Chart.layouts.addBox).toBeDefined(); expect(Chart.layouts.removeBox).toBeDefined(); expect(Chart.layouts.configure).toBeDefined(); expect(Chart.layouts.update).toBeDefined(); }); it('should fit a simple chart with 2 scales', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] } }, { canvas: { height: 150, width: 250 } }); expect(chart.chartArea.bottom).toBeCloseToPixel(120); expect(chart.chartArea.left).toBeCloseToPixel(31); expect(chart.chartArea.right).toBeCloseToPixel(250); expect(chart.chartArea.top).toBeCloseToPixel(32); // Is xScale at the right spot expect(chart.scales.x.bottom).toBeCloseToPixel(150); expect(chart.scales.x.left).toBeCloseToPixel(31); expect(chart.scales.x.right).toBeCloseToPixel(250); expect(chart.scales.x.top).toBeCloseToPixel(120); expect(chart.scales.x.labelRotation).toBeCloseTo(0); // Is yScale at the right spot expect(chart.scales.y.bottom).toBeCloseToPixel(120); expect(chart.scales.y.left).toBeCloseToPixel(0); expect(chart.scales.y.right).toBeCloseToPixel(31); expect(chart.scales.y.top).toBeCloseToPixel(32); expect(chart.scales.y.labelRotation).toBeCloseTo(0); }); it('should fit scales that are in the top and right positions', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, options: { scales: { x: { type: 'category', position: 'top' }, y: { type: 'linear', position: 'right' } } } }, { canvas: { height: 150, width: 250 } }); expect(chart.chartArea.bottom).toBeCloseToPixel(139); expect(chart.chartArea.left).toBeCloseToPixel(0); expect(chart.chartArea.right).toBeCloseToPixel(218); expect(chart.chartArea.top).toBeCloseToPixel(62); // Is xScale at the right spot expect(chart.scales.x.bottom).toBeCloseToPixel(62); expect(chart.scales.x.left).toBeCloseToPixel(0); expect(chart.scales.x.right).toBeCloseToPixel(218); expect(chart.scales.x.top).toBeCloseToPixel(32); expect(chart.scales.x.labelRotation).toBeCloseTo(0); // Is yScale at the right spot expect(chart.scales.y.bottom).toBeCloseToPixel(139); expect(chart.scales.y.left).toBeCloseToPixel(218); expect(chart.scales.y.right).toBeCloseToPixel(250); expect(chart.scales.y.top).toBeCloseToPixel(62); expect(chart.scales.y.labelRotation).toBeCloseTo(0); }); it('should fit scales that overlap the chart area', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, 25, 78, -10] }, { data: [-19, -20, 0, -99, -50, 0] }], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] } }); expect(chart.chartArea.bottom).toBeCloseToPixel(512); expect(chart.chartArea.left).toBeCloseToPixel(0); expect(chart.chartArea.right).toBeCloseToPixel(512); expect(chart.chartArea.top).toBeCloseToPixel(32); var scale = chart.scales.r; expect(scale.bottom).toBeCloseToPixel(512); expect(scale.left).toBeCloseToPixel(0); expect(scale.right).toBeCloseToPixel(512); expect(scale.top).toBeCloseToPixel(32); expect(scale.width).toBeCloseToPixel(496); expect(scale.height).toBeCloseToPixel(464); }); it('should fit multiple axes in the same position', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [10, 5, 0, 25, 78, -10] }, { yAxisID: 'y2', data: [-19, -20, 0, -99, -50, 0] }], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, options: { scales: { x: { type: 'category' }, y: { type: 'linear' }, y2: { type: 'linear' } } } }, { canvas: { height: 150, width: 250 } }); expect(chart.chartArea.bottom).toBeCloseToPixel(110); expect(chart.chartArea.left).toBeCloseToPixel(70); expect(chart.chartArea.right).toBeCloseToPixel(250); expect(chart.chartArea.top).toBeCloseToPixel(32); // Is xScale at the right spot expect(chart.scales.x.bottom).toBeCloseToPixel(150); expect(chart.scales.x.left).toBeCloseToPixel(70); expect(chart.scales.x.right).toBeCloseToPixel(250); expect(chart.scales.x.top).toBeCloseToPixel(110); expect(chart.scales.x.labelRotation).toBeCloseTo(40, -1); // Are yScales at the right spot expect(chart.scales.y.bottom).toBeCloseToPixel(110); expect(chart.scales.y.left).toBeCloseToPixel(38); expect(chart.scales.y.right).toBeCloseToPixel(70); expect(chart.scales.y.top).toBeCloseToPixel(32); expect(chart.scales.y.labelRotation).toBeCloseTo(0); expect(chart.scales.y2.bottom).toBeCloseToPixel(110); expect(chart.scales.y2.left).toBeCloseToPixel(0); expect(chart.scales.y2.right).toBeCloseToPixel(38); expect(chart.scales.y2.top).toBeCloseToPixel(32); expect(chart.scales.y2.labelRotation).toBeCloseTo(0); }); it ('should fit a full width box correctly', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ xAxisID: 'x', data: [10, 5, 0, 25, 78, -10] }, { xAxisID: 'x2', data: [-19, -20, 0, -99, -50, 0] }], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, options: { scales: { x: { type: 'category', offset: false }, x2: { type: 'category', position: 'top', fullSize: true, offset: false }, y: { type: 'linear' } } } }); expect(chart.chartArea.bottom).toBeCloseToPixel(484); expect(chart.chartArea.left).toBeCloseToPixel(39); expect(chart.chartArea.right).toBeCloseToPixel(496); expect(chart.chartArea.top).toBeCloseToPixel(62); // Are xScales at the right spot expect(chart.scales.x.bottom).toBeCloseToPixel(512); expect(chart.scales.x.left).toBeCloseToPixel(39); expect(chart.scales.x.right).toBeCloseToPixel(496); expect(chart.scales.x.top).toBeCloseToPixel(484); expect(chart.scales.x2.bottom).toBeCloseToPixel(62); expect(chart.scales.x2.left).toBeCloseToPixel(0); expect(chart.scales.x2.right).toBeCloseToPixel(512); expect(chart.scales.x2.top).toBeCloseToPixel(32); // Is yScale at the right spot expect(chart.scales.y.bottom).toBeCloseToPixel(484); expect(chart.scales.y.left).toBeCloseToPixel(0); expect(chart.scales.y.right).toBeCloseToPixel(39); expect(chart.scales.y.top).toBeCloseToPixel(62); }); describe('padding settings', function() { it('should apply a single padding to all dimensions', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ { data: [10, 5, 0, 25, 78, -10] } ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, options: { scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false } }, plugins: { legend: false, title: false }, layout: { padding: 10 } } }, { canvas: { height: 150, width: 250 } }); expect(chart.chartArea.bottom).toBeCloseToPixel(140); expect(chart.chartArea.left).toBeCloseToPixel(10); expect(chart.chartArea.right).toBeCloseToPixel(240); expect(chart.chartArea.top).toBeCloseToPixel(10); }); it('should apply padding in all positions', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ { data: [10, 5, 0, 25, 78, -10] } ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, options: { scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false } }, plugins: { legend: false, title: false }, layout: { padding: { left: 5, right: 15, top: 8, bottom: 12 } } } }, { canvas: { height: 150, width: 250 } }); expect(chart.chartArea.bottom).toBeCloseToPixel(138); expect(chart.chartArea.left).toBeCloseToPixel(5); expect(chart.chartArea.right).toBeCloseToPixel(235); expect(chart.chartArea.top).toBeCloseToPixel(8); }); it('should default to 0 padding if no dimensions specified', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ { data: [10, 5, 0, 25, 78, -10] } ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, options: { scales: { x: { type: 'category', display: false }, y: { type: 'linear', display: false } }, plugins: { legend: false, title: false }, layout: { padding: {} } } }, { canvas: { height: 150, width: 250 } }); expect(chart.chartArea.bottom).toBeCloseToPixel(150); expect(chart.chartArea.left).toBeCloseToPixel(0); expect(chart.chartArea.right).toBeCloseToPixel(250); expect(chart.chartArea.top).toBeCloseToPixel(0); }); }); describe('ordering by weight', function() { it('should keep higher weights outside', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ { data: [10, 5, 0, 25, 78, -10] } ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, options: { plugins: { legend: { display: true, position: 'left', }, title: { display: true, position: 'bottom', }, } }, }, { canvas: { height: 150, width: 250 } }); var xAxis = chart.scales.x; var yAxis = chart.scales.y; var legend = chart.legend; var title = chart.titleBlock; expect(yAxis.left).toBe(legend.right); expect(xAxis.bottom).toBe(title.top); }); it('should correctly set weights of scales and order them', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [ { data: [10, 5, 0, 25, 78, -10] } ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] }, options: { scales: { x: { type: 'category', position: 'bottom', display: true, weight: 1 }, x1: { type: 'category', position: 'bottom', display: true, weight: 2 }, x2: { type: 'category', position: 'bottom', display: true }, x3: { type: 'category', display: true, position: 'top', weight: 1 }, x4: { type: 'category', display: true, position: 'top', weight: 2 }, y: { type: 'linear', display: true, weight: 1 }, y1: { type: 'linear', position: 'left', display: true, weight: 2 }, y2: { type: 'linear', position: 'left', display: true }, y3: { type: 'linear', display: true, position: 'right', weight: 1 }, y4: { type: 'linear', display: true, position: 'right', weight: 2 } } } }, { canvas: { height: 150, width: 250 } }); var xScale0 = chart.scales.x; var xScale1 = chart.scales.x1; var xScale2 = chart.scales.x2; var xScale3 = chart.scales.x3; var xScale4 = chart.scales.x4; var yScale0 = chart.scales.y; var yScale1 = chart.scales.y1; var yScale2 = chart.scales.y2; var yScale3 = chart.scales.y3; var yScale4 = chart.scales.y4; expect(xScale0.weight).toBe(1); expect(xScale1.weight).toBe(2); expect(xScale2.weight).toBe(0); expect(xScale3.weight).toBe(1); expect(xScale4.weight).toBe(2); expect(yScale0.weight).toBe(1); expect(yScale1.weight).toBe(2); expect(yScale2.weight).toBe(0); expect(yScale3.weight).toBe(1); expect(yScale4.weight).toBe(2); var isOrderCorrect = false; // bottom axes isOrderCorrect = xScale2.top < xScale0.top && xScale0.top < xScale1.top; expect(isOrderCorrect).toBe(true); // top axes isOrderCorrect = xScale4.top < xScale3.top; expect(isOrderCorrect).toBe(true); // left axes isOrderCorrect = yScale1.left < yScale0.left && yScale0.left < yScale2.left; expect(isOrderCorrect).toBe(true); // right axes isOrderCorrect = yScale3.left < yScale4.left; expect(isOrderCorrect).toBe(true); }); }); describe('box sizing', function() { it('should correctly compute y-axis width to fit labels', function() { var chart = window.acquireChart({ type: 'bar', data: { labels: ['tick 1', 'tick 2', 'tick 3', 'tick 4', 'tick 5'], datasets: [{ data: [0, 2.25, 1.5, 1.25, 2.5] }], }, options: { plugins: { legend: false }, }, }, { canvas: { height: 256, width: 256 } }); var yAxis = chart.scales.y; // issue #4441: y-axis labels partially hidden. // minimum horizontal space required to fit labels expect(yAxis.width).toBeCloseToPixel(30); expect(getLabels(yAxis)).toEqual(['0', '0.5', '1.0', '1.5', '2.0', '2.5']); }); }); }); ================================================ FILE: test/specs/core.plugin.tests.js ================================================ describe('Chart.plugins', function() { describe('Chart.notifyPlugins', function() { it('should call inline plugins with arguments', function() { var plugin = {hook: function() {}}; var chart = window.acquireChart({ plugins: [plugin] }); var args = {value: 42}; spyOn(plugin, 'hook'); chart.notifyPlugins('hook', args); expect(plugin.hook.calls.count()).toBe(1); expect(plugin.hook.calls.first().args[0]).toBe(chart); expect(plugin.hook.calls.first().args[1]).toBe(args); expect(plugin.hook.calls.first().args[2]).toEqualOptions({}); }); it('should call global plugins with arguments', function() { var plugin = {id: 'a', hook: function() {}}; var chart = window.acquireChart({}); var args = {value: 42}; spyOn(plugin, 'hook'); Chart.register(plugin); chart.notifyPlugins('hook', args); expect(plugin.hook.calls.count()).toBe(1); expect(plugin.hook.calls.first().args[0]).toBe(chart); expect(plugin.hook.calls.first().args[1]).toBe(args); expect(plugin.hook.calls.first().args[2]).toEqualOptions({}); Chart.unregister(plugin); }); it('should call plugin only once even if registered multiple times', function() { var plugin = {id: 'test', hook: function() {}}; var chart = window.acquireChart({ plugins: [plugin, plugin] }); spyOn(plugin, 'hook'); Chart.register([plugin, plugin]); chart.notifyPlugins('hook'); expect(plugin.hook.calls.count()).toBe(1); Chart.unregister(plugin); }); it('should call plugins in the correct order (global first)', function() { var results = []; var chart = window.acquireChart({ plugins: [{ hook: function() { results.push(1); } }, { hook: function() { results.push(2); } }, { hook: function() { results.push(3); } }] }); var plugins = [{ id: 'a', hook: function() { results.push(4); } }, { id: 'b', hook: function() { results.push(5); } }, { id: 'c', hook: function() { results.push(6); } }]; Chart.register(plugins); var ret = chart.notifyPlugins('hook'); expect(ret).toBeTruthy(); expect(results).toEqual([4, 5, 6, 1, 2, 3]); Chart.unregister(plugins); }); it('should return TRUE if no plugin explicitly returns FALSE', function() { var chart = window.acquireChart({ plugins: [{ hook: function() {} }, { hook: function() { return null; } }, { hook: function() { return 0; } }, { hook: function() { return true; } }, { hook: function() { return 1; } }] }); var plugins = chart.config.plugins; plugins.forEach(function(plugin) { spyOn(plugin, 'hook').and.callThrough(); }); var ret = chart.notifyPlugins('hook'); expect(ret).toBeTruthy(); plugins.forEach(function(plugin) { expect(plugin.hook).toHaveBeenCalled(); }); }); it('should return FALSE if any plugin explicitly returns FALSE', function() { var chart = window.acquireChart({ plugins: [{ hook: function() {} }, { hook: function() { return null; } }, { hook: function() { return false; } }, { hook: function() { return 42; } }, { hook: function() { return 'bar'; } }] }); var plugins = chart.config.plugins; plugins.forEach(function(plugin) { spyOn(plugin, 'hook').and.callThrough(); }); var ret = chart.notifyPlugins('hook', {cancelable: true}); expect(ret).toBeFalsy(); expect(plugins[0].hook).toHaveBeenCalled(); expect(plugins[1].hook).toHaveBeenCalled(); expect(plugins[2].hook).toHaveBeenCalled(); expect(plugins[3].hook).not.toHaveBeenCalled(); expect(plugins[4].hook).not.toHaveBeenCalled(); }); }); describe('config.options.plugins', function() { it('should call plugins with options at last argument', function() { var plugin = {id: 'foo', hook: function() {}}; var chart = window.acquireChart({ options: { plugins: { foo: {a: '123'}, } } }); spyOn(plugin, 'hook'); Chart.register(plugin); chart.notifyPlugins('hook'); chart.notifyPlugins('hook', {arg1: 'bla'}); chart.notifyPlugins('hook', {arg1: 'bla', arg2: 42}); expect(plugin.hook.calls.count()).toBe(3); expect(plugin.hook.calls.argsFor(0)[2]).toEqualOptions({a: '123'}); expect(plugin.hook.calls.argsFor(1)[2]).toEqualOptions({a: '123'}); expect(plugin.hook.calls.argsFor(2)[2]).toEqualOptions({a: '123'}); Chart.unregister(plugin); }); it('should call plugins with options associated to their identifier', function() { var plugins = { a: {id: 'a', hook: function() {}}, b: {id: 'b', hook: function() {}}, c: {id: 'c', hook: function() {}} }; Chart.register(plugins.a); var chart = window.acquireChart({ plugins: [plugins.b, plugins.c], options: { plugins: { a: {a: '123'}, b: {b: '456'}, c: {c: '789'} } } }); spyOn(plugins.a, 'hook'); spyOn(plugins.b, 'hook'); spyOn(plugins.c, 'hook'); chart.notifyPlugins('hook'); expect(plugins.a.hook).toHaveBeenCalled(); expect(plugins.b.hook).toHaveBeenCalled(); expect(plugins.c.hook).toHaveBeenCalled(); expect(plugins.a.hook.calls.first().args[2]).toEqualOptions({a: '123'}); expect(plugins.b.hook.calls.first().args[2]).toEqualOptions({b: '456'}); expect(plugins.c.hook.calls.first().args[2]).toEqualOptions({c: '789'}); Chart.unregister(plugins.a); }); it('should not call plugins when config.options.plugins.{id} is FALSE', function() { var plugins = { a: {id: 'a', hook: function() {}}, b: {id: 'b', hook: function() {}}, c: {id: 'c', hook: function() {}} }; Chart.register(plugins.a); var chart = window.acquireChart({ plugins: [plugins.b, plugins.c], options: { plugins: { a: false, b: false } } }); spyOn(plugins.a, 'hook'); spyOn(plugins.b, 'hook'); spyOn(plugins.c, 'hook'); chart.notifyPlugins('hook'); expect(plugins.a.hook).not.toHaveBeenCalled(); expect(plugins.b.hook).not.toHaveBeenCalled(); expect(plugins.c.hook).toHaveBeenCalled(); Chart.unregister(plugins.a); }); it('should call plugins with default options when plugin options is TRUE', function() { var plugin = {id: 'a', hook: function() {}, defaults: {a: 42}}; Chart.register(plugin); var chart = window.acquireChart({ options: { plugins: { a: true } } }); spyOn(plugin, 'hook'); chart.notifyPlugins('hook'); expect(plugin.hook).toHaveBeenCalled(); expect(Object.keys(plugin.hook.calls.first().args[2])).toEqual(['a']); expect(plugin.hook.calls.first().args[2]).toEqual(jasmine.objectContaining({a: 42})); Chart.unregister(plugin); }); it('should call plugins with default options if plugin config options is undefined', function() { var plugin = {id: 'a', hook: function() {}, defaults: {a: 'foobar'}}; Chart.register(plugin); spyOn(plugin, 'hook'); var chart = window.acquireChart(); chart.notifyPlugins('hook'); expect(plugin.hook).toHaveBeenCalled(); expect(plugin.hook.calls.first().args[2]).toEqualOptions({a: 'foobar'}); Chart.unregister(plugin); }); // https://github.com/chartjs/Chart.js/issues/10482 it('should resolve defaults for local plugins', function() { var plugin = {id: 'a', hook: function() {}, defaults: {bar: 'bar'}}; var chart = window.acquireChart({ plugins: [plugin], options: { plugins: { a: { foo: 'foo' } } }, }); spyOn(plugin, 'hook'); chart.notifyPlugins('hook'); expect(plugin.hook).toHaveBeenCalled(); expect(plugin.hook.calls.first().args[2]).toEqualOptions({foo: 'foo', bar: 'bar'}); Chart.unregister(plugin); }); // https://github.com/chartjs/Chart.js/issues/5111#issuecomment-355934167 it('should update plugin options', function() { var plugin = {id: 'a', hook: function() {}}; var chart = window.acquireChart({ plugins: [plugin], options: { plugins: { a: { foo: 'foo' } } }, }); spyOn(plugin, 'hook'); chart.notifyPlugins('hook'); expect(plugin.hook).toHaveBeenCalled(); expect(plugin.hook.calls.first().args[2]).toEqualOptions({foo: 'foo'}); chart.options.plugins.a = {bar: 'bar'}; chart.update(); plugin.hook.calls.reset(); chart.notifyPlugins('hook'); expect(plugin.hook).toHaveBeenCalled(); expect(plugin.hook.calls.first().args[2]).toEqualOptions({bar: 'bar'}); }); // https://github.com/chartjs/Chart.js/issues/10654 it('should resolve options even if some subnodes are set as undefined', function() { var runtimeOptions; var plugin = { id: 'a', afterUpdate: function(chart, args, options) { options.l1.l2.l3.display = true; runtimeOptions = options; }, defaults: { l1: { l2: { l3: { display: false } } } } }; window.acquireChart({ plugins: [plugin], options: { plugins: { a: { l1: { l2: undefined } }, } }, }); expect(runtimeOptions.l1.l2.l3.display).toBe(true); Chart.unregister(plugin); }); it('should disable all plugins', function() { var plugin = {id: 'a', hook: function() {}}; var chart = window.acquireChart({ plugins: [plugin], options: { plugins: false } }); spyOn(plugin, 'hook'); chart.notifyPlugins('hook'); expect(plugin.hook).not.toHaveBeenCalled(); }); it('should not restart plugins when a double register occurs', function() { var results = []; var chart = window.acquireChart({ plugins: [{ start: function() { results.push(1); } }] }); Chart.register({id: 'abc', hook: function() {}}); Chart.register({id: 'def', hook: function() {}}); chart.update(); // The plugin on the chart should only be started once expect(results).toEqual([1]); }); it('should default to false for _scriptable, _indexable', function(done) { const plugin = { id: 'test', start: function(chart, args, opts) { expect(opts.fun).toEqual(jasmine.any(Function)); expect(opts.fun()).toEqual('test'); expect(opts.arr).toEqual([1, 2, 3]); expect(opts.sub.subfun).toEqual(jasmine.any(Function)); expect(opts.sub.subfun()).toEqual('subtest'); expect(opts.sub.subarr).toEqual([3, 2, 1]); done(); } }; window.acquireChart({ options: { plugins: { test: { fun: () => 'test', arr: [1, 2, 3], sub: { subfun: () => 'subtest', subarr: [3, 2, 1], } } } }, plugins: [plugin] }); }); it('should filter event callbacks by plugin events array', async function() { const results = []; const chart = window.acquireChart({ options: { events: ['mousemove', 'test', 'test2', 'pointerleave'], plugins: { testPlugin: { events: ['test', 'pointerleave'] } } }, plugins: [{ id: 'testPlugin', beforeEvent: function(_chart, args) { results.push('before' + args.event.type); }, afterEvent: function(_chart, args) { results.push('after' + args.event.type); } }] }); await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 0, y: 0}); await jasmine.triggerMouseEvent(chart, 'test', {x: 0, y: 0}); await jasmine.triggerMouseEvent(chart, 'test2', {x: 0, y: 0}); await jasmine.triggerMouseEvent(chart, 'pointerleave', {x: 0, y: 0}); expect(results).toEqual(['beforetest', 'aftertest', 'beforemouseout', 'aftermouseout']); }); it('should not call plugins after uninstall', async function() { const results = []; const chart = window.acquireChart({ options: { events: ['test'], plugins: { testPlugin: { events: ['test'] } } }, plugins: [{ id: 'testPlugin', reset: () => results.push('reset'), afterDestroy: () => results.push('afterDestroy'), uninstall: () => results.push('uninstall'), }] }); chart.reset(); expect(results).toEqual(['reset']); chart.destroy(); expect(results).toEqual(['reset', 'afterDestroy', 'uninstall']); chart.reset(); expect(results).toEqual(['reset', 'afterDestroy', 'uninstall']); }); }); }); ================================================ FILE: test/specs/core.registry.tests.js ================================================ describe('Chart.registry', function() { it('should handle an ES6 controller extension', function() { class CustomController extends Chart.DatasetController {} CustomController.id = 'custom'; CustomController.defaults = { foo: 'bar' }; CustomController.overrides = { bar: 'foo' }; Chart.register(CustomController); expect(Chart.registry.getController('custom')).toEqual(CustomController); expect(Chart.defaults.datasets.custom).toEqual(CustomController.defaults); expect(Chart.overrides.custom).toEqual(CustomController.overrides); Chart.unregister(CustomController); expect(function() { Chart.registry.getController('custom'); }).toThrow(new Error('"custom" is not a registered controller.')); expect(Chart.overrides.custom).not.toBeDefined(); expect(Chart.defaults.datasets.custom).not.toBeDefined(); }); it('should handle an ES6 scale extension', function() { class CustomScale extends Chart.Scale {} CustomScale.id = 'es6Scale'; CustomScale.defaults = { foo: 'bar' }; Chart.register(CustomScale); expect(Chart.registry.getScale('es6Scale')).toEqual(CustomScale); expect(Chart.defaults.scales.es6Scale).toEqual(CustomScale.defaults); Chart.unregister(CustomScale); expect(function() { Chart.registry.getScale('es6Scale'); }).toThrow(new Error('"es6Scale" is not a registered scale.')); expect(Chart.defaults.scales.es6Scale).not.toBeDefined(); }); it('should handle an ES6 element extension', function() { class CustomElement extends Chart.Element {} CustomElement.id = 'es6element'; CustomElement.defaults = { foo: 'bar' }; Chart.register(CustomElement); expect(Chart.registry.getElement('es6element')).toEqual(CustomElement); expect(Chart.defaults.elements.es6element).toEqual(CustomElement.defaults); Chart.unregister(CustomElement); expect(function() { Chart.registry.getElement('es6element'); }).toThrow(new Error('"es6element" is not a registered element.')); expect(Chart.defaults.elements.es6element).not.toBeDefined(); }); it('should handle an ES6 plugin', function() { class CustomPlugin {} CustomPlugin.id = 'es6plugin'; CustomPlugin.defaults = { foo: 'bar' }; Chart.register(CustomPlugin); expect(Chart.registry.getPlugin('es6plugin')).toEqual(CustomPlugin); expect(Chart.defaults.plugins.es6plugin).toEqual(CustomPlugin.defaults); Chart.unregister(CustomPlugin); expect(function() { Chart.registry.getPlugin('es6plugin'); }).toThrow(new Error('"es6plugin" is not a registered plugin.')); expect(Chart.defaults.plugins.es6plugin).not.toBeDefined(); }); it('should not accept an object without id', function() { expect(function() { Chart.register({foo: 'bar'}); }).toThrow(new Error('class does not have id: bar')); class FaultyPlugin {} expect(function() { Chart.register(FaultyPlugin); }).toThrow(new Error('class does not have id: class FaultyPlugin {}')); }); it('should not fail when unregistering an object that is not registered', function() { expect(function() { Chart.unregister({id: 'foo'}); }).not.toThrow(); }); describe('Should allow registering explicitly', function() { class customExtension {} customExtension.id = 'custom'; customExtension.defaults = { prop: true }; it('as controller', function() { Chart.registry.addControllers(customExtension); expect(Chart.registry.getController('custom')).toEqual(customExtension); expect(Chart.defaults.datasets.custom).toEqual(customExtension.defaults); Chart.registry.removeControllers(customExtension); expect(function() { Chart.registry.getController('custom'); }).toThrow(new Error('"custom" is not a registered controller.')); expect(Chart.defaults.datasets.custom).not.toBeDefined(); }); it('as scale', function() { Chart.registry.addScales(customExtension); expect(Chart.registry.getScale('custom')).toEqual(customExtension); expect(Chart.defaults.scales.custom).toEqual(customExtension.defaults); Chart.registry.removeScales(customExtension); expect(function() { Chart.registry.getScale('custom'); }).toThrow(new Error('"custom" is not a registered scale.')); expect(Chart.defaults.scales.custom).not.toBeDefined(); }); it('as element', function() { Chart.registry.addElements(customExtension); expect(Chart.registry.getElement('custom')).toEqual(customExtension); expect(Chart.defaults.elements.custom).toEqual(customExtension.defaults); Chart.registry.removeElements(customExtension); expect(function() { Chart.registry.getElement('custom'); }).toThrow(new Error('"custom" is not a registered element.')); expect(Chart.defaults.elements.custom).not.toBeDefined(); }); it('as plugin', function() { Chart.registry.addPlugins(customExtension); expect(Chart.registry.getPlugin('custom')).toEqual(customExtension); expect(Chart.defaults.plugins.custom).toEqual(customExtension.defaults); Chart.registry.removePlugins(customExtension); expect(function() { Chart.registry.getPlugin('custom'); }).toThrow(new Error('"custom" is not a registered plugin.')); expect(Chart.defaults.plugins.custom).not.toBeDefined(); }); }); it('should fire before/after callbacks', function() { let beforeRegisterCount = 0; let afterRegisterCount = 0; let beforeUnregisterCount = 0; let afterUnregisterCount = 0; class custom {} custom.id = 'custom'; custom.beforeRegister = () => beforeRegisterCount++; custom.afterRegister = () => afterRegisterCount++; custom.beforeUnregister = () => beforeUnregisterCount++; custom.afterUnregister = () => afterUnregisterCount++; Chart.registry.addControllers(custom); expect(beforeRegisterCount).withContext('beforeRegister').toBe(1); expect(afterRegisterCount).withContext('afterRegister').toBe(1); Chart.registry.removeControllers(custom); expect(beforeUnregisterCount).withContext('beforeUnregister').toBe(1); expect(afterUnregisterCount).withContext('afterUnregister').toBe(1); Chart.registry.addScales(custom); expect(beforeRegisterCount).withContext('beforeRegister').toBe(2); expect(afterRegisterCount).withContext('afterRegister').toBe(2); Chart.registry.removeScales(custom); expect(beforeUnregisterCount).withContext('beforeUnregister').toBe(2); expect(afterUnregisterCount).withContext('afterUnregister').toBe(2); Chart.registry.addElements(custom); expect(beforeRegisterCount).withContext('beforeRegister').toBe(3); expect(afterRegisterCount).withContext('afterRegister').toBe(3); Chart.registry.removeElements(custom); expect(beforeUnregisterCount).withContext('beforeUnregister').toBe(3); expect(afterUnregisterCount).withContext('afterUnregister').toBe(3); Chart.register(custom); expect(beforeRegisterCount).withContext('beforeRegister').toBe(4); expect(afterRegisterCount).withContext('afterRegister').toBe(4); Chart.unregister(custom); expect(beforeUnregisterCount).withContext('beforeUnregister').toBe(4); expect(afterUnregisterCount).withContext('afterUnregister').toBe(4); }); it('should preserve existing defaults', function() { Chart.defaults.datasets.test = {test1: true, test3: false}; Chart.overrides.test = {testA: true, testC: false}; class testController extends Chart.DatasetController {} testController.id = 'test'; testController.defaults = {test1: false, test2: true}; testController.overrides = {testA: false, testB: true}; Chart.register(testController); expect(Chart.defaults.datasets.test).toEqual({test1: false, test2: true, test3: false}); expect(Chart.overrides.test).toEqual({testA: false, testB: true, testC: false}); Chart.unregister(testController); expect(Chart.defaults.datasets.test).not.toBeDefined(); expect(Chart.overrides.test).not.toBeDefined(); }); describe('should handle multiple items', function() { class test1 extends Chart.DatasetController {} test1.id = 'test1'; class test2 extends Chart.Scale {} test2.id = 'test2'; it('separately', function() { Chart.register(test1, test2); expect(Chart.registry.getController('test1')).toEqual(test1); expect(Chart.registry.getScale('test2')).toEqual(test2); Chart.unregister(test1, test2); expect(function() { Chart.registry.getController('test1'); }).toThrow(); expect(function() { Chart.registry.getScale('test2'); }).toThrow(); }); it('as array', function() { Chart.register([test1, test2]); expect(Chart.registry.getController('test1')).toEqual(test1); expect(Chart.registry.getScale('test2')).toEqual(test2); Chart.unregister([test1, test2]); expect(function() { Chart.registry.getController('test1'); }).toThrow(); expect(function() { Chart.registry.getScale('test2'); }).toThrow(); }); it('as object', function() { Chart.register({test1, test2}); expect(Chart.registry.getController('test1')).toEqual(test1); expect(Chart.registry.getScale('test2')).toEqual(test2); Chart.unregister({test1, test2}); expect(function() { Chart.registry.getController('test1'); }).toThrow(); expect(function() { Chart.registry.getScale('test2'); }).toThrow(); }); }); }); ================================================ FILE: test/specs/core.scale.tests.js ================================================ function getLabels(scale) { return scale.ticks.map(t => t.label); } describe('Core.scale', function() { describe('auto', jasmine.fixture.specs('core.scale')); it('should provide default scale label options', function() { expect(Chart.defaults.scale.title).toEqual({ color: Chart.defaults.color, display: false, text: '', padding: { top: 4, bottom: 4 } }); }); describe('displaying xAxis ticks with autoSkip=true', function() { function getChart(data) { return window.acquireChart({ type: 'line', data: data, options: { scales: { x: { ticks: { autoSkip: true } } } } }); } function getChartBigData(maxTicksLimit) { return window.acquireChart({ type: 'line', data: { labels: new Array(300).fill('red'), datasets: [{ data: new Array(300).fill(5), }] }, options: { scales: { x: { ticks: { autoSkip: true, maxTicksLimit } } } } }); } function lastTick(chart) { var xAxis = chart.scales.x; var ticks = xAxis.getTicks(); return ticks[ticks.length - 1]; } it('should use autoSkip amount of ticks when maxTicksLimit is set to a larger number as autoSkip calculation', function() { var chart = getChartBigData(300); expect(chart.scales.x.ticks.length).toEqual(20); }); it('should use maxTicksLimit amount of ticks when maxTicksLimit is set to a smaller number as autoSkip calculation', function() { var chart = getChartBigData(3); expect(chart.scales.x.ticks.length).toEqual(3); }); it('should display the last tick if it fits evenly with other ticks', function() { var chart = getChart({ labels: [ 'January 2018', 'February 2018', 'March 2018', 'April 2018', 'May 2018', 'June 2018', 'July 2018', 'August 2018', 'September 2018' ], datasets: [{ data: [12, 19, 3, 5, 2, 3, 7, 8, 9] }] }); expect(lastTick(chart).label).toEqual('September 2018'); }); it('should not display the last tick if it does not fit evenly', function() { var chart = getChart({ labels: [ 'January 2018', 'February 2018', 'March 2018', 'April 2018', 'May 2018', 'June 2018', 'July 2018', 'August 2018', 'September 2018', 'October 2018', 'November 2018', 'December 2018', 'January 2019', 'February 2019', 'March 2019', 'April 2019', 'May 2019', 'June 2019', 'July 2019', 'August 2019', 'September 2019', 'October 2019', 'November 2019', 'December 2019', 'January 2020', 'February 2020', 'March 2020', 'April 2020' ], datasets: [{ data: [1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7] }] }); expect(lastTick(chart).label).toEqual('March 2020'); }); }); var gridLineTests = [{ labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], offsetGridLines: false, offset: false, expected: [0.5, 128.5, 256.5, 384.5, 512.5] }, { labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], offsetGridLines: false, offset: true, expected: [51.5, 153.5, 256.5, 358.5, 460.5] }, { labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], offsetGridLines: true, offset: false, expected: [64.5, 192.5, 320.5, 448.5] }, { labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], offsetGridLines: true, offset: true, expected: [0.5, 102.5, 204.5, 307.5, 409.5, 512.5] }, { labels: ['tick1'], offsetGridLines: false, offset: false, expected: [0.5] }, { labels: ['tick1'], offsetGridLines: false, offset: true, expected: [256.5] }, { labels: ['tick1'], offsetGridLines: true, offset: false, expected: [512.5] }, { labels: ['tick1'], offsetGridLines: true, offset: true, expected: [0.5, 512.5] }]; gridLineTests.forEach(function(test) { it('should get the correct pixels for gridLine(s) for the horizontal scale when offsetGridLines is ' + test.offsetGridLines + ' and offset is ' + test.offset, function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [] }], labels: test.labels }, options: { scales: { x: { grid: { offset: test.offsetGridLines, drawTicks: false }, ticks: { display: false }, offset: test.offset }, y: { display: false } }, plugins: { legend: false } } }); var xScale = chart.scales.x; xScale.ctx = window.createMockContext(); chart.draw(); expect(xScale.ctx.getCalls().filter(function(x) { return x.name === 'moveTo' && x.args[1] === 0; }).map(function(x) { return x.args[0]; })).toEqual(test.expected); }); }); gridLineTests.forEach(function(test) { it('should get the correct pixels for gridLine(s) for the vertical scale when offsetGridLines is ' + test.offsetGridLines + ' and offset is ' + test.offset, function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [] }], labels: test.labels }, options: { scales: { x: { display: false }, y: { type: 'category', grid: { offset: test.offsetGridLines, drawTicks: false }, ticks: { display: false }, offset: test.offset } }, plugins: { legend: false } } }); var yScale = chart.scales.y; yScale.ctx = window.createMockContext(); chart.draw(); expect(yScale.ctx.getCalls().filter(function(x) { return x.name === 'moveTo' && x.args[0] === 1; }).map(function(x) { return x.args[1]; })).toEqual(test.expected); }); }); it('should add the correct padding for long tick labels', function() { var chart = window.acquireChart({ type: 'line', data: { labels: [ 'This is a very long label', 'This is a very long label' ], datasets: [{ data: [0, 1] }] }, options: { scales: { y: { display: false } }, plugins: { legend: false } } }, { canvas: { height: 100, width: 200 } }); var scale = chart.scales.x; expect(scale.left).toBeGreaterThan(100); expect(scale.right).toBeGreaterThan(190); }); describe('given the axes display option is set to auto', function() { describe('for the x axes', function() { it('should draw the axes if at least one associated dataset is visible', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [100, 200, 100, 50], xAxisId: 'foo', hidden: true, labels: ['Q1', 'Q2', 'Q3', 'Q4'] }, { data: [100, 200, 100, 50], xAxisId: 'foo', labels: ['Q1', 'Q2', 'Q3', 'Q4'] }] }, options: { scales: { x: { display: 'auto' }, y: { type: 'category', } } } }); var scale = chart.scales.x; scale.ctx = window.createMockContext(); chart.draw(); expect(scale.ctx.getCalls().length).toBeGreaterThan(0); expect(scale.height).toBeGreaterThan(0); }); it('should not draw the axes if no associated datasets are visible', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [100, 200, 100, 50], xAxisId: 'foo', hidden: true, labels: ['Q1', 'Q2', 'Q3', 'Q4'] }] }, options: { scales: { x: { display: 'auto' } } } }); var scale = chart.scales.x; scale.ctx = window.createMockContext(); chart.draw(); expect(scale.ctx.getCalls().length).toBe(0); expect(scale.height).toBe(0); }); }); describe('for the y axes', function() { it('should draw the axes if at least one associated dataset is visible', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [100, 200, 100, 50], yAxisId: 'foo', hidden: true, labels: ['Q1', 'Q2', 'Q3', 'Q4'] }, { data: [100, 200, 100, 50], yAxisId: 'foo', labels: ['Q1', 'Q2', 'Q3', 'Q4'] }] }, options: { scales: { y: { display: 'auto' } } } }); var scale = chart.scales.y; scale.ctx = window.createMockContext(); chart.draw(); expect(scale.ctx.getCalls().length).toBeGreaterThan(0); expect(scale.width).toBeGreaterThan(0); }); it('should not draw the axes if no associated datasets are visible', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [100, 200, 100, 50], yAxisId: 'foo', hidden: true, labels: ['Q1', 'Q2', 'Q3', 'Q4'] }] }, options: { scales: { y: { display: 'auto' } } } }); var scale = chart.scales.y; scale.ctx = window.createMockContext(); chart.draw(); expect(scale.ctx.getCalls().length).toBe(0); expect(scale.width).toBe(0); }); }); }); describe('afterBuildTicks', function() { it('should allow filtering of ticks', function() { var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; var chart = window.acquireChart({ type: 'line', options: { scales: { x: { type: 'category', labels: labels, afterBuildTicks: function(scale) { scale.ticks = scale.ticks.slice(1); } } } } }); var scale = chart.scales.x; expect(getLabels(scale)).toEqual(labels.slice(1)); }); it('should allow no return value from callback', function() { var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; var chart = window.acquireChart({ type: 'line', options: { scales: { x: { type: 'category', labels: labels, afterBuildTicks: function() { } } } } }); var scale = chart.scales.x; expect(getLabels(scale)).toEqual(labels); }); it('should allow empty ticks', function() { var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; var chart = window.acquireChart({ type: 'line', options: { scales: { x: { type: 'category', labels: labels, afterBuildTicks: function(scale) { scale.ticks = []; } } } } }); var scale = chart.scales.x; expect(scale.ticks.length).toBe(0); }); }); describe('_layers', function() { it('should default to three layers', function() { var chart = window.acquireChart({ type: 'line', options: { scales: { x: { type: 'linear', } } } }); var scale = chart.scales.x; expect(scale._layers().length).toEqual(3); }); it('should create the chart with custom scale ids without axis or position options', function() { function createChart() { return window.acquireChart({ type: 'scatter', data: { datasets: [{ data: [{x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}], xAxisID: 'customIDx', yAxisID: 'customIDy' }] }, options: { scales: { customIDx: { type: 'linear', display: false }, customIDy: { type: 'linear', display: false } } } }); } expect(createChart).not.toThrow(); }); it('should default to one layer for custom scales', function() { class CustomScale extends Chart.Scale { draw() {} convertTicksToLabels() { return ['tick']; } } CustomScale.id = 'customScale'; CustomScale.defaults = {}; Chart.register(CustomScale); var chart = window.acquireChart({ type: 'line', options: { scales: { x: { type: 'customScale', grid: { z: 10 }, ticks: { z: 20 } } } } }); var scale = chart.scales.x; expect(scale._layers().length).toEqual(1); expect(scale._layers()[0].z).toEqual(20); }); it('should default to one layer for custom scales for axis', function() { class CustomScale1 extends Chart.Scale { draw() {} convertTicksToLabels() { return ['tick']; } } CustomScale1.id = 'customScale1'; CustomScale1.defaults = {axis: 'x'}; Chart.register(CustomScale1); var chart = window.acquireChart({ type: 'line', options: { scales: { my: { type: 'customScale1', grid: { z: 10 }, ticks: { z: 20 } } } } }); var scale = chart.scales.my; expect(scale._layers().length).toEqual(1); expect(scale._layers()[0].z).toEqual(20); }); it('should fail for custom scales without any axis or position', function() { class CustomScale2 extends Chart.Scale { draw() {} } CustomScale2.id = 'customScale2'; CustomScale2.defaults = {}; Chart.register(CustomScale2); function createChart() { return window.acquireChart({ type: 'line', options: { scales: { my: { type: 'customScale2' } } } }); } expect(createChart).toThrow(new Error('Cannot determine type of \'my\' axis. Please provide \'axis\' or \'position\' option.')); }); it('should return 3 layers when z is not equal between ticks and grid', function() { var chart = window.acquireChart({ type: 'line', options: { scales: { x: { type: 'linear', ticks: { z: 10 } } } } }); expect(chart.scales.x._layers().length).toEqual(3); chart = window.acquireChart({ type: 'line', options: { scales: { x: { type: 'linear', grid: { z: 11 } } } } }); expect(chart.scales.x._layers().length).toEqual(3); chart = window.acquireChart({ type: 'line', options: { scales: { x: { type: 'linear', ticks: { z: 10 }, grid: { z: 11 } } } } }); expect(chart.scales.x._layers().length).toEqual(3); }); }); describe('min and max', function() { it('should be limited to visible data', function() { var chart = window.acquireChart({ type: 'scatter', data: { datasets: [{ data: [{x: 100, y: 100}, {x: -100, y: -100}] }, { data: [{x: 10, y: 10}, {x: -10, y: -10}] }] }, options: { scales: { x: { id: 'x', type: 'linear', min: -20, max: 20 }, y: { id: 'y', type: 'linear' } } } }); expect(chart.scales.x.min).toEqual(-20); expect(chart.scales.x.max).toEqual(20); expect(chart.scales.y.min).toEqual(-10); expect(chart.scales.y.max).toEqual(10); }); }); describe('overrides', () => { it('should create new scale', () => { const chart = window.acquireChart({ type: 'scatter', data: { datasets: [{ data: [{x: 100, y: 100}, {x: -100, y: -100}] }, { data: [{x: 10, y: 10}, {x: -10, y: -10}] }] }, options: { scales: { x2: { type: 'linear', min: -20, max: 20 } } } }); expect(Object.keys(chart.scales).sort()).toEqual(['x', 'x2', 'y']); }); it('should create new scale with custom name', () => { const chart = window.acquireChart({ type: 'scatter', data: { datasets: [{ data: [{x: 100, y: 100}, {x: -100, y: -100}] }, { data: [{x: 10, y: 10}, {x: -10, y: -10}] }] }, options: { scales: { scaleX: { axis: 'x', type: 'linear', min: -20, max: 20 } } } }); expect(Object.keys(chart.scales).sort()).toEqual(['scaleX', 'x', 'y']); }); it('should throw error on scale with custom name without axis type', () => { expect(() => window.acquireChart({ type: 'scatter', data: { datasets: [{ data: [{x: 100, y: 100}, {x: -100, y: -100}] }, { data: [{x: 10, y: 10}, {x: -10, y: -10}] }] }, options: { scales: { scaleX: { type: 'linear', min: -20, max: 20 } } } })).toThrow(); }); it('should read options first to determine axis', () => { const chart = window.acquireChart({ type: 'scatter', data: { datasets: [{ data: [{x: 100, y: 100}, {x: -100, y: -100}] }, { data: [{x: 10, y: 10}, {x: -10, y: -10}] }] }, options: { scales: { xavier: { axis: 'y', type: 'linear', min: -20, max: 20 } } } }); expect(chart.scales.xavier.axis).toBe('y'); }); it('should center labels when rotated in x axis', () => { const chart = window.acquireChart({ type: 'line', data: { labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], datasets: [{ label: '# of Votes', data: [12, 19, 3, 5, 2, 3] }] }, options: { scales: { x: { ticks: { minRotation: 90, } } } } }); const mapper = item => parseFloat(item.options.translation[0].toFixed(2)); const expected = [20.15, 113.6, 207.05, 300.5, 393.95, 487.4]; const actual = chart.scales.x.getLabelItems().map(mapper); const len = expected.length; for (let i = 0; i < len; ++i) { const actualValue = actual[i]; const expectedValue = expected[i]; expect(actualValue).toBeCloseTo(expectedValue, 1); } }); }); describe('Scale Title stroke', ()=>{ function getChartWithScaleTitleStroke() { return window.acquireChart({ type: 'line', options: { scales: { x: { type: 'linear', title: { display: true, text: 'title-x', color: '#ddd', strokeWidth: 1, strokeColor: '#333' } }, y: { type: 'linear', title: { display: true, text: 'title-y', color: '#ddd', strokeWidth: 2, strokeColor: '#222' } } } } }); } function getChartWithoutScaleTitleStroke() { return window.acquireChart({ type: 'line', options: { scales: { x: { type: 'linear', title: { display: true, text: 'title-x', color: '#ddd' } }, y: { type: 'linear', title: { display: true, text: 'title-y', color: '#ddd' } } } } }); } it('should draw a scale title stroke when provided x-axis', function() { var chart = getChartWithScaleTitleStroke(); var scale = chart.scales.x; expect(scale.options.title.strokeColor).toEqual('#333'); expect(scale.options.title.strokeWidth).toEqual(1); }); it('should draw a scale title stroke when provided y-axis', function() { var chart = getChartWithScaleTitleStroke(); var scale = chart.scales.y; expect(scale.options.title.strokeColor).toEqual('#222'); expect(scale.options.title.strokeWidth).toEqual(2); }); it('should not draw a scale title stroke when not provided', function() { var chart = getChartWithoutScaleTitleStroke(); var scales = chart.scales; expect(scales.y.options.title.strokeColor).toBeUndefined(); expect(scales.y.options.title.strokeWidth).toBeUndefined(); expect(scales.x.options.title.strokeColor).toBeUndefined(); expect(scales.x.options.title.strokeWidth).toBeUndefined(); }); }); }); ================================================ FILE: test/specs/core.ticks.tests.js ================================================ function getLabels(scale) { return scale.ticks.map(t => t.label); } describe('Test tick generators', function() { // formatters are used as default config values so users want to be able to reference them it('Should expose formatters api', function() { expect(typeof Chart.Ticks).toBeDefined(); expect(typeof Chart.Ticks.formatters).toBeDefined(); expect(typeof Chart.Ticks.formatters.values).toBe('function'); expect(typeof Chart.Ticks.formatters.numeric).toBe('function'); }); it('Should generate linear spaced ticks with correct precision', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [] }], }, options: { plugins: { legend: false }, scales: { x: { type: 'linear', position: 'bottom', ticks: { callback: function(value) { return value.toString(); } } }, y: { type: 'linear', ticks: { callback: function(value) { return value.toString(); } } } } } }); var xLabels = getLabels(chart.scales.x); var yLabels = getLabels(chart.scales.y); expect(xLabels).toEqual(['0', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']); expect(yLabels).toEqual(['0', '0.1', '0.2', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']); }); it('Should generate logarithmic spaced ticks with correct precision', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [] }], }, options: { plugins: { legend: false }, scales: { x: { type: 'logarithmic', position: 'bottom', min: 0.1, max: 1, ticks: { autoSkip: false, callback: function(value) { return value.toString(); } } }, y: { type: 'logarithmic', min: 0.1, max: 1, ticks: { autoSkip: false, callback: function(value) { return value.toString(); } } } } } }); var xLabels = getLabels(chart.scales.x); var yLabels = getLabels(chart.scales.y); expect(xLabels).toEqual(['0.1', '0.11', '0.12', '0.13', '0.14', '0.15', '0.16', '0.17', '0.18', '0.19', '0.2', '0.25', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']); expect(yLabels).toEqual(['0.1', '0.11', '0.12', '0.13', '0.14', '0.15', '0.16', '0.17', '0.18', '0.19', '0.2', '0.25', '0.3', '0.4', '0.5', '0.6', '0.7', '0.8', '0.9', '1']); }); describe('formatters.numeric', function() { it('should not fail on empty or 1 item array', function() { const scale = {chart: {options: {locale: 'en'}}, options: {ticks: {format: {}}}}; expect(Chart.Ticks.formatters.numeric.apply(scale, [1, 0, []])).toEqual('1'); expect(Chart.Ticks.formatters.numeric.apply(scale, [1, 0, [{value: 1}]])).toEqual('1'); expect(Chart.Ticks.formatters.numeric.apply(scale, [1, 0, [{value: 1}, {value: 1.01}]])).toEqual('1.00'); }); }); }); ================================================ FILE: test/specs/element.arc.tests.js ================================================ // Test the rectangle element describe('Arc element tests', function() { it ('should determine if in range', function() { // Mock out the arc as if the controller put it there var arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: Math.PI / 2, x: 0, y: 0, innerRadius: 5, outerRadius: 10, options: { spacing: 0, offset: 0, borderWidth: 0 } }); expect(arc.inRange(2, 2)).toBe(false); expect(arc.inRange(7, 0)).toBe(true); expect(arc.inRange(0, 11)).toBe(false); expect(arc.inRange(Math.sqrt(32), Math.sqrt(32))).toBe(true); expect(arc.inRange(-1.0 * Math.sqrt(7), Math.sqrt(7))).toBe(false); }); it ('should determine if in range when full circle', function() { // Mock out the arc as if the controller put it there var arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: Math.PI * 2, x: 0, y: 0, innerRadius: 5, outerRadius: 10, options: { spacing: 0, offset: 0, borderWidth: 0 } }); for (const radius of [5, 7.5, 10]) { for (let angle = 0; angle <= 360; angle += 22.5) { const rad = angle / 180 * Math.PI; const x = Math.sin(rad) * radius; const y = Math.cos(rad) * radius; expect(arc.inRange(x, y)).withContext(`radius: ${radius}, angle: ${angle}`).toBeTrue(); } } for (const radius of [4, 11]) { for (let angle = 0; angle <= 360; angle += 22.5) { const rad = angle / 180 * Math.PI; const x = Math.sin(rad) * radius; const y = Math.cos(rad) * radius; expect(arc.inRange(x, y)).withContext(`radius: ${radius}, angle: ${angle}`).toBeFalse(); } } }); it ('should include spacing for in range check', function() { // Mock out the arc as if the controller put it there var arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: Math.PI / 2, x: 0, y: 0, innerRadius: 5, outerRadius: 10, options: { spacing: 10, offset: 0, borderWidth: 0 } }); expect(arc.inRange(7, 0)).toBe(false); expect(arc.inRange(15, 0)).toBe(true); }); it ('should include borderWidth for in range check', function() { // Mock out the arc as if the controller put it there var arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: Math.PI / 2, x: 0, y: 0, innerRadius: 5, outerRadius: 10, options: { spacing: 0, offset: 0, borderWidth: 10 } }); expect(arc.inRange(7, 0)).toBe(false); expect(arc.inRange(15, 0)).toBe(true); }); it ('should determine if in range, when full circle', function() { // Mock out the arc as if the controller put it there var arc = new Chart.elements.ArcElement({ startAngle: -Math.PI, endAngle: Math.PI * 1.5, x: 0, y: 0, innerRadius: 0, outerRadius: 10, circumference: Math.PI * 2, options: { spacing: 0, offset: 0, borderWidth: 0 } }); expect(arc.inRange(7, 7)).toBe(true); }); it ('should get the tooltip position', function() { // Mock out the arc as if the controller put it there var arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: Math.PI / 2, x: 0, y: 0, innerRadius: 0, outerRadius: Math.sqrt(2), options: { spacing: 0, offset: 0, borderWidth: 0 } }); var pos = arc.tooltipPosition(); expect(pos.x).toBeCloseTo(0.5); expect(pos.y).toBeCloseTo(0.5); }); it ('should get the center', function() { // Mock out the arc as if the controller put it there var arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: Math.PI / 2, x: 0, y: 0, innerRadius: 0, outerRadius: Math.sqrt(2), options: { spacing: 0, offset: 0, borderWidth: 0 } }); var center = arc.getCenterPoint(); expect(center.x).toBeCloseTo(0.5, 6); expect(center.y).toBeCloseTo(0.5, 6); }); it ('should get the center with offset and spacing', function() { // Mock out the arc as if the controller put it there var arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: Math.PI / 2, x: 0, y: 0, innerRadius: 0, outerRadius: Math.sqrt(2), options: { spacing: 10, offset: 10, borderWidth: 0 } }); var center = arc.getCenterPoint(); expect(center.x).toBeCloseTo(7.57, 1); expect(center.y).toBeCloseTo(7.57, 1); }); it ('should get the center of full circle before and after draw', function() { // Mock out the arc as if the controller put it there var arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: Math.PI * 2, x: 2, y: 2, innerRadius: 0, outerRadius: 2, options: { spacing: 0, offset: 0, borderWidth: 0 } }); var center = arc.getCenterPoint(); expect(center.x).toBeCloseTo(1, 6); expect(center.y).toBeCloseTo(2, 6); var ctx = window.createMockContext(); arc.draw(ctx); center = arc.getCenterPoint(); expect(center.x).toBeCloseTo(1, 6); expect(center.y).toBeCloseTo(2, 6); }); it('should not draw when radius < 0', function() { var ctx = window.createMockContext(); var arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: Math.PI / 2, x: 0, y: 0, innerRadius: -0.1, outerRadius: Math.sqrt(2), options: { spacing: 0, offset: 0, borderWidth: 0 } }); arc.draw(ctx); expect(ctx.getCalls().length).toBe(0); arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: Math.PI / 2, x: 0, y: 0, innerRadius: 0, outerRadius: -1, options: { spacing: 0, offset: 0, borderWidth: 0 } }); arc.draw(ctx); expect(ctx.getCalls().length).toBe(0); }); it('should draw when circular: false', function() { var arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: Math.PI * 2, x: 2, y: 2, innerRadius: 0, outerRadius: 2, options: { spacing: 0, offset: 0, borderWidth: 0, scales: { r: { grid: { circular: false, }, }, }, elements: { arc: { circular: false }, }, } }); var ctx = window.createMockContext(); arc.draw(ctx); expect(ctx.getCalls().length).toBeGreaterThan(0); }); it ('should determine not in range when angle 0', function() { // Mock out the arc as if the controller put it there var arc = new Chart.elements.ArcElement({ startAngle: 0, endAngle: 0, x: 0, y: 0, innerRadius: 0, outerRadius: 10, circumference: 0, options: { spacing: 0, offset: 0, borderWidth: 0 } }); var center = arc.getCenterPoint(); expect(arc.inRange(center.x, 1)).toBe(false); }); }); ================================================ FILE: test/specs/element.bar.tests.js ================================================ // Test the bar element describe('Bar element tests', function() { it('Should correctly identify as in range', function() { var bar = new Chart.elements.BarElement({ base: 0, width: 4, x: 10, y: 15 }); expect(bar.inRange(10, 15)).toBe(true); expect(bar.inRange(10, 10)).toBe(true); expect(bar.inRange(10, 16)).toBe(false); expect(bar.inRange(5, 5)).toBe(false); // Test when the y is below the base (negative bar) var negativeBar = new Chart.elements.BarElement({ base: 0, width: 4, x: 10, y: -15 }); expect(negativeBar.inRange(10, -16)).toBe(false); expect(negativeBar.inRange(10, 1)).toBe(false); expect(negativeBar.inRange(10, -5)).toBe(true); }); it('should get the correct tooltip position', function() { var bar = new Chart.elements.BarElement({ base: 0, width: 4, x: 10, y: 15 }); expect(bar.tooltipPosition()).toEqual({ x: 10, y: 15, }); // Test when the y is below the base (negative bar) var negativeBar = new Chart.elements.BarElement({ base: -10, width: 4, x: 10, y: -15 }); expect(negativeBar.tooltipPosition()).toEqual({ x: 10, y: -15, }); }); it('should get the center', function() { var bar = new Chart.elements.BarElement({ base: 0, width: 4, x: 10, y: 15 }); expect(bar.getCenterPoint()).toEqual({x: 10, y: 7.5}); }); }); ================================================ FILE: test/specs/element.line.tests.js ================================================ // Tests for the line element describe('Chart.elements.LineElement', function() { describe('auto', jasmine.fixture.specs('element.line')); it('should be constructed', function() { var line = new Chart.elements.LineElement({ points: [1, 2, 3, 4] }); expect(line).not.toBe(undefined); expect(line.points).toEqual([1, 2, 3, 4]); }); it('should not cache path when animations are enabled', function(done) { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [0, -1, 0], label: 'dataset1', }], labels: ['label1', 'label2', 'label3'] }, options: { animation: { duration: 50, onComplete: () => { expect(chart.getDatasetMeta(0).dataset._path).toBeUndefined(); done(); } } } }); }); }); ================================================ FILE: test/specs/element.point.tests.js ================================================ describe('Chart.elements.PointElement', function() { describe('auto', jasmine.fixture.specs('element.point')); it ('Should correctly identify as in range', function() { // Mock out the point as if we were made by the controller var point = new Chart.elements.PointElement({ options: { radius: 2, hitRadius: 3, }, x: 10, y: 15 }); expect(point.inRange(10, 15)).toBe(true); expect(point.inRange(10, 10)).toBe(false); expect(point.inRange(10, 5)).toBe(false); expect(point.inRange(5, 5)).toBe(false); }); it ('should get the correct tooltip position', function() { // Mock out the point as if we were made by the controller var point = new Chart.elements.PointElement({ options: { radius: 2, borderWidth: 6, }, x: 10, y: 15 }); expect(point.tooltipPosition()).toEqual({ x: 10, y: 15 }); }); it('should get the correct center point', function() { // Mock out the point as if we were made by the controller var point = new Chart.elements.PointElement({ options: { radius: 2, }, x: 10, y: 10 }); expect(point.getCenterPoint()).toEqual({x: 10, y: 10}); }); it ('should not draw if skipped', function() { var mockContext = window.createMockContext(); // Mock out the point as if we were made by the controller var point = new Chart.elements.PointElement({ options: { radius: 2, hitRadius: 3, }, x: 10, y: 15, skip: true }); point.draw(mockContext); expect(mockContext.getCalls()).toEqual([]); }); }); ================================================ FILE: test/specs/global.defaults.tests.js ================================================ describe('Default Configs', function() { describe('Doughnut Chart', function() { it('should return correct legend label objects', function() { var chart = window.acquireChart({ type: 'doughnut', data: { labels: ['label1', 'label2', 'label3'], datasets: [{ data: [10, 20, NaN], backgroundColor: ['red', 'green', 'blue'], borderWidth: 2, borderColor: '#000' }] }, }); var expectedCommon = { fontColor: '#666', hidden: false, strokeStyle: '#000', textAlign: undefined, lineWidth: 2, pointStyle: undefined, lineDash: [], lineDashOffset: 0, lineJoin: undefined, borderRadius: undefined, }; var expected = [{ text: 'label1', fillStyle: 'red', index: 0, ...expectedCommon, }, { text: 'label2', fillStyle: 'green', index: 1, ...expectedCommon, }, { text: 'label3', fillStyle: 'blue', index: 2, ...expectedCommon, }]; expect(chart.legend.legendItems).toEqual(expected); }); it('should return correct legend label objects with border radius', function() { var chart = window.acquireChart({ type: 'doughnut', data: { labels: ['label1'], datasets: [{ data: [10], backgroundColor: ['red'], borderWidth: 2, borderColor: '#000', borderDash: [1, 2, 3], borderDashOffset: 1, borderJoinStyle: 'miter', borderRadius: 3, }] }, options: { plugins: { legend: { labels: { useBorderRadius: true, borderRadius: 5, textAlign: 'left', } } } } }); var expected = [{ text: 'label1', fillStyle: 'red', index: 0, fontColor: '#666', hidden: false, strokeStyle: '#000', textAlign: 'left', lineWidth: 2, pointStyle: undefined, lineDash: [1, 2, 3], lineDashOffset: 1, lineJoin: 'miter', borderRadius: 5 }]; expect(chart.legend.legendItems).toEqual(expected); }); it('should hide the correct arc when a legend item is clicked', function() { var config = Chart.overrides.doughnut; var chart = window.acquireChart({ type: 'doughnut', data: { labels: ['label1', 'label2', 'label3'], datasets: [{ data: [10, 20, NaN], backgroundColor: ['red', 'green', 'blue'], borderWidth: 2, borderColor: '#000' }] }, }); spyOn(chart, 'update').and.callThrough(); var legendItem = chart.legend.legendItems[0]; config.plugins.legend.onClick(null, legendItem, chart.legend); expect(chart.getDataVisibility(0)).toBe(false); expect(chart.update).toHaveBeenCalled(); config.plugins.legend.onClick(null, legendItem, chart.legend); expect(chart.getDataVisibility(0)).toBe(true); }); }); describe('Polar Area Chart', function() { it('should return correct legend label objects', function() { var chart = window.acquireChart({ type: 'polarArea', data: { labels: ['label1', 'label2', 'label3'], datasets: [{ data: [10, 20, NaN], backgroundColor: ['red', 'green', 'blue'], borderWidth: 2, borderColor: '#000' }] }, }); var expected = [{ text: 'label1', fillStyle: 'red', fontColor: '#666', hidden: false, index: 0, strokeStyle: '#000', textAlign: undefined, lineWidth: 2, pointStyle: undefined }, { text: 'label2', fillStyle: 'green', fontColor: '#666', hidden: false, index: 1, strokeStyle: '#000', textAlign: undefined, lineWidth: 2, pointStyle: undefined }, { text: 'label3', fillStyle: 'blue', fontColor: '#666', hidden: false, index: 2, strokeStyle: '#000', textAlign: undefined, lineWidth: 2, pointStyle: undefined }]; expect(chart.legend.legendItems).toEqual(expected); }); it('should hide the correct arc when a legend item is clicked', function() { var config = Chart.overrides.polarArea; var chart = window.acquireChart({ type: 'polarArea', data: { labels: ['label1', 'label2', 'label3'], datasets: [{ data: [10, 20, NaN], backgroundColor: ['red', 'green', 'blue'], borderWidth: 2, borderColor: '#000' }] }, }); spyOn(chart, 'update').and.callThrough(); var legendItem = chart.legend.legendItems[0]; config.plugins.legend.onClick(null, legendItem, chart.legend); expect(chart.getDataVisibility(0)).toBe(false); expect(chart.update).toHaveBeenCalled(); config.plugins.legend.onClick(null, legendItem, chart.legend); expect(chart.getDataVisibility(0)).toBe(true); }); }); }); ================================================ FILE: test/specs/global.namespace.tests.js ================================================ describe('Chart namespace', function() { describe('Chart', function() { it('should a function (constructor)', function() { expect(Chart instanceof Function).toBeTruthy(); }); it('should define "core" properties', function() { expect(Chart instanceof Function).toBeTruthy(); expect(Chart.Animation instanceof Object).toBeTruthy(); expect(Chart.Animations instanceof Object).toBeTruthy(); expect(Chart.defaults instanceof Object).toBeTruthy(); expect(Chart.Element instanceof Object).toBeTruthy(); expect(Chart.Interaction instanceof Object).toBeTruthy(); expect(Chart.layouts instanceof Object).toBeTruthy(); expect(Chart.platforms.BasePlatform instanceof Function).toBeTruthy(); expect(Chart.platforms.BasicPlatform instanceof Function).toBeTruthy(); expect(Chart.platforms.DomPlatform instanceof Function).toBeTruthy(); expect(Chart.registry instanceof Object).toBeTruthy(); expect(Chart.Scale instanceof Object).toBeTruthy(); expect(Chart.Ticks instanceof Object).toBeTruthy(); }); }); describe('Chart.elements', function() { it('should contains "elements" classes', function() { expect(Chart.elements.ArcElement instanceof Function).toBeTruthy(); expect(Chart.elements.BarElement instanceof Function).toBeTruthy(); expect(Chart.elements.LineElement instanceof Function).toBeTruthy(); expect(Chart.elements.PointElement instanceof Function).toBeTruthy(); }); }); describe('Chart.helpers', function() { it('should be an object', function() { expect(Chart.helpers instanceof Object).toBeTruthy(); }); }); }); ================================================ FILE: test/specs/helpers.canvas.tests.js ================================================ 'use strict'; describe('Chart.helpers.canvas', function() { describe('auto', jasmine.fixture.specs('helpers')); var helpers = Chart.helpers; describe('clearCanvas', function() { it('should clear the chart canvas', function() { var chart = acquireChart({}, { canvas: { style: 'width: 150px; height: 245px' } }); spyOn(chart.ctx, 'clearRect'); helpers.clearCanvas(chart.canvas, chart.ctx); expect(chart.ctx.clearRect.calls.count()).toBe(1); expect(chart.ctx.clearRect.calls.first().object).toBe(chart.ctx); expect(chart.ctx.clearRect.calls.first().args).toEqual([0, 0, 150, 245]); }); it('should not throw error when chart is null', function() { function createAndClearChart() { var chart = acquireChart({}, { canvas: null }); // explicitly set canvas and ctx to null since setting it in acquireChart doesn't do anything chart.canvas = null; chart.ctx = null; helpers.clearCanvas(chart.canvas, chart.ctx); } expect(createAndClearChart).not.toThrow(); }); }); describe('isPointInArea', function() { it('should return true when no area is provided', function() { expect(helpers._isPointInArea({x: 1, y: 1})).toBe(true); }); it('should determine if a point is in the area', function() { var isPointInArea = helpers._isPointInArea; var area = {left: 0, top: 0, right: 512, bottom: 256}; expect(isPointInArea({x: 0, y: 0}, area)).toBe(true); expect(isPointInArea({x: -1e-12, y: -1e-12}, area)).toBe(true); expect(isPointInArea({x: 512, y: 256}, area)).toBe(true); expect(isPointInArea({x: 512 + 1e-12, y: 256 + 1e-12}, area)).toBe(true); expect(isPointInArea({x: -0.5, y: 0}, area)).toBe(false); expect(isPointInArea({x: 0, y: 256.5}, area)).toBe(false); }); }); it('should return the width of the longest text in an Array and 2D Array', function() { var context = window.createMockContext(); var font = "normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"; var arrayOfThings1D = ['FooBar', 'Bar']; var arrayOfThings2D = [['FooBar_1', 'Bar_2'], 'Foo_1']; // Regardless 'FooBar' is the longest label it should return (characters * 10) expect(helpers._longestText(context, font, arrayOfThings1D, {})).toEqual(60); expect(helpers._longestText(context, font, arrayOfThings2D, {})).toEqual(80); // We check to make sure we made the right calls to the canvas. expect(context.getCalls()).toEqual([{ name: 'save', args: [] }, { name: 'setFont', args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"], }, { name: 'measureText', args: ['FooBar'] }, { name: 'measureText', args: ['Bar'] }, { name: 'restore', args: [] }, { name: 'save', args: [] }, { name: 'setFont', args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"], }, { name: 'measureText', args: ['FooBar_1'] }, { name: 'measureText', args: ['Bar_2'] }, { name: 'measureText', args: ['Foo_1'] }, { name: 'restore', args: [] }]); }); it('compare text with current longest and update', function() { var context = window.createMockContext(); var data = {}; var gc = []; var longest = 70; expect(helpers._measureText(context, data, gc, longest, 'foobar')).toEqual(70); expect(helpers._measureText(context, data, gc, longest, 'foobar_')).toEqual(70); expect(helpers._measureText(context, data, gc, longest, 'foobar_1')).toEqual(80); // We check to make sure we made the right calls to the canvas. expect(context.getCalls()).toEqual([{ name: 'measureText', args: ['foobar'] }, { name: 'measureText', args: ['foobar_'] }, { name: 'measureText', args: ['foobar_1'] }]); }); describe('renderText', function() { it('should render multiple lines of text', function() { var context = window.createMockContext(); var font = {string: '12px arial', lineHeight: 20}; helpers.renderText(context, ['foo', 'foo2'], 0, 0, font); expect(context.getCalls()).toEqual([{ name: 'save', args: [], }, { name: 'setFont', args: ['12px arial'], }, { name: 'fillText', args: ['foo', 0, 0, undefined], }, { name: 'fillText', args: ['foo2', 0, 20, undefined], }, { name: 'restore', args: [], }]); }); it('should accept the text maxWidth', function() { var context = window.createMockContext(); var font = {string: '12px arial', lineHeight: 20}; helpers.renderText(context, 'foo', 0, 0, font, {maxWidth: 30}); expect(context.getCalls()).toEqual([{ name: 'save', args: [], }, { name: 'setFont', args: ['12px arial'], }, { name: 'fillText', args: ['foo', 0, 0, 30], }, { name: 'restore', args: [], }]); }); it('should underline the text', function() { var context = window.createMockContext(); var font = {string: '12px arial', lineHeight: 20}; helpers.renderText(context, 'foo', 0, 0, font, {decorationWidth: 3, underline: true}); expect(context.getCalls()).toEqual([{ name: 'save', args: [], }, { name: 'setFont', args: ['12px arial'], }, { name: 'fillText', args: ['foo', 0, 0, undefined], }, { name: 'measureText', args: ['foo'], }, { name: 'setStrokeStyle', args: [null], }, { name: 'beginPath', args: [], }, { name: 'setLineWidth', args: [3], }, { name: 'moveTo', args: [-15, 8], }, { name: 'lineTo', args: [25, 8], }, { name: 'stroke', args: [], }, { name: 'restore', args: [], }]); }); it('should strikethrough the text', function() { var context = window.createMockContext(); var font = {string: '12px arial', lineHeight: 20}; helpers.renderText(context, 'foo', 0, 0, font, {strikethrough: true}); expect(context.getCalls()).toEqual([{ name: 'save', args: [], }, { name: 'setFont', args: ['12px arial'], }, { name: 'fillText', args: ['foo', 0, 0, undefined], }, { name: 'measureText', args: ['foo'], }, { name: 'setStrokeStyle', args: [null], }, { name: 'beginPath', args: [], }, { name: 'setLineWidth', args: [2], }, { name: 'moveTo', args: [-15, 2], }, { name: 'lineTo', args: [25, 2], }, { name: 'stroke', args: [], }, { name: 'restore', args: [], }]); }); it('should set the fill style if supplied', function() { var context = window.createMockContext(); var font = {string: '12px arial', lineHeight: 20}; helpers.renderText(context, 'foo', 0, 0, font, {color: 'red'}); expect(context.getCalls()).toEqual([{ name: 'save', args: [], }, { name: 'setFont', args: ['12px arial'], }, { name: 'setFillStyle', args: ['red'], }, { name: 'fillText', args: ['foo', 0, 0, undefined], }, { name: 'restore', args: [], }]); }); it('should set the stroke style if supplied', function() { var context = window.createMockContext(); var font = {string: '12px arial', lineHeight: 20}; helpers.renderText(context, 'foo', 0, 0, font, {strokeColor: 'red', strokeWidth: 2}); expect(context.getCalls()).toEqual([{ name: 'save', args: [], }, { name: 'setFont', args: ['12px arial'], }, { name: 'setStrokeStyle', args: ['red'], }, { name: 'setLineWidth', args: [2], }, { name: 'strokeText', args: ['foo', 0, 0, undefined], }, { name: 'fillText', args: ['foo', 0, 0, undefined], }, { name: 'restore', args: [], }]); }); it('should set the text alignment', function() { var context = window.createMockContext(); var font = {string: '12px arial', lineHeight: 20}; helpers.renderText(context, 'foo', 0, 0, font, {textAlign: 'left', textBaseline: 'middle'}); expect(context.getCalls()).toEqual([{ name: 'save', args: [], }, { name: 'setFont', args: ['12px arial'], }, { name: 'setTextAlign', args: ['left'], }, { name: 'setTextBaseline', args: ['middle'], }, { name: 'fillText', args: ['foo', 0, 0, undefined], }, { name: 'restore', args: [], }]); }); it('should translate and rotate text', function() { var context = window.createMockContext(); var font = {string: '12px arial', lineHeight: 20}; helpers.renderText(context, 'foo', 0, 0, font, {rotation: 90, translation: [10, 20]}); expect(context.getCalls()).toEqual([{ name: 'save', args: [], }, { name: 'setFont', args: ['12px arial'], }, { name: 'translate', args: [10, 20], }, { name: 'rotate', args: [90], }, { name: 'fillText', args: ['foo', 0, 0, undefined], }, { name: 'restore', args: [], }]); }); }); }); ================================================ FILE: test/specs/helpers.collection.tests.js ================================================ const {_filterBetween, _lookup, _lookupByKey, _rlookupByKey} = Chart.helpers; describe('helpers.collection', function() { it('Should do binary search', function() { const data = [0, 2, 6, 9]; expect(_lookup(data, 0)).toEqual({lo: 0, hi: 1}); expect(_lookup(data, 1)).toEqual({lo: 0, hi: 1}); expect(_lookup(data, 3)).toEqual({lo: 1, hi: 2}); expect(_lookup(data, 6)).toEqual({lo: 1, hi: 2}); expect(_lookup(data, 9)).toEqual({lo: 2, hi: 3}); }); it('Should do binary search by key', function() { const data = [{x: 0}, {x: 2}, {x: 6}, {x: 9}]; expect(_lookupByKey(data, 'x', 0)).toEqual({lo: 0, hi: 1}); expect(_lookupByKey(data, 'x', 1)).toEqual({lo: 0, hi: 1}); expect(_lookupByKey(data, 'x', 3)).toEqual({lo: 1, hi: 2}); expect(_lookupByKey(data, 'x', 6)).toEqual({lo: 1, hi: 2}); expect(_lookupByKey(data, 'x', 9)).toEqual({lo: 2, hi: 3}); }); it('Should do binary search by key with last', () => { expect(_lookupByKey([{x: 0}, {x: 2}, {x: 6}, {x: 9}], 'x', 25, true)).toEqual({lo: 2, hi: 3}); expect(_lookupByKey([{x: 0}, {x: 2}, {x: 9}, {x: 9}], 'x', 25, true)).toEqual({lo: 2, hi: 3}); expect(_lookupByKey([{x: 0}, {x: 2}, {x: 9}, {x: 9}, {x: 22}], 'x', 25, true)).toEqual({lo: 3, hi: 4}); expect(_lookupByKey([{x: 0}, {x: 2}, {x: 25}, {x: 28}], 'x', 25, true)).toEqual({lo: 1, hi: 2}); expect(_lookupByKey([{x: 0}, {x: 2}, {x: 25}, {x: 25}], 'x', 25, true)).toEqual({lo: 2, hi: 3}); expect(_lookupByKey([{x: 0}, {x: 2}, {x: 25}, {x: 25}, {x: 28}], 'x', 25, true)).toEqual({lo: 2, hi: 3}); expect(_lookupByKey([{x: 0}, {x: 2}, {x: 25}, {x: 25}, {x: 25}, {x: 28}, {x: 29}], 'x', 25, true)).toEqual({lo: 3, hi: 4}); }); it('Should do reverse binary search by key', function() { const data = [{x: 10}, {x: 7}, {x: 3}, {x: 0}]; expect(_rlookupByKey(data, 'x', 0)).toEqual({lo: 2, hi: 3}); expect(_rlookupByKey(data, 'x', 3)).toEqual({lo: 2, hi: 3}); expect(_rlookupByKey(data, 'x', 5)).toEqual({lo: 1, hi: 2}); expect(_rlookupByKey(data, 'x', 8)).toEqual({lo: 0, hi: 1}); expect(_rlookupByKey(data, 'x', 10)).toEqual({lo: 0, hi: 1}); }); it('Should filter a sorted array', function() { expect(_filterBetween([1, 2, 3, 4, 5, 6, 7, 8, 9], 5, 8)).toEqual([5, 6, 7, 8]); expect(_filterBetween([1], 1, 1)).toEqual([1]); expect(_filterBetween([1583049600000], 1584816327553, 1585680327553)).toEqual([]); }); }); ================================================ FILE: test/specs/helpers.color.tests.js ================================================ const {color, getHoverColor} = Chart.helpers; describe('Color helper', function() { function isColorInstance(obj) { return typeof obj === 'object' && obj.valid; } it('should return a color when called with a color', function() { expect(isColorInstance(color('rgb(1, 2, 3)'))).toBe(true); }); }); describe('Background hover color helper', function() { it('should return a modified version of color when called with a color', function() { var originalColorRGB = 'rgb(70, 191, 189)'; expect(getHoverColor('#46BFBD')).not.toEqual(originalColorRGB); }); }); describe('color and getHoverColor helpers', function() { it('should return a CanvasPattern when called with a CanvasPattern', function(done) { var dots = new Image(); dots.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAOCAMAAAAolt3jAAAAD1BMVEUAAAD///////////////+PQt5oAAAABXRSTlMAHlFhZsfk/BEAAAAqSURBVHgBY2BgZGJmYmSAAUYWEIDzmcBcJhiXGcxlRpPFrhdmMiqgvX0AcGIBEUAo6UAAAAAASUVORK5CYII='; dots.onload = function() { var chartContext = document.createElement('canvas').getContext('2d'); var patternCanvas = document.createElement('canvas'); var patternContext = patternCanvas.getContext('2d'); var pattern = patternContext.createPattern(dots, 'repeat'); patternContext.fillStyle = pattern; var chartPattern = chartContext.createPattern(patternCanvas, 'repeat'); expect(color(chartPattern) instanceof CanvasPattern).toBe(true); expect(getHoverColor(chartPattern) instanceof CanvasPattern).toBe(true); done(); }; }); it('should return a CanvasGradient when called with a CanvasGradient', function() { var context = document.createElement('canvas').getContext('2d'); var gradient = context.createLinearGradient(0, 1, 2, 3); expect(color(gradient) instanceof CanvasGradient).toBe(true); expect(getHoverColor(gradient) instanceof CanvasGradient).toBe(true); }); }); ================================================ FILE: test/specs/helpers.config.tests.js ================================================ describe('Chart.helpers.config', function() { const {getHoverColor, _createResolver, _attachContext} = Chart.helpers; describe('_createResolver', function() { it('should resolve to raw values', function() { const defaults = { color: 'red', backgroundColor: 'green', hoverColor: (ctx, options) => getHoverColor(options.color) }; const options = { color: 'blue' }; const resolver = _createResolver([options, defaults]); expect(resolver.color).toEqual('blue'); expect(resolver.backgroundColor).toEqual('green'); expect(resolver.hoverColor).toEqual(defaults.hoverColor); }); it('should resolve to parent scopes, when _fallback is true', function() { const descriptors = { _fallback: true }; const defaults = { root: true, sub: { child: true } }; const options = { child: 'sub default comes before this', opt: 'opt' }; const resolver = _createResolver([options, defaults, descriptors]); const sub = resolver.sub; expect(sub.root).toEqual(true); expect(sub.child).toEqual(true); expect(sub.opt).toEqual('opt'); }); it('should support overriding options', function() { const defaults = { option1: 'defaults1', option2: 'defaults2', option3: 'defaults3', }; const options = { option1: 'options1', option2: 'options2' }; const overrides = { option1: 'override1' }; const resolver = _createResolver([options, defaults]); expect(resolver).toEqualOptions({ option1: 'options1', option2: 'options2', option3: 'defaults3' }); expect(resolver.override(overrides)).toEqualOptions({ option1: 'override1', option2: 'options2', option3: 'defaults3' }); }); it('should support common object methods', function() { const defaults = { option1: 'defaults' }; class Options { constructor() { this.option2 = 'options'; } get getter() { return 'options getter'; } } const options = new Options(); const resolver = _createResolver([options, defaults]); expect(Object.prototype.hasOwnProperty.call(resolver, 'option2')).toBeTrue(); expect(Object.prototype.hasOwnProperty.call(resolver, 'option1')).toBeFalse(); expect(Object.prototype.hasOwnProperty.call(resolver, 'getter')).toBeFalse(); expect(Object.prototype.hasOwnProperty.call(resolver, 'nonexistent')).toBeFalse(); expect(Object.keys(resolver)).toEqual(['option2']); expect(Object.getOwnPropertyNames(resolver)).toEqual(['option2', 'option1']); expect('option2' in resolver).toBeTrue(); expect('option1' in resolver).toBeTrue(); expect('getter' in resolver).toBeFalse(); expect('nonexistent' in resolver).toBeFalse(); expect(resolver instanceof Options).toBeTrue(); expect(resolver.getter).toEqual('options getter'); }); it('should not fail on when options are frozen', function() { function create() { const defaults = Object.freeze({default: true}); const options = Object.freeze({value: true}); return _createResolver([options, defaults]); } expect(create).not.toThrow(); }); describe('_fallback', function() { it('should follow simple _fallback', function() { const defaults = { interaction: { mode: 'test', priority: 'fall' }, hover: { _fallback: 'interaction', priority: 'main' } }; const options = { interaction: { a: 1 }, hover: { b: 2 } }; const resolver = _createResolver([options, defaults]); expect(resolver.hover).toEqualOptions({ mode: 'test', priority: 'main', a: 1, b: 2 }); }); it('should support _fallback as function', function() { const descriptors = { _fallback: (prop, value) => prop === 'hover' && value.shouldFall && 'interaction', }; const defaults = { interaction: { mode: 'test', priority: 'fall' }, hover: { priority: 'main' } }; const options = { interaction: { a: 1 }, hover: { shouldFall: true, b: 2 } }; const resolver = _createResolver([options, defaults, descriptors]); expect(resolver.hover).toEqualOptions({ mode: 'test', priority: 'main', a: 1, b: 2 }); }); it('should not fallback by default', function() { const defaults = { hover: { a: 'defaults.hover' }, controllers: { y: 'defaults.controllers', bar: { z: 'defaults.controllers.bar', hover: { b: 'defaults.controllers.bar.hover' } } }, x: 'defaults root' }; const options = { x: 'options', hover: { c: 'options.hover', sub: { f: 'options.hover.sub' } }, controllers: { y: 'options.controllers', bar: { z: 'options.controllers.bar', hover: { d: 'options.controllers.bar.hover', sub: { e: 'options.controllers.bar.hover.sub' } } } } }; const resolver = _createResolver([options, options.controllers.bar, options.controllers, defaults.controllers.bar, defaults.controllers, defaults]); expect(resolver.hover).toEqualOptions({ a: 'defaults.hover', b: 'defaults.controllers.bar.hover', c: 'options.hover', d: 'options.controllers.bar.hover', e: undefined, f: undefined, x: undefined, y: undefined, z: undefined }); expect(resolver.hover.sub).toEqualOptions({ a: undefined, b: undefined, c: undefined, d: undefined, e: 'options.controllers.bar.hover.sub', f: 'options.hover.sub', x: undefined, y: undefined, z: undefined }); }); it('should fallback to specific scope', function() { const defaults = { hover: { _fallback: 'hover', a: 'defaults.hover' }, controllers: { y: 'defaults.controllers', bar: { z: 'defaults.controllers.bar', hover: { b: 'defaults.controllers.bar.hover' } } }, x: 'defaults root' }; const options = { x: 'options', hover: { c: 'options.hover', sub: { f: 'options.hover.sub' } }, controllers: { y: 'options.controllers', bar: { z: 'options.controllers.bar', hover: { d: 'options.controllers.bar.hover', sub: { e: 'options.controllers.bar.hover.sub' } } } } }; const resolver = _createResolver([options, options.controllers.bar, options.controllers, defaults.controllers.bar, defaults.controllers, defaults]); expect(resolver.hover).toEqualOptions({ a: 'defaults.hover', b: 'defaults.controllers.bar.hover', c: 'options.hover', d: 'options.controllers.bar.hover', e: undefined, f: undefined, x: undefined, y: undefined, z: undefined }); expect(resolver.hover.sub).toEqualOptions({ a: 'defaults.hover', b: 'defaults.controllers.bar.hover', c: 'options.hover', d: 'options.controllers.bar.hover', e: 'options.controllers.bar.hover.sub', f: 'options.hover.sub', x: undefined, y: undefined, z: undefined }); }); it('should fallback through multiple routes', function() { const descriptors = { _fallback: 'level1', level1: { _fallback: 'root' }, level2: { _fallback: 'level1' } }; const defaults = { root: { a: 'root' }, level1: { b: 'level1', }, level2: { level1: { g: 'level2.level1' }, c: 'level2', sublevel1: { d: 'sublevel1' }, sublevel2: { e: 'sublevel2', level1: { f: 'sublevel2.level1' } } } }; const resolver = _createResolver([defaults, descriptors]); expect(resolver.level1).toEqualOptions({ a: 'root', b: 'level1', c: undefined }); expect(resolver.level2).toEqualOptions({ a: 'root', b: 'level1', c: 'level2', d: undefined }); expect(resolver.level2.sublevel1).toEqualOptions({ a: 'root', b: 'level1', c: undefined, d: 'sublevel1', e: undefined, f: undefined, g: 'level2.level1' }); expect(resolver.level2.sublevel2).toEqualOptions({ a: 'root', b: 'level1', c: undefined, d: undefined, e: 'sublevel2', f: undefined, g: 'level2.level1' }); expect(resolver.level2.sublevel2.level1).toEqualOptions({ a: 'root', b: 'level1', c: undefined, d: undefined, e: undefined, f: 'sublevel2.level1', g: undefined // same key only included from immediate parents and root }); }); it('should fallback through multiple routes (animations)', function() { const descriptors = { animations: { _fallback: 'animation', }, }; const defaults = { animation: { duration: 1000, easing: 'easeInQuad' }, animations: { colors: { properties: ['color', 'backgroundColor'], type: 'color' }, numbers: { properties: ['x', 'y'], type: 'number' } }, transitions: { resize: { animation: { duration: 0 } }, show: { animation: { duration: 400 }, animations: { colors: { from: 'transparent' } } } } }; const options = { animation: { easing: 'linear' }, animations: { colors: { properties: ['color', 'borderColor', 'backgroundColor'], }, duration: { properties: ['a', 'b'], type: 'boolean' } } }; const show = _createResolver([options, defaults.transitions.show, defaults, descriptors]); expect(show.animation).toEqualOptions({ duration: 400, easing: 'linear' }); expect(show.animations.colors._scopes).toEqual([ options.animations.colors, defaults.transitions.show.animations.colors, defaults.animations.colors, options.animation, defaults.transitions.show.animation, defaults.animation ]); expect(show.animations.colors).toEqualOptions({ duration: 400, from: 'transparent', easing: 'linear', type: 'color', properties: ['color', 'borderColor', 'backgroundColor'] }); expect(show.animations.duration).toEqualOptions({ duration: 400, easing: 'linear', type: 'boolean', properties: ['a', 'b'] }); expect(Object.getOwnPropertyNames(show.animations).filter(k => Chart.helpers.isObject(show.animations[k]))).toEqual([ 'colors', 'duration', 'numbers', ]); const def = _createResolver([options, defaults, descriptors]); expect(def.animation).toEqualOptions({ duration: 1000, easing: 'linear' }); expect(def.animations.colors._scopes).toEqual([ options.animations.colors, defaults.animations.colors, options.animation, defaults.animation ]); expect(def.animations.colors).toEqualOptions({ duration: 1000, easing: 'linear', type: 'color', properties: ['color', 'borderColor', 'backgroundColor'] }); expect(def.animations.duration).toEqualOptions({ duration: 1000, easing: 'linear', type: 'boolean', properties: ['a', 'b'] }); expect(Object.getOwnPropertyNames(def.animations).filter(k => Chart.helpers.isObject(show.animations[k]))).toEqual([ 'colors', 'duration', 'numbers', ]); }); }); describe('setting values', function() { it('should set values to first scope', function() { const defaults = { value: true }; const options = {}; const resolver = _createResolver([options, defaults]); resolver.value = false; expect(options.value).toBeFalse(); expect(defaults.value).toBeTrue(); expect(resolver.value).toBeFalse(); }); it('should set values of sub-objects to first scope', function() { const defaults = { sub: { value: true } }; const options = {}; const resolver = _createResolver([options, defaults]); resolver.sub.value = false; expect(options.sub.value).toBeFalse(); expect(defaults.sub.value).toBeTrue(); expect(resolver.sub.value).toBeFalse(); }); it('should throw when setting a value and options is frozen', function() { const defaults = Object.freeze({default: true}); const options = Object.freeze({value: true}); const resolver = _createResolver([options, defaults]); function set() { resolver.value = false; } expect(set).toThrow(); }); }); }); describe('_attachContext', function() { it('should resolve to final values', function() { const defaults = { color: 'red', backgroundColor: 'green', hoverColor: (ctx, options) => getHoverColor(options.color) }; const options = { color: ['white', 'blue'] }; const resolver = _createResolver([options, defaults]); const opts = _attachContext(resolver, {index: 1}); expect(opts.color).toEqual('blue'); expect(opts.backgroundColor).toEqual('green'); expect(opts.hoverColor).toEqual(getHoverColor('blue')); }); it('should thrown on recursion', function() { const options = { foo: (ctx, opts) => opts.bar, bar: (ctx, opts) => opts.xyz, xyz: (ctx, opts) => opts.foo }; const resolver = _createResolver([options]); const opts = _attachContext(resolver, {test: true}); expect(function() { return opts.foo; }).toThrowError('Recursion detected: foo->bar->xyz->foo'); }); it('should support scriptable options in subscopes', function() { const defaults = { elements: { point: { backgroundColor: 'red' } } }; const options = { elements: { point: { borderColor: (ctx, opts) => getHoverColor(opts.backgroundColor) } } }; const resolver = _createResolver([options, defaults]); const opts = _attachContext(resolver, {}); expect(opts.elements.point.borderColor).toEqual(getHoverColor('red')); expect(opts.elements.point.backgroundColor).toEqual('red'); }); it('same resolver should be usable with multiple contexts', function() { const defaults = { animation: { delay: 10 } }; const options = { animation: (ctx) => ctx.index === 0 ? {duration: 1000} : {duration: 500} }; const resolver = _createResolver([options, defaults]); const opts1 = _attachContext(resolver, {index: 0}); const opts2 = _attachContext(resolver, {index: 1}); expect(opts1.animation.duration).toEqual(1000); expect(opts1.animation.delay).toEqual(10); expect(opts2.animation.duration).toEqual(500); expect(opts2.animation.delay).toEqual(10); }); it('should fall back from object returned from scriptable option', function() { const defaults = { mainScope: { main: true, subScope: { sub: true } } }; const options = { mainScope: (ctx) => ({ mainTest: ctx.contextValue, subScope: { subText: 'a' } }) }; const opts = _attachContext(_createResolver([options, defaults]), {contextValue: 'test'}); expect(opts.mainScope).toEqualOptions({ main: true, mainTest: 'test', subScope: { sub: true, subText: 'a' } }); }); it('should resolve array of non-indexable objects properly', function() { const defaults = { label: { value: 42, text: (ctx) => ctx.text }, labels: { _fallback: 'label', _indexable: false } }; const options = { labels: [{text: 'a'}, {text: 'b'}, {value: 1}] }; const opts = _attachContext(_createResolver([options, defaults]), {text: 'context'}); expect(opts).toEqualOptions({ labels: [ { text: 'a', value: 42 }, { text: 'b', value: 42 }, { text: 'context', value: 1 } ] }); }); it('should call _fallback with proper value from array when descriptor is object', function() { const spy = jasmine.createSpy('fallback'); const descriptors = { items: { _fallback: spy } }; const options = { items: [{test: true}] }; const resolver = _createResolver([options, descriptors]); const opts = _attachContext(resolver, {dymmy: true}); const item0 = opts.items[0]; expect(item0.test).toEqual(true); expect(spy).toHaveBeenCalledWith('items', options.items[0]); }); it('should call _fallback with proper value from array when descriptor and defaults are objects', function() { const spy = jasmine.createSpy('fallback'); const descriptors = { items: { _fallback: spy } }; const defaults = { items: { type: 'defaultType' } }; const options = { items: [{test: true}] }; const resolver = _createResolver([options, defaults, descriptors]); const opts = _attachContext(resolver, {dymmy: true}); const item0 = opts.items[0]; expect(item0.test).toEqual(true); expect(spy).toHaveBeenCalledWith('items', options.items[0]); }); it('should support overriding options', function() { const options = { fn1: ctx => ctx.index, fn2: ctx => ctx.type }; const override = { fn1: ctx => ctx.index * 2 }; const opts = _attachContext(_createResolver([options]), {index: 2, type: 'test'}); expect(opts).toEqualOptions({ fn1: 2, fn2: 'test' }); expect(opts.override(override)).toEqualOptions({ fn1: 4, fn2: 'test' }); }); it('should support changing context', function() { const opts = _attachContext(_createResolver([{fn: ctx => ctx.test}]), {test: 1}); expect(opts.fn).toEqual(1); expect(opts.setContext({test: 2}).fn).toEqual(2); expect(opts.fn).toEqual(1); }); it('should support common object methods', function() { const defaults = { option1: 'defaults' }; class Options { constructor() { this.option2 = () => 'options'; } get getter() { return 'options getter'; } } const options = new Options(); const resolver = _createResolver([options, defaults]); const opts = _attachContext(resolver, {index: 1}); expect(Object.prototype.hasOwnProperty.call(opts, 'option2')).toBeTrue(); expect(Object.prototype.hasOwnProperty.call(opts, 'option1')).toBeFalse(); expect(Object.prototype.hasOwnProperty.call(opts, 'getter')).toBeFalse(); expect(Object.prototype.hasOwnProperty.call(opts, 'nonexistent')).toBeFalse(); expect(Object.keys(opts)).toEqual(['option2']); expect(Object.getOwnPropertyNames(opts)).toEqual(['option2', 'option1']); expect('option2' in opts).toBeTrue(); expect('option1' in opts).toBeTrue(); expect('getter' in opts).toBeFalse(); expect('nonexistent' in opts).toBeFalse(); expect(opts instanceof Options).toBeTrue(); expect(opts.getter).toEqual('options getter'); expect('test' in opts).toBeFalse(); expect(opts.test).toBeUndefined(); opts.test = true; expect('test' in opts).toBeTrue(); expect(opts.test).toBeTrue(); delete opts.test; expect('test' in opts).toBeFalse(); opts.test = (ctx) => ctx.index; expect('test' in opts).toBeTrue(); expect(opts.test).toBe(1); delete opts.test; expect('test' in opts).toBeFalse(); }); it('should not create proxy for adapters', function() { const defaults = { scales: { time: { adapters: { date: { locale: { method: (arg) => arg === undefined ? 'ok' : 'fail' } } } } } }; const resolver = _createResolver([{}, defaults]); const opts = _attachContext(resolver, {index: 1}); const fn = opts.scales.time.adapters.date.locale.method; expect(typeof fn).toBe('function'); expect(fn()).toEqual('ok'); }); it('should not create proxy for objects with custom constructor', function() { class MyClass { constructor() { this.string = 'test string'; } method(arg) { return arg === undefined ? 'ok' : 'fail'; } } const defaults = { test: new MyClass() }; const resolver = _createResolver([{}, defaults]); const opts = _attachContext(resolver, {index: 1}); const fn = opts.test.method; expect(typeof fn).toBe('function'); expect(fn()).toEqual('ok'); expect(opts.test.string).toEqual('test string'); expect(opts.test.constructor).toEqual(MyClass); }); it('should properly set value to object in array of objects', function() { const defaults = {}; const options = { annotations: [{ value: 10 }, { value: 20 }] }; const resolver = _attachContext(_createResolver([options, defaults]), {test: true}); expect(resolver.annotations[0].value).toEqual(10); resolver.annotations[0].value = 15; expect(options.annotations[0].value).toEqual(15); expect(options.annotations[1].value).toEqual(20); }); describe('_indexable and _scriptable', function() { it('should default to true', function() { const options = { array: [1, 2, 3], func: (ctx) => ctx.index * 10 }; const opts = _attachContext(_createResolver([options]), {index: 1}); expect(opts.array).toEqual(2); expect(opts.func).toEqual(10); }); it('should allow false', function() { const fn = () => 'test'; const options = { _indexable: false, _scriptable: false, array: [1, 2, 3], func: fn }; const opts = _attachContext(_createResolver([options]), {index: 1}); expect(opts.array).toEqual([1, 2, 3]); expect(opts.func).toEqual(fn); expect(opts.func()).toEqual('test'); }); it('should allow function', function() { const fn = () => 'test'; const options = { _indexable: (prop) => prop !== 'array', _scriptable: (prop) => prop === 'func', array: [1, 2, 3], array2: ['a', 'b', 'c'], func: fn }; const opts = _attachContext(_createResolver([options]), {index: 1}); expect(opts.array).toEqual([1, 2, 3]); expect(opts.func).toEqual('test'); expect(opts.array2).toEqual('b'); }); }); }); }); ================================================ FILE: test/specs/helpers.core.tests.js ================================================ 'use strict'; describe('Chart.helpers.core', function() { var helpers = Chart.helpers; describe('noop', function() { it('should be callable', function() { expect(helpers.noop).toBeDefined(); expect(typeof helpers.noop).toBe('function'); expect(typeof helpers.noop.call).toBe('function'); }); it('should returns "undefined"', function() { expect(helpers.noop(42)).not.toBeDefined(); expect(helpers.noop.call(this, 42)).not.toBeDefined(); }); }); describe('isArray', function() { it('should return true if value is an array', function() { expect(helpers.isArray([])).toBeTruthy(); expect(helpers.isArray([42])).toBeTruthy(); expect(helpers.isArray(new Array())).toBeTruthy(); expect(helpers.isArray(Array.prototype)).toBeTruthy(); expect(helpers.isArray(new Int8Array(2))).toBeTruthy(); expect(helpers.isArray(new Uint8Array())).toBeTruthy(); expect(helpers.isArray(new Uint8ClampedArray([128, 244]))).toBeTruthy(); expect(helpers.isArray(new Int16Array())).toBeTruthy(); expect(helpers.isArray(new Uint16Array())).toBeTruthy(); expect(helpers.isArray(new Int32Array())).toBeTruthy(); expect(helpers.isArray(new Uint32Array())).toBeTruthy(); expect(helpers.isArray(new Float32Array([1.2]))).toBeTruthy(); expect(helpers.isArray(new Float64Array([]))).toBeTruthy(); }); it('should return false if value is not an array', function() { expect(helpers.isArray()).toBeFalsy(); expect(helpers.isArray({})).toBeFalsy(); expect(helpers.isArray(undefined)).toBeFalsy(); expect(helpers.isArray(null)).toBeFalsy(); expect(helpers.isArray(true)).toBeFalsy(); expect(helpers.isArray(false)).toBeFalsy(); expect(helpers.isArray(42)).toBeFalsy(); expect(helpers.isArray('Array')).toBeFalsy(); expect(helpers.isArray({__proto__: Array.prototype})).toBeFalsy(); }); }); describe('isObject', function() { it('should return true if value is an object', function() { expect(helpers.isObject({})).toBeTruthy(); expect(helpers.isObject({a: 42})).toBeTruthy(); expect(helpers.isObject(new Object())).toBeTruthy(); }); it('should return false if value is not an object', function() { expect(helpers.isObject()).toBeFalsy(); expect(helpers.isObject(undefined)).toBeFalsy(); expect(helpers.isObject(null)).toBeFalsy(); expect(helpers.isObject(true)).toBeFalsy(); expect(helpers.isObject(false)).toBeFalsy(); expect(helpers.isObject(42)).toBeFalsy(); expect(helpers.isObject('Object')).toBeFalsy(); expect(helpers.isObject([])).toBeFalsy(); expect(helpers.isObject([42])).toBeFalsy(); expect(helpers.isObject(new Array())).toBeFalsy(); expect(helpers.isObject(new Date())).toBeFalsy(); }); }); describe('isFinite', function() { it('should return true if value is a finite number', function() { expect(helpers.isFinite(0)).toBeTruthy(); // eslint-disable-next-line no-new-wrappers expect(helpers.isFinite(new Number(10))).toBeTruthy(); }); it('should return false if the value is infinite', function() { expect(helpers.isFinite(Number.POSITIVE_INFINITY)).toBeFalsy(); expect(helpers.isFinite(Number.NEGATIVE_INFINITY)).toBeFalsy(); }); it('should return false if the value is not a number', function() { expect(helpers.isFinite('a')).toBeFalsy(); expect(helpers.isFinite({})).toBeFalsy(); }); }); describe('isNullOrUndef', function() { it('should return true if value is null/undefined', function() { expect(helpers.isNullOrUndef(null)).toBeTruthy(); expect(helpers.isNullOrUndef(undefined)).toBeTruthy(); }); it('should return false if value is not null/undefined', function() { expect(helpers.isNullOrUndef(true)).toBeFalsy(); expect(helpers.isNullOrUndef(false)).toBeFalsy(); expect(helpers.isNullOrUndef('')).toBeFalsy(); expect(helpers.isNullOrUndef('String')).toBeFalsy(); expect(helpers.isNullOrUndef(0)).toBeFalsy(); expect(helpers.isNullOrUndef([])).toBeFalsy(); expect(helpers.isNullOrUndef({})).toBeFalsy(); expect(helpers.isNullOrUndef([42])).toBeFalsy(); expect(helpers.isNullOrUndef(new Date())).toBeFalsy(); }); }); describe('valueOrDefault', function() { it('should return value if defined', function() { var object = {}; var array = []; expect(helpers.valueOrDefault(null, 42)).toBe(null); expect(helpers.valueOrDefault(false, 42)).toBe(false); expect(helpers.valueOrDefault(object, 42)).toBe(object); expect(helpers.valueOrDefault(array, 42)).toBe(array); expect(helpers.valueOrDefault('', 42)).toBe(''); expect(helpers.valueOrDefault(0, 42)).toBe(0); }); it('should return default if undefined', function() { expect(helpers.valueOrDefault(undefined, 42)).toBe(42); expect(helpers.valueOrDefault({}.foo, 42)).toBe(42); }); }); describe('callback', function() { it('should return undefined if fn is not a function', function() { expect(helpers.callback()).not.toBeDefined(); expect(helpers.callback(null)).not.toBeDefined(); expect(helpers.callback(42)).not.toBeDefined(); expect(helpers.callback([])).not.toBeDefined(); expect(helpers.callback({})).not.toBeDefined(); }); it('should call fn with the given args', function() { var spy = jasmine.createSpy('spy'); helpers.callback(spy); helpers.callback(spy, []); helpers.callback(spy, ['foo']); helpers.callback(spy, [42, 'bar']); expect(spy.calls.argsFor(0)).toEqual([]); expect(spy.calls.argsFor(1)).toEqual([]); expect(spy.calls.argsFor(2)).toEqual(['foo']); expect(spy.calls.argsFor(3)).toEqual([42, 'bar']); }); it('should call fn with the given scope', function() { var spy = jasmine.createSpy('spy'); var scope = {}; helpers.callback(spy); helpers.callback(spy, [], null); helpers.callback(spy, [], undefined); helpers.callback(spy, [], scope); expect(spy.calls.all()[0].object).toBe(window); expect(spy.calls.all()[1].object).toBe(window); expect(spy.calls.all()[2].object).toBe(window); expect(spy.calls.all()[3].object).toBe(scope); }); it('should return the value returned by fn', function() { expect(helpers.callback(helpers.noop, [41])).toBe(undefined); expect(helpers.callback(function(i) { return i + 1; }, [41])).toBe(42); }); }); describe('each', function() { it('should iterate over an array forward if reverse === false', function() { var scope = {}; var scopes = []; var items = []; var keys = []; helpers.each(['foo', 'bar', 42], function(item, key) { scopes.push(this); items.push(item); keys.push(key); }, scope); expect(scopes).toEqual([scope, scope, scope]); expect(items).toEqual(['foo', 'bar', 42]); expect(keys).toEqual([0, 1, 2]); }); it('should iterate over an array backward if reverse === true', function() { var scope = {}; var scopes = []; var items = []; var keys = []; helpers.each(['foo', 'bar', 42], function(item, key) { scopes.push(this); items.push(item); keys.push(key); }, scope, true); expect(scopes).toEqual([scope, scope, scope]); expect(items).toEqual([42, 'bar', 'foo']); expect(keys).toEqual([2, 1, 0]); }); it('should iterate over object properties', function() { var scope = {}; var scopes = []; var items = []; helpers.each({a: 'foo', b: 'bar', c: 42}, function(item, key) { scopes.push(this); items[key] = item; }, scope); expect(scopes).toEqual([scope, scope, scope]); expect(items).toEqual(jasmine.objectContaining({a: 'foo', b: 'bar', c: 42})); }); it('should not throw when called with a non iterable object', function() { expect(function() { helpers.each(undefined); }).not.toThrow(); expect(function() { helpers.each(null); }).not.toThrow(); expect(function() { helpers.each(42); }).not.toThrow(); }); }); describe('_elementsEqual', function() { it('should return true if arrays are the same', function() { expect(helpers._elementsEqual( [{datasetIndex: 0, index: 1}, {datasetIndex: 0, index: 2}], [{datasetIndex: 0, index: 1}, {datasetIndex: 0, index: 2}])).toBeTruthy(); }); it('should return false if arrays are not the same', function() { expect(helpers._elementsEqual([], [{datasetIndex: 0, index: 1}])).toBeFalsy(); expect(helpers._elementsEqual([{datasetIndex: 0, index: 2}], [{datasetIndex: 0, index: 1}])).toBeFalsy(); }); }); describe('clone', function() { it('should clone primitive values', function() { expect(helpers.clone()).toBe(undefined); expect(helpers.clone(null)).toBe(null); expect(helpers.clone(true)).toBe(true); expect(helpers.clone(42)).toBe(42); expect(helpers.clone('foo')).toBe('foo'); }); it('should perform a deep copy of arrays', function() { var o0 = {a: 42}; var o1 = {s: 's'}; var a0 = ['bar']; var a1 = [a0, o0, 2]; var f0 = function() {}; var input = [a1, o1, f0, 42, 'foo']; var output = helpers.clone(input); expect(output).toEqual(input); expect(output).not.toBe(input); expect(output[0]).not.toBe(a1); expect(output[0][0]).not.toBe(a0); expect(output[1]).not.toBe(o1); }); it('should perform a deep copy of objects', function() { var a0 = ['bar']; var a1 = [1, 2, 3]; var o0 = {a: a1, i: 42}; var f0 = function() {}; var input = {o: o0, a: a0, f: f0, s: 'foo', i: 42}; var output = helpers.clone(input); expect(output).toEqual(input); expect(output).not.toBe(input); expect(output.o).not.toBe(o0); expect(output.o.a).not.toBe(a1); expect(output.a).not.toBe(a0); }); }); describe('merge', function() { it('should not allow prototype pollution', function() { var test = helpers.merge({}, JSON.parse('{"__proto__":{"polluted": true}}')); expect(test.prototype).toBeUndefined(); expect(Object.prototype.polluted).toBeUndefined(); }); it('should update target and return it', function() { var target = {a: 1}; var result = helpers.merge(target, {a: 2, b: 'foo'}); expect(target).toEqual({a: 2, b: 'foo'}); expect(target).toBe(result); }); it('should return target if not an object', function() { expect(helpers.merge(undefined, {a: 42})).toEqual(undefined); expect(helpers.merge(null, {a: 42})).toEqual(null); expect(helpers.merge('foo', {a: 42})).toEqual('foo'); expect(helpers.merge(['foo', 'bar'], {a: 42})).toEqual(['foo', 'bar']); }); it('should ignore sources which are not objects', function() { expect(helpers.merge({a: 42})).toEqual({a: 42}); expect(helpers.merge({a: 42}, null)).toEqual({a: 42}); expect(helpers.merge({a: 42}, 42)).toEqual({a: 42}); }); it('should recursively overwrite target with source properties', function() { expect(helpers.merge({a: {b: 1}}, {a: {c: 2}})).toEqual({a: {b: 1, c: 2}}); expect(helpers.merge({a: {b: 1}}, {a: {b: 2}})).toEqual({a: {b: 2}}); expect(helpers.merge({a: [1, 2]}, {a: [3, 4]})).toEqual({a: [3, 4]}); expect(helpers.merge({a: 42}, {a: {b: 0}})).toEqual({a: {b: 0}}); expect(helpers.merge({a: 42}, {a: null})).toEqual({a: null}); expect(helpers.merge({a: 42}, {a: undefined})).toEqual({a: undefined}); }); it('should merge multiple sources in the correct order', function() { var t0 = {a: {b: 1, c: [1, 2]}}; var s0 = {a: {d: 3}, e: {f: 4}}; var s1 = {a: {b: 5}}; var s2 = {a: {c: [6, 7]}, e: 'foo'}; expect(helpers.merge(t0, [s0, s1, s2])).toEqual({a: {b: 5, c: [6, 7], d: 3}, e: 'foo'}); }); it('should deep copy merged values from sources', function() { var a0 = ['foo']; var a1 = [1, 2, 3]; var o0 = {a: a1, i: 42}; var output = helpers.merge({}, {a: a0, o: o0}); expect(output).toEqual({a: a0, o: o0}); expect(output.a).not.toBe(a0); expect(output.o).not.toBe(o0); expect(output.o.a).not.toBe(a1); }); }); describe('mergeIf', function() { it('should not allow prototype pollution', function() { var test = helpers.mergeIf({}, JSON.parse('{"__proto__":{"polluted": true}}')); expect(test.prototype).toBeUndefined(); expect(Object.prototype.polluted).toBeUndefined(); }); it('should update target and return it', function() { var target = {a: 1}; var result = helpers.mergeIf(target, {a: 2, b: 'foo'}); expect(target).toEqual({a: 1, b: 'foo'}); expect(target).toBe(result); }); it('should return target if not an object', function() { expect(helpers.mergeIf(undefined, {a: 42})).toEqual(undefined); expect(helpers.mergeIf(null, {a: 42})).toEqual(null); expect(helpers.mergeIf('foo', {a: 42})).toEqual('foo'); expect(helpers.mergeIf(['foo', 'bar'], {a: 42})).toEqual(['foo', 'bar']); }); it('should ignore sources which are not objects', function() { expect(helpers.mergeIf({a: 42})).toEqual({a: 42}); expect(helpers.mergeIf({a: 42}, null)).toEqual({a: 42}); expect(helpers.mergeIf({a: 42}, 42)).toEqual({a: 42}); }); it('should recursively copy source properties in target only if they do not exist in target', function() { expect(helpers.mergeIf({a: {b: 1}}, {a: {c: 2}})).toEqual({a: {b: 1, c: 2}}); expect(helpers.mergeIf({a: {b: 1}}, {a: {b: 2}})).toEqual({a: {b: 1}}); expect(helpers.mergeIf({a: [1, 2]}, {a: [3, 4]})).toEqual({a: [1, 2]}); expect(helpers.mergeIf({a: 0}, {a: {b: 2}})).toEqual({a: 0}); expect(helpers.mergeIf({a: null}, {a: 42})).toEqual({a: null}); expect(helpers.mergeIf({a: undefined}, {a: 42})).toEqual({a: undefined}); }); it('should merge multiple sources in the correct order', function() { var t0 = {a: {b: 1, c: [1, 2]}}; var s0 = {a: {d: 3}, e: {f: 4}}; var s1 = {a: {b: 5}}; var s2 = {a: {c: [6, 7]}, e: 'foo'}; expect(helpers.mergeIf(t0, [s0, s1, s2])).toEqual({a: {b: 1, c: [1, 2], d: 3}, e: {f: 4}}); }); it('should deep copy merged values from sources', function() { var a0 = ['foo']; var a1 = [1, 2, 3]; var o0 = {a: a1, i: 42}; var output = helpers.mergeIf({}, {a: a0, o: o0}); expect(output).toEqual({a: a0, o: o0}); expect(output.a).not.toBe(a0); expect(output.o).not.toBe(o0); expect(output.o.a).not.toBe(a1); }); }); describe('resolveObjectKey', function() { it('should resolve empty key to root object', function() { const obj = {test: true}; expect(helpers.resolveObjectKey(obj, '')).toEqual(obj); }); it('should resolve one level', function() { const obj = { bool: true, str: 'test', int: 42, obj: {name: 'object'} }; expect(helpers.resolveObjectKey(obj, 'bool')).toEqual(true); expect(helpers.resolveObjectKey(obj, 'str')).toEqual('test'); expect(helpers.resolveObjectKey(obj, 'int')).toEqual(42); expect(helpers.resolveObjectKey(obj, 'obj')).toEqual(obj.obj); }); it('should resolve multiple levels', function() { const obj = { child: { level: 1, child: { level: 2, child: { level: 3 } } } }; expect(helpers.resolveObjectKey(obj, 'child.level')).toEqual(1); expect(helpers.resolveObjectKey(obj, 'child.child.level')).toEqual(2); expect(helpers.resolveObjectKey(obj, 'child.child.child.level')).toEqual(3); }); it('should resolve circular reference', function() { const root = {}; const child = {root}; child.child = child; root.child = child; expect(helpers.resolveObjectKey(root, 'child')).toEqual(child); expect(helpers.resolveObjectKey(root, 'child.child.child.child.child.child')).toEqual(child); expect(helpers.resolveObjectKey(root, 'child.child.root')).toEqual(root); }); it('should break at empty key', function() { const obj = { child: { level: 1, child: { level: 2, child: { level: 3 } } } }; expect(helpers.resolveObjectKey(obj, 'child..level')).toEqual(obj.child); expect(helpers.resolveObjectKey(obj, 'child.child.level...')).toEqual(2); expect(helpers.resolveObjectKey(obj, '.')).toEqual(obj); expect(helpers.resolveObjectKey(obj, '..')).toEqual(obj); }); it('should resolve undefined', function() { const obj = { child: { level: 1, child: { level: 2, child: { level: 3 } } } }; expect(helpers.resolveObjectKey(obj, 'level')).toEqual(undefined); expect(helpers.resolveObjectKey(obj, 'child.level.a')).toEqual(undefined); }); it('should throw on invalid input', function() { expect(() => helpers.resolveObjectKey(undefined, undefined)).toThrow(); expect(() => helpers.resolveObjectKey({}, null)).toThrow(); expect(() => helpers.resolveObjectKey({}, false)).toThrow(); expect(() => helpers.resolveObjectKey({}, true)).toThrow(); expect(() => helpers.resolveObjectKey({}, 1)).toThrow(); }); it('should allow escaping dot symbol', function() { expect(helpers.resolveObjectKey({'test.dot': 10}, 'test\\.dot')).toEqual(10); expect(helpers.resolveObjectKey({test: {dot: 10}}, 'test\\.dot')).toEqual(undefined); }); it('should allow nested keys with a dot', function() { expect(helpers.resolveObjectKey({ a: { 'bb.ccc': 'works', bb: { ccc: 'fails' } } }, 'a.bb\\.ccc')).toEqual('works'); }); }); describe('_splitKey', function() { it('should return array with one entry for string without a dot', function() { expect(helpers._splitKey('')).toEqual(['']); expect(helpers._splitKey('test')).toEqual(['test']); const asciiWithoutDot = ' !"#$%&\'()*+,-/0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'; expect(helpers._splitKey(asciiWithoutDot)).toEqual([asciiWithoutDot]); }); it('should split on dot', function() { expect(helpers._splitKey('test1.test2')).toEqual(['test1', 'test2']); expect(helpers._splitKey('a.b.c')).toEqual(['a', 'b', 'c']); expect(helpers._splitKey('a.b.')).toEqual(['a', 'b', '']); expect(helpers._splitKey('a..c')).toEqual(['a', '', 'c']); }); it('should preserve escaped dot', function() { expect(helpers._splitKey('test1\\.test2')).toEqual(['test1.test2']); expect(helpers._splitKey('a\\.b.c')).toEqual(['a.b', 'c']); expect(helpers._splitKey('a.b\\.c')).toEqual(['a', 'b.c']); expect(helpers._splitKey('a.\\.c')).toEqual(['a', '.c']); }); }); describe('setsEqual', function() { it('should handle set comparison', function() { var a = new Set([1]); var b = new Set(['1']); var c = new Set([1]); expect(helpers.setsEqual(a, b)).toBeFalse(); expect(helpers.setsEqual(a, c)).toBeTrue(); }); }); }); ================================================ FILE: test/specs/helpers.curve.tests.js ================================================ describe('Curve helper tests', function() { let helpers; beforeAll(function() { helpers = window.Chart.helpers; }); it('should spline curves', function() { expect(helpers.splineCurve({ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 0 }, 0)).toEqual({ previous: { x: 1, y: 1, }, next: { x: 1, y: 1, } }); expect(helpers.splineCurve({ x: 0, y: 0 }, { x: 1, y: 1 }, { x: 2, y: 0 }, 1)).toEqual({ previous: { x: 0, y: 1, }, next: { x: 2, y: 1, } }); }); it('should spline curves with monotone cubic interpolation', function() { var dataPoints = [ {x: 0, y: 0, skip: false}, {x: 3, y: 6, skip: false}, {x: 9, y: 6, skip: false}, {x: 12, y: 60, skip: false}, {x: 15, y: 60, skip: false}, {x: 18, y: 120, skip: false}, {x: null, y: null, skip: true}, {x: 21, y: 180, skip: false}, {x: 24, y: 120, skip: false}, {x: 27, y: 125, skip: false}, {x: 30, y: 105, skip: false}, {x: 33, y: 110, skip: false}, {x: 33, y: 110, skip: false}, {x: 36, y: 170, skip: false} ]; helpers.splineCurveMonotone(dataPoints); expect(dataPoints).toEqual([{ x: 0, y: 0, skip: false, cp2x: 1, cp2y: 2 }, { x: 3, y: 6, skip: false, cp1x: 2, cp1y: 6, cp2x: 5, cp2y: 6 }, { x: 9, y: 6, skip: false, cp1x: 7, cp1y: 6, cp2x: 10, cp2y: 6 }, { x: 12, y: 60, skip: false, cp1x: 11, cp1y: 60, cp2x: 13, cp2y: 60 }, { x: 15, y: 60, skip: false, cp1x: 14, cp1y: 60, cp2x: 16, cp2y: 60 }, { x: 18, y: 120, skip: false, cp1x: 17, cp1y: 100 }, { x: null, y: null, skip: true }, { x: 21, y: 180, skip: false, cp2x: 22, cp2y: 160 }, { x: 24, y: 120, skip: false, cp1x: 23, cp1y: 120, cp2x: 25, cp2y: 120 }, { x: 27, y: 125, skip: false, cp1x: 26, cp1y: 125, cp2x: 28, cp2y: 125 }, { x: 30, y: 105, skip: false, cp1x: 29, cp1y: 105, cp2x: 31, cp2y: 105 }, { x: 33, y: 110, skip: false, cp1x: 32, cp1y: 110, cp2x: 33, cp2y: 110 }, { x: 33, y: 110, skip: false, cp1x: 33, cp1y: 110, cp2x: 34, cp2y: 110 }, { x: 36, y: 170, skip: false, cp1x: 35, cp1y: 150 }]); }); }); ================================================ FILE: test/specs/helpers.dom.tests.js ================================================ describe('DOM helpers tests', function() { let helpers; beforeAll(function() { helpers = window.Chart.helpers; }); it ('should get the maximum size for a node', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for var innerDiv = document.createElement('div'); div.appendChild(innerDiv); expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 200, height: 300})); document.body.removeChild(div); }); it ('should get the maximum width and height for a node in a ShadowRoot', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); if (!div.attachShadow) { // Shadow DOM is not natively supported return; } var shadow = div.attachShadow({mode: 'closed'}); // Create the div we want to get the max size for var innerDiv = document.createElement('div'); shadow.appendChild(innerDiv); expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 200, height: 300})); document.body.removeChild(div); }); it ('should get the maximum width of a node that has a max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-width style var innerDiv = document.createElement('div'); innerDiv.style.maxWidth = '150px'; div.appendChild(innerDiv); expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 150})); document.body.removeChild(div); }); it ('should get the maximum height of a node that has a max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-height style var innerDiv = document.createElement('div'); innerDiv.style.maxHeight = '150px'; div.appendChild(innerDiv); expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150})); document.body.removeChild(div); }); it ('should get the maximum width of a node when the parent has a max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-width style var parentDiv = document.createElement('div'); parentDiv.style.maxWidth = '150px'; div.appendChild(parentDiv); // Create the div we want to get the max size for var innerDiv = document.createElement('div'); parentDiv.appendChild(innerDiv); expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 150})); document.body.removeChild(div); }); it ('should get the maximum height of a node when the parent has a max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-height style var parentDiv = document.createElement('div'); parentDiv.style.maxHeight = '150px'; div.appendChild(parentDiv); // Create the div we want to get the max size for var innerDiv = document.createElement('div'); innerDiv.style.height = '300px'; // make it large parentDiv.appendChild(innerDiv); expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150})); document.body.removeChild(div); }); it ('should get the maximum width of a node that has a percentage max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-width style var innerDiv = document.createElement('div'); innerDiv.style.maxWidth = '50%'; div.appendChild(innerDiv); expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 100})); document.body.removeChild(div); }); it('should get the maximum height of a node that has a percentage max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create the div we want to get the max size for and set a max-height style var innerDiv = document.createElement('div'); innerDiv.style.maxHeight = '50%'; div.appendChild(innerDiv); expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150})); document.body.removeChild(div); }); it ('should get the maximum width of a node when the parent has a percentage max-width style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-width style var parentDiv = document.createElement('div'); parentDiv.style.maxWidth = '50%'; div.appendChild(parentDiv); // Create the div we want to get the max size for var innerDiv = document.createElement('div'); parentDiv.appendChild(innerDiv); expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({width: 100})); document.body.removeChild(div); }); it ('should get the maximum height of a node when the parent has a percentage max-height style', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '200px'; div.style.height = '300px'; document.body.appendChild(div); // Create an inner wrapper around our div we want to size and give that a max-height style var parentDiv = document.createElement('div'); parentDiv.style.maxHeight = '50%'; div.appendChild(parentDiv); var innerDiv = document.createElement('div'); innerDiv.style.height = '300px'; // make it large parentDiv.appendChild(innerDiv); expect(helpers.getMaximumSize(innerDiv)).toEqual(jasmine.objectContaining({height: 150})); document.body.removeChild(div); }); it ('Should get padding of parent as number (pixels) when defined as percent (returns incorrectly in IE11)', function() { // Create div with fixed size as a test bed var div = document.createElement('div'); div.style.width = '300px'; div.style.height = '300px'; document.body.appendChild(div); // Inner DIV to have 5% padding of parent var innerDiv = document.createElement('div'); div.appendChild(innerDiv); var canvas = document.createElement('canvas'); innerDiv.appendChild(canvas); // No padding expect(helpers.getMaximumSize(canvas)).toEqual(jasmine.objectContaining({width: 300})); // test with percentage innerDiv.style.padding = '5%'; expect(helpers.getMaximumSize(canvas)).toEqual(jasmine.objectContaining({width: 270})); // test with pixels innerDiv.style.padding = '10px'; expect(helpers.getMaximumSize(canvas)).toEqual(jasmine.objectContaining({width: 280})); document.body.removeChild(div); }); it ('should leave styled height and width on canvas if explicitly set', function() { var chart = window.acquireChart({}, { canvas: { height: 200, width: 200, style: 'height: 400px; width: 400px;' } }); helpers.retinaScale(chart, true); var canvas = chart.canvas; expect(canvas.style.height).toBe('400px'); expect(canvas.style.width).toBe('400px'); }); it ('should handle devicePixelRatio correctly', function() { const chartWidth = 800; const chartHeight = 400; let devicePixelRatio = 0.8999999761581421; // 1.7999999523162842; var chart = window.acquireChart({}, { canvas: { width: chartWidth, height: chartHeight, } }); helpers.retinaScale(chart, devicePixelRatio, true); var canvas = chart.canvas; expect(canvas.width).toBe(Math.round(chartWidth * devicePixelRatio)); expect(canvas.height).toBe(Math.round(chartHeight * devicePixelRatio)); expect(chart.width).toBe(chartWidth); expect(chart.height).toBe(chartHeight); expect(canvas.style.width).toBe(`${chartWidth}px`); expect(canvas.style.height).toBe(`${chartHeight}px`); }); describe('getRelativePosition', function() { it('should use offsetX/Y when available', function() { const event = {offsetX: 50, offsetY: 100}; const chart = window.acquireChart({}, { canvas: { height: 200, width: 200, } }); expect(helpers.getRelativePosition(event, chart)).toEqual({x: 50, y: 100}); const chart2 = window.acquireChart({}, { canvas: { height: 200, width: 200, style: 'padding: 10px' } }); expect(helpers.getRelativePosition(event, chart2)).toEqual({ x: Math.round((event.offsetX - 10) / 180 * 200), y: Math.round((event.offsetY - 10) / 180 * 200) }); const chart3 = window.acquireChart({}, { canvas: { height: 200, width: 200, style: 'width: 400px, height: 400px; padding: 10px' } }); expect(helpers.getRelativePosition(event, chart3)).toEqual({ x: Math.round((event.offsetX - 10) / 360 * 400), y: Math.round((event.offsetY - 10) / 360 * 400) }); const chart4 = window.acquireChart({}, { canvas: { height: 200, width: 200, style: 'width: 400px, height: 400px; padding: 10px; position: absolute; left: 20, top: 20' } }); expect(helpers.getRelativePosition(event, chart4)).toEqual({ x: Math.round((event.offsetX - 10) / 360 * 400), y: Math.round((event.offsetY - 10) / 360 * 400) }); }); it('should calculate from clientX/Y as fallback', function() { const chart = window.acquireChart({}, { canvas: { height: 200, width: 200, } }); const event = { clientX: 50, clientY: 100 }; const rect = chart.canvas.getBoundingClientRect(); const pos = helpers.getRelativePosition(event, chart); expect(Math.abs(pos.x - Math.round(event.clientX - rect.x))).toBeLessThanOrEqual(1); expect(Math.abs(pos.y - Math.round(event.clientY - rect.y))).toBeLessThanOrEqual(1); const chart2 = window.acquireChart({}, { canvas: { height: 200, width: 200, style: 'padding: 10px' } }); const rect2 = chart2.canvas.getBoundingClientRect(); const pos2 = helpers.getRelativePosition(event, chart2); expect(Math.abs(pos2.x - Math.round((event.clientX - rect2.x - 10) / 180 * 200))).toBeLessThanOrEqual(1); expect(Math.abs(pos2.y - Math.round((event.clientY - rect2.y - 10) / 180 * 200))).toBeLessThanOrEqual(1); const chart3 = window.acquireChart({}, { canvas: { height: 200, width: 200, style: 'width: 400px, height: 400px; padding: 10px' } }); const rect3 = chart3.canvas.getBoundingClientRect(); const pos3 = helpers.getRelativePosition(event, chart3); expect(Math.abs(pos3.x - Math.round((event.clientX - rect3.x - 10) / 360 * 400))).toBeLessThanOrEqual(1); expect(Math.abs(pos3.y - Math.round((event.clientY - rect3.y - 10) / 360 * 400))).toBeLessThanOrEqual(1); }); it ('should get the correct relative position for a node in a ShadowRoot', function() { const event = { offsetX: 50, offsetY: 100, clientX: 50, clientY: 100 }; const chart = window.acquireChart({}, { canvas: { height: 200, width: 200, }, useShadowDOM: true }); event.target = chart.canvas.parentNode.host; expect(event.target.shadowRoot).not.toEqual(null); const rect = chart.canvas.getBoundingClientRect(); const pos = helpers.getRelativePosition(event, chart); expect(Math.abs(pos.x - Math.round(event.clientX - rect.x))).toBeLessThanOrEqual(1); expect(Math.abs(pos.y - Math.round(event.clientY - rect.y))).toBeLessThanOrEqual(1); const chart2 = window.acquireChart({}, { canvas: { height: 200, width: 200, style: 'padding: 10px' }, useShadowDOM: true }); event.target = chart2.canvas.parentNode.host; const rect2 = chart2.canvas.getBoundingClientRect(); const pos2 = helpers.getRelativePosition(event, chart2); expect(Math.abs(pos2.x - Math.round((event.clientX - rect2.x - 10) / 180 * 200))).toBeLessThanOrEqual(1); expect(Math.abs(pos2.y - Math.round((event.clientY - rect2.y - 10) / 180 * 200))).toBeLessThanOrEqual(1); const chart3 = window.acquireChart({}, { canvas: { height: 200, width: 200, style: 'width: 400px, height: 400px; padding: 10px' }, useShadowDOM: true }); event.target = chart3.canvas.parentNode.host; const rect3 = chart3.canvas.getBoundingClientRect(); const pos3 = helpers.getRelativePosition(event, chart3); expect(Math.abs(pos3.x - Math.round((event.clientX - rect3.x - 10) / 360 * 400))).toBeLessThanOrEqual(1); expect(Math.abs(pos3.y - Math.round((event.clientY - rect3.y - 10) / 360 * 400))).toBeLessThanOrEqual(1); }); it('Should not return NaN with a custom event', async function() { let dataX = null; let dataY = null; const chart = window.acquireChart( { type: 'bar', data: { datasets: [{ data: [{x: 'first', y: 10}, {x: 'second', y: 5}, {x: 'third', y: 15}] }] }, options: { onHover: (e) => { const canvasPosition = Chart.helpers.getRelativePosition(e, chart); dataX = canvasPosition.x; dataY = canvasPosition.y; } } }); const point = chart.getDatasetMeta(0).data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(dataX).not.toEqual(NaN); expect(dataY).not.toEqual(NaN); }); it('Should give consistent results for native and chart events', async function() { let chartPosition = null; const chart = window.acquireChart( { type: 'bar', data: { datasets: [{ data: [{x: 'first', y: 10}, {x: 'second', y: 5}, {x: 'third', y: 15}] }] }, options: { onHover: (chartEvent) => { chartPosition = Chart.helpers.getRelativePosition(chartEvent, chart); } } }); const point = chart.getDatasetMeta(0).data[1]; const nativeEvent = await jasmine.triggerMouseEvent(chart, 'mousemove', point); const nativePosition = Chart.helpers.getRelativePosition(nativeEvent, chart); expect(chartPosition).not.toBeNull(); expect(nativePosition).toEqual({x: chartPosition.x, y: chartPosition.y}); }); }); it('should respect aspect ratio and container width', () => { const container = document.createElement('div'); container.style.width = '200px'; container.style.height = '500px'; document.body.appendChild(container); const target = document.createElement('div'); target.style.width = '500px'; target.style.height = '500px'; container.appendChild(target); expect(helpers.getMaximumSize(target, 200, 500, 1)).toEqual(jasmine.objectContaining({width: 200, height: 200})); document.body.removeChild(container); }); it('should respect aspect ratio and container height', () => { const container = document.createElement('div'); container.style.width = '500px'; container.style.height = '200px'; document.body.appendChild(container); const target = document.createElement('div'); target.style.width = '500px'; target.style.height = '500px'; container.appendChild(target); expect(helpers.getMaximumSize(target, 500, 200, 1)).toEqual(jasmine.objectContaining({width: 200, height: 200})); document.body.removeChild(container); }); it('should respect aspect ratio and skip container height', () => { const container = document.createElement('div'); container.style.width = '500px'; container.style.height = '200px'; document.body.appendChild(container); const target = document.createElement('div'); target.style.width = '500px'; target.style.height = '500px'; container.appendChild(target); expect(helpers.getMaximumSize(target, undefined, undefined, 1)).toEqual(jasmine.objectContaining({width: 500, height: 500})); document.body.removeChild(container); }); it('should round non-integer container dimensions', () => { const container = document.createElement('div'); container.style.width = '799.999px'; container.style.height = '299.999px'; document.body.appendChild(container); const target = document.createElement('div'); target.style.width = '200px'; target.style.height = '100px'; container.appendChild(target); expect(helpers.getMaximumSize(target, undefined, undefined, 2)).toEqual(jasmine.objectContaining({width: 800, height: 400})); document.body.removeChild(container); }); }); ================================================ FILE: test/specs/helpers.easing.tests.js ================================================ 'use strict'; describe('Chart.helpers.easingEffects', function() { var helpers = Chart.helpers; describe('effects', function() { var expected = { easeInOutBack: [-0, -0.03751855, -0.09255566, -0.07883348, 0.08992579, 0.5, 0.91007421, 1.07883348, 1.09255566, 1.03751855, 1], easeInOutBounce: [0, 0.03, 0.11375, 0.045, 0.34875, 0.5, 0.65125, 0.955, 0.88625, 0.97, 1], easeInOutCirc: [-0, 0.01010205, 0.04174243, 0.1, 0.2, 0.5, 0.8, 0.9, 0.95825757, 0.98989795, 1], easeInOutCubic: [0, 0.004, 0.032, 0.108, 0.256, 0.5, 0.744, 0.892, 0.968, 0.996, 1], easeInOutElastic: [0, 0.00033916, -0.00390625, 0.02393889, -0.11746158, 0.5, 1.11746158, 0.97606111, 1.00390625, 0.99966084, 1], easeInOutExpo: [0, 0.00195313, 0.0078125, 0.03125, 0.125, 0.5, 0.875, 0.96875, 0.9921875, 0.99804688, 1], easeInOutQuad: [0, 0.02, 0.08, 0.18, 0.32, 0.5, 0.68, 0.82, 0.92, 0.98, 1], easeInOutQuart: [0, 0.0008, 0.0128, 0.0648, 0.2048, 0.5, 0.7952, 0.9352, 0.9872, 0.9992, 1], easeInOutQuint: [0, 0.00016, 0.00512, 0.03888, 0.16384, 0.5, 0.83616, 0.96112, 0.99488, 0.99984, 1], easeInOutSine: [-0, 0.02447174, 0.0954915, 0.20610737, 0.3454915, 0.5, 0.6545085, 0.79389263, 0.9045085, 0.97552826, 1], easeInBack: [-0, -0.01431422, -0.04645056, -0.08019954, -0.09935168, -0.0876975, -0.02902752, 0.09286774, 0.29419776, 0.59117202, 1], easeInBounce: [0, 0.011875, 0.06, 0.069375, 0.2275, 0.234375, 0.09, 0.319375, 0.6975, 0.924375, 1], easeInCirc: [-0, 0.00501256, 0.0202041, 0.0460608, 0.08348486, 0.1339746, 0.2, 0.28585716, 0.4, 0.56411011, 1], easeInCubic: [0, 0.001, 0.008, 0.027, 0.064, 0.125, 0.216, 0.343, 0.512, 0.729, 1], easeInExpo: [0, 0.00195313, 0.00390625, 0.0078125, 0.015625, 0.03125, 0.0625, 0.125, 0.25, 0.5, 1], easeInElastic: [0, 0.00195313, -0.00195313, -0.00390625, 0.015625, -0.015625, -0.03125, 0.125, -0.125, -0.25, 1], easeInQuad: [0, 0.01, 0.04, 0.09, 0.16, 0.25, 0.36, 0.49, 0.64, 0.81, 1], easeInQuart: [0, 0.0001, 0.0016, 0.0081, 0.0256, 0.0625, 0.1296, 0.2401, 0.4096, 0.6561, 1], easeInQuint: [0, 0.00001, 0.00032, 0.00243, 0.01024, 0.03125, 0.07776, 0.16807, 0.32768, 0.59049, 1], easeInSine: [0, 0.01231166, 0.04894348, 0.10899348, 0.19098301, 0.29289322, 0.41221475, 0.5460095, 0.69098301, 0.84356553, 1], easeOutBack: [0, 0.40882798, 0.70580224, 0.90713226, 1.02902752, 1.0876975, 1.09935168, 1.08019954, 1.04645056, 1.01431422, 1], easeOutBounce: [0, 0.075625, 0.3025, 0.680625, 0.91, 0.765625, 0.7725, 0.930625, 0.94, 0.988125, 1], easeOutCirc: [0, 0.43588989, 0.6, 0.71414284, 0.8, 0.8660254, 0.91651514, 0.9539392, 0.9797959, 0.99498744, 1], easeOutElastic: [0, 1.25, 1.125, 0.875, 1.03125, 1.015625, 0.984375, 1.00390625, 1.00195313, 0.99804688, 1], easeOutExpo: [0, 0.5, 0.75, 0.875, 0.9375, 0.96875, 0.984375, 0.9921875, 0.99609375, 0.99804688, 1], easeOutCubic: [0, 0.271, 0.488, 0.657, 0.784, 0.875, 0.936, 0.973, 0.992, 0.999, 1], easeOutQuad: [0, 0.19, 0.36, 0.51, 0.64, 0.75, 0.84, 0.91, 0.96, 0.99, 1], easeOutQuart: [-0, 0.3439, 0.5904, 0.7599, 0.8704, 0.9375, 0.9744, 0.9919, 0.9984, 0.9999, 1], easeOutQuint: [0, 0.40951, 0.67232, 0.83193, 0.92224, 0.96875, 0.98976, 0.99757, 0.99968, 0.99999, 1], easeOutSine: [0, 0.15643447, 0.30901699, 0.4539905, 0.58778525, 0.70710678, 0.80901699, 0.89100652, 0.95105652, 0.98768834, 1], linear: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] }; function generate(method) { var fn = helpers.easingEffects[method]; var accuracy = Math.pow(10, 8); var count = 10; var values = []; var i; for (i = 0; i <= count; ++i) { values.push(Math.round(accuracy * fn(i / count)) / accuracy); } return values; } Object.keys(helpers.easingEffects).forEach(function(method) { it ('"' + method + '" should return expected values', function() { expect(generate(method)).toEqual(expected[method]); }); }); }); }); ================================================ FILE: test/specs/helpers.interpolation.tests.js ================================================ const {_pointInLine, _steppedInterpolation, _bezierInterpolation} = Chart.helpers; describe('helpers.interpolation', function() { it('Should interpolate a point in line', function() { expect(_pointInLine({x: 10, y: 10}, {x: 20, y: 20}, 0)).toEqual({x: 10, y: 10}); expect(_pointInLine({x: 10, y: 10}, {x: 20, y: 20}, 0.5)).toEqual({x: 15, y: 15}); expect(_pointInLine({x: 10, y: 10}, {x: 20, y: 20}, 1)).toEqual({x: 20, y: 20}); }); it('Should interpolate a point in stepped line', function() { expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0, 'before')).toEqual({x: 10, y: 10}); expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.4, 'before')).toEqual({x: 14, y: 20}); expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.5, 'before')).toEqual({x: 15, y: 20}); expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 1, 'before')).toEqual({x: 20, y: 20}); expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0, 'middle')).toEqual({x: 10, y: 10}); expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.4, 'middle')).toEqual({x: 14, y: 10}); expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.5, 'middle')).toEqual({x: 15, y: 20}); expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 1, 'middle')).toEqual({x: 20, y: 20}); expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0, 'after')).toEqual({x: 10, y: 10}); expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.4, 'after')).toEqual({x: 14, y: 10}); expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 0.5, 'after')).toEqual({x: 15, y: 10}); expect(_steppedInterpolation({x: 10, y: 10}, {x: 20, y: 20}, 1, 'after')).toEqual({x: 20, y: 20}); }); it('Should interpolate a point in curve', function() { const pt1 = {x: 10, y: 10, cp2x: 12, cp2y: 12}; const pt2 = {x: 20, y: 30, cp1x: 18, cp1y: 28}; expect(_bezierInterpolation(pt1, pt2, 0)).toEqual({x: 10, y: 10}); expect(_bezierInterpolation(pt1, pt2, 0.2)).toBeCloseToPoint({x: 11.616, y: 12.656}); expect(_bezierInterpolation(pt1, pt2, 1)).toEqual({x: 20, y: 30}); }); }); ================================================ FILE: test/specs/helpers.math.tests.js ================================================ const math = Chart.helpers; describe('Chart.helpers.math', function() { var factorize = math._factorize; var decimalPlaces = math._decimalPlaces; it('should factorize', function() { expect(factorize(1000)).toEqual([1, 2, 4, 5, 8, 10, 20, 25, 40, 50, 100, 125, 200, 250, 500]); expect(factorize(60)).toEqual([1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30]); expect(factorize(30)).toEqual([1, 2, 3, 5, 6, 10, 15]); expect(factorize(24)).toEqual([1, 2, 3, 4, 6, 8, 12]); expect(factorize(12)).toEqual([1, 2, 3, 4, 6]); expect(factorize(4)).toEqual([1, 2]); expect(factorize(-1)).toEqual([]); expect(factorize(2.76)).toEqual([]); }); it('should do a log10 operation', function() { expect(math.log10(0)).toBe(-Infinity); // Check all allowed powers of 10, which should return integer values var maxPowerOf10 = Math.floor(math.log10(Number.MAX_VALUE)); for (var i = 0; i < maxPowerOf10; i += 1) { expect(math.log10(Math.pow(10, i))).toBe(i); } }); it('should get the correct number of decimal places', function() { expect(decimalPlaces(100)).toBe(0); expect(decimalPlaces(1)).toBe(0); expect(decimalPlaces(0)).toBe(0); expect(decimalPlaces(0.01)).toBe(2); expect(decimalPlaces(-0.01)).toBe(2); expect(decimalPlaces('1')).toBe(undefined); expect(decimalPlaces('')).toBe(undefined); expect(decimalPlaces(undefined)).toBe(undefined); expect(decimalPlaces(12345678.1234)).toBe(4); expect(decimalPlaces(1234567890.1234567)).toBe(7); }); it('should get an angle from a point', function() { var center = { x: 0, y: 0 }; expect(math.getAngleFromPoint(center, { x: 0, y: 10 })).toEqual({ angle: Math.PI / 2, distance: 10, }); expect(math.getAngleFromPoint(center, { x: Math.sqrt(2), y: Math.sqrt(2) })).toEqual({ angle: Math.PI / 4, distance: 2 }); expect(math.getAngleFromPoint(center, { x: -1.0 * Math.sqrt(2), y: -1.0 * Math.sqrt(2) })).toEqual({ angle: Math.PI * 1.25, distance: 2 }); }); it('should convert between radians and degrees', function() { expect(math.toRadians(180)).toBe(Math.PI); expect(math.toRadians(90)).toBe(0.5 * Math.PI); expect(math.toDegrees(Math.PI)).toBe(180); expect(math.toDegrees(Math.PI * 3 / 2)).toBe(270); }); it('should correctly determine if two numbers are essentially equal', function() { expect(math.almostEquals(0, Number.EPSILON, 2 * Number.EPSILON)).toBe(true); expect(math.almostEquals(1, 1.1, 0.0001)).toBe(false); expect(math.almostEquals(1e30, 1e30 + Number.EPSILON, 0)).toBe(false); expect(math.almostEquals(1e30, 1e30 + Number.EPSILON, 2 * Number.EPSILON)).toBe(true); }); it('should get the correct sign', function() { expect(math.sign(0)).toBe(0); expect(math.sign(10)).toBe(1); expect(math.sign(-5)).toBe(-1); }); it('should correctly determine if a numbers are essentially whole', function() { expect(math.almostWhole(0.99999, 0.0001)).toBe(true); expect(math.almostWhole(0.9, 0.0001)).toBe(false); expect(math.almostWhole(1234567890123, 0.0001)).toBe(true); expect(math.almostWhole(1234567890123.001, 0.0001)).toBe(false); }); it('should detect a number', function() { expect(math.isNumber(123)).toBe(true); expect(math.isNumber('123')).toBe(true); expect(math.isNumber(null)).toBe(false); expect(math.isNumber(NaN)).toBe(false); expect(math.isNumber(undefined)).toBe(false); expect(math.isNumber('cbc')).toBe(false); expect(math.isNumber(Symbol())).toBe(false); expect(math.isNumber(Object.create(null))).toBe(false); }); it('should compute shortest distance between angles', function() { expect(math._angleDiff(1, 2)).toEqual(-1); expect(math._angleDiff(2, 1)).toEqual(1); expect(math._angleDiff(0, 3.15)).toBeCloseTo(3.13, 2); expect(math._angleDiff(0, 3.13)).toEqual(-3.13); expect(math._angleDiff(6.2, 0)).toBeCloseTo(-0.08, 2); expect(math._angleDiff(6.3, 0)).toBeCloseTo(0.02, 2); expect(math._angleDiff(4 * Math.PI, -4 * Math.PI)).toBeCloseTo(0, 4); expect(math._angleDiff(4 * Math.PI, -3 * Math.PI)).toBeCloseTo(-3.14, 2); expect(math._angleDiff(6.28, 3.1)).toBeCloseTo(-3.1, 2); expect(math._angleDiff(6.28, 3.2)).toBeCloseTo(3.08, 2); }); it('should normalize angles correctly', function() { expect(math._normalizeAngle(-Math.PI)).toEqual(Math.PI); expect(math._normalizeAngle(Math.PI)).toEqual(Math.PI); expect(math._normalizeAngle(2)).toEqual(2); expect(math._normalizeAngle(5 * Math.PI)).toEqual(Math.PI); expect(math._normalizeAngle(-50 * Math.PI)).toBeCloseTo(6.28, 2); }); it('should determine if angle is between boundaries', function() { expect(math._angleBetween(2, 1, 3)).toBeTrue(); expect(math._angleBetween(2, 3, 1)).toBeFalse(); expect(math._angleBetween(-3.14, 2, 4)).toBeTrue(); expect(math._angleBetween(-3.14, 4, 2)).toBeFalse(); expect(math._angleBetween(0, -1, 1)).toBeTrue(); expect(math._angleBetween(-1, 0, 1)).toBeFalse(); expect(math._angleBetween(-15 * Math.PI, 3.1, 3.2)).toBeTrue(); expect(math._angleBetween(15 * Math.PI, -3.2, -3.1)).toBeTrue(); }); }); ================================================ FILE: test/specs/helpers.options.tests.js ================================================ const {toLineHeight, toPadding, toFont, resolve, toTRBLCorners} = Chart.helpers; describe('Chart.helpers.options', function() { describe('toLineHeight', function() { it ('should support keyword values', function() { expect(toLineHeight('normal', 16)).toBe(16 * 1.2); }); it ('should support unitless values', function() { expect(toLineHeight(1.4, 16)).toBe(16 * 1.4); expect(toLineHeight('1.4', 16)).toBe(16 * 1.4); }); it ('should support length values', function() { expect(toLineHeight('42px', 16)).toBe(42); expect(toLineHeight('1.4em', 16)).toBe(16 * 1.4); }); it ('should support percentage values', function() { expect(toLineHeight('140%', 16)).toBe(16 * 1.4); }); it ('should fallback to default (1.2) for invalid values', function() { expect(toLineHeight(null, 16)).toBe(16 * 1.2); expect(toLineHeight(undefined, 16)).toBe(16 * 1.2); expect(toLineHeight('foobar', 16)).toBe(16 * 1.2); }); }); describe('toTRBLCorners', function() { it('should support number values', function() { expect(toTRBLCorners(4)).toEqual( {topLeft: 4, topRight: 4, bottomLeft: 4, bottomRight: 4}); expect(toTRBLCorners(4.5)).toEqual( {topLeft: 4.5, topRight: 4.5, bottomLeft: 4.5, bottomRight: 4.5}); }); it('should support string values', function() { expect(toTRBLCorners('4')).toEqual( {topLeft: 4, topRight: 4, bottomLeft: 4, bottomRight: 4}); expect(toTRBLCorners('4.5')).toEqual( {topLeft: 4.5, topRight: 4.5, bottomLeft: 4.5, bottomRight: 4.5}); }); it('should support object values', function() { expect(toTRBLCorners({topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4})).toEqual( {topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4}); expect(toTRBLCorners({topLeft: 1.5, topRight: 2.5, bottomLeft: 3.5, bottomRight: 4.5})).toEqual( {topLeft: 1.5, topRight: 2.5, bottomLeft: 3.5, bottomRight: 4.5}); expect(toTRBLCorners({topLeft: '1', topRight: '2', bottomLeft: '3', bottomRight: '4'})).toEqual( {topLeft: 1, topRight: 2, bottomLeft: 3, bottomRight: 4}); }); it('should fallback to 0 for invalid values', function() { expect(toTRBLCorners({topLeft: 'foo', topRight: 'foo', bottomLeft: 'foo', bottomRight: 'foo'})).toEqual( {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); expect(toTRBLCorners({topLeft: null, topRight: null, bottomLeft: null, bottomRight: null})).toEqual( {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); expect(toTRBLCorners({})).toEqual( {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); expect(toTRBLCorners('foo')).toEqual( {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); expect(toTRBLCorners(null)).toEqual( {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); expect(toTRBLCorners(undefined)).toEqual( {topLeft: 0, topRight: 0, bottomLeft: 0, bottomRight: 0}); }); }); describe('toPadding', function() { it ('should support number values', function() { expect(toPadding(4)).toEqual( {top: 4, right: 4, bottom: 4, left: 4, height: 8, width: 8}); expect(toPadding(4.5)).toEqual( {top: 4.5, right: 4.5, bottom: 4.5, left: 4.5, height: 9, width: 9}); }); it ('should support string values', function() { expect(toPadding('4')).toEqual( {top: 4, right: 4, bottom: 4, left: 4, height: 8, width: 8}); expect(toPadding('4.5')).toEqual( {top: 4.5, right: 4.5, bottom: 4.5, left: 4.5, height: 9, width: 9}); }); it ('should support object values', function() { expect(toPadding({top: 1, right: 2, bottom: 3, left: 4})).toEqual( {top: 1, right: 2, bottom: 3, left: 4, height: 4, width: 6}); expect(toPadding({top: 1.5, right: 2.5, bottom: 3.5, left: 4.5})).toEqual( {top: 1.5, right: 2.5, bottom: 3.5, left: 4.5, height: 5, width: 7}); expect(toPadding({top: '1', right: '2', bottom: '3', left: '4'})).toEqual( {top: 1, right: 2, bottom: 3, left: 4, height: 4, width: 6}); }); it ('should fallback to 0 for invalid values', function() { expect(toPadding({top: 'foo', right: 'foo', bottom: 'foo', left: 'foo'})).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); expect(toPadding({top: null, right: null, bottom: null, left: null})).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); expect(toPadding({})).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); expect(toPadding('foo')).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); expect(toPadding(null)).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); expect(toPadding(undefined)).toEqual( {top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0}); }); it('should support x / y shorthands', function() { expect(toPadding({x: 1, y: 2})).toEqual( {top: 2, right: 1, bottom: 2, left: 1, height: 4, width: 2}); expect(toPadding({x: 1, left: 0})).toEqual( {top: 0, right: 1, bottom: 0, left: 0, height: 0, width: 1}); expect(toPadding({y: 5, bottom: 0})).toEqual( {top: 5, right: 0, bottom: 0, left: 0, height: 5, width: 0}); }); }); describe('toFont', function() { it('should return a font with default values', function() { const defaultFont = Object.assign({}, Chart.defaults.font); Object.assign(Chart.defaults.font, { family: 'foobar', size: 42, style: 'oblique 9deg', lineHeight: 1.5 }); expect(toFont({})).toEqual({ family: 'foobar', lineHeight: 63, size: 42, string: 'oblique 9deg 42px foobar', style: 'oblique 9deg', weight: null }); Object.assign(Chart.defaults.font, defaultFont); }); it ('should return a font with given values', function() { expect(toFont({ family: 'bla', lineHeight: 8, size: 21, style: 'oblique -90deg' })).toEqual({ family: 'bla', lineHeight: 8 * 21, size: 21, string: 'oblique -90deg 21px bla', style: 'oblique -90deg', weight: null }); }); it ('should handle a string font size', function() { expect(toFont({ family: 'bla', lineHeight: 8, size: '21', style: 'italic' })).toEqual({ family: 'bla', lineHeight: 8 * 21, size: 21, string: 'italic 21px bla', style: 'italic', weight: null }); }); it('should return null as a font string if size or family are missing', function() { const fontFamily = Chart.defaults.font.family; const fontSize = Chart.defaults.font.size; delete Chart.defaults.font.family; delete Chart.defaults.font.size; expect(toFont({ style: 'italic', size: 12 }).string).toBeNull(); expect(toFont({ style: 'italic', family: 'serif' }).string).toBeNull(); Chart.defaults.font.family = fontFamily; Chart.defaults.font.size = fontSize; }); it('font.style should be optional for font strings', function() { const fontStyle = Chart.defaults.font.style; delete Chart.defaults.font.style; expect(toFont({ size: 12, family: 'serif' }).string).toBe('12px serif'); Chart.defaults.font.style = fontStyle; }); }); describe('resolve', function() { it ('should fallback to the first defined input', function() { expect(resolve([42])).toBe(42); expect(resolve([42, 'foo'])).toBe(42); expect(resolve([undefined, 42, 'foo'])).toBe(42); expect(resolve([42, 'foo', undefined])).toBe(42); expect(resolve([undefined])).toBe(undefined); }); it ('should correctly handle empty values (null, 0, "")', function() { expect(resolve([0, 'foo'])).toBe(0); expect(resolve(['', 'foo'])).toBe(''); expect(resolve([null, 'foo'])).toBe(null); }); it ('should support indexable options if index is provided', function() { var input = [42, 'foo', 'bar']; expect(resolve([input], undefined, 0)).toBe(42); expect(resolve([input], undefined, 1)).toBe('foo'); expect(resolve([input], undefined, 2)).toBe('bar'); }); it ('should fallback if an indexable option value is undefined', function() { var input = [42, undefined, 'bar']; expect(resolve([input], undefined, 1)).toBe(undefined); expect(resolve([input, 'foo'], undefined, 1)).toBe('foo'); }); it ('should loop if an indexable option index is out of bounds', function() { var input = [42, undefined, 'bar']; expect(resolve([input], undefined, 3)).toBe(42); expect(resolve([input, 'foo'], undefined, 4)).toBe('foo'); expect(resolve([input, 'foo'], undefined, 5)).toBe('bar'); }); it ('should not handle indexable options if index is undefined', function() { var array = [42, 'foo', 'bar']; expect(resolve([array])).toBe(array); expect(resolve([array], undefined, undefined)).toBe(array); }); it ('should support scriptable options if context is provided', function() { var input = function(context) { return context.v * 2; }; expect(resolve([42], {v: 42})).toBe(42); expect(resolve([input], {v: 42})).toBe(84); }); it ('should fallback if a scriptable option returns undefined', function() { var input = function() {}; expect(resolve([input], {v: 42})).toBe(undefined); expect(resolve([input, 'foo'], {v: 42})).toBe('foo'); expect(resolve([input, undefined, 'foo'], {v: 42})).toBe('foo'); }); it ('should not handle scriptable options if context is undefined', function() { var input = function(context) { return context.v * 2; }; expect(resolve([input])).toBe(input); expect(resolve([input], undefined)).toBe(input); }); it ('should handle scriptable and indexable option', function() { var input = function(context) { return [context.v, undefined, 'bar']; }; expect(resolve([input, 'foo'], {v: 42}, 0)).toBe(42); expect(resolve([input, 'foo'], {v: 42}, 1)).toBe('foo'); expect(resolve([input, 'foo'], {v: 42}, 5)).toBe('bar'); expect(resolve([input, ['foo', 'bar']], {v: 42}, 1)).toBe('bar'); }); }); }); ================================================ FILE: test/specs/helpers.segment.tests.js ================================================ const {_boundSegment} = Chart.helpers; describe('helpers.segments', function() { describe('_boundSegment', function() { const points = [{x: 10, y: 1}, {x: 20, y: 2}, {x: 30, y: 3}]; const segment = {start: 0, end: 2, loop: false}; it('should not find segment from before the line', function() { expect(_boundSegment(segment, points, {property: 'x', start: 5, end: 9.99999})).toEqual([]); }); it('should not find segment from after the line', function() { expect(_boundSegment(segment, points, {property: 'x', start: 30.00001, end: 800})).toEqual([]); }); it('should find segment when starting before line', function() { expect(_boundSegment(segment, points, {property: 'x', start: 5, end: 15})).toEqual([{start: 0, end: 1, loop: false, style: undefined}]); }); it('should find segment directly on point', function() { expect(_boundSegment(segment, points, {property: 'x', start: 10, end: 10})).toEqual([{start: 0, end: 0, loop: false, style: undefined}]); }); it('should find segment from range between points', function() { expect(_boundSegment(segment, points, {property: 'x', start: 11, end: 14})).toEqual([{start: 0, end: 1, loop: false, style: undefined}]); }); it('should find segment from point between points', function() { expect(_boundSegment(segment, points, {property: 'x', start: 22, end: 22})).toEqual([{start: 1, end: 2, loop: false, style: undefined}]); }); it('should find whole segment', function() { expect(_boundSegment(segment, points, {property: 'x', start: 0, end: 50})).toEqual([{start: 0, end: 2, loop: false, style: undefined}]); }); it('should find correct segment from near points', function() { expect(_boundSegment(segment, points, {property: 'x', start: 10.001, end: 29.999})).toEqual([{start: 0, end: 2, loop: false, style: undefined}]); }); it('should find segment from after the line', function() { expect(_boundSegment(segment, points, {property: 'x', start: 25, end: 35})).toEqual([{start: 1, end: 2, loop: false, style: undefined}]); }); it('should find multiple segments', function() { const points2 = [{x: 0, y: 100}, {x: 1, y: 50}, {x: 2, y: 70}, {x: 4, y: 80}, {x: 5, y: -100}]; expect(_boundSegment({start: 0, end: 4, loop: false}, points2, {property: 'y', start: 60, end: 60})).toEqual([ {start: 0, end: 1, loop: false, style: undefined}, {start: 1, end: 2, loop: false, style: undefined}, {start: 3, end: 4, loop: false, style: undefined}, ]); }); it('should find correct segments when there are multiple points with same property value', function() { const repeatedPoints = [{x: 1, y: 5}, {x: 1, y: 6}, {x: 2, y: 5}, {x: 2, y: 6}, {x: 3, y: 5}, {x: 3, y: 6}, {x: 3, y: 7}]; expect(_boundSegment({start: 0, end: 6, loop: false}, repeatedPoints, {property: 'x', start: 1, end: 1.1})).toEqual([{start: 0, end: 2, loop: false, style: undefined}]); expect(_boundSegment({start: 0, end: 6, loop: false}, repeatedPoints, {property: 'x', start: 2, end: 2.1})).toEqual([{start: 2, end: 4, loop: false, style: undefined}]); expect(_boundSegment({start: 0, end: 6, loop: false}, repeatedPoints, {property: 'x', start: 2, end: 3.1})).toEqual([{start: 2, end: 6, loop: false, style: undefined}]); expect(_boundSegment({start: 0, end: 6, loop: false}, repeatedPoints, {property: 'x', start: 0, end: 8})).toEqual([{start: 0, end: 6, loop: false, style: undefined}]); }); }); }); ================================================ FILE: test/specs/mixed.tests.js ================================================ describe('Mixed charts', function() { describe('auto', jasmine.fixture.specs('mixed')); it('shoud be constructed with doughnuts chart', function() { const chart = window.acquireChart({ data: { datasets: [{ type: 'line', data: [10, 20, 30, 40], }, { type: 'doughnut', data: [10, 20, 30, 50], } ], labels: [] } }); const meta0 = chart.getDatasetMeta(0); expect(meta0.type).toEqual('line'); const meta1 = chart.getDatasetMeta(1); expect(meta1.type).toEqual('doughnut'); }); it('shoud be constructed with pie chart', function() { const chart = window.acquireChart({ data: { datasets: [{ type: 'bar', data: [10, 20, 30, 40], }, { type: 'pie', data: [10, 20, 30, 50], } ], labels: [] } }); const meta0 = chart.getDatasetMeta(0); expect(meta0.type).toEqual('bar'); const meta1 = chart.getDatasetMeta(1); expect(meta1.type).toEqual('pie'); }); }); ================================================ FILE: test/specs/platform.basic.tests.js ================================================ describe('Platform.basic', function() { it('should automatically choose the BasicPlatform for offscreen canvas', function() { const chart = acquireChart({type: 'line'}, {useOffscreenCanvas: true}); expect(chart.platform).toBeInstanceOf(Chart.platforms.BasicPlatform); chart.destroy(); }); it('should disable animations', function() { const chart = acquireChart({type: 'line', options: {animation: {}}}, {useOffscreenCanvas: true}); expect(chart.options.animation).toEqual(false); chart.destroy(); }); it('supports choosing the BasicPlatform in a web worker', function(done) { const canvas = document.createElement('canvas'); if (!canvas.transferControlToOffscreen) { pending(); } const offscreenCanvas = canvas.transferControlToOffscreen(); const worker = new Worker('base/test/BasicChartWebWorker.js'); worker.onmessage = (event) => { worker.terminate(); const {type, errorMessage} = event.data; if (type === 'error') { done.fail(errorMessage); } else if (type === 'success') { expect(type).toEqual('success'); done(); } else { done.fail('invalid message type sent by worker: ' + type); } }; worker.postMessage({type: 'initialize', canvas: offscreenCanvas}, [offscreenCanvas]); }); describe('with offscreenCanvas', function() { it('supports laying out a simple chart', function() { const chart = acquireChart({ type: 'bar', data: { datasets: [ {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] } }, { canvas: { height: 150, width: 250 }, useOffscreenCanvas: true, }); expect(chart.platform).toBeInstanceOf(Chart.platforms.BasicPlatform); expect(chart.chartArea.bottom).toBeCloseToPixel(120); expect(chart.chartArea.left).toBeCloseToPixel(31); expect(chart.chartArea.right).toBeCloseToPixel(250); expect(chart.chartArea.top).toBeCloseToPixel(32); }); it('supports resizing a chart', function() { const chart = acquireChart({ type: 'bar', data: { datasets: [ {data: [10, 5, 0, 25, 78, -10]} ], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5', 'tick6'] } }, { canvas: { height: 150, width: 250 }, useOffscreenCanvas: true, }); expect(chart.platform).toBeInstanceOf(Chart.platforms.BasicPlatform); const canvasElement = chart.canvas; canvasElement.height = 200; canvasElement.width = 300; chart.resize(); expect(chart.chartArea.bottom).toBeCloseToPixel(150); expect(chart.chartArea.left).toBeCloseToPixel(31); expect(chart.chartArea.right).toBeCloseToPixel(300); expect(chart.chartArea.top).toBeCloseToPixel(32); }); }); }); ================================================ FILE: test/specs/platform.dom.tests.js ================================================ const DomPlatform = Chart.platforms.DomPlatform; describe('Platform.dom', function() { describe('context acquisition', function() { var canvasId = 'chartjs-canvas'; beforeEach(function() { var canvas = document.createElement('canvas'); canvas.setAttribute('id', canvasId); window.document.body.appendChild(canvas); }); afterEach(function() { document.getElementById(canvasId).remove(); }); it('should use the DomPlatform by default', function() { var chart = acquireChart({type: 'line'}); expect(chart.platform).toBeInstanceOf(Chart.platforms.DomPlatform); chart.destroy(); }); // see https://github.com/chartjs/Chart.js/issues/2807 it('should gracefully handle invalid item', function() { var chart = new Chart('foobar'); expect(chart).not.toBeValidChart(); chart.destroy(); }); it('should accept a DOM element id', function() { var canvas = document.getElementById(canvasId); var chart = new Chart(canvasId); expect(chart).toBeValidChart(); expect(chart.canvas).toBe(canvas); expect(chart.ctx).toBe(canvas.getContext('2d')); chart.destroy(); }); it('should accept a canvas element', function() { var canvas = document.getElementById(canvasId); var chart = new Chart(canvas); expect(chart).toBeValidChart(); expect(chart.canvas).toBe(canvas); expect(chart.ctx).toBe(canvas.getContext('2d')); chart.destroy(); }); it('should accept a canvas context2D', function() { var canvas = document.getElementById(canvasId); var context = canvas.getContext('2d'); var chart = new Chart(context); expect(chart).toBeValidChart(); expect(chart.canvas).toBe(canvas); expect(chart.ctx).toBe(context); chart.destroy(); }); it('should accept an array containing canvas', function() { var canvas = document.getElementById(canvasId); var chart = new Chart([canvas]); expect(chart).toBeValidChart(); expect(chart.canvas).toBe(canvas); expect(chart.ctx).toBe(canvas.getContext('2d')); chart.destroy(); }); it('should accept a canvas from an iframe', function(done) { var iframe = document.createElement('iframe'); iframe.onload = function() { var doc = iframe.contentDocument; doc.body.innerHTML += ''; var canvas = doc.getElementById('chart'); var chart = new Chart(canvas); expect(chart).toBeValidChart(); expect(chart.canvas).toBe(canvas); expect(chart.ctx).toBe(canvas.getContext('2d')); chart.destroy(); canvas.remove(); iframe.remove(); done(); }; document.body.appendChild(iframe); }); }); describe('config.options.aspectRatio', function() { it('should use default "global" aspect ratio for render and display sizes', function() { var chart = acquireChart({ options: { responsive: false } }, { canvas: { style: 'width: 620px' } }); expect(chart).toBeChartOfSize({ dw: 620, dh: 310, rw: 620, rh: 310, }); }); it('should use default "chart" aspect ratio for render and display sizes', function() { var ratio = Chart.overrides.doughnut.aspectRatio; Chart.overrides.doughnut.aspectRatio = 1; var chart = acquireChart({ type: 'doughnut', options: { responsive: false } }, { canvas: { style: 'width: 425px' } }); Chart.overrides.doughnut.aspectRatio = ratio; expect(chart).toBeChartOfSize({ dw: 425, dh: 425, rw: 425, rh: 425, }); }); it('should use "user" aspect ratio for render and display sizes', function() { var chart = acquireChart({ options: { responsive: false, aspectRatio: 3 } }, { canvas: { style: 'width: 405px' } }); expect(chart).toBeChartOfSize({ dw: 405, dh: 135, rw: 405, rh: 135, }); }); it('should not apply aspect ratio when height specified', function() { var chart = acquireChart({ options: { responsive: false, aspectRatio: 3 } }, { canvas: { style: 'width: 400px; height: 410px' } }); expect(chart).toBeChartOfSize({ dw: 400, dh: 410, rw: 400, rh: 410, }); }); }); describe('config.options.responsive: false', function() { it('should use default canvas size for render and display sizes', function() { var chart = acquireChart({ options: { responsive: false } }, { canvas: { style: '' } }); expect(chart).toBeChartOfSize({ dw: 300, dh: 150, rw: 300, rh: 150, }); }); it('should use canvas attributes for render and display sizes', function() { var chart = acquireChart({ options: { responsive: false } }, { canvas: { style: '', width: 305, height: 245, } }); expect(chart).toBeChartOfSize({ dw: 305, dh: 245, rw: 305, rh: 245, }); }); it('should use canvas style for render and display sizes (if no attributes)', function() { var chart = acquireChart({ options: { responsive: false } }, { canvas: { style: 'width: 345px; height: 125px' } }); expect(chart).toBeChartOfSize({ dw: 345, dh: 125, rw: 345, rh: 125, }); }); it('should use attributes for the render size and style for the display size', function() { var chart = acquireChart({ options: { responsive: false } }, { canvas: { style: 'width: 345px; height: 125px;', width: 165, height: 85, } }); expect(chart).toBeChartOfSize({ dw: 345, dh: 125, rw: 165, rh: 85, }); }); // https://github.com/chartjs/Chart.js/issues/3860 it('should support decimal display width and/or height', function() { var chart = acquireChart({ options: { responsive: false } }, { canvas: { style: 'width: 345.42px; height: 125.42px;' } }); expect(chart).toBeChartOfSize({ dw: 345, dh: 125, rw: 345, rh: 125, }); }); }); describe('config.options.responsive: true (maintainAspectRatio: true)', function() { it('should fit parent using aspect ratio to calculate size', function() { var chart = acquireChart({ options: { responsive: true, maintainAspectRatio: true } }, { canvas: { style: 'width: 150px; height: 245px' }, wrapper: { style: 'width: 300px; height: 350px' } }); waitForResize(chart, () => { expect(chart).toBeChartOfSize({ dw: 214, dh: 350, rw: 214, rh: 350, }); }); }); }); describe('controller.destroy', function() { it('should reset context to default values', function() { var wrapper = document.createElement('div'); var canvas = document.createElement('canvas'); wrapper.appendChild(canvas); window.document.body.appendChild(wrapper); var chart = new Chart(canvas, {}); var context = chart.ctx; chart.destroy(); // https://www.w3.org/TR/2dcontext/#conformance-requirements Chart.helpers.each({ fillStyle: '#000000', font: '10px sans-serif', lineJoin: 'miter', lineCap: 'butt', lineWidth: 1, miterLimit: 10, shadowBlur: 0, shadowColor: 'rgba(0, 0, 0, 0)', shadowOffsetX: 0, shadowOffsetY: 0, strokeStyle: '#000000', textAlign: 'start', textBaseline: 'alphabetic' }, function(value, key) { expect(context[key]).toBe(value); }); wrapper.parentNode.removeChild(wrapper); }); it('should restore canvas initial values', function(done) { var wrapper = document.createElement('div'); var canvas = document.createElement('canvas'); canvas.setAttribute('width', 180); canvas.setAttribute('style', 'width: 512px; height: 480px'); wrapper.setAttribute('style', 'width: 450px; height: 450px; position: relative'); wrapper.appendChild(canvas); window.document.body.appendChild(wrapper); var chart = new Chart(canvas.getContext('2d'), { options: { responsive: true, maintainAspectRatio: false } }); waitForResize(chart, function() { expect(chart).toBeChartOfSize({ dw: 475, dh: 450, rw: 475, rh: 450, }); chart.destroy(); expect(canvas.getAttribute('width')).toBe('180'); expect(canvas.getAttribute('height')).toBe(null); expect(canvas.style.width).toBe('512px'); expect(canvas.style.height).toBe('480px'); expect(canvas.style.display).toBe(''); wrapper.parentNode.removeChild(wrapper); done(); }); wrapper.style.width = '475px'; }); }); describe('event handling', function() { it('should notify plugins about events', async function() { var notifiedEvent; var plugin = { afterEvent: function(chart, args) { notifiedEvent = args.event; } }; var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { responsive: true }, plugins: [plugin] }); await jasmine.triggerMouseEvent(chart, 'click', { x: chart.width / 2, y: chart.height / 2 }); // Check that notifiedEvent is correct expect(notifiedEvent).not.toBe(undefined); // Is type correctly translated expect(notifiedEvent.type).toBe('click'); // Relative Position expect(notifiedEvent.x).toBeCloseToPixel(chart.width / 2); expect(notifiedEvent.y).toBeCloseToPixel(chart.height / 2); }); }); describe('isAttached', function() { it('should detect detached when canvas is attached to DOM', function() { var platform = new DomPlatform(); var canvas = document.createElement('canvas'); var div = document.createElement('div'); var anotherDiv = document.createElement('div'); expect(platform.isAttached(canvas)).toEqual(false); div.appendChild(canvas); expect(platform.isAttached(canvas)).toEqual(false); anotherDiv.appendChild(div); expect(platform.isAttached(canvas)).toEqual(false); document.body.appendChild(anotherDiv); expect(platform.isAttached(canvas)).toEqual(true); anotherDiv.removeChild(div); expect(platform.isAttached(canvas)).toEqual(false); div.removeChild(canvas); expect(platform.isAttached(canvas)).toEqual(false); document.body.removeChild(anotherDiv); expect(platform.isAttached(canvas)).toEqual(false); }); }); }); ================================================ FILE: test/specs/plugin.colors.tests.js ================================================ describe('Plugin.colors', () => { describe('auto', jasmine.fixture.specs('plugin.colors')); describe('Plugin.colors.chartDefaults', () => { beforeAll(() => { Chart.defaults.backgroundColor = ['green', 'yellow']; }); afterAll(() => { Chart.defaults.backgroundColor = 'rgba(0,0,0,0.1)'; }); it('should not use colors plugin when chart defaults are given', () => { const chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 10], label: 'dataset1' }], labels: ['label1', 'label2'] }, options: { plugins: { colors: { enabled: true } } } }); const meta = chart.getDatasetMeta(0); expect(meta.data[0].options.backgroundColor).toBe('green'); expect(meta.data[1].options.backgroundColor).toBe('yellow'); }); }); }); ================================================ FILE: test/specs/plugin.decimation.tests.js ================================================ describe('Plugin.decimation', function() { describe('auto', jasmine.fixture.specs('plugin.decimation')); describe('lttb', function() { const originalData = [ {x: 0, y: 0}, {x: 1, y: 1}, {x: 2, y: 2}, {x: 3, y: 3}, {x: 4, y: 4}, {x: 5, y: 5}, {x: 6, y: 6}, {x: 7, y: 7}, {x: 8, y: 8}, {x: 9, y: 9}]; it('should draw all element if sample is greater than data based on canvas width', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: originalData, label: 'dataset1' }] }, scales: { x: { type: 'linear', min: 0, max: 9 } }, options: { plugins: { decimation: { enabled: true, algorithm: 'lttb', samples: 100 } } } }, { canvas: { height: 1, width: 1 }, wrapper: { height: 1, width: 1 } }); expect(chart.data.datasets[0].data.length).toBe(10); }); it('should draw the specified number of elements based on canvas width', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: originalData, label: 'dataset1' }] }, options: { parsing: false, scales: { x: { type: 'linear', min: 0, max: 9 } }, plugins: { decimation: { enabled: true, algorithm: 'lttb', samples: 7 } } } }, { canvas: { height: 1, width: 1 }, wrapper: { height: 1, width: 1 } }); expect(chart.data.datasets[0].data.length).toBe(7); }); it('should draw the specified number of elements based on threshold', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: originalData, label: 'dataset1' }] }, options: { parsing: false, scales: { x: { type: 'linear' } }, plugins: { decimation: { enabled: true, algorithm: 'lttb', samples: 5, threshold: 7 } } } }, { canvas: { height: 100, width: 100 }, wrapper: { height: 100, width: 100 } }); expect(chart.data.datasets[0].data.length).toBe(5); }); it('should draw all element only in range', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: originalData, label: 'dataset1' }] }, options: { parsing: false, scales: { x: { type: 'linear', min: 3, max: 6 } }, plugins: { decimation: { enabled: true, algorithm: 'lttb', samples: 7 } } } }, { canvas: { height: 1, width: 1 }, wrapper: { height: 1, width: 1 } }); // Data range is 4 (3->6) and the first point is added const expectedPoints = 5; expect(chart.data.datasets[0].data.length).toBe(expectedPoints); expect(chart.data.datasets[0].data[0].x).toBe(originalData[2].x); expect(chart.data.datasets[0].data[1].x).toBe(originalData[3].x); expect(chart.data.datasets[0].data[2].x).toBe(originalData[4].x); expect(chart.data.datasets[0].data[3].x).toBe(originalData[5].x); expect(chart.data.datasets[0].data[4].x).toBe(originalData[6].x); }); it('should not crash with uneven points', function() { const data = []; for (let i = 0; i < 15552; i++) { data.push({x: i, y: i}); } function createChart() { return window.acquireChart({ type: 'line', data: { datasets: [{ data }] }, options: { devicePixelRatio: 1.25, parsing: false, scales: { x: { type: 'linear' } }, plugins: { decimation: { enabled: true, algorithm: 'lttb' } } } }, { canvas: {width: 511, height: 511}, }); } expect(createChart).not.toThrow(); }); }); }); ================================================ FILE: test/specs/plugin.filler.tests.js ================================================ describe('Plugin.filler', function() { const fillerPluginRegisterWarning = 'Tried to use the \'fill\' option without the \'Filler\' plugin enabled. Please import and register the \'Filler\' plugin and make sure it is not disabled in the options'; function decodedFillValues(chart) { return chart.data.datasets.map(function(dataset, index) { var meta = chart.getDatasetMeta(index) || {}; expect(meta.$filler).toBeDefined(); return meta.$filler.fill; }); } describe('auto', jasmine.fixture.specs('plugin.filler')); describe('dataset.fill', function() { it('Should show a warning when trying to use the filler plugin in the dataset when it\'s not registered', function() { spyOn(console, 'warn'); Chart.unregister(Chart.Filler); window.acquireChart({ type: 'line', data: { datasets: [{ fill: true }] } }); expect(console.warn).toHaveBeenCalledWith(fillerPluginRegisterWarning); Chart.register(Chart.Filler); }); it('Should show a warning when trying to use the filler plugin in the root options when it\'s not registered', function() { // jasmine.createSpy('warn'); spyOn(console, 'warn'); Chart.unregister(Chart.Filler); window.acquireChart({ type: 'line', data: { datasets: [{ }] }, options: { fill: true } }); expect(console.warn).toHaveBeenCalledWith(fillerPluginRegisterWarning); Chart.register(Chart.Filler); }); it('should support boundaries', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [ {fill: 'origin'}, {fill: 'start'}, {fill: 'end'}, ] } }); expect(decodedFillValues(chart)).toEqual(['origin', 'start', 'end']); }); it('should support absolute dataset index', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [ {fill: 1}, {fill: 3}, {fill: 0}, {fill: 2}, ] } }); expect(decodedFillValues(chart)).toEqual([1, 3, 0, 2]); }); it('should support relative dataset index', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [ {fill: '+3'}, {fill: '-1'}, {fill: '+1'}, {fill: '-2'}, ] } }); expect(decodedFillValues(chart)).toEqual([ 3, // 0 + 3 0, // 1 - 1 3, // 2 + 1 1, // 3 - 2 ]); }); it('should handle default fill when true (origin)', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [ {fill: true}, {fill: false}, ] } }); expect(decodedFillValues(chart)).toEqual(['origin', false]); }); it('should ignore self dataset index', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [ {fill: 0}, {fill: '-0'}, {fill: '+0'}, {fill: 3}, ] } }); expect(decodedFillValues(chart)).toEqual([ false, // 0 === 0 false, // 1 === 1 - 0 false, // 2 === 2 + 0 false, // 3 === 3 ]); }); it('should ignore out of bounds dataset index', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [ {fill: -2}, {fill: 4}, {fill: '-3'}, {fill: '+1'}, ] } }); expect(decodedFillValues(chart)).toEqual([ false, // 0 - 2 < 0 false, // 1 + 4 > 3 false, // 2 - 3 < 0 false, // 3 + 1 > 3 ]); }); it('should ignore invalid values', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [ {fill: 'foo'}, {fill: '+foo'}, {fill: '-foo'}, {fill: '+1.1'}, {fill: '-2.2'}, {fill: 3.3}, {fill: -4.4}, {fill: NaN}, {fill: Infinity}, {fill: ''}, {fill: null}, {fill: []}, ] } }); expect(decodedFillValues(chart)).toEqual([ false, // NaN (string) false, // NaN (string) false, // NaN (string) false, // float (string) false, // float (string) false, // float (number) false, // float (number) false, // NaN false, // !isFinite false, // empty string false, // null false, // array ]); }); }); describe('options.plugins.filler.propagate', function() { it('should compute propagated fill targets if true', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [ {fill: 'start', hidden: true}, {fill: '-1', hidden: true}, {fill: 1, hidden: true}, {fill: '-2', hidden: true}, {fill: '+1'}, {fill: '+2'}, {fill: '-1'}, {fill: 'end', hidden: true}, ] }, options: { plugins: { filler: { propagate: true } } } }); expect(decodedFillValues(chart)).toEqual([ 'start', // 'start' 'start', // 1 - 1 -> 0 (hidden) -> 'start' 'start', // 1 (hidden) -> 0 (hidden) -> 'start' 'start', // 3 - 2 -> 1 (hidden) -> 0 (hidden) -> 'start' 5, // 4 + 1 'end', // 5 + 2 -> 7 (hidden) -> 'end' 5, // 6 - 1 -> 5 'end', // 'end' ]); }); it('should preserve initial fill targets if false', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [ {fill: 'start', hidden: true}, {fill: '-1', hidden: true}, {fill: 1, hidden: true}, {fill: '-2', hidden: true}, {fill: '+1'}, {fill: '+2'}, {fill: '-1'}, {fill: 'end', hidden: true}, ] }, options: { plugins: { filler: { propagate: false } } } }); expect(decodedFillValues(chart)).toEqual([ 'start', // 'origin' 0, // 1 - 1 1, // 1 1, // 3 - 2 5, // 4 + 1 7, // 5 + 2 5, // 6 - 1 'end', // 'end' ]); }); it('should prevent recursive propagation', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [ {fill: '+2', hidden: true}, {fill: '-1', hidden: true}, {fill: '-1', hidden: true}, {fill: '-2'} ] }, options: { plugins: { filler: { propagate: true } } } }); expect(decodedFillValues(chart)).toEqual([ false, // 0 + 2 -> 2 (hidden) -> 1 (hidden) -> 0 (loop) false, // 1 - 1 -> 0 (hidden) -> 2 (hidden) -> 1 (loop) false, // 2 - 1 -> 1 (hidden) -> 0 (hidden) -> 2 (loop) false, // 3 - 2 -> 1 (hidden) -> 0 (hidden) -> 2 (hidden) -> 1 (loop) ]); }); }); }); ================================================ FILE: test/specs/plugin.legend.tests.js ================================================ // Test the rectangle element describe('Legend block tests', function() { describe('auto', jasmine.fixture.specs('plugin.legend')); it('should have the correct default config', function() { expect(Chart.defaults.plugins.legend).toEqual({ display: true, position: 'top', align: 'center', fullSize: true, reverse: false, weight: 1000, // a callback that will handle onClick: jasmine.any(Function), onHover: null, onLeave: null, labels: { color: jasmine.any(Function), boxWidth: 40, padding: 10, generateLabels: jasmine.any(Function) }, title: { color: jasmine.any(Function), display: false, position: 'center', text: '', } }); }); it('should update bar chart correctly', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ label: 'dataset1', backgroundColor: '#f31', borderCapStyle: 'butt', borderDash: [2, 2], borderDashOffset: 5.5, data: [] }, { label: 'dataset2', hidden: true, borderJoinStyle: 'miter', data: [] }, { label: 'dataset3', borderWidth: 10, borderColor: 'green', pointStyle: 'crossRot', data: [] }], labels: [] } }); expect(chart.legend.legendItems).toEqual([{ text: 'dataset1', borderRadius: undefined, fillStyle: '#f31', fontColor: '#666', hidden: false, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 0, strokeStyle: 'rgba(0,0,0,0.1)', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 0 }, { text: 'dataset2', borderRadius: undefined, fillStyle: 'rgba(0,0,0,0.1)', fontColor: '#666', hidden: true, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 0, strokeStyle: 'rgba(0,0,0,0.1)', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 1 }, { text: 'dataset3', borderRadius: undefined, fillStyle: 'rgba(0,0,0,0.1)', fontColor: '#666', hidden: false, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 10, strokeStyle: 'green', pointStyle: 'crossRot', rotation: undefined, textAlign: undefined, datasetIndex: 2 }]); }); it('should update line chart correctly', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'dataset1', backgroundColor: '#f31', borderCapStyle: 'round', borderDash: [2, 2], borderDashOffset: 5.5, data: [] }, { label: 'dataset2', hidden: true, borderJoinStyle: 'round', data: [] }, { label: 'dataset3', borderWidth: 10, borderColor: 'green', pointStyle: 'crossRot', fill: false, data: [] }], labels: [] } }); expect(chart.legend.legendItems).toEqual([{ text: 'dataset1', borderRadius: undefined, fillStyle: '#f31', fontColor: '#666', hidden: false, lineCap: 'round', lineDash: [2, 2], lineDashOffset: 5.5, lineJoin: 'miter', lineWidth: 3, strokeStyle: 'rgba(0,0,0,0.1)', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 0 }, { text: 'dataset2', borderRadius: undefined, fillStyle: 'rgba(0,0,0,0.1)', fontColor: '#666', hidden: true, lineCap: 'butt', lineDash: [], lineDashOffset: 0, lineJoin: 'round', lineWidth: 3, strokeStyle: 'rgba(0,0,0,0.1)', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 1 }, { text: 'dataset3', borderRadius: undefined, fillStyle: 'rgba(0,0,0,0.1)', fontColor: '#666', hidden: false, lineCap: 'butt', lineDash: [], lineDashOffset: 0, lineJoin: 'miter', lineWidth: 10, strokeStyle: 'green', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 2 }]); }); it('should reverse correctly', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'dataset1', backgroundColor: '#f31', borderCapStyle: 'round', borderDash: [2, 2], borderDashOffset: 5.5, data: [] }, { label: 'dataset2', hidden: true, borderJoinStyle: 'round', data: [] }, { label: 'dataset3', borderWidth: 10, borderColor: 'green', pointStyle: 'crossRot', fill: false, data: [] }], labels: [] }, options: { plugins: { legend: { reverse: true } } } }); expect(chart.legend.legendItems).toEqual([{ text: 'dataset3', borderRadius: undefined, fillStyle: 'rgba(0,0,0,0.1)', fontColor: '#666', hidden: false, lineCap: 'butt', lineDash: [], lineDashOffset: 0, lineJoin: 'miter', lineWidth: 10, strokeStyle: 'green', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 2 }, { text: 'dataset2', borderRadius: undefined, fillStyle: 'rgba(0,0,0,0.1)', fontColor: '#666', hidden: true, lineCap: 'butt', lineDash: [], lineDashOffset: 0, lineJoin: 'round', lineWidth: 3, strokeStyle: 'rgba(0,0,0,0.1)', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 1 }, { text: 'dataset1', borderRadius: undefined, fillStyle: '#f31', fontColor: '#666', hidden: false, lineCap: 'round', lineDash: [2, 2], lineDashOffset: 5.5, lineJoin: 'miter', lineWidth: 3, strokeStyle: 'rgba(0,0,0,0.1)', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 0 }]); }); it('should filter items', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ label: 'dataset1', backgroundColor: '#f31', borderCapStyle: 'butt', borderDash: [2, 2], borderDashOffset: 5.5, data: [] }, { label: 'dataset2', hidden: true, borderJoinStyle: 'miter', data: [], legendHidden: true, }, { label: 'dataset3', borderWidth: 10, borderRadius: 10, borderColor: 'green', pointStyle: 'crossRot', data: [] }], labels: [] }, options: { plugins: { legend: { labels: { filter: function(legendItem, data) { var dataset = data.datasets[legendItem.datasetIndex]; return !dataset.legendHidden; } } } } } }); expect(chart.legend.legendItems).toEqual([{ text: 'dataset1', borderRadius: undefined, fillStyle: '#f31', fontColor: '#666', hidden: false, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 0, strokeStyle: 'rgba(0,0,0,0.1)', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 0 }, { text: 'dataset3', borderRadius: undefined, fillStyle: 'rgba(0,0,0,0.1)', fontColor: '#666', hidden: false, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 10, strokeStyle: 'green', pointStyle: 'crossRot', rotation: undefined, textAlign: undefined, datasetIndex: 2 }]); }); it('should sort items', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'dataset1', backgroundColor: '#f31', borderCapStyle: 'round', borderDash: [2, 2], borderDashOffset: 5.5, data: [] }, { label: 'dataset2', hidden: true, borderJoinStyle: 'round', data: [] }, { label: 'dataset3', borderWidth: 10, borderColor: 'green', pointStyle: 'crossRot', fill: false, data: [] }], labels: [] }, options: { plugins: { legend: { labels: { sort: function(a, b) { return b.datasetIndex > a.datasetIndex ? 1 : -1; } } } } } }); expect(chart.legend.legendItems).toEqual([{ text: 'dataset3', borderRadius: undefined, fillStyle: 'rgba(0,0,0,0.1)', fontColor: '#666', hidden: false, lineCap: 'butt', lineDash: [], lineDashOffset: 0, lineJoin: 'miter', lineWidth: 10, strokeStyle: 'green', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 2 }, { text: 'dataset2', borderRadius: undefined, fillStyle: 'rgba(0,0,0,0.1)', fontColor: '#666', hidden: true, lineCap: 'butt', lineDash: [], lineDashOffset: 0, lineJoin: 'round', lineWidth: 3, strokeStyle: 'rgba(0,0,0,0.1)', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 1 }, { text: 'dataset1', borderRadius: undefined, fillStyle: '#f31', fontColor: '#666', hidden: false, lineCap: 'round', lineDash: [2, 2], lineDashOffset: 5.5, lineJoin: 'miter', lineWidth: 3, strokeStyle: 'rgba(0,0,0,0.1)', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 0 }]); }); it('should not throw when the label options are missing', function() { var makeChart = function() { window.acquireChart({ type: 'bar', data: { datasets: [{ label: 'dataset1', backgroundColor: '#f31', borderCapStyle: 'butt', borderDash: [2, 2], borderDashOffset: 5.5, data: [] }], labels: [] }, options: { plugins: { legend: { labels: false, } } } }); }; expect(makeChart).not.toThrow(); }); it('should not draw legend items outside of the chart bounds', function() { var chart = window.acquireChart( { type: 'line', data: { datasets: [1, 2, 3].map(function(n) { return { label: 'dataset' + n, data: [] }; }), labels: [] }, options: { plugins: { legend: { position: 'right' } } } }, { canvas: { width: 512, height: 105 } } ); // Check some basic assertions about the test setup expect(chart.width).toBe(512); expect(chart.legend.legendHitBoxes.length).toBe(3); // Check whether any legend items reach outside the established bounds chart.legend.legendHitBoxes.forEach(function(item) { expect(item.left + item.width).toBeLessThanOrEqual(chart.width); }); }); it('should draw legend with multiline labels', function() { const chart = window.acquireChart({ type: 'doughnut', data: { labels: [ 'ABCDE', [ 'ABCDE', 'ABCDE', ], [ 'Some Text', 'Some Text', 'Some Text', ], 'ABCDE', ], datasets: [ { label: 'test', data: [ 73.42, 18.13, 7.54, 0.9, 0.0025, 1.8e-5, ], backgroundColor: [ '#0078C2', '#56CAF5', '#B1E3F9', '#FBBC8D', '#F6A3BE', '#4EC2C1', ], }, ], }, options: { plugins: { legend: { labels: { usePointStyle: true, pointStyle: 'rect', }, position: 'right', align: 'center', maxWidth: 860, }, }, aspectRatio: 3, }, }); // Check some basic assertions about the test setup expect(chart.legend.legendHitBoxes.length).toBe(4); // Check whether any legend items reach outside the established bounds chart.legend.legendHitBoxes.forEach(function(item) { expect(item.left + item.width).toBeLessThanOrEqual(chart.width); }); }); it('should draw items with a custom boxHeight', function() { var chart = window.acquireChart( { type: 'line', data: { datasets: [{ label: 'dataset1', data: [] }], labels: [] }, options: { plugins: { legend: { position: 'right', labels: { boxHeight: 40 } } } } }, { canvas: { width: 512, height: 105 } } ); const hitBox = chart.legend.legendHitBoxes[0]; expect(hitBox.height).toBe(40); }); it('should pick up the first item when the property is an array', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ label: 'dataset1', backgroundColor: ['#f31', '#666', '#14e'], borderWidth: [5, 10, 15], borderColor: ['red', 'green', 'blue'], data: [] }], labels: [] } }); expect(chart.legend.legendItems).toEqual([{ text: 'dataset1', borderRadius: undefined, fillStyle: '#f31', fontColor: '#666', hidden: false, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 5, strokeStyle: 'red', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 0 }]); }); it('should use the borderRadius in the legend', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ label: 'dataset1', backgroundColor: ['#f31', '#666', '#14e'], borderWidth: [5, 10, 15], borderColor: ['red', 'green', 'blue'], borderRadius: 10, data: [] }], labels: [] }, options: { plugins: { legend: { labels: { useBorderRadius: true, } } } } }); expect(chart.legend.legendItems).toEqual([{ text: 'dataset1', borderRadius: 10, fillStyle: '#f31', fontColor: '#666', hidden: false, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 5, strokeStyle: 'red', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 0 }]); }); it('should use the value for the first item when the property is a function', function() { var helpers = window.Chart.helpers; var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ label: 'dataset1', backgroundColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return helpers.color({r: value * 10, g: 0, b: 0}).rgbString(); }, borderWidth: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return value; }, borderColor: function(ctx) { var value = ctx.dataset.data[ctx.dataIndex] || 0; return helpers.color({r: 255 - value * 10, g: 0, b: 0}).rgbString(); }, data: [5, 10, 15, 20] }], labels: ['A', 'B', 'C', 'D'] } }); expect(chart.legend.legendItems).toEqual([{ text: 'dataset1', borderRadius: undefined, fillStyle: 'rgb(50, 0, 0)', fontColor: '#666', hidden: false, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 5, strokeStyle: 'rgb(205, 0, 0)', pointStyle: undefined, rotation: undefined, textAlign: undefined, datasetIndex: 0 }]); }); it('should draw correctly when usePointStyle is true', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'dataset1', backgroundColor: '#f31', borderCapStyle: 'butt', borderDash: [2, 2], borderDashOffset: 5.5, borderWidth: 0, borderColor: '#f31', pointStyle: 'crossRot', pointBackgroundColor: 'rgba(0,0,0,0.1)', pointBorderWidth: 5, pointBorderColor: 'green', data: [] }, { label: 'dataset2', backgroundColor: '#f31', borderJoinStyle: 'miter', borderWidth: 2, borderColor: '#f31', pointStyle: 'crossRot', pointRotation: 15, data: [] }], labels: [] }, options: { plugins: { legend: { labels: { usePointStyle: true } } } } }); expect(chart.legend.legendItems).toEqual([{ text: 'dataset1', borderRadius: undefined, fillStyle: 'rgba(0,0,0,0.1)', fontColor: '#666', hidden: false, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 5, strokeStyle: 'green', pointStyle: 'crossRot', rotation: 0, textAlign: undefined, datasetIndex: 0 }, { text: 'dataset2', borderRadius: undefined, fillStyle: '#f31', fontColor: '#666', hidden: false, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 2, strokeStyle: '#f31', pointStyle: 'crossRot', rotation: 15, textAlign: undefined, datasetIndex: 1 }]); }); it('should draw correctly when usePointStyle is true and pointStyle override is set', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'dataset1', backgroundColor: '#f31', borderCapStyle: 'butt', borderDash: [2, 2], borderDashOffset: 5.5, borderWidth: 0, borderColor: '#f31', pointStyle: 'crossRot', pointBackgroundColor: 'rgba(0,0,0,0.1)', pointBorderWidth: 5, pointBorderColor: 'green', data: [] }, { label: 'dataset2', backgroundColor: '#f31', borderJoinStyle: 'miter', borderWidth: 2, borderColor: '#f31', pointStyle: 'crossRot', pointRotation: 15, data: [] }], labels: [] }, options: { plugins: { legend: { labels: { usePointStyle: true, pointStyle: 'star' } } } } }); expect(chart.legend.legendItems).toEqual([{ text: 'dataset1', borderRadius: undefined, fillStyle: 'rgba(0,0,0,0.1)', fontColor: '#666', hidden: false, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 5, strokeStyle: 'green', pointStyle: 'star', rotation: 0, textAlign: undefined, datasetIndex: 0 }, { text: 'dataset2', borderRadius: undefined, fillStyle: '#f31', fontColor: '#666', hidden: false, lineCap: undefined, lineDash: undefined, lineDashOffset: undefined, lineJoin: undefined, lineWidth: 2, strokeStyle: '#f31', pointStyle: 'star', rotation: 15, textAlign: undefined, datasetIndex: 1 }]); }); it('should not crash when the legend defaults are false', function() { const oldDefaults = Chart.defaults.plugins.legend; Chart.defaults.set({ plugins: { legend: false, }, }); var chart = window.acquireChart({ type: 'doughnut', data: { datasets: [{ label: 'dataset1', data: [1, 2, 3, 4] }], labels: ['', '', '', ''] }, }); expect(chart).toBeDefined(); Chart.defaults.set({ plugins: { legend: oldDefaults, }, }); }); it('should not read onClick from chart options', function() { var chart = window.acquireChart({ type: 'bar', data: { labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'], datasets: [{ label: 'dataset', backgroundColor: 'red', borderColor: 'red', data: [120, 23, 24, 45, 51] }] }, options: { responsive: true, onClick() { }, plugins: { legend: { display: true } } } }); expect(chart.legend.options.onClick).toBe(Chart.defaults.plugins.legend.onClick); }); it('should read labels.color from chart options', function() { var chart = window.acquireChart({ type: 'bar', data: { labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May'], datasets: [{ label: 'dataset', backgroundColor: 'red', borderColor: 'red', data: [120, 23, 24, 45, 51] }] }, options: { responsive: true, color: 'green', plugins: { legend: { display: true } } } }); expect(chart.legend.options.labels.color).toBe('green'); expect(chart.legend.options.title.color).toBe('green'); }); describe('config update', function() { it('should update the options', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { plugins: { legend: { display: true } } } }); expect(chart.legend.options.display).toBe(true); chart.options.plugins.legend.display = false; chart.update(); expect(chart.legend.options.display).toBe(false); }); it('should update the associated layout item', function() { var chart = acquireChart({ type: 'line', data: {}, options: { plugins: { legend: { fullSize: true, position: 'top', weight: 150 } } } }); expect(chart.legend.fullSize).toBe(true); expect(chart.legend.position).toBe('top'); expect(chart.legend.weight).toBe(150); chart.options.plugins.legend.fullSize = false; chart.options.plugins.legend.position = 'left'; chart.options.plugins.legend.weight = 42; chart.update(); expect(chart.legend.fullSize).toBe(false); expect(chart.legend.position).toBe('left'); expect(chart.legend.weight).toBe(42); }); it('should remove the legend if the new options are false', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] } }); expect(chart.legend).not.toBe(undefined); chart.options.plugins.legend = false; chart.update(); expect(chart.legend).toBe(undefined); }); it('should create the legend if the legend options are changed to exist', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { plugins: { legend: false } } }); expect(chart.legend).toBe(undefined); chart.options.plugins.legend = {}; chart.update(); expect(chart.legend).not.toBe(undefined); expect(chart.legend.options).toEqualOptions(Object.assign({}, // replace scriptable options with resolved values Chart.defaults.plugins.legend, { labels: {color: Chart.defaults.color}, title: {color: Chart.defaults.color} } )); }); }); describe('callbacks', function() { it('should call onClick, onHover and onLeave at the correct times', async function() { var clickItem = null; var hoverItem = null; var leaveItem = null; var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { plugins: { legend: { onClick: function(_, item) { clickItem = item; }, onHover: function(_, item) { hoverItem = item; }, onLeave: function(_, item) { leaveItem = item; } } } } }); var hb = chart.legend.legendHitBoxes[0]; var el = { x: hb.left + (hb.width / 2), y: hb.top + (hb.height / 2) }; await jasmine.triggerMouseEvent(chart, 'click', el); expect(clickItem).toBe(chart.legend.legendItems[0]); await jasmine.triggerMouseEvent(chart, 'mousemove', el); expect(hoverItem).toBe(chart.legend.legendItems[0]); await jasmine.triggerMouseEvent(chart, 'mousemove', chart.getDatasetMeta(0).data[0]); expect(leaveItem).toBe(chart.legend.legendItems[0]); }); it('should call onLeave when the mouse leaves the canvas', async function() { var hoverItem = null; var leaveItem = null; var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { plugins: { legend: { onHover: function(_, item) { hoverItem = item; }, onLeave: function(_, item) { leaveItem = item; } } } } }); var hb = chart.legend.legendHitBoxes[0]; var el = { x: hb.left + (hb.width / 2), y: hb.top + (hb.height / 2) }; await jasmine.triggerMouseEvent(chart, 'mousemove', el); expect(hoverItem).toBe(chart.legend.legendItems[0]); await jasmine.triggerMouseEvent(chart, 'mouseout'); expect(leaveItem).toBe(chart.legend.legendItems[0]); }); it('should call onClick for the correct item when in RTL mode', async function() { var clickItem = null; var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100], label: 'dataset 1' }, { data: [10, 20, 30, 100], label: 'dataset 2' }] }, options: { plugins: { legend: { onClick: function(_, item) { clickItem = item; }, } } } }); var hb = chart.legend.legendHitBoxes[0]; var el = { x: hb.left + (hb.width / 2), y: hb.top + (hb.height / 2) }; await jasmine.triggerMouseEvent(chart, 'click', el); expect(clickItem).toBe(chart.legend.legendItems[0]); }); }); }); ================================================ FILE: test/specs/plugin.subtitle.tests.js ================================================ describe('plugin.subtitle', function() { describe('auto', jasmine.fixture.specs('plugin.subtitle')); }); ================================================ FILE: test/specs/plugin.title.tests.js ================================================ // Test the rectangle element var Title = Chart.registry.getPlugin('title')._element; describe('Plugin.title', function() { describe('auto', jasmine.fixture.specs('plugin.title')); it('Should have the correct default config', function() { expect(Chart.defaults.plugins.title).toEqual({ align: 'center', color: Chart.defaults.color, display: false, position: 'top', fullSize: true, weight: 2000, font: { weight: 'bold' }, padding: 10, text: '' }); }); it('should update correctly', function() { var chart = { options: Chart.helpers.clone(Chart.defaults) }; var options = Chart.helpers.clone(Chart.defaults.plugins.title); options.text = 'My title'; var title = new Title({ chart: chart, options: options }); title.update(400, 200); expect(title.width).toEqual(0); expect(title.height).toEqual(0); // Now we have a height since we display title.options.display = true; title.update(400, 200); expect(title.width).toEqual(400); expect(title.height).toEqual(34.4); }); it('should update correctly when vertical', function() { var chart = { options: Chart.helpers.clone(Chart.defaults) }; var options = Chart.helpers.clone(Chart.defaults.plugins.title); options.text = 'My title'; options.position = 'left'; var title = new Title({ chart: chart, options: options }); title.update(200, 400); expect(title.width).toEqual(0); expect(title.height).toEqual(0); // Now we have a height since we display title.options.display = true; title.update(200, 400); expect(title.width).toEqual(34.4); expect(title.height).toEqual(400); }); it('should have the correct size when there are multiple lines of text', function() { var chart = { options: Chart.helpers.clone(Chart.defaults) }; var options = Chart.helpers.clone(Chart.defaults.plugins.title); options.text = ['line1', 'line2']; options.position = 'left'; options.display = true; options.font.lineHeight = 1.5; var title = new Title({ chart: chart, options: options }); title.update(200, 400); expect(title.width).toEqual(56); expect(title.height).toEqual(400); }); it('should draw correctly horizontally', function() { var chart = { options: Chart.helpers.clone(Chart.defaults) }; var context = window.createMockContext(); var options = Chart.helpers.clone(Chart.defaults.plugins.title); options.text = 'My title'; var title = new Title({ chart: chart, options: options, ctx: context }); title.update(400, 200); title.draw(); expect(context.getCalls()).toEqual([]); // Now we have a height since we display title.options.display = true; title.update(400, 200); title.top = 50; title.left = 100; title.bottom = title.top + title.height; title.right = title.left + title.width; title.draw(); expect(context.getCalls()).toEqual([{ name: 'save', args: [] }, { name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"], }, { name: 'translate', args: [300, 67.2] }, { name: 'rotate', args: [0] }, { name: 'setFillStyle', args: ['#666'] }, { name: 'setTextAlign', args: ['center'], }, { name: 'setTextBaseline', args: ['middle'], }, { name: 'fillText', args: ['My title', 0, 0, 400] }, { name: 'restore', args: [] }]); }); it ('should draw correctly vertically', function() { var chart = { options: Chart.helpers.clone(Chart.defaults) }; var context = window.createMockContext(); var options = Chart.helpers.clone(Chart.defaults.plugins.title); options.text = 'My title'; options.position = 'left'; var title = new Title({ chart: chart, options: options, ctx: context }); title.update(200, 400); title.draw(); expect(context.getCalls()).toEqual([]); // Now we have a height since we display title.options.display = true; title.update(200, 400); title.top = 50; title.left = 100; title.bottom = title.top + title.height; title.right = title.left + title.width; title.draw(); expect(context.getCalls()).toEqual([{ name: 'save', args: [] }, { name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"], }, { name: 'translate', args: [117.2, 250] }, { name: 'rotate', args: [-0.5 * Math.PI] }, { name: 'setFillStyle', args: ['#666'] }, { name: 'setTextAlign', args: ['center'], }, { name: 'setTextBaseline', args: ['middle'], }, { name: 'fillText', args: ['My title', 0, 0, 400] }, { name: 'restore', args: [] }]); // Rotation is other way on right side title.options.position = 'right'; // Reset call tracker context.resetCalls(); title.update(200, 400); title.top = 50; title.left = 100; title.bottom = title.top + title.height; title.right = title.left + title.width; title.draw(); expect(context.getCalls()).toEqual([{ name: 'save', args: [] }, { name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"], }, { name: 'translate', args: [117.2, 250] }, { name: 'rotate', args: [0.5 * Math.PI] }, { name: 'setFillStyle', args: ['#666'] }, { name: 'setTextAlign', args: ['center'], }, { name: 'setTextBaseline', args: ['middle'], }, { name: 'fillText', args: ['My title', 0, 0, 400] }, { name: 'restore', args: [] }]); }); describe('config update', function() { it ('should update the options', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { plugins: { title: { display: true } } } }); expect(chart.titleBlock.options.display).toBe(true); chart.options.plugins.title.display = false; chart.update(); expect(chart.titleBlock.options.display).toBe(false); }); it ('should update the associated layout item', function() { var chart = acquireChart({ type: 'line', data: {}, options: { plugins: { title: { fullSize: true, position: 'top', weight: 150 } } } }); expect(chart.titleBlock.fullSize).toBe(true); expect(chart.titleBlock.position).toBe('top'); expect(chart.titleBlock.weight).toBe(150); chart.options.plugins.title.fullSize = false; chart.options.plugins.title.position = 'left'; chart.options.plugins.title.weight = 42; chart.update(); expect(chart.titleBlock.fullSize).toBe(false); expect(chart.titleBlock.position).toBe('left'); expect(chart.titleBlock.weight).toBe(42); }); it ('should remove the title if the new options are false', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] } }); expect(chart.titleBlock).not.toBe(undefined); chart.options.plugins.title = false; chart.update(); expect(chart.titleBlock).toBe(undefined); }); it ('should create the title if the title options are changed to exist', function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }] }, options: { plugins: { title: false } } }); expect(chart.titleBlock).toBe(undefined); chart.options.plugins.title = {}; chart.update(); expect(chart.titleBlock).not.toBe(undefined); expect(chart.titleBlock.options).toEqualOptions(Chart.defaults.plugins.title); }); }); }); ================================================ FILE: test/specs/plugin.tooltip.tests.js ================================================ // Test the rectangle element const tooltipPlugin = Chart.registry.getPlugin('tooltip'); const Tooltip = tooltipPlugin._element; describe('Plugin.Tooltip', function() { describe('auto', jasmine.fixture.specs('plugin.tooltip')); describe('config', function() { it('should not include the dataset label in the body string if not defined', function() { var data = { datasets: [{ data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }; var tooltipItem = { index: 1, datasetIndex: 0, dataset: data.datasets[0], label: 'Point 2', formattedValue: '20' }; var label = Chart.defaults.plugins.tooltip.callbacks.label(tooltipItem); expect(label).toBe('20'); data.datasets[0].label = 'My dataset'; label = Chart.defaults.plugins.tooltip.callbacks.label(tooltipItem); expect(label).toBe('My dataset: 20'); }); }); describe('index mode', function() { it('Should only use x distance when intersect is false', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'index', intersect: false, padding: { left: 6, top: 6, right: 6, bottom: 6 } } }, hover: { mode: 'index', intersect: false } } }); // Trigger an event over top of the var meta = chart.getDatasetMeta(0); var point = meta.data[1]; // Check and see if tooltip was displayed var tooltip = chart.tooltip; var defaults = Chart.defaults; await jasmine.triggerMouseEvent(chart, 'mousemove', {x: point.x, y: chart.chartArea.top + 10}); expect(tooltip.options.padding).toEqualOptions({ left: 6, top: 6, right: 6, bottom: 6, }); expect(tooltip.xAlign).toEqual('left'); expect(tooltip.yAlign).toEqual('center'); expect(tooltip.options.bodyColor).toEqual('#fff'); expect(tooltip.options.bodyFont).toEqualOptions({ family: defaults.font.family, style: defaults.font.style, size: defaults.font.size, }); expect(tooltip.options).toEqualOptions({ bodyAlign: 'left', bodySpacing: 2, }); expect(tooltip.options.titleColor).toEqual('#fff'); expect(tooltip.options.titleFont).toEqualOptions({ family: defaults.font.family, weight: 'bold', size: defaults.font.size, }); expect(tooltip.options).toEqualOptions({ titleAlign: 'left', titleSpacing: 2, titleMarginBottom: 6, }); expect(tooltip.options.footerColor).toEqual('#fff'); expect(tooltip.options.footerFont).toEqualOptions({ family: defaults.font.family, weight: 'bold', size: defaults.font.size, }); expect(tooltip.options).toEqualOptions({ footerAlign: 'left', footerSpacing: 2, footerMarginTop: 6, }); expect(tooltip.options).toEqualOptions({ // Appearance caretSize: 5, caretPadding: 2, cornerRadius: 6, backgroundColor: 'rgba(0,0,0,0.8)', multiKeyBackground: '#fff', displayColors: true }); expect(tooltip).toEqual(jasmine.objectContaining({ opacity: 1, // Text title: ['Point 2'], beforeBody: [], body: [{ before: [], lines: ['Dataset 1: 20'], after: [] }, { before: [], lines: ['Dataset 2: 40'], after: [] }], afterBody: [], footer: [], labelColors: [{ borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }, { borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }] })); expect(tooltip.x).toBeCloseToPixel(266); expect(tooltip.y).toBeCloseToPixel(150); }); it('Should only display if intersecting if intersect is set', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'index', intersect: true } } } }); // Trigger an event over top of the var meta = chart.getDatasetMeta(0); var point = meta.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', {x: point.x, y: 0}); // Check and see if tooltip was displayed var tooltip = chart.tooltip; expect(tooltip).toEqual(jasmine.objectContaining({ opacity: 0, })); }); }); it('Should display in single mode', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'nearest', intersect: true } } } }); // Trigger an event over top of the var meta = chart.getDatasetMeta(0); var point = meta.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); // Check and see if tooltip was displayed var tooltip = chart.tooltip; var defaults = Chart.defaults; expect(tooltip.options.padding).toEqual(6); expect(tooltip.xAlign).toEqual('left'); expect(tooltip.yAlign).toEqual('center'); expect(tooltip.options.bodyFont).toEqual(jasmine.objectContaining({ family: defaults.font.family, style: defaults.font.style, size: defaults.font.size, })); expect(tooltip.options).toEqualOptions({ bodyAlign: 'left', bodySpacing: 2, }); expect(tooltip.options.titleFont).toEqual(jasmine.objectContaining({ family: defaults.font.family, weight: 'bold', size: defaults.font.size, })); expect(tooltip.options).toEqualOptions({ titleAlign: 'left', titleSpacing: 2, titleMarginBottom: 6, }); expect(tooltip.options.footerFont).toEqualOptions({ family: defaults.font.family, weight: 'bold', size: defaults.font.size, }); expect(tooltip.options).toEqualOptions({ footerAlign: 'left', footerSpacing: 2, footerMarginTop: 6, }); expect(tooltip.options).toEqualOptions({ // Appearance caretSize: 5, caretPadding: 2, cornerRadius: 6, backgroundColor: 'rgba(0,0,0,0.8)', multiKeyBackground: '#fff', displayColors: true }); expect(tooltip.opacity).toEqual(1); expect(tooltip.title).toEqual(['Point 2']); expect(tooltip.beforeBody).toEqual([]); expect(tooltip.body).toEqual([{ before: [], lines: ['Dataset 1: 20'], after: [] }]); expect(tooltip.afterBody).toEqual([]); expect(tooltip.footer).toEqual([]); expect(tooltip.labelTextColors).toEqual(['#fff']); expect(tooltip.labelColors).toEqual([{ borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }]); expect(tooltip.x).toBeCloseToPixel(267); expect(tooltip.y).toBeCloseToPixel(308); }); it('Should display information from user callbacks', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'index', callbacks: { beforeTitle: function() { return 'beforeTitle'; }, title: function() { return 'title'; }, afterTitle: function() { return 'afterTitle'; }, beforeBody: function() { return 'beforeBody'; }, beforeLabel: function() { return 'beforeLabel'; }, label: function() { return 'label'; }, afterLabel: function() { return 'afterLabel'; }, afterBody: function() { return 'afterBody'; }, beforeFooter: function() { return 'beforeFooter'; }, footer: function() { return 'footer'; }, afterFooter: function() { return 'afterFooter'; }, labelTextColor: function() { return 'labelTextColor'; }, labelPointStyle: function() { return { pointStyle: 'labelPointStyle', rotation: 42 }; } } } } } }); // Trigger an event over top of the var meta = chart.getDatasetMeta(0); var point = meta.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); // Check and see if tooltip was displayed var tooltip = chart.tooltip; var defaults = Chart.defaults; expect(tooltip.options.padding).toEqual(6); expect(tooltip.xAlign).toEqual('left'); expect(tooltip.yAlign).toEqual('center'); expect(tooltip.options.bodyFont).toEqual(jasmine.objectContaining({ family: defaults.font.family, style: defaults.font.style, size: defaults.font.size, })); expect(tooltip.options).toEqualOptions({ bodyAlign: 'left', bodySpacing: 2, }); expect(tooltip.options.titleFont).toEqual(jasmine.objectContaining({ family: defaults.font.family, weight: 'bold', size: defaults.font.size, })); expect(tooltip.options).toEqualOptions({ titleSpacing: 2, titleMarginBottom: 6, }); expect(tooltip.options.footerFont).toEqual(jasmine.objectContaining({ family: defaults.font.family, weight: 'bold', size: defaults.font.size, })); expect(tooltip.options).toEqualOptions({ footerAlign: 'left', footerSpacing: 2, footerMarginTop: 6, }); expect(tooltip.options).toEqualOptions({ // Appearance caretSize: 5, caretPadding: 2, cornerRadius: 6, backgroundColor: 'rgba(0,0,0,0.8)', multiKeyBackground: '#fff', }); expect(tooltip).toEqual(jasmine.objectContaining({ opacity: 1, // Text title: ['beforeTitle', 'title', 'afterTitle'], beforeBody: ['beforeBody'], body: [{ before: ['beforeLabel'], lines: ['label'], after: ['afterLabel'] }, { before: ['beforeLabel'], lines: ['label'], after: ['afterLabel'] }], afterBody: ['afterBody'], footer: ['beforeFooter', 'footer', 'afterFooter'], labelTextColors: ['labelTextColor', 'labelTextColor'], labelColors: [{ borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }, { borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }], labelPointStyles: [{ pointStyle: 'labelPointStyle', rotation: 42 }, { pointStyle: 'labelPointStyle', rotation: 42 }] })); expect(tooltip.x).toBeCloseToPixel(267); expect(tooltip.y).toBeCloseToPixel(58); }); it('Should provide context object to user callbacks', async function() { const chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [{x: 1, y: 10}, {x: 2, y: 20}, {x: 3, y: 30}] }] }, options: { scales: { x: { type: 'linear' } }, plugins: { tooltip: { mode: 'index', callbacks: { beforeLabel: function(ctx) { return ctx.parsed.x + ',' + ctx.parsed.y; } } } } } }); // Trigger an event over top of the const meta = chart.getDatasetMeta(0); const point = meta.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(chart.tooltip.body[0].before).toEqual(['2,20']); }); it('Should allow sorting items', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'index', itemSort: function(a, b) { return a.datasetIndex > b.datasetIndex ? -1 : 1; } } } } }); // Trigger an event over top of the var meta0 = chart.getDatasetMeta(0); var point0 = meta0.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point0); // Check and see if tooltip was displayed var tooltip = chart.tooltip; var defaults = Chart.defaults; expect(tooltip).toEqual(jasmine.objectContaining({ // Positioning xAlign: 'left', yAlign: 'center', // Text title: ['Point 2'], beforeBody: [], body: [{ before: [], lines: ['Dataset 2: 40'], after: [] }, { before: [], lines: ['Dataset 1: 20'], after: [] }], afterBody: [], footer: [], labelColors: [{ borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }, { borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }] })); expect(tooltip.x).toBeCloseToPixel(267); expect(tooltip.y).toBeCloseToPixel(150); }); it('Should allow reversing items', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'index', reverse: true } } } }); // Trigger an event over top of the var meta0 = chart.getDatasetMeta(0); var point0 = meta0.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point0); // Check and see if tooltip was displayed var tooltip = chart.tooltip; var defaults = Chart.defaults; expect(tooltip).toEqual(jasmine.objectContaining({ // Positioning xAlign: 'left', yAlign: 'center', // Text title: ['Point 2'], beforeBody: [], body: [{ before: [], lines: ['Dataset 2: 40'], after: [] }, { before: [], lines: ['Dataset 1: 20'], after: [] }], afterBody: [], footer: [], labelColors: [{ borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }, { borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }] })); expect(tooltip.x).toBeCloseToPixel(267); expect(tooltip.y).toBeCloseToPixel(150); }); it('Should follow dataset order', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)', order: 10 }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)', order: 5 }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'index' } } } }); // Trigger an event over top of the var meta0 = chart.getDatasetMeta(0); var point0 = meta0.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point0); // Check and see if tooltip was displayed var tooltip = chart.tooltip; var defaults = Chart.defaults; expect(tooltip).toEqual(jasmine.objectContaining({ // Positioning xAlign: 'left', yAlign: 'center', // Text title: ['Point 2'], beforeBody: [], body: [{ before: [], lines: ['Dataset 2: 40'], after: [] }, { before: [], lines: ['Dataset 1: 20'], after: [] }], afterBody: [], footer: [], labelColors: [{ borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }, { borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }] })); expect(tooltip.x).toBeCloseToPixel(267); expect(tooltip.y).toBeCloseToPixel(150); }); it('should filter items from the tooltip using the callback', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)', tooltipHidden: true }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'index', filter: function(tooltipItem, index, tooltipItems, data) { // For testing purposes remove the first dataset that has a tooltipHidden property return !data.datasets[tooltipItem.datasetIndex].tooltipHidden; } } } } }); // Trigger an event over top of the var meta0 = chart.getDatasetMeta(0); var point0 = meta0.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point0); // Check and see if tooltip was displayed var tooltip = chart.tooltip; var defaults = Chart.defaults; expect(tooltip).toEqual(jasmine.objectContaining({ // Positioning xAlign: 'left', yAlign: 'center', // Text title: ['Point 2'], beforeBody: [], body: [{ before: [], lines: ['Dataset 2: 40'], after: [] }], afterBody: [], footer: [], labelColors: [{ borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }] })); }); it('should set the caretPadding based on a config setting', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)', tooltipHidden: true }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { caretPadding: 10 } } } }); // Trigger an event over top of the var meta0 = chart.getDatasetMeta(0); var point0 = meta0.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point0); // Check and see if tooltip was displayed var tooltip = chart.tooltip; expect(tooltip.options).toEqualOptions({ // Positioning caretPadding: 10, }); }); ['line', 'bar'].forEach(function(type) { it('Should have dataPoints in a ' + type + ' chart', async function() { var chart = window.acquireChart({ type: type, data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'nearest', intersect: true } } } }); // Trigger an event over top of the element var pointIndex = 1; var datasetIndex = 0; var point = chart.getDatasetMeta(datasetIndex).data[pointIndex]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); // Check and see if tooltip was displayed var tooltip = chart.tooltip; expect(tooltip instanceof Object).toBe(true); expect(tooltip.dataPoints instanceof Array).toBe(true); expect(tooltip.dataPoints.length).toBe(1); var tooltipItem = tooltip.dataPoints[0]; expect(tooltipItem.dataIndex).toBe(pointIndex); expect(tooltipItem.datasetIndex).toBe(datasetIndex); expect(typeof tooltipItem.label).toBe('string'); expect(tooltipItem.label).toBe(chart.data.labels[pointIndex]); expect(typeof tooltipItem.formattedValue).toBe('string'); expect(tooltipItem.formattedValue).toBe('' + chart.data.datasets[datasetIndex].data[pointIndex]); }); }); it('Should not update if active element has not changed', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'nearest', intersect: true, callbacks: { title: function() { return 'registering callback...'; } } } } } }); // Trigger an event over top of the var meta = chart.getDatasetMeta(0); var firstPoint = meta.data[1]; var tooltip = chart.tooltip; spyOn(tooltip, 'update').and.callThrough(); // First dispatch change event, should update tooltip await jasmine.triggerMouseEvent(chart, 'mousemove', firstPoint); expect(tooltip.update).toHaveBeenCalledWith(true, undefined); // Reset calls tooltip.update.calls.reset(); // Second dispatch change event (same event), should not update tooltip await jasmine.triggerMouseEvent(chart, 'mousemove', firstPoint); expect(tooltip.update).not.toHaveBeenCalled(); }); it('Should update if active elements are the same, but the position has changed', async function() { const chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { scales: { x: { stacked: true, }, y: { stacked: true } }, plugins: { tooltip: { mode: 'nearest', position: 'nearest', intersect: true, callbacks: { title: function() { return 'registering callback...'; } } } } } }); // Trigger an event over top of the const meta = chart.getDatasetMeta(0); const firstPoint = meta.data[1]; const meta2 = chart.getDatasetMeta(1); const secondPoint = meta2.data[1]; const tooltip = chart.tooltip; spyOn(tooltip, 'update'); // First dispatch change event, should update tooltip await jasmine.triggerMouseEvent(chart, 'mousemove', firstPoint); expect(tooltip.update).toHaveBeenCalledWith(true, undefined); // Reset calls tooltip.update.calls.reset(); // Second dispatch change event (same event), should update tooltip // because position mode is 'nearest' await jasmine.triggerMouseEvent(chart, 'mousemove', secondPoint); expect(tooltip.update).toHaveBeenCalledWith(true, undefined); }); describe('positioners', function() { it('Should call custom positioner with correct parameters and scope', async function() { tooltipPlugin.positioners.test = function() { return {x: 0, y: 0}; }; spyOn(tooltipPlugin.positioners, 'test').and.callThrough(); var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'nearest', position: 'test' } } } }); // Trigger an event over top of the var pointIndex = 1; var datasetIndex = 0; var meta = chart.getDatasetMeta(datasetIndex); var point = meta.data[pointIndex]; var fn = tooltipPlugin.positioners.test; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(fn.calls.count()).toBe(2); expect(fn.calls.first().args[0] instanceof Array).toBe(true); expect(Object.prototype.hasOwnProperty.call(fn.calls.first().args[1], 'x')).toBe(true); expect(Object.prototype.hasOwnProperty.call(fn.calls.first().args[1], 'y')).toBe(true); expect(fn.calls.first().object instanceof Tooltip).toBe(true); }); it('Should ignore same x position when calculating average position with index interaction on stacked bar', async function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)', stack: 'stack1', }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)', stack: 'stack1', }, { label: 'Dataset 3', data: [90, 100, 110], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { interaction: { mode: 'index' }, plugins: { position: 'average', }, } }); // Trigger an event over top of the var pointIndex = 1; var datasetIndex = 0; var meta = chart.getDatasetMeta(datasetIndex); var point = meta.data[pointIndex]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); var tooltipModel = chart.tooltip; const activeElements = tooltipModel.getActiveElements(); const xPositionArray = activeElements.map((element) => element.element.x); const xPositionArrayAverage = xPositionArray.reduce((a, b) => a + b) / xPositionArray.length; const xPositionSet = new Set(xPositionArray); const xPositionSetAverage = [...xPositionSet].reduce((a, b) => a + b) / xPositionSet.size; expect(xPositionArray.length).toBe(3); expect(xPositionSet.size).toBe(2); expect(tooltipModel.caretX).not.toBe(xPositionArrayAverage); expect(tooltipModel.caretX).toBe(xPositionSetAverage); }); it('Should not fail with all hiden data elements on the average positioner', function() { const averagePositioner = tooltipPlugin.positioners.average; // Simulate `hasValue` returns false expect(() => averagePositioner([{x: 'invalidNumber', y: 'invalidNumber'}])).not.toThrow(); const result = averagePositioner([{x: 'invalidNumber', y: 'invalidNumber'}]); expect(result).toBe(false); }); }); it('Should avoid tooltip truncation in x axis if there is enough space to show tooltip without truncation', async function() { var chart = window.acquireChart({ type: 'pie', data: { datasets: [{ data: [ 50, 50 ], backgroundColor: [ 'rgb(255, 0, 0)', 'rgb(0, 255, 0)' ], label: 'Dataset 1' }], labels: [ 'Red long tooltip text to avoid unnecessary loop steps', 'Green long tooltip text to avoid unnecessary loop steps' ] }, options: { responsive: true, animation: { // without this slice center point is calculated wrong animateRotate: false }, plugins: { tooltip: { animation: false } } } }); async function testSlice(slice, count) { var meta = chart.getDatasetMeta(0); var point = meta.data[slice].getCenterPoint(); var tooltipPosition = meta.data[slice].tooltipPosition(); async function recursive(left) { chart.config.data.labels[slice] = chart.config.data.labels[slice] + 'XX'; chart.update(); await jasmine.triggerMouseEvent(chart, 'mouseout', point); await jasmine.triggerMouseEvent(chart, 'mousemove', point); var tooltip = chart.tooltip; expect(tooltip.dataPoints.length).toBe(1); expect(tooltip.x).toBeGreaterThanOrEqual(0); if (tooltip.width <= chart.width) { expect(tooltip.x + tooltip.width).toBeLessThanOrEqual(chart.width); } expect(tooltip.caretX).toBeCloseToPixel(tooltipPosition.x); // if tooltip is longer than chart area then all tests done if (left === 0) { throw new Error('max iterations reached'); } if (tooltip.width < chart.width) { await recursive(left - 1); } } await recursive(count); } // Trigger an event over top of the slice for (var slice = 0; slice < 2; slice++) { await testSlice(slice, 20); } }); it('Should split newlines into separate lines in user callbacks', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'index', callbacks: { beforeTitle: function() { return 'beforeTitle\nnewline'; }, title: function() { return 'title\nnewline'; }, afterTitle: function() { return 'afterTitle\nnewline'; }, beforeBody: function() { return 'beforeBody\nnewline'; }, beforeLabel: function() { return 'beforeLabel\nnewline'; }, label: function() { return 'label'; }, afterLabel: function() { return 'afterLabel\nnewline'; }, afterBody: function() { return 'afterBody\nnewline'; }, beforeFooter: function() { return 'beforeFooter\nnewline'; }, footer: function() { return 'footer\nnewline'; }, afterFooter: function() { return 'afterFooter\nnewline'; }, labelTextColor: function() { return 'labelTextColor'; } } } } } }); // Trigger an event over top of the var meta = chart.getDatasetMeta(0); var point = meta.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); // Check and see if tooltip was displayed var tooltip = chart.tooltip; var defaults = Chart.defaults; expect(tooltip.options.padding).toEqual(6); expect(tooltip.xAlign).toEqual('center'); expect(tooltip.yAlign).toEqual('top'); expect(tooltip.options.bodyFont).toEqualOptions({ family: defaults.font.family, style: defaults.font.style, size: defaults.font.size, }); expect(tooltip.options).toEqualOptions({ bodyAlign: 'left', bodySpacing: 2, }); expect(tooltip.options.titleFont).toEqualOptions({ family: defaults.font.family, weight: 'bold', size: defaults.font.size, }); expect(tooltip.options).toEqualOptions({ titleAlign: 'left', titleSpacing: 2, titleMarginBottom: 6, }); expect(tooltip.options.footerFont).toEqualOptions({ family: defaults.font.family, weight: 'bold', size: defaults.font.size, }); expect(tooltip.options).toEqualOptions({ footerAlign: 'left', footerSpacing: 2, footerMarginTop: 6, }); expect(tooltip.options).toEqualOptions({ // Appearance caretSize: 5, caretPadding: 2, cornerRadius: 6, backgroundColor: 'rgba(0,0,0,0.8)', multiKeyBackground: '#fff', }); expect(tooltip).toEqualOptions({ opacity: 1, // Text title: ['beforeTitle', 'newline', 'title', 'newline', 'afterTitle', 'newline'], beforeBody: ['beforeBody', 'newline'], body: [{ before: ['beforeLabel', 'newline'], lines: ['label'], after: ['afterLabel', 'newline'] }, { before: ['beforeLabel', 'newline'], lines: ['label'], after: ['afterLabel', 'newline'] }], afterBody: ['afterBody', 'newline'], footer: ['beforeFooter', 'newline', 'footer', 'newline', 'afterFooter', 'newline'], labelTextColors: ['labelTextColor', 'labelTextColor'], labelColors: [{ borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor }, { borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor }] }); }); describe('text align', function() { var defaults = Chart.defaults; var makeView = function(title, body, footer) { const model = { // Positioning x: 100, y: 100, width: 100, height: 100, xAlign: 'left', yAlign: 'top', options: { setContext: () => model.options, enabled: true, padding: 5, // Body bodyFont: { family: defaults.font.family, style: defaults.font.style, size: defaults.font.size, }, bodyColor: '#fff', bodyAlign: body, bodySpacing: 2, // Title titleFont: { family: defaults.font.family, weight: 'bold', size: defaults.font.size, }, titleColor: '#fff', titleAlign: title, titleSpacing: 2, titleMarginBottom: 6, // Footer footerFont: { family: defaults.font.family, weight: 'bold', size: defaults.font.size, }, footerColor: '#fff', footerAlign: footer, footerSpacing: 2, footerMarginTop: 6, // Appearance caretSize: 5, cornerRadius: 6, caretPadding: 2, borderColor: '#aaa', borderWidth: 1, backgroundColor: 'rgba(0,0,0,0.8)', multiKeyBackground: '#fff', displayColors: false }, opacity: 1, // Text title: ['title'], beforeBody: [], body: [{ before: [], lines: ['label'], after: [] }], afterBody: [], footer: ['footer'], labelTextColors: ['#fff'], labelColors: [{ borderColor: 'rgb(255, 0, 0)', backgroundColor: 'rgb(0, 255, 0)' }, { borderColor: 'rgb(0, 0, 255)', backgroundColor: 'rgb(0, 255, 255)' }] }; return model; }; var drawBody = [ {name: 'save', args: []}, {name: 'setFillStyle', args: ['rgba(0,0,0,0.8)']}, {name: 'setStrokeStyle', args: ['#aaa']}, {name: 'setLineWidth', args: [1]}, {name: 'beginPath', args: []}, {name: 'moveTo', args: [106, 100]}, {name: 'lineTo', args: [106, 100]}, {name: 'lineTo', args: [111, 95]}, {name: 'lineTo', args: [116, 100]}, {name: 'lineTo', args: [194, 100]}, {name: 'quadraticCurveTo', args: [200, 100, 200, 106]}, {name: 'lineTo', args: [200, 194]}, {name: 'quadraticCurveTo', args: [200, 200, 194, 200]}, {name: 'lineTo', args: [106, 200]}, {name: 'quadraticCurveTo', args: [100, 200, 100, 194]}, {name: 'lineTo', args: [100, 106]}, {name: 'quadraticCurveTo', args: [100, 100, 106, 100]}, {name: 'closePath', args: []}, {name: 'fill', args: []}, {name: 'stroke', args: []} ]; var mockContext = window.createMockContext(); var tooltip = new Tooltip({ chart: { getContext: () => ({}), options: { plugins: { tooltip: { animation: false, } } } } }); it('Should go left', function() { mockContext.resetCalls(); Chart.helpers.merge(tooltip, makeView('left', 'left', 'left')); tooltip.draw(mockContext); expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['left']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'fillText', args: ['title', 105, 112.2]}, {name: 'setTextAlign', args: ['left']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFont', args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['label', 105, 132.6]}, {name: 'setTextAlign', args: ['left']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'fillText', args: ['footer', 105, 153]}, {name: 'restore', args: []} ])); }); it('Should go right', function() { mockContext.resetCalls(); Chart.helpers.merge(tooltip, makeView('right', 'right', 'right')); tooltip.draw(mockContext); expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['right']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'fillText', args: ['title', 195, 112.2]}, {name: 'setTextAlign', args: ['right']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFont', args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['label', 195, 132.6]}, {name: 'setTextAlign', args: ['right']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'fillText', args: ['footer', 195, 153]}, {name: 'restore', args: []} ])); }); it('Should center', function() { mockContext.resetCalls(); Chart.helpers.merge(tooltip, makeView('center', 'center', 'center')); tooltip.draw(mockContext); expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['center']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'fillText', args: ['title', 150, 112.2]}, {name: 'setTextAlign', args: ['center']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFont', args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['label', 150, 132.6]}, {name: 'setTextAlign', args: ['center']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'fillText', args: ['footer', 150, 153]}, {name: 'restore', args: []} ])); }); it('Should allow mixed', function() { mockContext.resetCalls(); Chart.helpers.merge(tooltip, makeView('right', 'center', 'left')); tooltip.draw(mockContext); expect(mockContext.getCalls()).toEqual(Array.prototype.concat(drawBody, [ {name: 'setTextAlign', args: ['right']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'fillText', args: ['title', 195, 112.2]}, {name: 'setTextAlign', args: ['center']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFont', args: ["normal 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'fillText', args: ['label', 150, 132.6]}, {name: 'setTextAlign', args: ['left']}, {name: 'setTextBaseline', args: ['middle']}, {name: 'setFillStyle', args: ['#fff']}, {name: 'setFont', args: ["normal bold 12px 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif"]}, {name: 'fillText', args: ['footer', 105, 153]}, {name: 'restore', args: []} ])); }); }); describe('active elements', function() { it('should set the active elements', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, }); const meta = chart.getDatasetMeta(0); chart.tooltip.setActiveElements([{datasetIndex: 0, index: 0}], {x: 0, y: 0}); expect(chart.tooltip.getActiveElements()[0].element).toBe(meta.data[0]); }); it('should not replace the user set active elements by event replay', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { events: ['pointerdown', 'pointerup'] } }); const meta = chart.getDatasetMeta(0); const point0 = meta.data[0]; const point1 = meta.data[1]; await jasmine.triggerMouseEvent(chart, 'pointerdown', {x: point0.x, y: point0.y}); expect(chart.tooltip.opacity).toBe(1); expect(chart.tooltip.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point0}]); chart.tooltip.setActiveElements([{datasetIndex: 0, index: 1}]); chart.update(); expect(chart.tooltip.opacity).toBe(1); expect(chart.tooltip.getActiveElements()).toEqual([{datasetIndex: 0, index: 1, element: point1}]); chart.tooltip.setActiveElements([]); chart.update(); expect(chart.tooltip.opacity).toBe(0); expect(chart.tooltip.getActiveElements().length).toBe(0); }); it('should not change the active elements on events outside chartArea, except for mouseout', async function() { var chart = acquireChart({ type: 'line', data: { labels: ['A', 'B', 'C', 'D'], datasets: [{ data: [10, 20, 30, 100] }], }, options: { scales: { x: {display: false}, y: {display: false} }, layout: { padding: 5 } } }); var point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', {x: point.x, y: point.y}); expect(chart.tooltip.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point}]); await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 1, y: 1}); expect(chart.tooltip.getActiveElements()).toEqual([{datasetIndex: 0, index: 0, element: point}]); await jasmine.triggerMouseEvent(chart, 'mouseout', {x: 1, y: 1}); expect(chart.tooltip.getActiveElements()).toEqual([]); }); it('should update active elements when datasets are removed and added', async function() { var dataset = { label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }; var chart = window.acquireChart({ type: 'line', data: { datasets: [dataset], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'nearest', intersect: true } } } }); var meta = chart.getDatasetMeta(0); var point = meta.data[1]; var expectedPoint = jasmine.objectContaining({datasetIndex: 0, index: 1}); await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(chart.tooltip.getActiveElements()).toEqual([expectedPoint]); chart.data.datasets = []; chart.update(); expect(chart.tooltip.getActiveElements()).toEqual([]); chart.data.datasets = [dataset]; chart.update(); expect(chart.tooltip.getActiveElements()).toEqual([expectedPoint]); }); }); it('should tolerate datasets removed on events outside chartArea', async function() { const dataset1 = { label: 'Dataset 1', data: [10, 20, 30], }; const dataset2 = { label: 'Dataset 2', data: [10, 25, 35], }; const chart = window.acquireChart({ type: 'line', data: { datasets: [dataset1, dataset2], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'index', intersect: false } } } }); const meta = chart.getDatasetMeta(0); const point = meta.data[1]; const expectedPoints = [jasmine.objectContaining({datasetIndex: 0, index: 1}), jasmine.objectContaining({datasetIndex: 1, index: 1})]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); await jasmine.triggerMouseEvent(chart, 'mousemove', {x: chart.chartArea.left - 5, y: point.y}); expect(chart.tooltip.getActiveElements()).toEqual(expectedPoints); chart.data.datasets = [dataset1]; chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 2, y: 1}); expect(chart.tooltip.getActiveElements()).toEqual([expectedPoints[0]]); }); it('should tolerate elements removed on events outside chartArea', async function() { const dataset1 = { label: 'Dataset 1', data: [10, 20, 30], }; const dataset2 = { label: 'Dataset 2', data: [10, 25, 35], }; const chart = window.acquireChart({ type: 'line', data: { datasets: [dataset1, dataset2], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { mode: 'index', intersect: false } } } }); const meta = chart.getDatasetMeta(0); const point = meta.data[1]; const expectedPoints = [jasmine.objectContaining({datasetIndex: 0, index: 1}), jasmine.objectContaining({datasetIndex: 1, index: 1})]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); await jasmine.triggerMouseEvent(chart, 'mousemove', {x: chart.chartArea.left - 5, y: point.y}); expect(chart.tooltip.getActiveElements()).toEqual(expectedPoints); dataset1.data = dataset1.data.slice(0, 1); chart.data.datasets = [dataset1]; chart.update(); await jasmine.triggerMouseEvent(chart, 'mousemove', {x: 2, y: 1}); expect(chart.tooltip.getActiveElements()).toEqual([]); }); describe('events', function() { it('should not be called on events not in plugin events array', async function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { events: ['click'] } } } }); const meta = chart.getDatasetMeta(0); const point = meta.data[1]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(chart.tooltip.opacity).toEqual(0); await jasmine.triggerMouseEvent(chart, 'click', point); expect(chart.tooltip.opacity).toEqual(1); }); }); it('should use default callback if user callback returns undefined', async() => { const chart = window.acquireChart({ type: 'line', data: { datasets: [{ label: 'Dataset 1', data: [10, 20, 30], pointHoverBorderColor: 'rgb(255, 0, 0)', pointHoverBackgroundColor: 'rgb(0, 255, 0)' }, { label: 'Dataset 2', data: [40, 40, 40], pointHoverBorderColor: 'rgb(0, 0, 255)', pointHoverBackgroundColor: 'rgb(0, 255, 255)' }], labels: ['Point 1', 'Point 2', 'Point 3'] }, options: { plugins: { tooltip: { callbacks: { beforeTitle() { return undefined; }, title() { return undefined; }, afterTitle() { return undefined; }, beforeBody() { return undefined; }, beforeLabel() { return undefined; }, label() { return undefined; }, afterLabel() { return undefined; }, afterBody() { return undefined; }, beforeFooter() { return undefined; }, footer() { return undefined; }, afterFooter() { return undefined; }, labelTextColor() { return undefined; }, labelPointStyle() { return undefined; } } } } } }); const {defaults} = Chart; const {tooltip} = chart; const point = chart.getDatasetMeta(0).data[0]; await jasmine.triggerMouseEvent(chart, 'mousemove', point); expect(tooltip).toEqual(jasmine.objectContaining({ opacity: 1, // Text title: ['Point 1'], beforeBody: [], body: [{ before: [], lines: ['Dataset 1: 10'], after: [] }], afterBody: [], footer: [], labelTextColors: ['#fff'], labelColors: [{ borderColor: defaults.borderColor, backgroundColor: defaults.backgroundColor, borderWidth: 1, borderDash: undefined, borderDashOffset: undefined, borderRadius: 0, }], labelPointStyles: [{ pointStyle: 'circle', rotation: 0 }] })); }); }); ================================================ FILE: test/specs/scale.category.tests.js ================================================ function getLabels(scale) { return scale.ticks.map(t => t.label); } describe('Category scale tests', function() { describe('auto', jasmine.fixture.specs('scale.category')); it('Should register the constructor with the registry', function() { var Constructor = Chart.registry.getScale('category'); expect(Constructor).not.toBe(undefined); expect(typeof Constructor).toBe('function'); }); it('Should have the correct default config', function() { var defaultConfig = Chart.defaults.scales.category; expect(defaultConfig).toEqual({ ticks: { callback: Chart.registry.getScale('category').defaults.ticks.callback } }); }); it('Should generate ticks from the data xLabels', function() { var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; var chart = window.acquireChart({ type: 'line', data: { xLabels: labels, datasets: [{ data: [10, 5, 0, 25, 78] }] }, options: { scales: { x: { type: 'category', } } } }); var scale = chart.scales.x; expect(getLabels(scale)).toEqual(labels); }); it('Should generate ticks from the data yLabels', function() { var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; var chart = window.acquireChart({ type: 'line', data: { yLabels: labels, datasets: [{ data: [10, 5, 0, 25, 78] }] }, options: { scales: { y: { type: 'category' } } } }); var scale = chart.scales.y; expect(getLabels(scale)).toEqual(labels); }); it('Should generate ticks from the axis labels', function() { var labels = ['tick1', 'tick2', 'tick3', 'tick4', 'tick5']; var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, 5, 0, 25, 78] }] }, options: { scales: { x: { type: 'category', labels: labels } } } }); var scale = chart.scales.x; expect(getLabels(scale)).toEqual(labels); }); it('Should generate missing labels', function() { var labels = ['a', 'b', 'c', 'd']; var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: {a: 1, b: 3, c: -1, d: 10} }] }, options: { scales: { x: { type: 'category', labels: ['a'] } } } }); var scale = chart.scales.x; expect(getLabels(scale)).toEqual(labels); }); it('should parse only to a valid index', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [10, 5, 0, 25, 78] }], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'] }, options: { scales: { x: { type: 'category', position: 'bottom' }, y: { type: 'linear' } } } }); var scale = chart.scales.x; expect(scale.parse(-10)).toEqual(0); expect(scale.parse(-0.1)).toEqual(0); expect(scale.parse(4.1)).toEqual(4); expect(scale.parse(5)).toEqual(4); expect(scale.parse(1)).toEqual(1); expect(scale.parse(1.4)).toEqual(1); expect(scale.parse(1.5)).toEqual(2); expect(scale.parse('tick2')).toEqual(1); }); it('should get the correct label for the index', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [10, 5, 0, 25, 78] }], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'] }, options: { scales: { x: { type: 'category', position: 'bottom' }, y: { type: 'linear' } } } }); var scale = chart.scales.x; expect(scale.getLabelForValue(1)).toBe('tick2'); }); it('Should get the correct pixel for a value when horizontal', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [10, 5, 0, 25, 78] }], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] }, options: { scales: { x: { type: 'category', position: 'bottom' }, y: { type: 'linear' } } } }); var xScale = chart.scales.x; expect(xScale.getPixelForValue(0)).toBeCloseToPixel(23 + 6); // plus lineHeight expect(xScale.getValueForPixel(23)).toBe(0); expect(xScale.getPixelForValue(4)).toBeCloseToPixel(487); expect(xScale.getValueForPixel(487)).toBe(4); xScale.options.offset = true; chart.update(); expect(xScale.getPixelForValue(0)).toBeCloseToPixel(71 + 6); // plus lineHeight expect(xScale.getValueForPixel(69)).toBe(0); expect(xScale.getPixelForValue(4)).toBeCloseToPixel(461); expect(xScale.getValueForPixel(417)).toBe(4); }); it('Should get the correct pixel for a value when there are repeated labels', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [10, 5, 0, 25, 78] }], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] }, options: { scales: { x: { type: 'category', position: 'bottom' }, y: { type: 'linear' } } } }); var xScale = chart.scales.x; expect(xScale.getPixelForValue('tick1')).toBeCloseToPixel(23 + 6); // plus lineHeight }); it('Should get the correct pixel for a value when horizontal and zoomed', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [10, 5, 0, 25, 78] }], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick_last'] }, options: { scales: { x: { type: 'category', position: 'bottom', min: 'tick2', max: 'tick4' }, y: { type: 'linear' } } } }); var xScale = chart.scales.x; expect(xScale.getPixelForValue(1)).toBeCloseToPixel(23 + 6); // plus lineHeight expect(xScale.getPixelForValue(3)).toBeCloseToPixel(496); xScale.options.offset = true; chart.update(); expect(xScale.getPixelForValue(1)).toBeCloseToPixel(103 + 6); // plus lineHeight expect(xScale.getPixelForValue(3)).toBeCloseToPixel(429); }); it('should get the correct pixel for a value when vertical', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: ['3', '5', '1', '4', '2'] }], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], yLabels: ['1', '2', '3', '4', '5'] }, options: { scales: { x: { type: 'category', position: 'bottom', }, y: { type: 'category', position: 'left' } } } }); var yScale = chart.scales.y; expect(yScale.getPixelForValue(0)).toBeCloseToPixel(32); expect(yScale.getValueForPixel(257)).toBe(2); expect(yScale.getPixelForValue(4)).toBeCloseToPixel(484); expect(yScale.getValueForPixel(144)).toBe(1); yScale.options.offset = true; chart.update(); expect(yScale.getPixelForValue(0)).toBeCloseToPixel(77); expect(yScale.getValueForPixel(256)).toBe(2); expect(yScale.getPixelForValue(4)).toBeCloseToPixel(438); expect(yScale.getValueForPixel(167)).toBe(1); }); it('should get the correct pixel for a value when vertical and zoomed', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: ['3', '5', '1', '4', '2'] }], labels: ['tick1', 'tick2', 'tick3', 'tick4', 'tick5'], yLabels: ['1', '2', '3', '4', '5'] }, options: { scales: { x: { type: 'category', position: 'bottom', }, y: { type: 'category', position: 'left', min: '2', max: '4' } } } }); var yScale = chart.scales.y; expect(yScale.getPixelForValue(1)).toBeCloseToPixel(32); expect(yScale.getPixelForValue(3)).toBeCloseToPixel(482); yScale.options.offset = true; chart.update(); expect(yScale.getPixelForValue(1)).toBeCloseToPixel(107); expect(yScale.getPixelForValue(3)).toBeCloseToPixel(407); }); it('Should get the correct pixel for an object value when horizontal', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [ {x: 0, y: 10}, {x: 1, y: 5}, {x: 2, y: 0}, {x: 3, y: 25}, {x: 0, y: 78} ] }], labels: [0, 1, 2, 3] }, options: { scales: { x: { type: 'category', position: 'bottom' }, y: { type: 'linear' } } } }); var xScale = chart.scales.x; expect(xScale.getPixelForValue(0)).toBeCloseToPixel(29); expect(xScale.getPixelForValue(3)).toBeCloseToPixel(506); expect(xScale.getPixelForValue(4)).toBeCloseToPixel(664); }); it('Should get the correct pixel for an object value when vertical', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [ {x: 0, y: 2}, {x: 1, y: 4}, {x: 2, y: 0}, {x: 3, y: 3}, {x: 0, y: 1} ] }], labels: [0, 1, 2, 3], yLabels: [0, 1, 2, 3, 4] }, options: { scales: { x: { type: 'category', position: 'bottom' }, y: { type: 'category', position: 'left' } } } }); var yScale = chart.scales.y; expect(yScale.getPixelForValue(0)).toBeCloseToPixel(32); expect(yScale.getPixelForValue(4)).toBeCloseToPixel(483); }); it('Should get the correct pixel for an object value in a bar chart', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [ {x: 0, y: 10}, {x: 1, y: 5}, {x: 2, y: 0}, {x: 3, y: 25}, {x: 0, y: 78} ] }], labels: [0, 1, 2, 3] }, options: { scales: { x: { type: 'category', position: 'bottom' }, y: { type: 'linear' } } } }); var xScale = chart.scales.x; expect(xScale.getPixelForValue(0)).toBeCloseToPixel(89); expect(xScale.getPixelForValue(3)).toBeCloseToPixel(451); expect(xScale.getPixelForValue(4)).toBeCloseToPixel(572); }); it('Should get the correct pixel for an object value in a horizontal bar chart', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [ {x: 10, y: 0}, {x: 5, y: 1}, {x: 0, y: 2}, {x: 25, y: 3}, {x: 78, y: 0} ] }], labels: [0, 1, 2, 3] }, options: { indexAxis: 'y', scales: { x: { type: 'linear', position: 'bottom' }, y: { type: 'category' } } } }); var yScale = chart.scales.y; expect(yScale.getPixelForValue(0)).toBeCloseToPixel(88); expect(yScale.getPixelForValue(3)).toBeCloseToPixel(426); expect(yScale.getPixelForValue(4)).toBeCloseToPixel(538); }); it('Should be consistent on pixels and values with autoSkipped ticks', function() { var labels = []; for (let i = 0; i < 50; i++) { labels.push('very long label ' + i); } var chart = window.acquireChart({ type: 'bar', data: { labels, datasets: [{ data: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] }] } }); var scale = chart.scales.x; expect(scale.ticks.length).toBeLessThan(50); let x = 0; for (let i = 0; i < 50; i++) { var x2 = scale.getPixelForValue(labels[i]); var x3 = scale.getPixelForValue(i); expect(x2).toEqual(x3); expect(x2).toBeGreaterThan(x); expect(scale.getValueForPixel(x2)).toBe(i); x = x2; } }); it('Should bound to ticks/data', function() { var chart = window.acquireChart({ type: 'line', data: { labels: ['a', 'b', 'c', 'd'], datasets: [{ data: {b: 1, c: 99} }] }, options: { scales: { x: { type: 'category', bounds: 'data' } } } }); expect(chart.scales.x.min).toEqual(1); expect(chart.scales.x.max).toEqual(2); chart.options.scales.x.bounds = 'ticks'; chart.update(); expect(chart.scales.x.min).toEqual(0); expect(chart.scales.x.max).toEqual(3); }); }); ================================================ FILE: test/specs/scale.linear.tests.js ================================================ function getLabels(scale) { return scale.ticks.map(t => t.label); } describe('Linear Scale', function() { describe('auto', jasmine.fixture.specs('scale.linear')); it('Should register the constructor with the registry', function() { var Constructor = Chart.registry.getScale('linear'); expect(Constructor).not.toBe(undefined); expect(typeof Constructor).toBe('function'); }); it('Should have the correct default config', function() { var defaultConfig = Chart.defaults.scales.linear; expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); }); it('Should correctly determine the max & min data values', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [10, 5, 0, -5, 78, -100] }, { yAxisID: 'y2', data: [-1000, 1000], }, { yAxisID: 'y', data: [150] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear' }, y2: { type: 'linear', position: 'right', } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(-100); expect(chart.scales.y.max).toBe(150); }); it('Should handle when only a min value is provided', () => { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ yAxisID: 'y', data: [200] }], }, options: { scales: { y: { type: 'linear', min: 250 } } } }); expect(chart.scales.y.min).toBe(250); }); it('Should handle when only a max value is provided', () => { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ yAxisID: 'y', data: [200] }], }, options: { scales: { y: { type: 'linear', max: 150 } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.max).toBe(150); }); it('Should correctly determine the max & min of string data values', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: ['10', '5', '0', '-5', '78', '-100'] }, { yAxisID: 'y2', data: ['-1000', '1000'], }, { yAxisID: 'y', data: ['150'] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear' }, y2: { type: 'linear', position: 'right' } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(-100); expect(chart.scales.y.max).toBe(150); }); it('Should correctly determine the max & min when no values provided and suggested minimum and maximum are set', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear', suggestedMin: -10, suggestedMax: 15 } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(-10); expect(chart.scales.y.max).toBe(15); }); it('Should correctly determine the max & min when no datasets are associated and suggested minimum and maximum are set', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [] }, options: { scales: { y: { type: 'linear', suggestedMin: -10, suggestedMax: 0 } } } }); expect(chart.scales.y.min).toBe(-10); expect(chart.scales.y.max).toBe(0); }); it('Should correctly determine the max & min data values ignoring hidden datasets', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: ['10', '5', '0', '-5', '78', '-100'] }, { yAxisID: 'y2', data: ['-1000', '1000'], }, { yAxisID: 'y', data: ['150'], hidden: true }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear' }, y2: { position: 'right', type: 'linear' } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(-100); expect(chart.scales.y.max).toBe(80); }); it('Should correctly determine the max & min data values ignoring data that is NaN', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [null, 90, NaN, undefined, 45, 30, Infinity, -Infinity] }], labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] }, options: { scales: { y: { type: 'linear', beginAtZero: false } } } }); expect(chart.scales.y.min).toBe(30); expect(chart.scales.y.max).toBe(90); // Scale is now stacked chart.scales.y.options.stacked = true; chart.update(); expect(chart.scales.y.min).toBe(30); expect(chart.scales.y.max).toBe(90); chart.scales.y.options.beginAtZero = true; chart.update(); expect(chart.scales.y.min).toBe(0); expect(chart.scales.y.max).toBe(90); }); it('Should correctly determine the max & min data values for small numbers', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [-1e-8, 3e-8, -4e-8, 6e-8] }], labels: ['a', 'b', 'c', 'd'] }, options: { scales: { y: { type: 'linear' } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min * 1e8).toBeCloseTo(-4); expect(chart.scales.y.max * 1e8).toBeCloseTo(6); }); it('Should correctly determine the max & min for scatter data', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [{ x: 10, y: 100 }, { x: -10, y: 0 }, { x: 0, y: 0 }, { x: 99, y: 7 }] }], }, options: { scales: { x: { type: 'linear', position: 'bottom' }, y: { type: 'linear' } } } }); chart.update(); expect(chart.scales.x.min).toBe(-20); expect(chart.scales.x.max).toBe(100); expect(chart.scales.y.min).toBe(0); expect(chart.scales.y.max).toBe(100); }); it('Should correctly get the label for the given index', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [{ x: 10, y: 100 }, { x: -10, y: 0 }, { x: 0, y: 0 }, { x: 99, y: 7 }] }], }, options: { scales: { x: { type: 'linear', position: 'bottom' }, y: { type: 'linear' } } } }); chart.update(); expect(chart.scales.y.getLabelForValue(7)).toBe('7'); }); it('Should correctly use the locale setting when getting a label', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [{ x: 10, y: 100 }, { x: -10, y: 0 }, { x: 0, y: 0 }, { x: 99, y: 7 }] }], }, options: { locale: 'de-DE', scales: { x: { type: 'linear', position: 'bottom' }, y: { type: 'linear' } } } }); chart.update(); expect(chart.scales.y.getLabelForValue(7.07)).toBe('7,07'); }); it('Should correctly determine the min and max data values when stacked mode is turned on', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ yAxisID: 'y', data: [10, 5, 0, -5, 78, -100], type: 'bar' }, { yAxisID: 'y2', data: [-1000, 1000], }, { yAxisID: 'y', data: [150, 0, 0, -100, -10, 9], type: 'bar' }, { yAxisID: 'y', data: [10, 10, 10, 10, 10, 10], type: 'line' }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear', stacked: true }, y2: { position: 'right', type: 'linear' } } } }); chart.update(); expect(chart.scales.y.min).toBe(-150); expect(chart.scales.y.max).toBe(200); }); it('Should correctly determine the min and max data values when stacked mode is turned on and there are hidden datasets', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [10, 5, 0, -5, 78, -100], }, { yAxisID: 'y2', data: [-1000, 1000], }, { yAxisID: 'y', data: [150, 0, 0, -100, -10, 9], }, { yAxisID: 'y', data: [10, 20, 30, 40, 50, 60], hidden: true }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear', stacked: true }, y2: { position: 'right', type: 'linear' } } } }); chart.update(); expect(chart.scales.y.min).toBe(-150); expect(chart.scales.y.max).toBe(200); }); it('Should correctly determine the min and max data values when stacked mode is turned on there are multiple types of datasets', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', type: 'bar', data: [10, 5, 0, -5, 78, -100] }, { type: 'line', data: [10, 10, 10, 10, 10, 10], }, { type: 'bar', data: [150, 0, 0, -100, -10, 9] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear', stacked: true } } } }); chart.scales.y.determineDataLimits(); expect(chart.scales.y.min).toBe(-105); expect(chart.scales.y.max).toBe(160); }); it('Should ensure that the scale has a max and min that are not equal', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear' } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(0); expect(chart.scales.y.max).toBe(1); }); it('Should ensure that the scale has a max and min that are not equal - large positive numbers', function() { // https://github.com/chartjs/Chart.js/issues/9377 var chart = window.acquireChart({ type: 'line', data: { datasets: [{ // Value larger than Number.MAX_SAFE_INTEGER data: [10000000000000000] }], labels: ['a'] }, options: { scales: { y: { type: 'linear' } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(10000000000000000 * 0.95); expect(chart.scales.y.max).toBe(10000000000000000 * 1.05); }); it('Should ensure that the scale has a max and min that are not equal - large negative numbers', function() { // https://github.com/chartjs/Chart.js/issues/9377 var chart = window.acquireChart({ type: 'line', data: { datasets: [{ // Value larger than Number.MAX_SAFE_INTEGER data: [-10000000000000000] }], labels: ['a'] }, options: { scales: { y: { type: 'linear' } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.max).toBe(-10000000000000000 * 0.95); expect(chart.scales.y.min).toBe(-10000000000000000 * 1.05); }); it('Should ensure that the scale has a max and min that are not equal when beginAtZero is set', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear', beginAtZero: true } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(0); expect(chart.scales.y.max).toBe(1); }); it('Should use the suggestedMin and suggestedMax options', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [1, 1, 1, 2, 1, 0] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear', suggestedMax: 10, suggestedMin: -10 } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(-10); expect(chart.scales.y.max).toBe(10); }); it('Should use the min and max options', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [1, 1, 1, 2, 1, 0] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear', max: 1010, min: -1010 } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(-1010); expect(chart.scales.y.max).toBe(1010); var labels = getLabels(chart.scales.y); expect(labels[0]).toBe('-1,010'); expect(labels[labels.length - 1]).toBe('1,010'); }); it('Should use min, max and stepSize to create fixed spaced ticks', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [10, 3, 6, 8, 3, 1] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear', min: 1, max: 11, ticks: { stepSize: 2 } } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(1); expect(chart.scales.y.max).toBe(11); expect(getLabels(chart.scales.y)).toEqual(['1', '3', '5', '7', '9', '11']); }); it('Should not generate any ticks > max if max is specified', function() { var chart = window.acquireChart({ type: 'line', options: { scales: { x: { type: 'linear', min: 2.404e-8, max: 2.4143e-8, ticks: { includeBounds: false, }, }, }, }, }); expect(chart.scales.x.min).toBe(2.404e-8); expect(chart.scales.x.max).toBe(2.4143e-8); expect(chart.scales.x.ticks[chart.scales.x.ticks.length - 1].value).toBeLessThanOrEqual(2.4143e-8); }); it('Should not generate insane amounts of ticks with small stepSize and large range', function() { var chart = window.acquireChart({ type: 'bar', options: { scales: { y: { type: 'linear', min: 1, max: 1E10, ticks: { stepSize: 2, autoSkip: false } } } } }); expect(chart.scales.y.min).toBe(1); expect(chart.scales.y.max).toBe(1E10); expect(chart.scales.y.ticks.length).toBeLessThanOrEqual(1000); }); it('Should create decimal steps if stepSize is a decimal number', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [10, 3, 6, 8, 3, 1] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear', ticks: { stepSize: 2.5 } } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(0); expect(chart.scales.y.max).toBe(10); expect(getLabels(chart.scales.y)).toEqual(['0', '2.5', '5', '7.5', '10']); }); describe('precision', function() { it('Should create integer steps if precision is 0', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [0, 1, 2, 1, 0, 1] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear', ticks: { precision: 0 } } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(0); expect(chart.scales.y.max).toBe(2); expect(getLabels(chart.scales.y)).toEqual(['0', '1', '2']); }); it('Should round the step size to the given number of decimal places', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [0, 0.001, 0.002, 0.003, 0, 0.001] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'linear', ticks: { precision: 2 } } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(0); expect(chart.scales.y.max).toBe(0.01); expect(getLabels(chart.scales.y)).toEqual(['0', '0.01']); }); }); it('should forcibly include 0 in the range if the beginAtZero option is used', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [20, 30, 40, 50] }], labels: ['a', 'b', 'c', 'd'] }, options: { scales: { y: { type: 'linear', beginAtZero: false } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(getLabels(chart.scales.y)).toEqual(['20', '25', '30', '35', '40', '45', '50']); chart.scales.y.options.beginAtZero = true; chart.update(); expect(getLabels(chart.scales.y)).toEqual(['0', '5', '10', '15', '20', '25', '30', '35', '40', '45', '50']); chart.data.datasets[0].data = [-20, -30, -40, -50]; chart.update(); expect(getLabels(chart.scales.y)).toEqual(['-50', '-45', '-40', '-35', '-30', '-25', '-20', '-15', '-10', '-5', '0']); chart.scales.y.options.beginAtZero = false; chart.update(); expect(getLabels(chart.scales.y)).toEqual(['-50', '-45', '-40', '-35', '-30', '-25', '-20']); }); it('Should generate tick marks in the correct order in reversed mode', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [10, 5, 0, 25, 78] }], labels: ['a', 'b', 'c', 'd'] }, options: { scales: { y: { type: 'linear', reverse: true } } } }); expect(getLabels(chart.scales.y)).toEqual(['80', '70', '60', '50', '40', '30', '20', '10', '0']); expect(chart.scales.y.start).toBe(80); expect(chart.scales.y.end).toBe(0); }); it('should use the correct number of decimal places in the default format function', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [0.06, 0.005, 0, 0.025, 0.0078] }], labels: ['a', 'b', 'c', 'd'] }, options: { scales: { y: { type: 'linear', } } } }); expect(getLabels(chart.scales.y)).toEqual(['0', '0.01', '0.02', '0.03', '0.04', '0.05', '0.06']); }); it('Should correctly limit the maximum number of ticks', function() { var chart = window.acquireChart({ type: 'bar', data: { labels: ['a', 'b'], datasets: [{ data: [0.5, 2.5] }] }, options: { scales: { y: { beginAtZero: false } } } }); expect(getLabels(chart.scales.y)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); chart.options.scales.y.ticks.maxTicksLimit = 11; chart.update(); expect(getLabels(chart.scales.y)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); chart.options.scales.y.ticks.maxTicksLimit = 21; chart.update(); expect(getLabels(chart.scales.y)).toEqual([ '0.5', '0.6', '0.7', '0.8', '0.9', '1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '2.0', '2.1', '2.2', '2.3', '2.4', '2.5' ]); chart.options.scales.y.ticks.maxTicksLimit = 11; chart.options.scales.y.ticks.stepSize = 0.01; chart.update(); expect(getLabels(chart.scales.y)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); chart.options.scales.y.min = 0.3; chart.options.scales.y.max = 2.8; chart.update(); expect(getLabels(chart.scales.y)).toEqual(['0.3', '0.8', '1.3', '1.8', '2.3', '2.8']); }); it('Should bound to data', function() { var chart = window.acquireChart({ type: 'line', data: { labels: ['a', 'b'], datasets: [{ data: [1, 99] }] }, options: { scales: { y: { bounds: 'data' } } } }); expect(chart.scales.y.min).toEqual(1); expect(chart.scales.y.max).toEqual(99); }); it('Should build labels using the user supplied callback', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [10, 5, 0, 25, 78] }], labels: ['a', 'b', 'c', 'd'] }, options: { scales: { y: { type: 'linear', ticks: { callback: function(value, index) { return index.toString(); } } } } } }); // Just the index expect(getLabels(chart.scales.y)).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8']); }); it('Should get the correct pixel value for a point', function() { var chart = window.acquireChart({ type: 'line', data: { labels: [-1, 1], datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [-1, 1] }], }, options: { scales: { x: { type: 'linear', position: 'bottom' }, y: { type: 'linear' } } } }); var xScale = chart.scales.x; expect(xScale.getPixelForValue(1)).toBeCloseToPixel(501); // right - paddingRight expect(xScale.getPixelForValue(-1)).toBeCloseToPixel(31 + 3); // left + paddingLeft + tick padding expect(xScale.getPixelForValue(0)).toBeCloseToPixel(266 + 3 / 2); // halfway*/ expect(xScale.getValueForPixel(501)).toBeCloseTo(1, 1e-2); expect(xScale.getValueForPixel(31)).toBeCloseTo(-1, 1e-2); expect(xScale.getValueForPixel(266)).toBeCloseTo(0, 1e-2); var yScale = chart.scales.y; expect(yScale.getPixelForValue(1)).toBeCloseToPixel(32); // right - paddingRight expect(yScale.getPixelForValue(-1)).toBeCloseToPixel(484); // left + paddingLeft expect(yScale.getPixelForValue(0)).toBeCloseToPixel(258); // halfway*/ expect(yScale.getValueForPixel(32)).toBeCloseTo(1, 1e-2); expect(yScale.getValueForPixel(484)).toBeCloseTo(-1, 1e-2); expect(yScale.getValueForPixel(258)).toBeCloseTo(0, 1e-2); }); it('should fit correctly', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [{ x: 10, y: 100 }, { x: -10, y: 0 }, { x: 0, y: 0 }, { x: 99, y: 7 }] }], }, options: { scales: { x: { type: 'linear', position: 'bottom' }, y: { type: 'linear' } } } }); var xScale = chart.scales.x; var yScale = chart.scales.y; expect(xScale.paddingTop).toBeCloseToPixel(0); expect(xScale.paddingBottom).toBeCloseToPixel(0); expect(xScale.paddingLeft).toBeCloseToPixel(12); expect(xScale.paddingRight).toBeCloseToPixel(13.5); expect(xScale.width).toBeCloseToPixel(468 - 3); // minus tick padding expect(xScale.height).toBeCloseToPixel(30); expect(yScale.paddingTop).toBeCloseToPixel(10); expect(yScale.paddingBottom).toBeCloseToPixel(10); expect(yScale.paddingLeft).toBeCloseToPixel(0); expect(yScale.paddingRight).toBeCloseToPixel(0); expect(yScale.width).toBeCloseToPixel(31 + 3); // plus tick padding expect(yScale.height).toBeCloseToPixel(450); // Extra size when scale label showing xScale.options.title.display = true; yScale.options.title.display = true; chart.update(); expect(xScale.paddingTop).toBeCloseToPixel(0); expect(xScale.paddingBottom).toBeCloseToPixel(0); expect(xScale.paddingLeft).toBeCloseToPixel(12); expect(xScale.paddingRight).toBeCloseToPixel(13.5); expect(xScale.width).toBeCloseToPixel(442); expect(xScale.height).toBeCloseToPixel(50); expect(yScale.paddingTop).toBeCloseToPixel(10); expect(yScale.paddingBottom).toBeCloseToPixel(10); expect(yScale.paddingLeft).toBeCloseToPixel(0); expect(yScale.paddingRight).toBeCloseToPixel(0); expect(yScale.width).toBeCloseToPixel(58); expect(yScale.height).toBeCloseToPixel(429); }); it('should fit correctly when display is turned off', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', yAxisID: 'y', data: [{ x: 10, y: 100 }, { x: -10, y: 0 }, { x: 0, y: 0 }, { x: 99, y: 7 }] }], }, options: { scales: { x: { type: 'linear', position: 'bottom' }, y: { type: 'linear', grid: { drawTicks: false, }, border: { display: false }, title: { display: false, lineHeight: 1.2 }, ticks: { display: false, padding: 0 } } } } }); var yScale = chart.scales.y; expect(yScale.width).toBeCloseToPixel(0); }); it('max and min value should be valid and finite when charts datasets are hidden', function() { var barData = { labels: ['S1', 'S2', 'S3'], datasets: [{ label: 'Closed', backgroundColor: '#382765', data: [2500, 2000, 1500] }, { label: 'In Progress', backgroundColor: '#7BC225', data: [1000, 2000, 1500] }, { label: 'Assigned', backgroundColor: '#ffC225', data: [1000, 2000, 1500] }] }; var chart = window.acquireChart({ type: 'bar', data: barData, options: { indexAxis: 'y', scales: { x: { stacked: true }, y: { stacked: true } } } }); barData.datasets.forEach(function(data, index) { var meta = chart.getDatasetMeta(index); meta.hidden = true; chart.update(); }); expect(chart.scales.x.min).toEqual(0); expect(chart.scales.x.max).toEqual(1); }); it('max and min value should be valid when min is set and all datasets are hidden', function() { var barData = { labels: ['S1', 'S2', 'S3'], datasets: [{ label: 'dataset 1', backgroundColor: '#382765', data: [2500, 2000, 1500], hidden: true, }] }; var chart = window.acquireChart({ type: 'bar', data: barData, options: { indexAxis: 'y', scales: { x: { min: 20 } } } }); expect(chart.scales.x.min).toEqual(20); expect(chart.scales.x.max).toEqual(21); }); it('min settings should be used if set to zero', function() { var barData = { labels: ['S1', 'S2', 'S3'], datasets: [{ label: 'dataset 1', backgroundColor: '#382765', data: [2500, 2000, 1500] }] }; var chart = window.acquireChart({ type: 'bar', data: barData, options: { indexAxis: 'y', scales: { x: { min: 0, max: 3000 } } } }); expect(chart.scales.x.min).toEqual(0); }); it('max settings should be used if set to zero', function() { var barData = { labels: ['S1', 'S2', 'S3'], datasets: [{ label: 'dataset 1', backgroundColor: '#382765', data: [-2500, -2000, -1500] }] }; var chart = window.acquireChart({ type: 'bar', data: barData, options: { indexAxis: 'y', scales: { x: { min: -3000, max: 0 } } } }); expect(chart.scales.x.max).toEqual(0); }); it('Should get correct pixel values when horizontal', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [0.05, -25, 10, 15, 20, 25, 30, 35] }] }, options: { indexAxis: 'y', scales: { x: { type: 'linear', } } } }); var start = chart.chartArea.left; var end = chart.chartArea.right; var min = -30; var max = 40; var scale = chart.scales.x; expect(scale.getPixelForValue(max)).toBeCloseToPixel(end); expect(scale.getPixelForValue(min)).toBeCloseToPixel(start); expect(scale.getValueForPixel(end)).toBeCloseTo(max, 4); expect(scale.getValueForPixel(start)).toBeCloseTo(min, 4); scale.options.reverse = true; chart.update(); start = chart.chartArea.left; end = chart.chartArea.right; expect(scale.getPixelForValue(max)).toBeCloseToPixel(start); expect(scale.getPixelForValue(min)).toBeCloseToPixel(end); expect(scale.getValueForPixel(end)).toBeCloseTo(min, 4); expect(scale.getValueForPixel(start)).toBeCloseTo(max, 4); }); it('Should get correct pixel values when vertical', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [0.05, -25, 10, 15, 20, 25, 30, 35] }] }, options: { scales: { y: { type: 'linear', } } } }); var start = chart.chartArea.bottom; var end = chart.chartArea.top; var min = -30; var max = 40; var scale = chart.scales.y; expect(scale.getPixelForValue(max)).toBeCloseToPixel(end); expect(scale.getPixelForValue(min)).toBeCloseToPixel(start); expect(scale.getValueForPixel(end)).toBeCloseTo(max, 4); expect(scale.getValueForPixel(start)).toBeCloseTo(min, 4); scale.options.reverse = true; chart.update(); start = chart.chartArea.bottom; end = chart.chartArea.top; expect(scale.getPixelForValue(max)).toBeCloseToPixel(start); expect(scale.getPixelForValue(min)).toBeCloseToPixel(end); expect(scale.getValueForPixel(end)).toBeCloseTo(min, 4); expect(scale.getValueForPixel(start)).toBeCloseTo(max, 4); }); it('should not throw errors when chart size is negative', function() { function createChart() { return window.acquireChart({ type: 'bar', data: { labels: [0, 1, 2, 3, 4, 5, 6, 7, '7+'], datasets: [{ data: [29.05, 4, 15.69, 11.69, 2.84, 4, 0, 3.84, 4], }], }, options: { plugins: false, layout: { padding: {top: 30, left: 1, right: 1, bottom: 1} } } }, { canvas: { height: 0, width: 0 } }); } expect(createChart).not.toThrow(); }); }); ================================================ FILE: test/specs/scale.logarithmic.tests.js ================================================ function getLabels(scale) { return scale.ticks.map(t => t.label); } describe('Logarithmic Scale tests', function() { describe('auto', jasmine.fixture.specs('scale.logarithmic')); it('should register', function() { var Constructor = Chart.registry.getScale('logarithmic'); expect(Constructor).not.toBe(undefined); expect(typeof Constructor).toBe('function'); }); it('should have the correct default config', function() { var defaultConfig = Chart.defaults.scales.logarithmic; expect(defaultConfig).toEqual({ ticks: { callback: Chart.Ticks.formatters.logarithmic, major: { enabled: true } } }); // Is this actually a function expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); }); it('should correctly determine the max & min data values', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [42, 1000, 64, 100], }, { yAxisID: 'y1', data: [10, 5, 5000, 78, 450] }, { yAxisID: 'y1', data: [150] }, { yAxisID: 'y2', data: [20, 0, 150, 1800, 3040] }, { yAxisID: 'y3', data: [67, 0.0004, 0, 820, 0.001] }], labels: ['a', 'b', 'c', 'd', 'e'] }, options: { scales: { y: { id: 'y', type: 'logarithmic' }, y1: { type: 'logarithmic', position: 'right' }, y2: { type: 'logarithmic', position: 'right' }, y3: { position: 'right', type: 'logarithmic' } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(10); expect(chart.scales.y.max).toBe(1000); expect(chart.scales.y1).not.toEqual(undefined); // must construct expect(chart.scales.y1.min).toBe(1); expect(chart.scales.y1.max).toBe(5000); expect(chart.scales.y2).not.toEqual(undefined); // must construct expect(chart.scales.y2.min).toBe(10); expect(chart.scales.y2.max).toBe(4000); expect(chart.scales.y3).not.toEqual(undefined); // must construct expect(chart.scales.y3.min).toBeCloseTo(0.0001, 4); expect(chart.scales.y3.max).toBe(900); }); it('should correctly determine the max & min of string data values', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ yAxisID: 'y', data: ['42', '1000', '64', '100'], }, { yAxisID: 'y1', data: ['10', '5', '5000', '78', '450'] }, { yAxisID: 'y1', data: ['150'] }, { yAxisID: 'y2', data: ['20', '0', '150', '1800', '3040'] }, { yAxisID: 'y3', data: ['67', '0.0004', '0', '820', '0.001'] }], labels: ['a', 'b', 'c', 'd', 'e'] }, options: { scales: { y: { type: 'logarithmic' }, y1: { position: 'right', type: 'logarithmic' }, y2: { position: 'right', type: 'logarithmic' }, y3: { position: 'right', type: 'logarithmic' } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(40); expect(chart.scales.y.max).toBe(1000); expect(chart.scales.y1).not.toEqual(undefined); // must construct expect(chart.scales.y1.min).toBe(5); expect(chart.scales.y1.max).toBe(5000); expect(chart.scales.y2).not.toEqual(undefined); // must construct expect(chart.scales.y2.min).toBe(10); expect(chart.scales.y2.max).toBe(4000); expect(chart.scales.y3).not.toEqual(undefined); // must construct expect(chart.scales.y3.min).toBeCloseTo(0.0001, 4); expect(chart.scales.y3.max).toBe(900); }); it('should correctly determine the max & min data values when there are hidden datasets', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ yAxisID: 'y1', data: [10, 5, 5000, 78, 450] }, { yAxisID: 'y', data: [42, 1000, 64, 100], }, { yAxisID: 'y1', data: [50000], hidden: true }, { yAxisID: 'y2', data: [20, 0, 7400, 14, 291] }, { yAxisID: 'y2', data: [6, 0.0007, 9, 890, 60000], hidden: true }], labels: ['a', 'b', 'c', 'd', 'e'] }, options: { scales: { y: { type: 'logarithmic' }, y1: { position: 'right', type: 'logarithmic' }, y2: { position: 'right', type: 'logarithmic' } } } }); expect(chart.scales.y1).not.toEqual(undefined); // must construct expect(chart.scales.y1.min).toBe(5); expect(chart.scales.y1.max).toBe(5000); expect(chart.scales.y2).not.toEqual(undefined); // must construct expect(chart.scales.y2.min).toBe(10); expect(chart.scales.y2.max).toBe(8000); }); it('should correctly determine the max & min data values when there is NaN data', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [undefined, 10, null, 5, 5000, NaN, 78, 450] }, { yAxisID: 'y', data: [undefined, 28, null, 1000, 500, NaN, 50, 42, Infinity, -Infinity] }, { yAxisID: 'y1', data: [undefined, 30, null, 9400, 0, NaN, 54, 836] }, { yAxisID: 'y1', data: [undefined, 0, null, 800, 9, NaN, 894, 21] }], labels: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i'] }, options: { scales: { y: { type: 'logarithmic' }, y1: { position: 'right', type: 'logarithmic' } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(1); expect(chart.scales.y.max).toBe(5000); // Turn on stacked mode since it uses it's own chart.options.scales.y.stacked = true; chart.update(); expect(chart.scales.y.min).toBe(1); expect(chart.scales.y.max).toBe(6000); expect(chart.scales.y1).not.toEqual(undefined); // must construct expect(chart.scales.y1.min).toBe(1); expect(chart.scales.y1.max).toBe(10000); }); it('should correctly determine the max & min for scatter data', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [ {x: 10, y: 100}, {x: 2, y: 6}, {x: 65, y: 121}, {x: 99, y: 7} ] }] }, options: { scales: { x: { type: 'logarithmic', position: 'bottom' }, y: { type: 'logarithmic' } } } }); expect(chart.scales.x.min).toBe(2); expect(chart.scales.x.max).toBe(100); expect(chart.scales.y.min).toBe(6); expect(chart.scales.y.max).toBe(150); }); it('should correctly determine the max & min for scatter data when 0 values are present', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [ {x: 7, y: 950}, {x: 289, y: 0}, {x: 0, y: 8}, {x: 23, y: 0.04} ] }] }, options: { scales: { x: { type: 'logarithmic', position: 'bottom' }, y: { type: 'logarithmic' } } } }); expect(chart.scales.x.min).toBe(1); expect(chart.scales.x.max).toBe(30); expect(chart.scales.y.min).toBe(0.01); expect(chart.scales.y.max).toBe(1000); }); it('should correctly determine the min and max data values when stacked mode is turned on', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ type: 'bar', yAxisID: 'y', data: [10, 5, 1, 5, 78, 100] }, { yAxisID: 'y1', data: [0, 1000], }, { type: 'bar', yAxisID: 'y', data: [150, 10, 10, 100, 10, 9] }, { type: 'line', yAxisID: 'y', data: [100, 100, 100, 100, 100, 100] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'logarithmic', stacked: true }, y1: { position: 'right', type: 'logarithmic' } } } }); expect(chart.scales.y.min).toBe(0.1); expect(chart.scales.y.max).toBe(200); }); it('should correctly determine the min and max data values when stacked mode is turned on ignoring hidden datasets', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [10, 5, 1, 5, 78, 100], type: 'bar' }, { yAxisID: 'y1', data: [0, 1000], type: 'bar' }, { yAxisID: 'y', data: [150, 10, 10, 100, 10, 9], type: 'bar' }, { yAxisID: 'y', data: [10000, 10000, 10000, 10000, 10000, 10000], hidden: true, type: 'bar' }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'logarithmic', stacked: true }, y1: { position: 'right', type: 'logarithmic' } } } }); expect(chart.scales.y.min).toBe(0.1); expect(chart.scales.y.max).toBe(200); }); it('should ensure that the scale has a max and min that are not equal', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [] }], labels: [] }, options: { scales: { y: { type: 'logarithmic' } } } }); expect(chart.scales.y.min).toBe(1); expect(chart.scales.y.max).toBe(10); chart.data.datasets[0].data = [0.15, 0.15]; chart.update(); expect(chart.scales.y.min).toBe(0.1); expect(chart.scales.y.max).toBe(0.15); }); it('should use the min and max options', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 1, 1, 2, 1, 0] }], labels: [] }, options: { scales: { y: { type: 'logarithmic', min: 10, max: 1010, ticks: { callback: function(value) { return value; } } } } } }); var yScale = chart.scales.y; var tickCount = yScale.ticks.length; expect(yScale.min).toBe(10); expect(yScale.max).toBe(1010); expect(yScale.ticks[0].value).toBe(10); expect(yScale.ticks[tickCount - 1].value).toBe(1010); }); it('should ignore negative min and max options', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 1, 1, 2, 1, 0] }], labels: [] }, options: { scales: { y: { type: 'logarithmic', min: -10, max: -1010, ticks: { callback: function(value) { return value; } } } } } }); var y = chart.scales.y; expect(y.min).toBe(0.1); expect(y.max).toBe(2); }); it('should ignore invalid min and max options', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [1, 1, 1, 2, 1, 0] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'logarithmic', min: 'zero', max: null, ticks: { callback: function(value) { return value; } } } } } }); var y = chart.scales.y; expect(y.min).toBe(0.1); expect(y.max).toBe(2); }); it('should generate tick marks', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [10, 5, 2, 25, 78] }], labels: [] }, options: { scales: { y: { type: 'logarithmic', ticks: { autoSkip: false, callback: function(value) { return value; } } } } } }); var scale = chart.scales.y; expect(getLabels(scale)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30, 40, 50, 60, 70, 80]); expect(scale.start).toEqual(1); expect(scale.end).toEqual(80); }); it('should generate tick marks when 0 values are present', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [11, 0.8, 0, 28, 7] }], labels: [] }, options: { scales: { y: { type: 'logarithmic', ticks: { callback: function(value) { return value; } } } } } }); var scale = chart.scales.y; // Counts down because the lines are drawn top to bottom expect(getLabels(scale)).toEqual([0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1, 1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 30]); expect(scale.start).toEqual(0.1); expect(scale.end).toEqual(30); }); it('should generate tick marks in the correct order in reversed mode', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, 5, 1, 25, 78] }], labels: [] }, options: { scales: { y: { type: 'logarithmic', reverse: true, ticks: { autoSkip: false, callback: function(value) { return value; } } } } } }); var scale = chart.scales.y; expect(getLabels(scale)).toEqual([80, 70, 60, 50, 40, 30, 20, 15, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); expect(scale.start).toEqual(80); expect(scale.end).toEqual(1); }); it('should generate tick marks in the correct order in reversed mode when 0 values are present', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [21, 9, 0, 10, 25] }], labels: [] }, options: { scales: { y: { type: 'logarithmic', reverse: true, ticks: { callback: function(value) { return value; } } } } } }); var scale = chart.scales.y; expect(getLabels(scale)).toEqual([30, 20, 15, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1]); expect(scale.start).toEqual(30); expect(scale.end).toEqual(1); }); it('should build labels using the default template', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [10, 5, 1.1, 25, 0, 78] }], labels: [] }, options: { scales: { y: { type: 'logarithmic', ticks: { autoSkip: false } } } } }); expect(getLabels(chart.scales.y)).toEqual(['1', '2', '3', '', '5', '', '', '', '', '10', '15', '20', '30', '', '50', '60', '70', '80']); }); it('should build labels using the user supplied callback', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [10, 5, 2, 25, 78] }], labels: [] }, options: { scales: { y: { type: 'logarithmic', ticks: { callback: function(value, index) { return index.toString(); } } } } } }); // Just the index expect(getLabels(chart.scales.y)).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17']); }); it('should correctly get the correct label for a data item', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [10, 5, 5000, 78, 450] }, { yAxisID: 'y1', data: [1, 1000, 10, 100], }, { yAxisID: 'y', data: [150] }], labels: [] }, options: { scales: { y: { type: 'logarithmic' }, y1: { position: 'right', type: 'logarithmic' } } } }); expect(chart.scales.y.getLabelForValue(150)).toBe('150'); }); it('should correctly use the locale when generating the label', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [10, 5, 5000, 78, 450] }, { yAxisID: 'y1', data: [1, 1000, 10, 100], }, { yAxisID: 'y', data: [150] }], labels: [] }, options: { locale: 'de-DE', scales: { y: { type: 'logarithmic' }, y1: { position: 'right', type: 'logarithmic' } } } }); expect(chart.scales.y.getLabelForValue(10.25)).toBe('10,25'); }); describe('when', function() { var data = [ { data: [1, 39], stack: 'stack' }, { data: [1, 39], stack: 'stack' }, ]; var dataWithEmptyStacks = [ { data: [] }, { data: [] } ].concat(data); var config = [ { axis: 'y', firstTick: 1, // start of the axis (minimum) describe: 'all stacks are defined' }, { axis: 'y', data: dataWithEmptyStacks, firstTick: 1, describe: 'not all stacks are defined' }, { axis: 'y', scale: { y: { min: 0 } }, firstTick: 0.1, describe: 'all stacks are defined and min: 0' }, { axis: 'y', data: dataWithEmptyStacks, scale: { y: { min: 0 } }, firstTick: 0.1, describe: 'not stacks are defined and min: 0' }, { axis: 'x', firstTick: 1, describe: 'all stacks are defined' }, { axis: 'x', data: dataWithEmptyStacks, firstTick: 1, describe: 'not all stacks are defined' }, { axis: 'x', scale: { x: { min: 0 } }, firstTick: 0.1, describe: 'all stacks are defined and min: 0' }, { axis: 'x', data: dataWithEmptyStacks, scale: { x: { min: 0 } }, firstTick: 0.1, describe: 'not all stacks are defined and min: 0' }, ]; config.forEach(function(setup) { var scaleConfig = {}; var indexAxis, chartStart, chartEnd; if (setup.axis === 'x') { indexAxis = 'y'; chartStart = 'left'; chartEnd = 'right'; } else { indexAxis = 'x'; chartStart = 'bottom'; chartEnd = 'top'; } scaleConfig[setup.axis] = { type: 'logarithmic', beginAtZero: false }; Object.assign(scaleConfig, setup.scale); scaleConfig[setup.axis].type = 'logarithmic'; var description = 'dataset has stack option and ' + setup.describe + ' and axis is "' + setup.axis + '";'; describe(description, function() { it('should define the correct axis limits', function() { var chart = window.acquireChart({ type: 'bar', data: { labels: ['category 1', 'category 2'], datasets: setup.data || data, }, options: { indexAxis, scales: scaleConfig } }); var axisID = setup.axis; var scale = chart.scales[axisID]; var firstTick = setup.firstTick; var lastTick = 80; // last tick (should be first available tick after: 2 * 39) var start = chart.chartArea[chartStart]; var end = chart.chartArea[chartEnd]; expect(scale.getPixelForValue(firstTick)).toBeCloseToPixel(start); expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); chart.scales[axisID].options.reverse = true; // Reverse mode chart.update(); // chartArea might have been resized in update start = chart.chartArea[chartEnd]; end = chart.chartArea[chartStart]; expect(scale.getPixelForValue(firstTick)).toBeCloseToPixel(start); expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); }); }); }); }); describe('when', function() { var config = [ { dataset: [], firstTick: 1, // value of the first tick lastTick: 10, // value of the last tick describe: 'empty dataset, without min/max' }, { dataset: [], scale: {stacked: true}, firstTick: 1, lastTick: 10, describe: 'empty dataset, without min/max, with stacked: true' }, { data: { datasets: [ {data: [], stack: 'stack'}, {data: [], stack: 'stack'}, ], }, type: 'bar', firstTick: 1, lastTick: 10, describe: 'empty dataset with stack option, without min/max' }, { dataset: [], scale: {min: 1}, firstTick: 1, lastTick: 10, describe: 'empty dataset, min: 1, without max' }, { dataset: [], scale: {max: 80}, firstTick: 1, lastTick: 80, describe: 'empty dataset, max: 80, without min' }, { dataset: [], scale: {max: 0.8}, firstTick: 0.01, lastTick: 0.8, describe: 'empty dataset, max: 0.8, without min' }, { dataset: [{x: 10, y: 10}, {x: 5, y: 5}, {x: 1, y: 1}, {x: 25, y: 25}, {x: 78, y: 78}], firstTick: 1, lastTick: 80, describe: 'dataset min point {x: 1, y: 1}, max point {x:78, y:78}' }, ]; config.forEach(function(setup) { var axes = [ { id: 'x', // horizontal scale start: 'left', end: 'right' }, { id: 'y', // vertical scale start: 'bottom', end: 'top' } ]; axes.forEach(function(axis) { var expectation = 'min = ' + setup.firstTick + ', max = ' + setup.lastTick; describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { beforeEach(function() { var xConfig = { type: 'logarithmic', position: 'bottom' }; var yConfig = { type: 'logarithmic', position: 'left' }; var data = setup.data || { datasets: [{ data: setup.dataset }], }; Object.assign(xConfig, setup.scale); Object.assign(yConfig, setup.scale); Object.assign(data, setup.data || {}); this.chart = window.acquireChart({ type: 'line', data: data, options: { scales: { x: xConfig, y: yConfig } } }); }); it('should get the correct pixel value for a point', function() { var chart = this.chart; var axisID = axis.id; var scale = chart.scales[axisID]; var firstTick = setup.firstTick; var lastTick = setup.lastTick; var start = chart.chartArea[axis.start]; var end = chart.chartArea[axis.end]; expect(scale.getPixelForValue(firstTick)).toBeCloseToPixel(start); expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); expect(scale.getPixelForValue(0)).toBeCloseToPixel(start); // 0 is invalid, put it at the start. expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); chart.scales[axisID].options.reverse = true; // Reverse mode chart.update(); // chartArea might have been resized in update start = chart.chartArea[axis.end]; end = chart.chartArea[axis.start]; expect(scale.getPixelForValue(firstTick)).toBeCloseToPixel(start); expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); expect(scale.getValueForPixel(start)).toBeCloseTo(firstTick, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); }); }); }); }); }); describe('when', function() { var config = [ { dataset: [], scale: {min: 0}, lastTick: 10, // value of the last tick describe: 'empty dataset, min: 0, without max' }, { dataset: [], scale: {min: 0, max: 80}, lastTick: 80, describe: 'empty dataset, min: 0, max: 80' }, { dataset: [], scale: {min: 0, max: 0.8}, lastTick: 0.8, describe: 'empty dataset, min: 0, max: 0.8' }, { dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], lastTick: 80, describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}' }, { dataset: [{x: 0, y: 0}, {x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], lastTick: 80, describe: 'dataset min point {x: 0, y: 0}, max point {x:78, y:78}' }, { dataset: [{x: 10, y: 10}, {x: 1.2, y: 1.2}, {x: 25, y: 25}, {x: 78, y: 78}], scale: {min: 0}, lastTick: 80, describe: 'dataset min point {x: 1.2, y: 1.2}, max point {x:78, y:78}, min: 0' }, { dataset: [{x: 10, y: 10}, {x: 6.3, y: 6.3}, {x: 25, y: 25}, {x: 78, y: 78}], scale: {min: 0}, lastTick: 80, describe: 'dataset min point {x: 6.3, y: 6.3}, max point {x:78, y:78}, min: 0' }, ]; config.forEach(function(setup) { var axes = [ { id: 'x', // horizontal scale start: 'left', end: 'right' }, { id: 'y', // vertical scale start: 'bottom', end: 'top' } ]; axes.forEach(function(axis) { var expectation = 'min = 0, max = ' + setup.lastTick; describe(setup.describe + ' and axis is "' + axis.id + '"; expect: ' + expectation + ';', function() { beforeEach(function() { var xConfig = { type: 'logarithmic', position: 'bottom' }; var yConfig = { type: 'logarithmic', position: 'left' }; var data = setup.data || { datasets: [{ data: setup.dataset }], }; Object.assign(xConfig, setup.scale); Object.assign(yConfig, setup.scale); Object.assign(data, setup.data || {}); this.chart = window.acquireChart({ type: 'line', data: data, options: { scales: { x: xConfig, y: yConfig } } }); }); it('should get the correct pixel value for a point', function() { var chart = this.chart; var axisID = axis.id; var scale = chart.scales[axisID]; var lastTick = setup.lastTick; var start = chart.chartArea[axis.start]; var end = chart.chartArea[axis.end]; expect(scale.getPixelForValue(0)).toBeCloseToPixel(start); expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); expect(scale.getValueForPixel(start)).toBeCloseTo(scale.min, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); chart.scales[axisID].options.reverse = true; // Reverse mode chart.update(); // chartArea might have been resized in update start = chart.chartArea[axis.end]; end = chart.chartArea[axis.start]; expect(scale.getPixelForValue(0)).toBeCloseToPixel(start); expect(scale.getPixelForValue(lastTick)).toBeCloseToPixel(end); expect(scale.getValueForPixel(start)).toBeCloseTo(scale.min, 4); expect(scale.getValueForPixel(end)).toBeCloseTo(lastTick, 4); }); }); }); }); }); it('Should correctly determine the max & min when no values provided and suggested minimum and maximum are set', function() { var chart = window.acquireChart({ type: 'bar', data: { datasets: [{ yAxisID: 'y', data: [] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] }, options: { scales: { y: { type: 'logarithmic', suggestedMin: 10, suggestedMax: 100 } } } }); expect(chart.scales.y).not.toEqual(undefined); // must construct expect(chart.scales.y.min).toBe(10); expect(chart.scales.y.max).toBe(100); }); it('Should bound to data', function() { var chart = window.acquireChart({ type: 'line', data: { labels: ['a', 'b'], datasets: [{ data: [1.1, 99] }] }, options: { scales: { y: { type: 'logarithmic', bounds: 'data' } } } }); expect(chart.scales.y.min).toEqual(1.1); expect(chart.scales.y.max).toEqual(99); }); }); ================================================ FILE: test/specs/scale.radialLinear.tests.js ================================================ function getLabels(scale) { return scale.ticks.map(t => t.label); } // Tests for the radial linear scale used by the polar area and radar charts describe('Test the radial linear scale', function() { describe('auto', jasmine.fixture.specs('scale.radialLinear')); it('Should register the constructor with the registry', function() { var Constructor = Chart.registry.getScale('radialLinear'); expect(Constructor).not.toBe(undefined); expect(typeof Constructor).toBe('function'); }); it('Should have the correct default config', function() { var defaultConfig = Chart.defaults.scales.radialLinear; expect(defaultConfig).toEqual({ display: true, animate: true, position: 'chartArea', angleLines: { display: true, color: 'rgba(0,0,0,0.1)', lineWidth: 1, borderDash: [], borderDashOffset: 0.0 }, grid: { circular: false }, startAngle: 0, ticks: { color: Chart.defaults.color, showLabelBackdrop: true, callback: defaultConfig.ticks.callback }, pointLabels: { backdropColor: undefined, backdropPadding: 2, color: Chart.defaults.color, display: true, font: { size: 10 }, callback: defaultConfig.pointLabels.callback, padding: 5, centerPointLabels: false } }); // Is this actually a function expect(defaultConfig.ticks.callback).toEqual(jasmine.any(Function)); expect(defaultConfig.pointLabels.callback).toEqual(jasmine.any(Function)); }); it('Should correctly determine the max & min data values', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, -5, 78, -100] }, { data: [150] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] }, options: { scales: {} } }); expect(chart.scales.r.min).toBe(-100); expect(chart.scales.r.max).toBe(150); }); it('Should correctly determine the max & min of string data values', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: ['10', '5', '0', '-5', '78', '-100'] }, { data: ['150'] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] }, options: { scales: {} } }); expect(chart.scales.r.min).toBe(-100); expect(chart.scales.r.max).toBe(150); }); it('Should correctly determine the max & min data values when there are hidden datasets', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: ['10', '5', '0', '-5', '78', '-100'] }, { data: ['150'] }, { data: [1000], hidden: true }], labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] }, options: { scales: {} } }); expect(chart.scales.r.min).toBe(-100); expect(chart.scales.r.max).toBe(150); }); it('Should correctly determine the max & min data values when there is NaN data', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [50, 60, NaN, 70, null, undefined, Infinity, -Infinity] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6', 'label7', 'label8'] }, options: { scales: {} } }); expect(chart.scales.r.min).toBe(50); expect(chart.scales.r.max).toBe(70); }); it('Should ensure that the scale has a max and min that are not equal', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [], labels: [] }, options: { scales: { rScale: {} } } }); var scale = chart.scales.rScale; expect(scale.min).toBe(-1); expect(scale.max).toBe(1); }); it('Should use the suggestedMin and suggestedMax options', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [1, 1, 1, 2, 1, 0] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] }, options: { scales: { r: { suggestedMin: -10, suggestedMax: 10 } } } }); expect(chart.scales.r.min).toBe(-10); expect(chart.scales.r.max).toBe(10); }); it('Should use the min and max options', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [1, 1, 1, 2, 1, 0] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5', 'label6'] }, options: { scales: { r: { min: -1010, max: 1010 } } } }); expect(chart.scales.r.min).toBe(-1010); expect(chart.scales.r.max).toBe(1010); expect(getLabels(chart.scales.r)).toEqual(['-1,010', '-500', '0', '500', '1,010']); }); it('should forcibly include 0 in the range if the beginAtZero option is used', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [20, 30, 40, 50] }], labels: ['label1', 'label2', 'label3', 'label4'] }, options: { scales: { r: { beginAtZero: false } } } }); expect(getLabels(chart.scales.r)).toEqual(['20', '25', '30', '35', '40', '45', '50']); chart.scales.r.options.beginAtZero = true; chart.update(); expect(getLabels(chart.scales.r)).toEqual(['0', '5', '10', '15', '20', '25', '30', '35', '40', '45', '50']); chart.data.datasets[0].data = [-20, -30, -40, -50]; chart.update(); expect(getLabels(chart.scales.r)).toEqual(['-50', '-45', '-40', '-35', '-30', '-25', '-20', '-15', '-10', '-5', '0']); chart.scales.r.options.beginAtZero = false; chart.update(); expect(getLabels(chart.scales.r)).toEqual(['-50', '-45', '-40', '-35', '-30', '-25', '-20']); }); it('Should generate tick marks in the correct order in reversed mode', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, 25, 78] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5'] }, options: { scales: { r: { reverse: true } } } }); expect(getLabels(chart.scales.r)).toEqual(['80', '70', '60', '50', '40', '30', '20', '10', '0']); expect(chart.scales.r.start).toBe(80); expect(chart.scales.r.end).toBe(0); }); it('Should correctly limit the maximum number of ticks', function() { var chart = window.acquireChart({ type: 'radar', data: { labels: ['label1', 'label2', 'label3'], datasets: [{ data: [0.5, 1.5, 2.5] }] }, options: { scales: { r: { pointLabels: { display: false } } } } }); expect(getLabels(chart.scales.r)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); chart.options.scales.r.ticks.maxTicksLimit = 11; chart.update(); expect(getLabels(chart.scales.r)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); chart.options.scales.r.ticks.stepSize = 0.01; chart.update(); expect(getLabels(chart.scales.r)).toEqual(['0.5', '1.0', '1.5', '2.0', '2.5']); chart.options.scales.r.min = 0.3; chart.options.scales.r.max = 2.8; chart.update(); expect(getLabels(chart.scales.r)).toEqual(['0.3', '0.8', '1.3', '1.8', '2.3', '2.8']); }); it('Should build labels using the user supplied callback', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, 25, 78] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5'] }, options: { scales: { r: { ticks: { callback: function(value, index) { return index.toString(); } } } } } }); expect(getLabels(chart.scales.r)).toEqual(['0', '1', '2', '3', '4', '5', '6', '7', '8']); expect(chart.scales.r._pointLabels).toEqual(['label1', 'label2', 'label3', 'label4', 'label5']); }); it('Should build point labels using the user supplied callback', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, 25, 78] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5'] }, options: { scales: { r: { pointLabels: { callback: function(value, index) { return index.toString(); } } } } } }); expect(chart.scales.r._pointLabels).toEqual(['0', '1', '2', '3', '4']); }); it('Should build point labels from falsy values', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, 25, 78, 20] }], labels: [0, '', undefined, null, NaN, false] } }); expect(chart.scales.r._pointLabels).toEqual([0, '', '', '', '', '']); }); it('Should build point labels considering hidden data', function() { const chart = window.acquireChart({ type: 'polarArea', data: { datasets: [{ data: [10, 5, 0, 25, 78, 20] }], labels: ['a', 'b', 'c', 'd', 'e', 'f'] } }); chart.toggleDataVisibility(3); chart.update(); expect(chart.scales.r._pointLabels).toEqual(['a', 'b', 'c', 'e', 'f']); }); it('should correctly set the center point', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, 25, 78] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5'] }, options: { scales: { r: { pointLabels: { callback: function(value, index) { return index.toString(); } } } } } }); expect(chart.scales.r.drawingArea).toBe(215); expect(chart.scales.r.xCenter).toBe(256); expect(chart.scales.r.yCenter).toBe(280); }); it('should correctly get the label for a given data index', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, 25, 78] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5'] }, options: { scales: { r: { pointLabels: { callback: function(value, index) { return index.toString(); } } } } } }); expect(chart.scales.r.getLabelForValue(5)).toBe('5'); }); it('should get the correct distance from the center point', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, 25, 78] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5'] }, options: { scales: { r: { pointLabels: { callback: function(value, index) { return index.toString(); } } } } } }); expect(chart.scales.r.getDistanceFromCenterForValue(chart.scales.r.min)).toBe(0); expect(chart.scales.r.getDistanceFromCenterForValue(chart.scales.r.max)).toBe(215); var position = chart.scales.r.getPointPositionForValue(1, 5); expect(position.x).toBeCloseToPixel(269); expect(position.y).toBeCloseToPixel(276); chart.scales.r.options.reverse = true; chart.update(); expect(chart.scales.r.getDistanceFromCenterForValue(chart.scales.r.min)).toBe(215); expect(chart.scales.r.getDistanceFromCenterForValue(chart.scales.r.max)).toBe(0); }); it('should get the correct value for a distance from the center point', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, 25, 78] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5'] }, options: { scales: { r: { pointLabels: { callback: function(value, index) { return index.toString(); } } } } } }); expect(chart.scales.r.getValueForDistanceFromCenter(0)).toBe(chart.scales.r.min); expect(chart.scales.r.getValueForDistanceFromCenter(215)).toBe(chart.scales.r.max); var dist = chart.scales.r.getDistanceFromCenterForValue(5); expect(chart.scales.r.getValueForDistanceFromCenter(dist)).toBe(5); chart.scales.r.options.reverse = true; chart.update(); expect(chart.scales.r.getValueForDistanceFromCenter(0)).toBe(chart.scales.r.max); expect(chart.scales.r.getValueForDistanceFromCenter(215)).toBe(chart.scales.r.min); }); it('should correctly get angles for all points', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, 25, 78] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5'] }, options: { scales: { r: { startAngle: 15, pointLabels: { callback: function(value, index) { return index.toString(); } } } }, } }); var radToNearestDegree = function(rad) { return Math.round((360 * rad) / (2 * Math.PI)); }; var slice = 72; // (360 / 5) for (var i = 0; i < 5; i++) { expect(radToNearestDegree(chart.scales.r.getIndexAngle(i))).toBe(15 + (slice * i)); } chart.scales.r.options.startAngle = 0; chart.update(); for (var x = 0; x < 5; x++) { expect(radToNearestDegree(chart.scales.r.getIndexAngle(x))).toBe((slice * x)); } }); it('should correctly get the correct label alignment for all points', function() { var chart = window.acquireChart({ type: 'radar', data: { datasets: [{ data: [10, 5, 0, 25, 78] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5'] }, options: { scales: { r: { pointLabels: { callback: function(value, index) { return index.toString(); } }, ticks: { display: false } } } } }); var scale = chart.scales.r; [{ startAngle: 30, textAlign: ['right', 'right', 'left', 'left', 'left'], }, { startAngle: -30, textAlign: ['right', 'right', 'left', 'left', 'right'], }, { startAngle: 750, textAlign: ['right', 'right', 'left', 'left', 'left'], }].forEach(function(expected) { scale.options.startAngle = expected.startAngle; chart.update(); scale.ctx = window.createMockContext(); chart.draw(); scale.ctx.getCalls().filter(function(x) { return x.name === 'setTextAlign'; }).forEach(function(x, i) { expect(x.args[0]).withContext('startAngle: ' + expected.startAngle + ', tick: ' + i).toBe(expected.textAlign[i]); }); }); }); it('should correctly get the point positions in center', function() { var chart = window.acquireChart({ type: 'polarArea', data: { datasets: [{ data: [10, 5, 0, 25, 78] }], labels: ['label1', 'label2', 'label3', 'label4', 'label5'] }, options: { scales: { r: { pointLabels: { display: true, padding: 5, centerPointLabels: true }, ticks: { display: false } } } } }); const PI = Math.PI; const lavelNum = 5; const padding = 5; const pointLabelItems = chart.scales.r._pointLabelItems; const additionalAngle = PI / lavelNum; const opts = chart.scales.r.options; const outerDistance = chart.scales.r.getDistanceFromCenterForValue(opts.ticks.reverse ? chart.scales.r.min : chart.scales.r.max); const tickBackdropHeight = 0; const yForAngle = function(y, h, angle) { if (angle === 90 || angle === 270) { y -= (h / 2); } else if (angle > 270 || angle < 90) { y -= h; } return y; }; const toDegrees = function(radians) { return radians * (180 / PI); }; for (var i = 0; i < 5; i++) { const extra = (i === 0 ? tickBackdropHeight / 2 : 0); const pointLabelItem = pointLabelItems[i]; const pointPosition = chart.scales.r.getPointPosition(i, outerDistance + extra + padding, additionalAngle); expect(pointLabelItem.x).toBe(pointPosition.x); expect(pointLabelItem.y).toBe(yForAngle(pointPosition.y, 12, toDegrees(pointPosition.angle + PI / 2))); } }); }); ================================================ FILE: test/specs/scale.time.tests.js ================================================ // Time scale tests describe('Time scale tests', function() { describe('auto', jasmine.fixture.specs('scale.time')); function createScale(data, options, dimensions) { var width = (dimensions && dimensions.width) || 400; var height = (dimensions && dimensions.height) || 50; options = options || {}; options.type = 'time'; options.id = 'xScale0'; var chart = window.acquireChart({ type: 'line', data: data, options: { scales: { x: options } } }, {canvas: {width: width, height: height}}); return chart.scales.x; } function getLabels(scale) { return scale.ticks.map(t => t.label); } beforeEach(function() { // Need a time matcher for getValueFromPixel jasmine.addMatchers({ toBeCloseToTime: function() { return { compare: function(time, expected) { var result = false; var actual = moment(time); var diff = actual.diff(expected.value, expected.unit, true); result = Math.abs(diff) < (expected.threshold !== undefined ? expected.threshold : 0.01); return { pass: result }; } }; } }); }); it('should load moment.js as a dependency', function() { expect(window.moment).not.toBe(undefined); }); it('should register the constructor with the registry', function() { var Constructor = Chart.registry.getScale('time'); expect(Constructor).not.toBe(undefined); expect(typeof Constructor).toBe('function'); }); it('should have the correct default config', function() { var defaultConfig = Chart.defaults.scales.time; expect(defaultConfig).toEqual({ bounds: 'data', adapters: {}, time: { parser: false, // false == a pattern string from or a custom callback that converts its argument to a timestamp unit: false, // false == automatic or override with week, month, year, etc. round: false, // none, or override with week, month, year, etc. isoWeekday: false, // override week start day minUnit: 'millisecond', displayFormats: {} }, ticks: { source: 'auto', callback: false, major: { enabled: false } } }); }); it('should correctly determine the unit', function() { var date = moment('Jan 01 1990', 'MMM DD YYYY'); var data = []; for (var i = 0; i < 60; i++) { data.push({x: date.valueOf(), y: Math.random()}); date = date.clone().add(1, 'month'); } var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', data: data }], }, options: { scales: { x: { type: 'time', ticks: { source: 'data', autoSkip: true } }, } } }); var scale = chart.scales.x; expect(scale._unit).toEqual('month'); }); describe('when specifying limits', function() { var mockData = { labels: ['2015-01-01T20:00:00', '2015-01-02T20:00:00', '2015-01-03T20:00:00'], }; var config; beforeEach(function() { config = Chart.helpers.clone(Chart.defaults.scales.time); config.ticks.source = 'labels'; config.time.unit = 'day'; }); it('should use the min option when less than first label for building ticks', function() { config.min = '2014-12-29T04:00:00'; var labels = getLabels(createScale(mockData, config)); expect(labels[0]).toEqual('Jan 1'); }); it('should use the min option when greater than first label for building ticks', function() { config.min = '2015-01-02T04:00:00'; var labels = getLabels(createScale(mockData, config)); expect(labels[0]).toEqual('Jan 2'); }); it('should use the max option when greater than last label for building ticks', function() { config.max = '2015-01-05T06:00:00'; var labels = getLabels(createScale(mockData, config)); expect(labels[labels.length - 1]).toEqual('Jan 3'); }); it('should use the max option when less than last label for building ticks', function() { config.max = '2015-01-02T23:00:00'; var labels = getLabels(createScale(mockData, config)); expect(labels[labels.length - 1]).toEqual('Jan 2'); }); }); it('should use the isoWeekday option', function() { var mockData = { labels: [ '2015-01-01T20:00:00', // Thursday '2015-01-02T20:00:00', // Friday '2015-01-03T20:00:00' // Saturday ] }; var config = Chart.helpers.mergeIf({ bounds: 'ticks', time: { unit: 'week', isoWeekday: 3 // Wednesday } }, Chart.defaults.scales.time); var scale = createScale(mockData, config); var ticks = getLabels(scale); expect(ticks).toEqual(['Dec 31, 2014', 'Jan 7, 2015']); }); describe('when rendering several days', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', data: [] }], labels: [ '2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00' ] }, options: { scales: { x: { type: 'time', position: 'bottom' }, } } }); this.scale = this.chart.scales.x; }); it('should be bounded by the nearest week beginnings', function() { var chart = this.chart; var scale = this.scale; expect(scale.getValueForPixel(scale.left)).toBeGreaterThan(moment(chart.data.labels[0]).startOf('week')); expect(scale.getValueForPixel(scale.right)).toBeLessThan(moment(chart.data.labels[chart.data.labels.length - 1]).add(1, 'week').endOf('week')); }); it('should convert between screen coordinates and times', function() { var chart = this.chart; var scale = this.scale; var timeRange = moment(scale.max).valueOf() - moment(scale.min).valueOf(); var msPerPix = timeRange / scale.width; var firstPointOffsetMs = moment(chart.config.data.labels[0]).valueOf() - scale.min; var firstPointPixel = scale.left + firstPointOffsetMs / msPerPix; var lastPointOffsetMs = moment(chart.config.data.labels[chart.config.data.labels.length - 1]).valueOf() - scale.min; var lastPointPixel = scale.left + lastPointOffsetMs / msPerPix; expect(scale.getPixelForValue(moment('2015-01-01T20:00:00').valueOf())).toBeCloseToPixel(firstPointPixel); expect(scale.getPixelForValue(moment(chart.data.labels[0]).valueOf())).toBeCloseToPixel(firstPointPixel); expect(scale.getValueForPixel(firstPointPixel)).toBeCloseToTime({ value: moment(chart.data.labels[0]), unit: 'hour', }); expect(scale.getPixelForValue(moment('2015-01-10T12:00').valueOf())).toBeCloseToPixel(lastPointPixel); expect(scale.getValueForPixel(lastPointPixel)).toBeCloseToTime({ value: moment(chart.data.labels[6]), unit: 'hour' }); }); }); describe('when rendering several years', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { labels: ['2005-07-04', '2017-01-20'], }, options: { scales: { x: { type: 'time', bounds: 'ticks', position: 'bottom' }, } } }, {canvas: {width: 800, height: 200}}); this.scale = this.chart.scales.x; }); it('should be bounded by nearest step\'s year start and end', function() { var scale = this.scale; var ticks = scale.getTicks(); var step = ticks[1].value - ticks[0].value; var stepsAmount = Math.floor((scale.max - scale.min) / step); expect(scale.getValueForPixel(scale.left)).toBeCloseToTime({ value: moment(scale.min).startOf('year'), unit: 'hour', }); expect(scale.getValueForPixel(scale.right)).toBeCloseToTime({ value: moment(scale.min + step * stepsAmount).endOf('year'), unit: 'hour', }); }); it('should build the correct ticks', function() { expect(getLabels(this.scale)).toEqual(['2005', '2006', '2007', '2008', '2009', '2010', '2011', '2012', '2013', '2014', '2015', '2016', '2017', '2018']); }); it('should have ticks with accurate labels', function() { var scale = this.scale; var ticks = scale.getTicks(); // pixelsPerTick is an approximation which assumes same number of milliseconds per year (not true) // we use a threshold of 1 day so that we still match these values var pixelsPerTick = scale.width / (ticks.length - 1); for (var i = 0; i < ticks.length - 1; i++) { var offset = pixelsPerTick * i; expect(scale.getValueForPixel(scale.left + offset)).toBeCloseToTime({ value: moment(ticks[i].label + '-01-01'), unit: 'day', threshold: 1, }); } }); }); it('should get the correct label for a data value', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', data: [null, 10, 3] }], labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days }, options: { scales: { x: { type: 'time', position: 'bottom', ticks: { source: 'labels', autoSkip: false } } } } }); var xScale = chart.scales.x; var controller = chart.getDatasetMeta(0).controller; expect(xScale.getLabelForValue(controller.getParsed(0)[xScale.id])).toBeTruthy(); expect(xScale.getLabelForValue(controller.getParsed(0)[xScale.id])).toBe('Jan 1, 2015, 8:00:00 pm'); expect(xScale.getLabelForValue(xScale.getValueForPixel(xScale.getPixelForTick(6)))).toBe('Jan 10, 2015, 12:00:00 pm'); }); describe('when ticks.callback is specified', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', data: [0, 0] }], labels: ['2015-01-01T20:00:00', '2015-01-01T20:01:00'] }, options: { scales: { x: { type: 'time', time: { displayFormats: { second: 'h:mm:ss' } }, ticks: { callback: function(_, i) { return '<' + i + '>'; } } } } } }); this.scale = this.chart.scales.x; }); it('should get the correct labels for ticks', function() { var labels = getLabels(this.scale); expect(labels.length).toEqual(21); expect(labels[0]).toEqual('<0>'); expect(labels[labels.length - 1]).toEqual('<60>'); }); it('should update ticks.callback correctly', function() { var chart = this.chart; chart.options.scales.x.ticks.callback = function(_, i) { return '{' + i + '}'; }; chart.update(); var labels = getLabels(this.scale); expect(labels.length).toEqual(21); expect(labels[0]).toEqual('{0}'); expect(labels[labels.length - 1]).toEqual('{60}'); }); }); it('should get the correct label when time is specified as a string', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', data: [{x: '2015-01-01T20:00:00', y: 10}, {x: '2015-01-02T21:00:00', y: 3}] }], }, options: { scales: { x: { type: 'time', position: 'bottom' }, } } }); var xScale = chart.scales.x; var controller = chart.getDatasetMeta(0).controller; var value = controller.getParsed(0)[xScale.id]; expect(xScale.getLabelForValue(value)).toBeTruthy(); expect(xScale.getLabelForValue(value)).toBe('Jan 1, 2015, 8:00:00 pm'); }); it('should get the correct label for a data value by format', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', data: [null, 10, 3] }], labels: ['2015-01-01T20:00:00', '2015-01-02T21:00:00', '2015-01-03T22:00:00', '2015-01-05T23:00:00', '2015-01-07T03:00', '2015-01-08T10:00', '2015-01-10T12:00'], // days }, options: { scales: { x: { type: 'time', time: { unit: 'day', displayFormats: { day: 'YYYY-MM-DD' } }, position: 'bottom', ticks: { source: 'labels', autoSkip: false } } } } }); var xScale = chart.scales.x; for (const lbl of chart.data.labels) { var dd = xScale._adapter.parse(lbl); var parsed = lbl.split('T'); expect(xScale.format(dd)).toBe(parsed[0]); } for (const lbl of chart.data.labels) { var mm = xScale._adapter.parse(lbl); var yearMonth = lbl.substring(0, 7); expect(xScale.format(mm, 'YYYY-MM')).toBe(yearMonth); } }); it('should round to isoWeekday', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [{x: '2020-04-12T20:00:00', y: 1}, {x: '2020-04-13T20:00:00', y: 2}] }] }, options: { scales: { x: { type: 'time', ticks: { source: 'data' }, time: { unit: 'week', round: 'week', isoWeekday: 1, displayFormats: { week: 'WW' } } }, } } }); expect(getLabels(chart.scales.x)).toEqual(['15', '16']); }); it('should get the correct label for a timestamp', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', data: [ // Normally (at least with the moment.js adapter), times would be in // the user's local time zone. To allow for more stable tests, our // tests/index.js sets moment.js to use UTC; use `Z` here to match. {t: +new Date('2018-01-08 05:14:23.234Z'), y: 10}, {t: +new Date('2018-01-09 06:17:43.426Z'), y: 3} ] }], }, options: { parsing: {xAxisKey: 't'}, scales: { x: { type: 'time', position: 'bottom' }, } } }); var xScale = chart.scales.x; var controller = chart.getDatasetMeta(0).controller; var label = xScale.getLabelForValue(controller.getParsed(0)[xScale.id]); expect(label).toEqual('Jan 8, 2018, 5:14:23 am'); }); it('should get the correct pixel for only one data in the dataset', function() { var chart = window.acquireChart({ type: 'line', data: { labels: ['2016-05-27'], datasets: [{ xAxisID: 'x', data: [5] }] }, options: { scales: { x: { display: true, type: 'time' } } } }); var xScale = chart.scales.x; var pixel = xScale.getPixelForValue(moment('2016-05-27').valueOf()); expect(xScale.getValueForPixel(pixel)).toEqual(moment(chart.data.labels[0]).valueOf()); }); it('does not create a negative width chart when hidden', function() { var chart = window.acquireChart({ type: 'line', data: { datasets: [{ data: [] }] }, options: { scales: { x: { type: 'time', ticks: { min: moment().subtract(1, 'months'), max: moment(), } }, }, responsive: true, }, }, { wrapper: { style: 'display: none', }, }); expect(chart.scales.y.width).toEqual(0); expect(chart.scales.y.maxWidth).toEqual(0); expect(chart.width).toEqual(0); }); describe('when ticks.source', function() { describe('is "labels"', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4, 5]}] }, options: { scales: { x: { type: 'time', time: { parser: 'YYYY' }, ticks: { source: 'labels' } } } } }); }); it ('should generate ticks from "data.labels"', function() { var scale = this.chart.scales.x; expect(scale.min).toEqual(+moment('2017', 'YYYY')); expect(scale.max).toEqual(+moment('2042', 'YYYY')); expect(getLabels(scale)).toEqual([ '2017', '2019', '2020', '2025', '2042']); }); it ('should not add ticks for min and max if they extend the labels range', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.min = '2012'; options.max = '2051'; chart.update(); expect(scale.min).toEqual(+moment('2012', 'YYYY')); expect(scale.max).toEqual(+moment('2051', 'YYYY')); expect(getLabels(scale)).toEqual([ '2017', '2019', '2020', '2025', '2042']); }); it ('should not duplicate ticks if min and max are the labels limits', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.min = '2017'; options.max = '2042'; chart.update(); expect(scale.min).toEqual(+moment('2017', 'YYYY')); expect(scale.max).toEqual(+moment('2042', 'YYYY')); expect(getLabels(scale)).toEqual([ '2017', '2019', '2020', '2025', '2042']); }); it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { var chart = this.chart; var scale = chart.scales.x; chart.data.labels = []; chart.update(); expect(scale.min).toEqual(+moment().startOf('day')); expect(scale.max).toEqual(+moment().endOf('day') + 1); expect(getLabels(scale)).toEqual([]); }); it ('should correctly handle empty `data.labels` using `time.unit`', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.time.unit = 'year'; chart.data.labels = []; chart.update(); expect(scale.min).toEqual(+moment().startOf('year')); expect(scale.max).toEqual(+moment().endOf('year') + 1); expect(getLabels(scale)).toEqual([]); }); }); describe('is "data"', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [ {data: [0, 1, 2, 3, 4, 5]}, {data: [ {x: '2018', y: 6}, {x: '2020', y: 7}, {x: '2043', y: 8} ]} ] }, options: { scales: { x: { type: 'time', time: { parser: 'YYYY' }, ticks: { source: 'data' } } } } }); }); it ('should generate ticks from "datasets.data"', function() { var scale = this.chart.scales.x; expect(scale.min).toEqual(+moment('2017', 'YYYY')); expect(scale.max).toEqual(+moment('2043', 'YYYY')); expect(getLabels(scale)).toEqual([ '2017', '2018', '2019', '2020', '2025', '2042', '2043']); }); it ('should not add ticks for min and max if they extend the labels range', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.min = '2012'; options.max = '2051'; chart.update(); expect(scale.min).toEqual(+moment('2012', 'YYYY')); expect(scale.max).toEqual(+moment('2051', 'YYYY')); expect(getLabels(scale)).toEqual([ '2017', '2018', '2019', '2020', '2025', '2042', '2043']); }); it ('should not duplicate ticks if min and max are the labels limits', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.min = '2017'; options.max = '2043'; chart.update(); expect(scale.min).toEqual(+moment('2017', 'YYYY')); expect(scale.max).toEqual(+moment('2043', 'YYYY')); expect(getLabels(scale)).toEqual([ '2017', '2018', '2019', '2020', '2025', '2042', '2043']); }); it ('should correctly handle empty `data.labels` using "day" if `time.unit` is undefined`', function() { var chart = this.chart; var scale = chart.scales.x; chart.data.labels = []; chart.update(); expect(scale.min).toEqual(+moment('2018', 'YYYY')); expect(scale.max).toEqual(+moment('2043', 'YYYY')); expect(getLabels(scale)).toEqual([ '2018', '2020', '2043']); }); it ('should correctly handle empty `data.labels` and hidden datasets using `time.unit`', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.time.unit = 'year'; chart.data.labels = []; var meta = chart.getDatasetMeta(1); meta.hidden = true; chart.update(); expect(scale.min).toEqual(+moment().startOf('year')); expect(scale.max).toEqual(+moment().endOf('year') + 1); expect(getLabels(scale)).toEqual([]); }); }); }); [true, false].forEach(function(normalized) { describe('when normalized is ' + normalized + ' and scale type', function() { describe('is "timeseries"', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4]}] }, options: { normalized, scales: { x: { type: 'timeseries', time: { parser: 'YYYY' }, ticks: { source: 'labels' } }, y: { display: false } } } }); }); it ('should space data out with the same gap, whatever their time values', function() { var scale = this.chart.scales.x; var start = scale.left; var slice = scale.width / 4; expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start); expect(scale.getPixelForValue(moment('2019').valueOf(), 1)).toBeCloseToPixel(start + slice); expect(scale.getPixelForValue(moment('2020').valueOf(), 2)).toBeCloseToPixel(start + slice * 2); expect(scale.getPixelForValue(moment('2025').valueOf(), 3)).toBeCloseToPixel(start + slice * 3); expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(start + slice * 4); }); it ('should add a step before if scale.min is before the first data', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.min = '2012'; chart.update(); var start = scale.left; var slice = scale.width / 5; expect(scale.getPixelForValue(moment('2017').valueOf(), 1)).toBeCloseToPixel(86); expect(scale.getPixelForValue(moment('2042').valueOf(), 5)).toBeCloseToPixel(start + slice * 5); }); it ('should add a step after if scale.max is after the last data', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.max = '2050'; chart.update(); var start = scale.left; expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start); expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(388); }); it ('should add steps before and after if scale.min/max are outside the data range', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.min = '2012'; options.max = '2050'; chart.update(); expect(scale.getPixelForValue(moment('2017').valueOf(), 1)).toBeCloseToPixel(71); expect(scale.getPixelForValue(moment('2042').valueOf(), 5)).toBeCloseToPixel(401); }); }); describe('is "time"', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4, 5]}] }, options: { scales: { x: { type: 'time', time: { parser: 'YYYY' }, ticks: { source: 'labels' } }, y: { display: false } } } }); }); it ('should space data out with a gap relative to their time values', function() { var scale = this.chart.scales.x; var start = scale.left; var slice = scale.width / (2042 - 2017); expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start); expect(scale.getPixelForValue(moment('2019').valueOf(), 1)).toBeCloseToPixel(start + slice * (2019 - 2017)); expect(scale.getPixelForValue(moment('2020').valueOf(), 2)).toBeCloseToPixel(start + slice * (2020 - 2017)); expect(scale.getPixelForValue(moment('2025').valueOf(), 3)).toBeCloseToPixel(start + slice * (2025 - 2017)); expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(start + slice * (2042 - 2017)); }); it ('should take in account scale min and max if outside the ticks range', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.min = '2012'; options.max = '2050'; chart.update(); var start = scale.left; var slice = scale.width / (2050 - 2012); expect(scale.getPixelForValue(moment('2017').valueOf(), 0)).toBeCloseToPixel(start + slice * (2017 - 2012)); expect(scale.getPixelForValue(moment('2019').valueOf(), 1)).toBeCloseToPixel(start + slice * (2019 - 2012)); expect(scale.getPixelForValue(moment('2020').valueOf(), 2)).toBeCloseToPixel(start + slice * (2020 - 2012)); expect(scale.getPixelForValue(moment('2025').valueOf(), 3)).toBeCloseToPixel(start + slice * (2025 - 2012)); expect(scale.getPixelForValue(moment('2042').valueOf(), 4)).toBeCloseToPixel(start + slice * (2042 - 2012)); }); }); }); }); describe('when bounds', function() { describe('is "data"', function() { it ('should preserve the data range', function() { var chart = window.acquireChart({ type: 'line', data: { labels: ['02/20 08:00', '02/21 09:00', '02/22 10:00', '02/23 11:00'], datasets: [{data: [0, 1, 2, 3, 4, 5]}] }, options: { scales: { x: { type: 'time', bounds: 'data', time: { parser: 'MM/DD HH:mm', unit: 'day' } }, y: { display: false } } } }); var scale = chart.scales.x; expect(scale.min).toEqual(+moment('02/20 08:00', 'MM/DD HH:mm')); expect(scale.max).toEqual(+moment('02/23 11:00', 'MM/DD HH:mm')); expect(scale.getPixelForValue(moment('02/20 08:00', 'MM/DD HH:mm').valueOf())).toBeCloseToPixel(scale.left); expect(scale.getPixelForValue(moment('02/23 11:00', 'MM/DD HH:mm').valueOf())).toBeCloseToPixel(scale.left + scale.width); expect(getLabels(scale)).toEqual([ 'Feb 21', 'Feb 22', 'Feb 23']); }); }); describe('is "labels"', function() { it('should preserve the label range', function() { var chart = window.acquireChart({ type: 'line', data: { labels: ['02/20 08:00', '02/21 09:00', '02/22 10:00', '02/23 11:00'], datasets: [{data: [0, 1, 2, 3, 4, 5]}] }, options: { scales: { x: { type: 'time', bounds: 'ticks', time: { parser: 'MM/DD HH:mm', unit: 'day' } }, y: { display: false } } } }); var scale = chart.scales.x; var ticks = scale.getTicks(); expect(scale.min).toEqual(ticks[0].value); expect(scale.max).toEqual(ticks[ticks.length - 1].value); expect(scale.getPixelForValue(moment('02/20 08:00', 'MM/DD HH:mm').valueOf())).toBeCloseToPixel(60); expect(scale.getPixelForValue(moment('02/23 11:00', 'MM/DD HH:mm').valueOf())).toBeCloseToPixel(426); expect(getLabels(scale)).toEqual([ 'Feb 20', 'Feb 21', 'Feb 22', 'Feb 23', 'Feb 24']); }); }); }); describe('when min and/or max are defined', function() { ['auto', 'data', 'labels'].forEach(function(source) { ['data', 'ticks'].forEach(function(bounds) { describe('and ticks.source is "' + source + '" and bounds "' + bounds + '"', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { labels: ['02/20 08:00', '02/21 09:00', '02/22 10:00', '02/23 11:00'], datasets: [{data: [0, 1, 2, 3, 4, 5]}] }, options: { scales: { x: { type: 'time', bounds: bounds, time: { parser: 'MM/DD HH:mm', unit: 'day' }, ticks: { source: source } }, y: { display: false } } } }); }); it ('should expand scale to the min/max range', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; var min = '02/19 07:00'; var max = '02/24 08:00'; var minMillis = +moment(min, 'MM/DD HH:mm'); var maxMillis = +moment(max, 'MM/DD HH:mm'); options.min = min; options.max = max; chart.update(); expect(scale.min).toEqual(minMillis); expect(scale.max).toEqual(maxMillis); expect(scale.getPixelForValue(minMillis)).toBeCloseToPixel(scale.left); expect(scale.getPixelForValue(maxMillis)).toBeCloseToPixel(scale.left + scale.width); scale.getTicks().forEach(function(tick) { expect(tick.value >= minMillis).toBeTruthy(); expect(tick.value <= maxMillis).toBeTruthy(); }); }); it ('should shrink scale to the min/max range', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; var min = '02/21 07:00'; var max = '02/22 20:00'; var minMillis = +moment(min, 'MM/DD HH:mm'); var maxMillis = +moment(max, 'MM/DD HH:mm'); options.min = min; options.max = max; chart.update(); expect(scale.min).toEqual(minMillis); expect(scale.max).toEqual(maxMillis); expect(scale.getPixelForValue(minMillis)).toBeCloseToPixel(scale.left); expect(scale.getPixelForValue(maxMillis)).toBeCloseToPixel(scale.left + scale.width); scale.getTicks().forEach(function(tick) { expect(tick.value >= minMillis).toBeTruthy(); expect(tick.value <= maxMillis).toBeTruthy(); }); }); }); }); }); }); ['auto', 'data', 'labels'].forEach(function(source) { ['timeseries', 'time'].forEach(function(type) { describe('when ticks.source is "' + source + '" and scale type is "' + type + '"', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { labels: ['2017', '2018', '2019', '2020', '2021'], datasets: [{data: [0, 1, 2, 3, 4]}] }, options: { scales: { x: { type: type, time: { parser: 'YYYY', unit: 'year' }, ticks: { source: source } } } } }); }); it ('should not add offset from the edges', function() { var scale = this.chart.scales.x; expect(scale.getPixelForValue(moment('2017').valueOf())).toBeCloseToPixel(scale.left); expect(scale.getPixelForValue(moment('2021').valueOf())).toBeCloseToPixel(scale.left + scale.width); }); it ('should add offset from the edges if offset is true', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.offset = true; chart.update(); var numTicks = scale.ticks.length; var firstTickInterval = scale.getPixelForTick(1) - scale.getPixelForTick(0); var lastTickInterval = scale.getPixelForTick(numTicks - 1) - scale.getPixelForTick(numTicks - 2); expect(scale.getPixelForValue(moment('2017').valueOf())).toBeCloseToPixel(scale.left + firstTickInterval / 2); expect(scale.getPixelForValue(moment('2021').valueOf())).toBeCloseToPixel(scale.left + scale.width - lastTickInterval / 2); }); it ('should not add offset if min and max extend the labels range', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.min = '2012'; options.max = '2051'; chart.update(); expect(scale.getPixelForValue(moment('2012').valueOf())).toBeCloseToPixel(scale.left); expect(scale.getPixelForValue(moment('2051').valueOf())).toBeCloseToPixel(scale.left + scale.width); }); }); }); }); it ('should handle offset when there are more data points than ticks', function() { const chart = window.acquireChart({ type: 'bar', data: { datasets: [{ data: [{x: 631180800000, y: '31.84'}, {x: 631267200000, y: '30.89'}, {x: 631353600000, y: '33.00'}, {x: 631440000000, y: '33.52'}, {x: 631526400000, y: '32.24'}, {x: 631785600000, y: '32.74'}, {x: 631872000000, y: '31.45'}, {x: 631958400000, y: '32.60'}, {x: 632044800000, y: '31.77'}, {x: 632131200000, y: '32.45'}, {x: 632390400000, y: '31.13'}, {x: 632476800000, y: '31.82'}, {x: 632563200000, y: '30.81'}, {x: 632649600000, y: '30.07'}, {x: 632736000000, y: '29.31'}, {x: 632995200000, y: '29.82'}, {x: 633081600000, y: '30.20'}, {x: 633168000000, y: '30.78'}, {x: 633254400000, y: '30.72'}, {x: 633340800000, y: '31.62'}, {x: 633600000000, y: '30.64'}, {x: 633686400000, y: '32.36'}, {x: 633772800000, y: '34.66'}, {x: 633859200000, y: '33.96'}, {x: 633945600000, y: '34.20'}, {x: 634204800000, y: '32.20'}, {x: 634291200000, y: '32.44'}, {x: 634377600000, y: '32.72'}, {x: 634464000000, y: '32.95'}, {x: 634550400000, y: '32.95'}, {x: 634809600000, y: '30.88'}, {x: 634896000000, y: '29.44'}, {x: 634982400000, y: '29.36'}, {x: 635068800000, y: '28.84'}, {x: 635155200000, y: '30.85'}, {x: 635414400000, y: '32.00'}, {x: 635500800000, y: '32.74'}, {x: 635587200000, y: '33.16'}, {x: 635673600000, y: '34.73'}, {x: 635760000000, y: '32.89'}, {x: 636019200000, y: '32.41'}, {x: 636105600000, y: '31.15'}, {x: 636192000000, y: '30.63'}, {x: 636278400000, y: '29.60'}, {x: 636364800000, y: '29.31'}, {x: 636624000000, y: '29.83'}, {x: 636710400000, y: '27.97'}, {x: 636796800000, y: '26.18'}, {x: 636883200000, y: '26.06'}, {x: 636969600000, y: '26.34'}, {x: 637228800000, y: '27.75'}, {x: 637315200000, y: '29.05'}, {x: 637401600000, y: '28.82'}, {x: 637488000000, y: '29.43'}, {x: 637574400000, y: '29.53'}, {x: 637833600000, y: '28.50'}, {x: 637920000000, y: '28.87'}, {x: 638006400000, y: '28.11'}, {x: 638092800000, y: '27.79'}, {x: 638179200000, y: '28.18'}, {x: 638438400000, y: '28.27'}, {x: 638524800000, y: '28.29'}, {x: 638611200000, y: '29.63'}, {x: 638697600000, y: '29.13'}, {x: 638784000000, y: '26.57'}, {x: 639039600000, y: '27.19'}, {x: 639126000000, y: '27.48'}, {x: 639212400000, y: '27.79'}, {x: 639298800000, y: '28.48'}, {x: 639385200000, y: '27.88'}, {x: 639644400000, y: '25.63'}, {x: 639730800000, y: '25.02'}, {x: 639817200000, y: '25.26'}, {x: 639903600000, y: '25.00'}, {x: 639990000000, y: '26.23'}, {x: 640249200000, y: '26.22'}, {x: 640335600000, y: '26.36'}, {x: 640422000000, y: '25.45'}, {x: 640508400000, y: '24.62'}, {x: 640594800000, y: '26.65'}, {x: 640854000000, y: '26.28'}, {x: 640940400000, y: '27.25'}, {x: 641026800000, y: '25.93'}], backgroundColor: '#ff6666' }] }, options: { scales: { x: { type: 'timeseries', offset: true, ticks: { source: 'data', autoSkip: true, autoSkipPadding: 0, maxRotation: 0 } }, y: { type: 'linear', border: { display: false } } } }, plugins: { legend: false } }); const scale = chart.scales.x; expect(scale.getPixelForDecimal(0)).toBeCloseToPixel(29); expect(scale.getPixelForDecimal(1.0)).toBeCloseToPixel(512); }); ['data', 'labels'].forEach(function(source) { ['timeseries', 'time'].forEach(function(type) { describe('when ticks.source is "' + source + '" and scale type is "' + type + '"', function() { beforeEach(function() { this.chart = window.acquireChart({ type: 'line', data: { labels: ['2017', '2019', '2020', '2025', '2042'], datasets: [{data: [0, 1, 2, 3, 4, 5]}] }, options: { scales: { x: { id: 'x', type: type, time: { parser: 'YYYY' }, ticks: { source: source } } } } }); }); it ('should add offset if min and max extend the labels range and offset is true', function() { var chart = this.chart; var scale = chart.scales.x; var options = chart.options.scales.x; options.min = '2012'; options.max = '2051'; options.offset = true; chart.update(); var numTicks = scale.ticks.length; var firstTickInterval = scale.getPixelForTick(1) - scale.getPixelForTick(0); var lastTickInterval = scale.getPixelForTick(numTicks - 1) - scale.getPixelForTick(numTicks - 2); expect(scale.getPixelForValue(moment('2012').valueOf())).toBeCloseToPixel(scale.left + firstTickInterval / 2); expect(scale.getPixelForValue(moment('2051').valueOf())).toBeCloseToPixel(scale.left + scale.width - lastTickInterval / 2); }); }); }); }); describe('Deprecations', function() { describe('options.time.displayFormats', function() { it('should generate defaults from adapter presets', function() { var chart = window.acquireChart({ type: 'line', data: {}, options: { scales: { x: { type: 'time' } } } }); // NOTE: the test suite is configured to use moment var expected = { datetime: 'MMM D, YYYY, h:mm:ss a', millisecond: 'h:mm:ss.SSS a', second: 'h:mm:ss a', minute: 'h:mm a', hour: 'hA', day: 'MMM D', week: 'll', month: 'MMM YYYY', quarter: '[Q]Q - YYYY', year: 'YYYY' }; expect(chart.scales.x.options.time.displayFormats).toEqual(expected); expect(chart.options.scales.x.time.displayFormats).toEqual(expected); }); it('should merge user formats with adapter presets', function() { var chart = window.acquireChart({ type: 'line', data: {}, options: { scales: { x: { type: 'time', time: { displayFormats: { millisecond: 'foo', hour: 'bar', month: 'bla' } } } } } }); // NOTE: the test suite is configured to use moment var expected = { datetime: 'MMM D, YYYY, h:mm:ss a', millisecond: 'foo', second: 'h:mm:ss a', minute: 'h:mm a', hour: 'bar', day: 'MMM D', week: 'll', month: 'bla', quarter: '[Q]Q - YYYY', year: 'YYYY' }; expect(chart.scales.x.options.time.displayFormats).toEqual(expected); expect(chart.options.scales.x.time.displayFormats).toEqual(expected); }); }); }); it('should pass chart options to date adapter', function() { let chartOptions; Chart._adapters._date.override({ init(options) { chartOptions = options; } }); var chart = window.acquireChart({ type: 'line', data: {}, options: { locale: 'es', scales: { x: { type: 'time' }, } } }); expect(chartOptions).toEqual(chart.options); }); it('should pass timestamp to ticks callback', () => { let callbackValue; window.acquireChart({ type: 'line', data: { datasets: [{ xAxisID: 'x', data: [0, 0] }], labels: ['2015-01-01T20:00:00', '2015-01-01T20:01:00'] }, options: { scales: { x: { type: 'time', ticks: { callback(value) { callbackValue = value; return value; } } } } } }); expect(typeof callbackValue).toBe('number'); }); }); ================================================ FILE: test/types/.eslintrc.yml ================================================ rules: '@typescript-eslint/no-unused-vars': 'off' object-curly-spacing: ["warn", "always"] '@typescript-eslint/no-empty-interface': "warn" '@typescript-eslint/ban-types': "warn" '@typescript-eslint/adjacent-overload-signatures': "warn" ================================================ FILE: test/types/animation.ts ================================================ import { Chart } from '../../src/types.js'; const chart = new Chart('id', { type: 'bar', data: { labels: [], datasets: [{ data: [] }] }, options: { animation: false, animations: { colors: false, numbers: { properties: ['a', 'b'], type: 'number', from: 0, to: 10, delay: (ctx) => ctx.dataIndex * 100, duration: (ctx) => ctx.datasetIndex * 1000, loop: true, easing: 'linear' } }, transitions: { show: { animation: { duration: 10 }, animations: { numbers: false } }, custom: { animation: { duration: 10 } } } }, }); const pie = new Chart('id', { type: 'pie', data: { labels: [], datasets: [{ data: [] }] }, options: { animation: false, } }); const polarArea = new Chart('id', { type: 'polarArea', data: { labels: [], datasets: [{ data: [] }] }, options: { animation: false, } }); ================================================ FILE: test/types/autogen.js ================================================ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import * as helpers from '../../dist/helpers.js'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); let fd; try { const fn = path.resolve(__dirname, 'autogen_helpers.ts'); fd = fs.openSync(fn, 'w+'); fs.writeSync(fd, 'import * as helpers from \'../../dist/helpers/index.js\';\n\n'); fs.writeSync(fd, 'const testKeys: unknown[] = [];\n'); for (const key of Object.keys(helpers)) { if (key[0] !== '_' && typeof helpers[key] === 'function') { fs.writeSync(fd, `testKeys.push(helpers.${key});\n`); } } } finally { if (fd !== undefined) { fs.closeSync(fd); } } ================================================ FILE: test/types/chart_types.ts ================================================ import { Chart } from '../../src/types.js'; const chart = new Chart('chart', { type: 'bar', data: { labels: ['1', '2', '3'], datasets: [{ data: [1, 2, 3] }, { data: [1, 2, 3], categoryPercentage: 10 }], } }); const chart2 = new Chart('chart', { type: 'bar', data: { labels: ['1', '2', '3'], datasets: [{ type: 'line', data: [1, 2, 3], // @ts-expect-error should not allow bar properties to be defined in a line dataset categoryPercentage: 10 }, { type: 'line', pointBackgroundColor: 'red', data: [1, 2, 3] }, { data: [1, 2, 3], categoryPercentage: 10 }], } }); const chart3 = new Chart('chart', { data: { labels: ['1', '2', '3'], datasets: [{ type: 'bar', data: [1, 2, 3], categoryPercentage: 10 }, { type: 'bar', data: [1, 2, 3], // @ts-expect-error should not allow line properties to be defined in a bar dataset pointBackgroundColor: 'red', }], } }); // @ts-expect-error all datasets should have a type property or a default fallback type should be set const chart4 = new Chart('chart', { data: { labels: ['1', '2', '3'], datasets: [{ type: 'bar', data: [1, 2, 3], categoryPercentage: 10 }, { data: [1, 2, 3] }], } }); ================================================ FILE: test/types/controllers/bar_floating_data.ts ================================================ import { Chart } from '../../../src/types.js'; const chart = new Chart('id', { type: 'bar', data: { labels: ['1', '2', '3'], datasets: [{ data: [[1, 2], [3, 4], [5, 6]] }] }, }); ================================================ FILE: test/types/controllers/bubble_chart_options.ts ================================================ import { Chart, ChartOptions } from '../../../src/types.js'; const chart = new Chart('test', { type: 'bubble', data: { datasets: [] }, options: { scales: { x: { min: 0, max: 30, ticks: {} }, y: { min: 0, max: 30, ticks: {}, }, } } }); ================================================ FILE: test/types/controllers/doughnut_meta_total.ts ================================================ import { Chart, ChartMeta, Element } from '../../../src/types.js'; const chart = new Chart('id', { type: 'doughnut', data: { labels: [], datasets: [{ data: [], }] }, }); // A cast is required because the exact type of ChartMeta will vary with // mixed charts const meta = >chart.getDatasetMeta(0); const total = meta.total; ================================================ FILE: test/types/controllers/doughnut_offset.ts ================================================ import { Chart, ChartMeta, Element } from '../../../src/types.js'; const chart = new Chart('id', { type: 'doughnut', data: { labels: [], datasets: [{ data: [], offset: 40, }] }, options: { offset: 20, } }); ================================================ FILE: test/types/controllers/doughnut_outer_radius.ts ================================================ import { Chart } from '../../../src/types.js'; const chart = new Chart('id', { type: 'doughnut', data: { labels: [], datasets: [{ data: [], }] }, options: { radius: () => Math.random() > 0.5 ? 50 : '50%', } }); ================================================ FILE: test/types/controllers/doughnut_spacing_offset.ts ================================================ import { Chart, ChartMeta, Element } from '../../../src/types.js'; const chart = new Chart('id', { type: 'doughnut', data: { datasets: [{ data: [10, 20, 40, 50, 5], label: 'Dataset 1', backgroundColor: [ 'red', 'orange', 'yellow', 'green', 'blue' ] }], labels: [ 'Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5' ], }, options: { spacing: 50, offset: [0, 50, 0, 0, 0], } }); ================================================ FILE: test/types/controllers/line_scriptable_parsed_data.ts ================================================ import { Chart } from '../../../src/types.js'; const chart = new Chart('id', { type: 'line', data: { labels: [], datasets: [{ data: [], backgroundColor: (context) => { return context.parsed.y > 10 ? 'green' : 'red'; } }] }, }); ================================================ FILE: test/types/controllers/line_segments.ts ================================================ import { Chart } from '../../../src/types.js'; const chart = new Chart('id', { type: 'line', data: { labels: [], datasets: [{ data: [], segment: { backgroundColor: ctx => ctx.p0.skip ? 'transparent' : undefined, borderColor: ctx => ctx.p0.skip ? 'gray' : undefined, borderWidth: ctx => ctx.p1.parsed.y > 10 ? 5 : undefined, } }] }, }); ================================================ FILE: test/types/controllers/line_span_gaps.ts ================================================ import { Chart } from '../../../src/types.js'; const chart = new Chart('id', { type: 'line', data: { datasets: [ { label: 'Cats', data: [], } ] }, options: { elements: { line: { spanGaps: true } }, scales: { x: { type: 'linear', min: 1, max: 10 }, y: { type: 'linear', min: 0, max: 50 } } } }); ================================================ FILE: test/types/controllers/line_styling_array.ts ================================================ import { Chart } from '../../../src/types.js'; const chart = new Chart('id', { type: 'line', data: { labels: [], datasets: [{ data: [], backgroundColor: ['red', 'blue'], hoverBackgroundColor: ['red', 'blue'], }] }, }); ================================================ FILE: test/types/controllers/radar_dataset_indexable_options.ts ================================================ import { Chart, ChartOptions } from '../../../src/types.js'; const chart = new Chart('test', { type: 'radar', data: { labels: ['a', 'b', 'c'], datasets: [{ data: [1, 2, 3], backgroundColor: ['red', 'green', 'blue'], borderColor: ['red', 'green', 'blue'], hoverRadius: [1, 2, 3], pointBackgroundColor: ['red', 'green', 'blue'], pointBorderColor: ['red', 'green', 'blue'], pointBorderWidth: [1, 2, 3], pointHitRadius: [1, 2, 3], pointHoverBackgroundColor: ['red', 'green', 'blue'], pointHoverBorderColor: ['red', 'green', 'blue'], pointHoverBorderWidth: [1, 2, 3], pointHoverRadius: [1, 2, 3], pointRadius: [1, 2, 3], pointRotation: [1, 2, 3], pointStyle: ['circle', 'cross', 'crossRot'], radius: [1, 2, 3], }] }, }); ================================================ FILE: test/types/data_types.ts ================================================ import { Chart } from '../../src/types.js'; const chart = new Chart('chart', { type: 'bar', data: { labels: ['1', '2', '3'], datasets: [{ data: [[1, 2], [1, 2], [1, 2]] }], } }); const chart2 = new Chart('chart2', { type: 'bar', data: { datasets: [{ data: [{ id: 'Sales', nested: { value: 1500 } }, { id: 'Purchases', nested: { value: 500 } }], }], }, options: { parsing: { xAxisKey: 'id', yAxisKey: 'nested.value' }, }, }); ================================================ FILE: test/types/dataset_null_data.ts ================================================ import type { ChartDataset } from '../../src/types.js'; const dataset: ChartDataset = { data: [10, null, 20], }; const lineDataset: ChartDataset<'line'> = { data: [10, null, 20], }; const scatterDataset: ChartDataset<'scatter'> = { data: [10, null, 20], }; const radarDataset: ChartDataset<'radar'> = { data: [10, null, 20], }; ================================================ FILE: test/types/date_adapter.ts ================================================ import { _adapters } from '../../src/types.js'; _adapters._date.override<{myOption: boolean}>({ init() { const booleanOption: boolean = this.options.myOption; // @ts-expect-error Options is readonly. this.options = {}; }, // @ts-expect-error Should return string. format(timestamp) { const numberArg: number = timestamp; } }); ================================================ FILE: test/types/defaults.ts ================================================ import { Chart } from '../../src/types.js'; Chart.defaults.scales.time.time.minUnit = 'day'; Chart.defaults.plugins.title.display = false; Chart.defaults.datasets.bar.backgroundColor = 'red'; Chart.defaults.animation = { duration: 500 }; Chart.defaults.font.size = 8; // @ts-expect-error should be number Chart.defaults.font.size = '8'; // @ts-expect-error should be number Chart.defaults.font.size = () => '10'; Chart.defaults.backgroundColor = 'red'; Chart.defaults.backgroundColor = ['red', 'blue']; Chart.defaults.backgroundColor = (ctx) => ctx.datasetIndex % 2 === 0 ? 'red' : 'blue'; Chart.defaults.borderColor = 'red'; Chart.defaults.borderColor = ['red', 'blue']; Chart.defaults.borderColor = (ctx) => ctx.datasetIndex % 2 === 0 ? 'red' : 'blue'; Chart.defaults.hoverBackgroundColor = 'red'; Chart.defaults.hoverBackgroundColor = ['red', 'blue']; Chart.defaults.hoverBackgroundColor = (ctx) => ctx.datasetIndex % 2 === 0 ? 'red' : 'blue'; Chart.defaults.hoverBorderColor = 'red'; Chart.defaults.hoverBorderColor = ['red', 'blue']; Chart.defaults.hoverBorderColor = (ctx) => ctx.datasetIndex % 2 === 0 ? 'red' : 'blue'; Chart.defaults.font = { family: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif", size: 10 }; Chart.defaults.layout = { padding: { bottom: 10, }, }; Chart.defaults.plugins.tooltip.boxPadding = 3; ================================================ FILE: test/types/elements/scriptable_element_options.ts ================================================ import { Chart } from '../../../src/types.js'; const chart = new Chart('id', { type: 'line', data: { labels: [], datasets: [] }, options: { elements: { line: { borderWidth: () => 2, }, point: { pointStyle: (ctx) => 'star', } } } }); const chart2 = new Chart('id', { type: 'bar', data: { labels: [], datasets: [] }, options: { elements: { bar: { borderWidth: (ctx) => 2, } } } }); const chart3 = new Chart('id', { type: 'doughnut', data: { labels: [], datasets: [] }, options: { elements: { arc: { borderWidth: (ctx) => 3, borderJoinStyle: (ctx) => 'miter' } } } }); ================================================ FILE: test/types/extensions/plugin.ts ================================================ import { Chart } from '../../../src/types.js'; Chart.register({ id: 'my-plugin', afterDraw: (chart: Chart) => { // noop } }); Chart.register([{ id: 'my-plugin', afterDraw: (chart: Chart) => { // noop }, }]); // @ts-expect-error not assignable Chart.register({ id: 'fail', noComponentHasThisMethod: () => 'test' }); // @ts-expect-error missing id Chart.register([{ afterDraw: (chart: Chart) => { // noop }, }]); ================================================ FILE: test/types/extensions/scale.ts ================================================ import { AnyObject } from '../../../src/types/basic.js'; import { CartesianScaleOptions, Chart, Scale } from '../../../src/types.js'; export type TestScaleOptions = CartesianScaleOptions & { testOption?: boolean } export class TestScale extends Scale { static id: 'test'; getBasePixel(): number { return 0; } testMethod(): void { // } } declare module '../../../src/types/index.js' { interface CartesianScaleTypeRegistry { test: { options: TestScaleOptions } } } Chart.register(TestScale); const chart = new Chart('id', { type: 'line', data: { datasets: [] }, options: { scales: { x: { type: 'test', position: 'bottom', testOption: true, min: 0 } } } }); Chart.unregister([TestScale]); ================================================ FILE: test/types/helpers/dom.ts ================================================ import { getRelativePosition } from '../../../src/helpers/helpers.dom.js'; import { Chart, ChartOptions } from '../../../src/types.js'; const chart = new Chart('test', { type: 'line', data: { datasets: [] } }); getRelativePosition(new MouseEvent('click'), chart); ================================================ FILE: test/types/helpers/options.ts ================================================ import { createContext } from '../../../src/helpers/helpers.options.js'; const context1 = createContext(null, { type: 'test1', parent: true }); const context2 = createContext(context1, { type: 'test2' }); const sSest: string = context1.type + context2.type; const bTest: boolean = context1.parent && context2.parent; // @ts-expect-error Property 'notThere' does not exist on type '{ type: string; parent: boolean; } & { type: string; }' context2.notThere = ''; ================================================ FILE: test/types/interaction.ts ================================================ import { Chart, ChartData, ChartConfiguration, Element } from '../../src/types.js'; const data: ChartData<'line'> = { datasets: [] }; const chartItem = 'item'; const config: ChartConfiguration<'line'> = { type: 'line', data }; const chart: Chart = new Chart(chartItem, config); type Item = { element: Element, datasetIndex: number, index: number } const elements: Item[] = []; chart.updateHoverStyle(elements, 'dataset', true); ================================================ FILE: test/types/layout/position.ts ================================================ import type { LayoutPosition } from '../../../src/types.js'; const left: LayoutPosition = 'left'; const right: LayoutPosition = 'right'; const top: LayoutPosition = 'top'; const bottom: LayoutPosition = 'bottom'; const center: LayoutPosition = 'center'; const axis: LayoutPosition = { x: 10 }; // @ts-expect-error invalid position const invalid: LayoutPosition = 'none'; ================================================ FILE: test/types/options.ts ================================================ import { Chart, ChartOptions, ChartType, DoughnutControllerChartOptions } from '../../src/types.js'; const chart = new Chart('test', { type: 'bar', data: { labels: ['a'], datasets: [{ data: [1], }, { type: 'line', data: [{ x: 1, y: 1 }] }] }, options: { animation: { duration: 500 }, backgroundColor: 'red', datasets: { line: { animation: { duration: 600 }, backgroundColor: 'blue', } }, elements: { point: { backgroundColor: 'red' } } } }); const doughnutOptions: DoughnutControllerChartOptions = { circumference: 360, cutout: '50%', offset: 0, radius: 100, rotation: 0, spacing: 0, animation: false, }; const chartOptions: ChartOptions = doughnutOptions; ================================================ FILE: test/types/overrides.ts ================================================ import { Chart } from '../../src/types.js'; Chart.overrides.bar.scales.x.type = 'time'; Chart.overrides.bar.plugins.title.display = false; Chart.overrides.line.datasets.bar.backgroundColor = 'red'; Chart.overrides.line.animation = false; Chart.overrides.line.datasets.bar.animation = { duration: 100 }; ================================================ FILE: test/types/parsed.data.type.ts ================================================ import type { ParsedDataType } from '../../src/types.js'; interface test { pie: ParsedDataType<'pie'>, line: ParsedDataType<'line'>, testA: ParsedDataType<'pie' | 'line' | 'bar'> testB: ParsedDataType<'pie' | 'line' | 'bar'> testC: ParsedDataType<'pie' | 'line' | 'bar'> } const testImpl: test = { pie: 1, line: { x: 1, y: 2 }, testA: 1, testB: { x: 1, y: 2 }, // @ts-expect-error testC should be limited to pie/line datatypes testC: 'test' }; ================================================ FILE: test/types/plugins/defaults.ts ================================================ import { defaults } from '../../../src/types.js'; // https://github.com/chartjs/Chart.js/issues/8711 const original = defaults.plugins.legend.labels.generateLabels; defaults.plugins.legend.labels.generateLabels = function(chart) { return [{ datasetIndex: 0, text: 'test' }]; }; ================================================ FILE: test/types/plugins/plugin.colors/colors.ts ================================================ import { Chart } from '../../../../src/types.js'; const chart = new Chart('id', { type: 'bubble', data: { labels: [], datasets: [{ data: [] }] }, options: { plugins: { colors: { enabled: true, forceOverride: false, } } } }); ================================================ FILE: test/types/plugins/plugin.decimation/decimation_algorithm.ts ================================================ import { Chart, DecimationAlgorithm } from '../../../../src/types.js'; const chart = new Chart('id', { type: 'bubble', data: { labels: [], datasets: [{ data: [] }] }, options: { plugins: { decimation: { algorithm: DecimationAlgorithm.lttb, } } } }); const chart2 = new Chart('id', { type: 'bubble', data: { labels: [], datasets: [{ data: [] }] }, options: { plugins: { decimation: { algorithm: 'lttb', } } } }); const chart3 = new Chart('id', { type: 'bubble', data: { labels: [], datasets: [{ data: [] }] }, options: { plugins: { decimation: { algorithm: DecimationAlgorithm.minmax, } } } }); const chart4 = new Chart('id', { type: 'bubble', data: { labels: [], datasets: [{ data: [] }] }, options: { plugins: { decimation: { algorithm: 'min-max', } } } }); ================================================ FILE: test/types/plugins/plugin.filler/fill_target_true.ts ================================================ import type { ChartDataset } from '../../../../src/types.js'; const dataset: ChartDataset = { data: [], fill: true, }; ================================================ FILE: test/types/plugins/plugin.tooltip/chart.tooltip.ts ================================================ import { Chart } from '../../../../src/types.js'; const chart = new Chart('id', { type: 'line', data: { labels: [], datasets: [{ data: [] }] }, }); const tooltip = chart.tooltip; const active = tooltip && tooltip.getActiveElements(); ================================================ FILE: test/types/plugins/plugin.tooltip/tooltip_dataset_type.ts ================================================ import { Chart } from '../../../../src/types.js'; const chart = new Chart('id', { type: 'line', data: { labels: [], datasets: [{ data: [] }] }, options: { plugins: { tooltip: { callbacks: { label: (item) => { return `Y Axis ${item.dataset.yAxisID}`; } } } } }, }); ================================================ FILE: test/types/plugins/plugin.tooltip/tooltip_parsed_data.ts ================================================ import { Chart } from '../../../../src/types.js'; const chart = new Chart('id', { type: 'bar', data: { labels: [], datasets: [{ data: [] }] }, options: { plugins: { tooltip: { callbacks: { label: (item) => { return `Foo data ${item.parsed.y}`; } } } } }, }); ================================================ FILE: test/types/plugins/plugin.tooltip/tooltip_parsed_data_chart_defaults.ts ================================================ import { Chart } from '../../../../src/types.js'; Chart.overrides.bubble.plugins.tooltip.callbacks.label = (item) => { const { x, y, _custom: r } = item.parsed; return `${item.label}: (${x}, ${y}, ${r})`; }; const chart = new Chart('id', { type: 'bubble', data: { labels: [], datasets: [{ data: [] }] }, }); ================================================ FILE: test/types/plugins/plugin.tooltip/tooltip_scriptable_background_color.ts ================================================ import { Chart } from '../../../../src/types.js'; const chart = new Chart('id', { type: 'bar', data: { labels: [], datasets: [{ data: [] }] }, options: { plugins: { tooltip: { backgroundColor: (ctx) => 'black', } } }, }); ================================================ FILE: test/types/register.ts ================================================ import { Chart, ArcElement, LineElement, BarElement, PointElement, BarController, BubbleController, DoughnutController, LineController, PieController, PolarAreaController, RadarController, ScatterController, CategoryScale, LinearScale, LogarithmicScale, RadialLinearScale, TimeScale, TimeSeriesScale, Decimation, Filler, Legend, Title, SubTitle, Tooltip, Colors } from '../../src/types.js'; Chart.register( ArcElement, LineElement, BarElement, PointElement, BarController, BubbleController, DoughnutController, LineController, PieController, PolarAreaController, RadarController, ScatterController, CategoryScale, LinearScale, LogarithmicScale, RadialLinearScale, TimeScale, TimeSeriesScale, Decimation, Filler, Legend, Title, SubTitle, Tooltip, Colors ); ================================================ FILE: test/types/scales/chart_options.ts ================================================ import type { ChartOptions } from '../../../src/types.js'; const chartOptions: ChartOptions<'line'> = { scales: { x: { type: 'time', time: { unit: 'year' } }, } }; ================================================ FILE: test/types/scales/options.ts ================================================ import { Chart, ScaleOptions } from '../../../src/types.js'; const chart = new Chart('test', { type: 'bar', data: { labels: ['a'], datasets: [{ data: [1], }, { type: 'line', data: [{ x: 1, y: 1 }] }] }, options: { scales: { x: { type: 'time', time: { unit: 'year' }, ticks: { stepSize: 1 } }, x1: { type: 'linear', // @ts-expect-error 'time' does not exist in 'linear' options time: { unit: 'year' } }, y: { ticks: { callback(tickValue) { const value = this.getLabelForValue(tickValue as number); return '$' + value; } } } } } }); function makeChartScale(range: number): ScaleOptions<'linear'> { return { type: 'linear', min: 0, suggestedMax: range, }; } const composedChart = new Chart('test2', { type: 'bar', data: { labels: ['a'], datasets: [{ data: [1], }, { type: 'line', data: [{ x: 1, y: 1 }] }] }, options: { scales: { x: makeChartScale(10) } } }); ================================================ FILE: test/types/scales/time_string_max.ts ================================================ import { Chart } from '../../../src/types.js'; const chart = new Chart('id', { type: 'line', data: { datasets: [ { label: 'Pie', data: [ ], borderColor: '#000000', backgroundColor: '#00FF00' } ] }, options: { scales: { x: { type: 'time', min: '2021-01-01', max: '2021-12-01' }, y: { type: 'linear', min: 0, max: 10 } } } }); ================================================ FILE: test/types/scriptable.ts ================================================ import type { ChartType, Scriptable, ScriptableContext } from '../../src/types.js'; interface test { pie?: Scriptable>, line?: Scriptable>, testA?: Scriptable> testB?: Scriptable> testC?: Scriptable> testD?: Scriptable> } const testImpl: test = { pie: (ctx) => ctx.parsed + ctx.chart.width, line: (ctx) => ctx.parsed.x + ctx.parsed.y, testA: (ctx) => ctx.parsed + ctx.dataset.data[0], testB: (ctx) => ctx.parsed.x + ctx.parsed.y, // @ts-expect-error combined type should not be any testC: (ctx) => ctx.fail, // combined types are intersections and permit invalid usage testD: (ctx) => ctx.parsed + ctx.parsed.x + ctx.parsed.r + ctx.parsed._custom.barEnd }; ================================================ FILE: test/types/scriptable_core_chart_options.ts ================================================ import type { ChartConfiguration } from '../../src/types.js'; const getConfig = (): ChartConfiguration<'bar'> => { return { type: 'bar', data: { datasets: [] }, options: { backgroundColor: (context) => context.active ? '#fff' : undefined, } }; }; ================================================ FILE: test/types/test_instance_assignment.ts ================================================ import { Chart } from '../../src/types.js'; const chart = new Chart('id', { type: 'scatter', data: { labels: [], datasets: [{ data: [{ x: 0, y: 1 }], pointRadius: (ctx) => ctx.parsed.x, }] }, }); interface Context { chart: Chart; } const ctx: Context = { chart: chart }; // @ts-expect-error Type '{ x: number; y: number; }[]' is not assignable to type 'number[]'. const dataArray: number[] = chart.data.datasets[0].data; ================================================ FILE: test/types/ticks/ticks.ts ================================================ import { Chart, Ticks } from '../../../src/types.js'; // @ts-expect-error The 'this' context... is not assignable to method's 'this' of type 'Scale'. Ticks.formatters.numeric(0, 0, [{ value: 0 }]); const chart = new Chart('test', { type: 'line', data: { datasets: [{ data: [{ x: 1, y: 1 }] }] }, }); Ticks.formatters.numeric.call(chart.scales.x, 0, 0, [{ value: 0 }]); ================================================ FILE: test/types/tsconfig.json ================================================ { "extends": "../../tsconfig.json", "compilerOptions": { "noEmit": true, "rootDir": "../../" }, "include": [ "./", "../../src/", "../../dist/**/*.d.ts" ], "exclude": [ "./**/*.js" ] } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { /* Type Checking */ "alwaysStrict": true, "strictBindCallApply": true, "strictFunctionTypes": true, /* todo: uncomment after transition to TS */ // "noFallthroughCasesInSwitch": true, // "noImplicitOverride": true, // "noImplicitReturns": true, // "noUnusedLocals": true, // "noUnusedParameters": true, /* Modules */ "baseUrl": ".", "module": "ESNext", "moduleResolution": "NodeNext", "resolveJsonModule": true, "rootDir": "src", "types": ["offscreencanvas"], /* Emit */ "declaration": true, "importsNotUsedAsValues": "error", "inlineSourceMap": true, "outDir": "dist", /* JavaScript Support */ "allowJs": true, "checkJs": true, /* Interop Constraints */ "allowSyntheticDefaultImports": true, /* Language and Environment */ "target": "ES6", "lib": ["es2018", "DOM"] }, "typedocOptions": { "name": "Chart.js", "entryPoints": ["src/types/index.d.ts"], "readme": "none", "excludeExternals": true, "includeVersion": true, "out": "./dist/docs/typedoc" }, "include": [ "./src/**/*" ], "exclude": [ "./dist/**" ] }