191 lines
5.0 KiB
JavaScript
191 lines
5.0 KiB
JavaScript
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();
|
|
},
|
|
});
|