FEATURE: Poll breakdown 2.0 (#10345)
The poll breakdown modal replaces the grouped pie charts feature.
Includes:
* MODAL: Untangle `onSelectPanel`
Previously modal-tab component would call on click the onSelectPanel callback with itself (modal-tab) as `this` which severely limited its usefulness. Now showModal binds the callback to its controller.
"The PR includes a fix/change to d-modal (b7f6ec6) that hasn't been extracted to a separate PR because it's not currently possible to test a change like this in abstract, i.e. with dynamically created controllers/components in tests. The percentage/count toggle test for the poll breakdown feature is essentially a test for that d-modal modification."
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
import I18n from "I18n";
|
||||
import Component from "@ember/component";
|
||||
import { mapBy } from "@ember/object/computed";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { PIE_CHART_TYPE } from "discourse/plugins/poll/controllers/poll-ui-builder";
|
||||
import { getColors } from "discourse/plugins/poll/lib/chart-colors";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
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: {
|
||||
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 },
|
||||
tooltips: false,
|
||||
onHover: (event, activeElements) => {
|
||||
if (!activeElements.length) {
|
||||
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;
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
import I18n from "I18n";
|
||||
import Component from "@ember/component";
|
||||
import { action } from "@ember/object";
|
||||
import { equal } from "@ember/object/computed";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { propertyEqual } from "discourse/lib/computed";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import { getColors } from "discourse/plugins/poll/lib/chart-colors";
|
||||
|
||||
export default Component.extend({
|
||||
// Arguments:
|
||||
option: null,
|
||||
index: null,
|
||||
totalVotes: null,
|
||||
optionsCount: null,
|
||||
displayMode: null,
|
||||
highlightedOption: null,
|
||||
onMouseOver: null,
|
||||
onMouseOut: null,
|
||||
|
||||
tagName: "",
|
||||
|
||||
highlighted: propertyEqual("highlightedOption", "index"),
|
||||
showPercentage: equal("displayMode", "percentage"),
|
||||
|
||||
@discourseComputed("option.votes", "totalVotes")
|
||||
percent(votes, total) {
|
||||
return I18n.toNumber((votes / total) * 100.0, { precision: 1 });
|
||||
},
|
||||
|
||||
@discourseComputed("optionsCount")
|
||||
optionColors(optionsCount) {
|
||||
return getColors(optionsCount);
|
||||
},
|
||||
|
||||
@discourseComputed("highlighted")
|
||||
colorBackgroundStyle(highlighted) {
|
||||
if (highlighted) {
|
||||
// TODO: Use CSS variables (#10341)
|
||||
return htmlSafe("background: rgba(0, 0, 0, 0.1);");
|
||||
}
|
||||
},
|
||||
|
||||
@discourseComputed("highlighted", "optionColors", "index")
|
||||
colorPreviewStyle(highlighted, optionColors, index) {
|
||||
const color = highlighted
|
||||
? window.Chart.helpers.getHoverColor(optionColors[index])
|
||||
: optionColors[index];
|
||||
|
||||
return htmlSafe(`background: ${color};`);
|
||||
},
|
||||
|
||||
@action
|
||||
onHover(active) {
|
||||
if (active) {
|
||||
this.onMouseOver();
|
||||
} else {
|
||||
this.onMouseOut();
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user