import Component from "@ember/component"; import I18n from "I18n"; import { PIE_CHART_TYPE } from "discourse/plugins/poll/controllers/poll-ui-builder"; import discourseComputed from "discourse-common/utils/decorators"; import { getColors } from "discourse/plugins/poll/lib/chart-colors"; import { htmlSafe } from "@ember/template"; import { mapBy } from "@ember/object/computed"; import { next } from "@ember/runloop"; export default Component.extend({ // Arguments: group: null, options: null, displayMode: null, highlightedOption: null, setHighlightedOption: null, classNames: "poll-breakdown-chart-container", _optionToSlice: null, _previousHighlightedSliceIndex: null, _previousDisplayMode: null, data: mapBy("options", "votes"), init() { this._super(...arguments); this._optionToSlice = {}; }, didInsertElement() { this._super(...arguments); const canvas = this.element.querySelector("canvas"); this._chart = new window.Chart(canvas.getContext("2d"), this.chartConfig); }, didReceiveAttrs() { this._super(...arguments); if (this._chart) { this._updateDisplayMode(); this._updateHighlight(); } }, willDestroy() { this._super(...arguments); if (this._chart) { this._chart.destroy(); } }, @discourseComputed("optionColors", "index") colorStyle(optionColors, index) { return htmlSafe(`background: ${optionColors[index]};`); }, @discourseComputed("data", "displayMode") chartConfig(data, displayMode) { const transformedData = []; let counter = 0; this._optionToSlice = {}; data.forEach((votes, index) => { if (votes > 0) { transformedData.push(votes); this._optionToSlice[index] = counter++; } }); const totalVotes = transformedData.reduce((sum, votes) => sum + votes, 0); const colors = getColors(data.length).filter( (color, index) => data[index] > 0 ); return { type: PIE_CHART_TYPE, plugins: [window.ChartDataLabels], data: { datasets: [ { data: transformedData, backgroundColor: colors, // TODO: It's a workaround for Chart.js' terrible hover styling. // It will break on non-white backgrounds. // Should be updated after #10341 lands hoverBorderColor: "#fff", }, ], }, options: { plugins: { tooltip: false, datalabels: { color: "#333", backgroundColor: "rgba(255, 255, 255, 0.5)", borderRadius: 2, font: { family: getComputedStyle(document.body).fontFamily, size: 16, }, padding: { top: 2, right: 6, bottom: 2, left: 6, }, formatter(votes) { if (displayMode !== "percentage") { return votes; } const percent = I18n.toNumber((votes / totalVotes) * 100.0, { precision: 1, }); return `${percent}%`; }, }, }, responsive: true, aspectRatio: 1.1, animation: { duration: 0 }, // wrapping setHighlightedOption in next block as hover can create many events // prevents two sets to happen in the same computation onHover: (event, activeElements) => { if (!activeElements.length) { next(() => { this.setHighlightedOption(null); }); return; } const sliceIndex = activeElements[0]._index; const optionIndex = Object.keys(this._optionToSlice).find( (option) => this._optionToSlice[option] === sliceIndex ); // Clear the array to avoid issues in Chart.js activeElements.length = 0; next(() => { this.setHighlightedOption(Number(optionIndex)); }); }, }, }; }, _updateDisplayMode() { if (this.displayMode !== this._previousDisplayMode) { const config = this.chartConfig; this._chart.data.datasets = config.data.datasets; this._chart.options = config.options; this._chart.update(); this._previousDisplayMode = this.displayMode; } }, _updateHighlight() { const meta = this._chart.getDatasetMeta(0); if (this._previousHighlightedSliceIndex !== null) { const slice = meta.data[this._previousHighlightedSliceIndex]; meta.controller.removeHoverStyle(slice); this._chart.draw(); } if (this.highlightedOption === null) { this._previousHighlightedSliceIndex = null; return; } const sliceIndex = this._optionToSlice[this.highlightedOption]; if (typeof sliceIndex === "undefined") { this._previousHighlightedSliceIndex = null; return; } const slice = meta.data[sliceIndex]; this._previousHighlightedSliceIndex = sliceIndex; meta.controller.setHoverStyle(slice); this._chart.draw(); }, });