mirror of
https://github.com/discourse/discourse.git
synced 2024-11-22 11:23:25 +08:00
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:
parent
76c02cac65
commit
cd4f251891
|
@ -20,6 +20,10 @@ export default Component.extend({
|
|||
},
|
||||
|
||||
click() {
|
||||
this.onSelectPanel(this.panel);
|
||||
this.set("selectedPanel", this.panel);
|
||||
|
||||
if (this.onSelectPanel) {
|
||||
this.onSelectPanel(this.panel);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -50,7 +50,10 @@ export default function(name, opts) {
|
|||
});
|
||||
|
||||
if (controller.actions.onSelectPanel) {
|
||||
modalController.set("onSelectPanel", controller.actions.onSelectPanel);
|
||||
modalController.set(
|
||||
"onSelectPanel",
|
||||
controller.actions.onSelectPanel.bind(controller)
|
||||
);
|
||||
}
|
||||
|
||||
modalController.set(
|
||||
|
|
|
@ -18,10 +18,6 @@ export default Mixin.create({
|
|||
closeModal() {
|
||||
this.modal.send("closeModal");
|
||||
this.set("panels", []);
|
||||
},
|
||||
|
||||
onSelectPanel(panel) {
|
||||
this.set("selectedPanel", panel);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -148,7 +148,7 @@
|
|||
}
|
||||
|
||||
&:not(.history-modal) {
|
||||
.modal-body:not(.reorder-categories):not(.poll-ui-builder) {
|
||||
.modal-body:not(.reorder-categories):not(.poll-ui-builder):not(.poll-breakdown) {
|
||||
max-height: 80vh !important;
|
||||
@media screen and (max-height: 500px) {
|
||||
max-height: 65vh !important;
|
||||
|
|
|
@ -69,6 +69,9 @@ task 'javascript:update' do
|
|||
}, {
|
||||
source: 'chart.js/dist/Chart.min.js',
|
||||
public: true
|
||||
}, {
|
||||
source: 'chartjs-plugin-datalabels/dist/chartjs-plugin-datalabels.min.js',
|
||||
public: true
|
||||
}, {
|
||||
source: 'magnific-popup/dist/jquery.magnific-popup.min.js',
|
||||
public: true
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"bootbox": "3.2.0",
|
||||
"bootstrap": "v3.4.1",
|
||||
"chart.js": "2.9.3",
|
||||
"chartjs-plugin-datalabels": "^0.7.0",
|
||||
"eslint-plugin-lodash": "^6.0.0",
|
||||
"favcount": "https://github.com/chrishunt/favcount",
|
||||
"handlebars": "^4.7.0",
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
|
@ -0,0 +1,83 @@
|
|||
import I18n from "I18n";
|
||||
import Controller from "@ember/controller";
|
||||
import { action } from "@ember/object";
|
||||
import { classify } from "@ember/string";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import ModalFunctionality from "discourse/mixins/modal-functionality";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
export default Controller.extend(ModalFunctionality, {
|
||||
model: null,
|
||||
charts: null,
|
||||
groupedBy: null,
|
||||
highlightedOption: null,
|
||||
displayMode: "percentage",
|
||||
|
||||
@discourseComputed("model.groupableUserFields")
|
||||
groupableUserFields(fields) {
|
||||
return fields.map(field => {
|
||||
const transformed = field.split("_").filter(Boolean);
|
||||
|
||||
if (transformed.length > 1) {
|
||||
transformed[0] = classify(transformed[0]);
|
||||
}
|
||||
|
||||
return { id: field, label: transformed.join(" ") };
|
||||
});
|
||||
},
|
||||
|
||||
@discourseComputed("model.poll.options")
|
||||
totalVotes(options) {
|
||||
return options.reduce((sum, option) => sum + option.votes, 0);
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.set("charts", null);
|
||||
this.set("displayMode", "percentage");
|
||||
this.set("groupedBy", this.model.groupableUserFields[0]);
|
||||
|
||||
loadScript("/javascripts/Chart.min.js")
|
||||
.then(() => loadScript("/javascripts/chartjs-plugin-datalabels.min.js"))
|
||||
.then(() => {
|
||||
window.Chart.plugins.unregister(window.ChartDataLabels);
|
||||
this.fetchGroupedPollData();
|
||||
});
|
||||
},
|
||||
|
||||
fetchGroupedPollData() {
|
||||
return ajax("/polls/grouped_poll_results.json", {
|
||||
data: {
|
||||
post_id: this.model.post.id,
|
||||
poll_name: this.model.poll.name,
|
||||
user_field_name: this.groupedBy
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error) {
|
||||
popupAjaxError(error);
|
||||
} else {
|
||||
bootbox.alert(I18n.t("poll.error_while_fetching_voters"));
|
||||
}
|
||||
})
|
||||
.then(result => {
|
||||
if (this.isDestroying || this.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set("charts", result.grouped_results);
|
||||
});
|
||||
},
|
||||
|
||||
@action
|
||||
setGrouping(value) {
|
||||
this.set("groupedBy", value);
|
||||
this.fetchGroupedPollData();
|
||||
},
|
||||
|
||||
@action
|
||||
onSelectPanel(panel) {
|
||||
this.set("displayMode", panel.id);
|
||||
}
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
import I18n from "I18n";
|
||||
import Controller from "@ember/controller";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
import EmberObject from "@ember/object";
|
||||
import discourseComputed, { observes } from "discourse-common/utils/decorators";
|
||||
|
||||
export const BAR_CHART_TYPE = "bar";
|
||||
export const PIE_CHART_TYPE = "pie";
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
<label class="poll-breakdown-chart-label">{{@group}}</label>
|
||||
<canvas class="poll-breakdown-chart-chart"></canvas>
|
|
@ -0,0 +1,17 @@
|
|||
<li
|
||||
class="poll-breakdown-option"
|
||||
style={{this.colorBackgroundStyle}}
|
||||
{{on "mouseover" @onMouseOver}}
|
||||
{{on "mouseout" @onMouseOut}}
|
||||
>
|
||||
<span class="poll-breakdown-option-color" style={{this.colorPreviewStyle}}></span>
|
||||
|
||||
<span class="poll-breakdown-option-count">
|
||||
{{#if showPercentage}}
|
||||
{{this.percent}}%
|
||||
{{else}}
|
||||
{{@option.votes}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="poll-breakdown-option-text">{{{@option.html}}}</span>
|
||||
</li>
|
|
@ -0,0 +1,49 @@
|
|||
{{#d-modal-body title="poll.breakdown.title"}}
|
||||
<div class="poll-breakdown-sidebar">
|
||||
{{!-- TODO: replace with the (optional) poll title --}}
|
||||
<p class="poll-breakdown-title">{{this.model.post.topic.title}}</p>
|
||||
|
||||
<div class="poll-breakdown-total-votes">{{i18n "poll.breakdown.votes" count=this.model.poll.voters}}</div>
|
||||
|
||||
<ul class="poll-breakdown-options">
|
||||
{{#each this.model.poll.options as |option index|}}
|
||||
{{poll-breakdown-option
|
||||
option=option
|
||||
index=index
|
||||
totalVotes=this.totalVotes
|
||||
optionsCount=this.model.poll.options.length
|
||||
displayMode=this.displayMode
|
||||
highlightedOption=this.highlightedOption
|
||||
onMouseOver=(fn (mut this.highlightedOption) index)
|
||||
onMouseOut=(fn (mut this.highlightedOption) null)
|
||||
}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="poll-breakdown-body">
|
||||
<div class="poll-breakdown-body-header">
|
||||
<label class="poll-breakdown-body-header-label">{{i18n "poll.breakdown.breakdown"}}</label>
|
||||
|
||||
{{combo-box
|
||||
content=this.groupableUserFields
|
||||
value=this.groupedBy
|
||||
nameProperty="label"
|
||||
class="poll-breakdown-dropdown"
|
||||
onChange=(action this.setGrouping)
|
||||
}}
|
||||
</div>
|
||||
|
||||
<div class="poll-breakdown-charts">
|
||||
{{#each this.charts as |chart|}}
|
||||
{{poll-breakdown-chart
|
||||
group=(get chart "group")
|
||||
options=(get chart "options")
|
||||
displayMode=this.displayMode
|
||||
highlightedOption=this.highlightedOption
|
||||
setHighlightedOption=(fn (mut this.highlightedOption))
|
||||
}}
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
{{/d-modal-body}}
|
|
@ -1,6 +1,6 @@
|
|||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import discourseComputed from "discourse-common/utils/decorators";
|
||||
|
||||
function initializePollUIBuilder(api) {
|
||||
api.modifyClass("controller:composer", {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import EmberObject from "@ember/object";
|
||||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
import { getRegister } from "discourse-common/lib/get-owner";
|
||||
import WidgetGlue from "discourse/widgets/glue";
|
||||
import { getRegister } from "discourse-common/lib/get-owner";
|
||||
import { observes } from "discourse-common/utils/decorators";
|
||||
|
||||
function initializePolls(api) {
|
||||
const register = getRegister(api);
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import I18n from "I18n";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { h } from "virtual-dom";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import { ajax } from "discourse/lib/ajax";
|
||||
import { popupAjaxError } from "discourse/lib/ajax-error";
|
||||
import evenRound from "discourse/plugins/poll/lib/even-round";
|
||||
import { avatarFor } from "discourse/widgets/post";
|
||||
import round from "discourse/lib/round";
|
||||
import { relativeAge } from "discourse/lib/formatter";
|
||||
import loadScript from "discourse/lib/load-script";
|
||||
import { getColors } from "../lib/chart-colors";
|
||||
import { classify } from "@ember/string";
|
||||
import { PIE_CHART_TYPE } from "../controllers/poll-ui-builder";
|
||||
import round from "discourse/lib/round";
|
||||
import showModal from "discourse/lib/show-modal";
|
||||
import { avatarFor } from "discourse/widgets/post";
|
||||
import RawHtml from "discourse/widgets/raw-html";
|
||||
import { createWidget } from "discourse/widgets/widget";
|
||||
import { iconNode } from "discourse-common/lib/icon-library";
|
||||
import { PIE_CHART_TYPE } from "discourse/plugins/poll/controllers/poll-ui-builder";
|
||||
import { getColors } from "discourse/plugins/poll/lib/chart-colors";
|
||||
import evenRound from "discourse/plugins/poll/lib/even-round";
|
||||
|
||||
function optionHtml(option) {
|
||||
const $node = $(`<span>${option.html}</span>`);
|
||||
|
@ -453,120 +453,6 @@ createWidget("discourse-poll-info", {
|
|||
}
|
||||
});
|
||||
|
||||
function transformUserFieldToLabel(fieldName) {
|
||||
let transformed = fieldName.split("_").filter(Boolean);
|
||||
if (transformed.length > 1) {
|
||||
transformed[0] = classify(transformed[0]);
|
||||
}
|
||||
return transformed.join(" ");
|
||||
}
|
||||
|
||||
createWidget("discourse-poll-grouped-pies", {
|
||||
tagName: "div.poll-grouped-pies",
|
||||
buildAttributes(attrs) {
|
||||
return {
|
||||
id: `poll-results-grouped-pie-charts-${attrs.id}`
|
||||
};
|
||||
},
|
||||
|
||||
html(attrs) {
|
||||
const fields = Object.assign({}, attrs.groupableUserFields);
|
||||
const fieldSelectId = `field-select-${attrs.id}`;
|
||||
attrs.groupedBy = attrs.groupedBy || fields[0];
|
||||
|
||||
let contents = [];
|
||||
|
||||
const btn = this.attach("button", {
|
||||
className: "btn-default poll-group-by-toggle",
|
||||
label: "poll.ungroup-results.label",
|
||||
title: "poll.ungroup-results.title",
|
||||
icon: "far-eye-slash",
|
||||
action: "toggleGroupedPieCharts"
|
||||
});
|
||||
const select = h(
|
||||
`select#${fieldSelectId}.poll-group-by-selector`,
|
||||
{ value: attrs.groupBy },
|
||||
attrs.groupableUserFields.map(field => {
|
||||
return h("option", { value: field }, transformUserFieldToLabel(field));
|
||||
})
|
||||
);
|
||||
contents.push(h("div.poll-grouped-pies-controls", [btn, select]));
|
||||
|
||||
ajax("/polls/grouped_poll_results.json", {
|
||||
data: {
|
||||
post_id: attrs.post.id,
|
||||
poll_name: attrs.poll.name,
|
||||
user_field_name: attrs.groupedBy
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
if (error) {
|
||||
popupAjaxError(error);
|
||||
} else {
|
||||
bootbox.alert(I18n.t("poll.error_while_fetching_voters"));
|
||||
}
|
||||
})
|
||||
.then(result => {
|
||||
let groupBySelect = document.getElementById(fieldSelectId);
|
||||
if (!groupBySelect) return;
|
||||
|
||||
groupBySelect.value = attrs.groupedBy;
|
||||
const parent = document.getElementById(
|
||||
`poll-results-grouped-pie-charts-${attrs.id}`
|
||||
);
|
||||
|
||||
for (
|
||||
let chartIdx = 0;
|
||||
chartIdx < result.grouped_results.length;
|
||||
chartIdx++
|
||||
) {
|
||||
const data = result.grouped_results[chartIdx].options.mapBy("votes");
|
||||
const labels = result.grouped_results[chartIdx].options.mapBy("html");
|
||||
const chartConfig = pieChartConfig(data, labels, {
|
||||
aspectRatio: 1.2
|
||||
});
|
||||
const canvasId = `pie-${attrs.id}-${chartIdx}`;
|
||||
let el = document.querySelector(`#${canvasId}`);
|
||||
if (!el) {
|
||||
const container = document.createElement("div");
|
||||
container.classList.add("poll-grouped-pie-container");
|
||||
|
||||
const label = document.createElement("label");
|
||||
label.classList.add("poll-pie-label");
|
||||
label.textContent = result.grouped_results[chartIdx].group;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.classList.add(`poll-grouped-pie-${attrs.id}`);
|
||||
canvas.id = canvasId;
|
||||
|
||||
container.appendChild(label);
|
||||
container.appendChild(canvas);
|
||||
parent.appendChild(container);
|
||||
// eslint-disable-next-line
|
||||
new Chart(canvas.getContext("2d"), chartConfig);
|
||||
} else {
|
||||
// eslint-disable-next-line
|
||||
Chart.helpers.each(Chart.instances, function(instance) {
|
||||
if (instance.chart.canvas.id === canvasId && el.$chartjs) {
|
||||
instance.destroy();
|
||||
// eslint-disable-next-line
|
||||
new Chart(el.getContext("2d"), chartConfig);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return contents;
|
||||
},
|
||||
|
||||
click(e) {
|
||||
let select = $(e.target).closest("select");
|
||||
if (select.length) {
|
||||
this.sendWidgetAction("refreshCharts", select[0].value);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function clearPieChart(id) {
|
||||
let el = document.querySelector(`#poll-results-chart-${id}`);
|
||||
el && el.parentNode.removeChild(el);
|
||||
|
@ -607,27 +493,23 @@ createWidget("discourse-poll-pie-chart", {
|
|||
return contents;
|
||||
}
|
||||
|
||||
let btn;
|
||||
let chart;
|
||||
if (attrs.groupResults && attrs.groupableUserFields.length > 0) {
|
||||
chart = this.attach("discourse-poll-grouped-pies", attrs);
|
||||
clearPieChart(attrs.id);
|
||||
} else {
|
||||
if (attrs.groupableUserFields.length) {
|
||||
btn = this.attach("button", {
|
||||
className: "btn-default poll-group-by-toggle",
|
||||
label: "poll.group-results.label",
|
||||
title: "poll.group-results.title",
|
||||
icon: "far-eye",
|
||||
action: "toggleGroupedPieCharts"
|
||||
});
|
||||
}
|
||||
if (attrs.groupableUserFields.length) {
|
||||
const button = this.attach("button", {
|
||||
className: "btn-default poll-show-breakdown",
|
||||
label: "poll.group-results.label",
|
||||
title: "poll.group-results.title",
|
||||
icon: "far-eye",
|
||||
action: "showBreakdown"
|
||||
});
|
||||
|
||||
chart = this.attach("discourse-poll-pie-canvas", attrs);
|
||||
contents.push(button);
|
||||
}
|
||||
contents.push(btn);
|
||||
|
||||
const chart = this.attach("discourse-poll-pie-canvas", attrs);
|
||||
contents.push(chart);
|
||||
|
||||
contents.push(h(`div#poll-results-legend-${attrs.id}.pie-chart-legends`));
|
||||
|
||||
return contents;
|
||||
}
|
||||
});
|
||||
|
@ -1072,19 +954,13 @@ export default createWidget("discourse-poll", {
|
|||
});
|
||||
},
|
||||
|
||||
toggleGroupedPieCharts() {
|
||||
this.attrs.groupResults = !this.attrs.groupResults;
|
||||
},
|
||||
|
||||
refreshCharts(newGroupedByValue) {
|
||||
let el = document.getElementById(
|
||||
`poll-results-grouped-pie-charts-${this.attrs.id}`
|
||||
);
|
||||
Array.from(el.getElementsByClassName("poll-grouped-pie-container")).forEach(
|
||||
container => {
|
||||
el.removeChild(container);
|
||||
}
|
||||
);
|
||||
this.attrs.groupedBy = newGroupedByValue;
|
||||
showBreakdown() {
|
||||
showModal("poll-breakdown", {
|
||||
model: this.attrs,
|
||||
panels: [
|
||||
{ id: "percentage", title: "poll.breakdown.percentage" },
|
||||
{ id: "count", title: "poll.breakdown.count" }
|
||||
]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
116
plugins/poll/assets/stylesheets/common/poll-breakdown.scss
Normal file
116
plugins/poll/assets/stylesheets/common/poll-breakdown.scss
Normal file
|
@ -0,0 +1,116 @@
|
|||
.poll-breakdown-modal {
|
||||
.modal-inner-container {
|
||||
max-width: unset;
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
.modal-tabs {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: grid;
|
||||
height: 80vh;
|
||||
grid: auto-flow / 1fr 2fr;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-breakdown-sidebar {
|
||||
background: var(--primary-very-low);
|
||||
box-sizing: border-box;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.poll-breakdown-title {
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.poll-breakdown-total-votes {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin-top: 0.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.poll-breakdown-options {
|
||||
display: grid;
|
||||
list-style: none;
|
||||
margin: 1.5rem 0 0 0;
|
||||
}
|
||||
|
||||
.poll-breakdown-option {
|
||||
align-items: center;
|
||||
border-radius: 5px;
|
||||
column-gap: 0.66rem;
|
||||
cursor: default;
|
||||
display: grid;
|
||||
grid-template-columns: 2.5rem 1fr;
|
||||
row-gap: 0.1rem;
|
||||
justify-content: start;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.poll-breakdown-option-color {
|
||||
align-self: end;
|
||||
border: 1px solid rgba(0, 0, 0, 0.15);
|
||||
grid-column: 1;
|
||||
height: 0.6rem;
|
||||
justify-self: center;
|
||||
width: 1.2rem;
|
||||
}
|
||||
|
||||
.poll-breakdown-option-count {
|
||||
align-self: start;
|
||||
font-size: 0.9rem;
|
||||
grid-column: 1;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.poll-breakdown-option-text {
|
||||
grid-column: 2;
|
||||
grid-row: 1/3;
|
||||
}
|
||||
|
||||
.poll-breakdown-body {
|
||||
box-sizing: border-box;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
.poll-breakdown-body-header {
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.poll-breakdown-body-header-label {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.poll-breakdown-dropdown {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.poll-breakdown-charts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(33.3%, 0.33fr));
|
||||
}
|
||||
|
||||
.poll-breakdown-chart-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.poll-breakdown-chart-label {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
|
@ -149,41 +149,22 @@ div.poll {
|
|||
}
|
||||
|
||||
.poll-results-chart {
|
||||
height: 310px;
|
||||
height: 320px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.poll-group-by-toggle {
|
||||
.poll-show-breakdown {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.poll-group-by-selector {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.poll-grouped-pie-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 15px 0;
|
||||
|
||||
.poll-pie-label {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.poll.pie {
|
||||
.poll-container {
|
||||
display: inline-block;
|
||||
height: 310px;
|
||||
max-height: 310px;
|
||||
height: 320px;
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
|
||||
.poll-grouped-pie-container {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
.poll-info {
|
||||
display: inline-block;
|
||||
|
|
|
@ -49,11 +49,8 @@ div.poll {
|
|||
div.poll.pie {
|
||||
.poll-container {
|
||||
width: calc(100% - 190px);
|
||||
|
||||
.poll-grouped-pie-container {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-info {
|
||||
display: inline-block;
|
||||
width: 150px;
|
||||
|
|
|
@ -28,15 +28,15 @@ div.poll {
|
|||
margin-left: 0.25em;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-show-breakdown {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
div.poll.pie {
|
||||
.poll-container {
|
||||
width: calc(100% - 30px);
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
|
||||
.poll-grouped-pie-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,10 +52,6 @@ en:
|
|||
title: "Group votes by user field"
|
||||
label: "Show breakdown"
|
||||
|
||||
ungroup-results:
|
||||
title: "Combine all votes"
|
||||
label: "Hide breakdown"
|
||||
|
||||
export-results:
|
||||
title: "Export the poll results"
|
||||
label: "Export"
|
||||
|
@ -74,6 +70,13 @@ en:
|
|||
closes_in: "Closes in <strong>%{timeLeft}</strong>."
|
||||
age: "Closed <strong>%{age}</strong>"
|
||||
|
||||
breakdown:
|
||||
title: "Poll results"
|
||||
votes: "%{count} votes"
|
||||
breakdown: "Breakdown"
|
||||
percentage: "Percentage"
|
||||
count: "Count"
|
||||
|
||||
error_while_toggling_status: "Sorry, there was an error toggling the status of this poll."
|
||||
error_while_casting_votes: "Sorry, there was an error casting your votes."
|
||||
error_while_fetching_voters: "Sorry, there was an error displaying the voters."
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
register_asset "stylesheets/common/poll.scss"
|
||||
register_asset "stylesheets/common/poll-ui-builder.scss"
|
||||
register_asset "stylesheets/common/poll-breakdown.scss"
|
||||
register_asset "stylesheets/desktop/poll.scss", :desktop
|
||||
register_asset "stylesheets/mobile/poll.scss", :mobile
|
||||
register_asset "stylesheets/mobile/poll-ui-builder.scss", :mobile
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
import { acceptance } from "helpers/qunit-helpers";
|
||||
import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer";
|
||||
import { Promise } from "rsvp";
|
||||
|
||||
acceptance("Poll breakdown", {
|
||||
loggedIn: true,
|
||||
settings: { poll_enabled: true, poll_groupable_user_fields: "something" },
|
||||
beforeEach() {
|
||||
clearPopupMenuOptionsCallback();
|
||||
},
|
||||
pretend(server, helper) {
|
||||
server.get("/polls/grouped_poll_results.json", () => {
|
||||
return new Promise(resolve => {
|
||||
resolve(
|
||||
helper.response({
|
||||
grouped_results: [
|
||||
{
|
||||
group: "Engineering",
|
||||
options: [
|
||||
{
|
||||
digest: "687a1ccf3c6a260f9aeeb7f68a1d463c",
|
||||
html: "This Is",
|
||||
votes: 1
|
||||
},
|
||||
{
|
||||
digest: "9377906763a1221d31d656ea0c4a4495",
|
||||
html: "A test for sure",
|
||||
votes: 1
|
||||
},
|
||||
{
|
||||
digest: "ecf47c65a85a0bb20029072b1b721977",
|
||||
html: "Why not give it some more",
|
||||
votes: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: "Marketing",
|
||||
options: [
|
||||
{
|
||||
digest: "687a1ccf3c6a260f9aeeb7f68a1d463c",
|
||||
html: "This Is",
|
||||
votes: 1
|
||||
},
|
||||
{
|
||||
digest: "9377906763a1221d31d656ea0c4a4495",
|
||||
html: "A test for sure",
|
||||
votes: 1
|
||||
},
|
||||
{
|
||||
digest: "ecf47c65a85a0bb20029072b1b721977",
|
||||
html: "Why not give it some more",
|
||||
votes: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("Displaying the poll breakdown modal", async assert => {
|
||||
await visit("/t/-/topic_with_pie_chart_poll");
|
||||
|
||||
assert.equal(
|
||||
find(".poll-show-breakdown").text(),
|
||||
"Show breakdown",
|
||||
"shows the breakdown button when poll_groupable_user_fields is non-empty"
|
||||
);
|
||||
|
||||
await click(".poll-show-breakdown:first");
|
||||
|
||||
assert.equal(
|
||||
find(".poll-breakdown-total-votes")[0].textContent.trim(),
|
||||
"2 votes",
|
||||
"display the correct total vote count"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".poll-breakdown-chart-container").length,
|
||||
2,
|
||||
"renders a chart for each of the groups in group_results response"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
find(".poll-breakdown-chart-container > canvas")[0].$chartjs,
|
||||
"$chartjs is defined on the pie charts"
|
||||
);
|
||||
});
|
||||
|
||||
test("Changing the display mode from percentage to count", async assert => {
|
||||
await visit("/t/-/topic_with_pie_chart_poll");
|
||||
await click(".poll-show-breakdown:first");
|
||||
|
||||
assert.equal(
|
||||
find(".poll-breakdown-option-count:first")[0].textContent.trim(),
|
||||
"40.0%",
|
||||
"displays the correct vote percentage"
|
||||
);
|
||||
|
||||
await click(".modal-tabs .count");
|
||||
|
||||
assert.equal(
|
||||
find(".poll-breakdown-option-count:first")[0].textContent.trim(),
|
||||
"2",
|
||||
"displays the correct vote count"
|
||||
);
|
||||
|
||||
await click(".modal-tabs .percentage");
|
||||
|
||||
assert.equal(
|
||||
find(".poll-breakdown-option-count:last")[0].textContent.trim(),
|
||||
"20.0%",
|
||||
"displays the percentage again"
|
||||
);
|
||||
});
|
|
@ -1,68 +1,11 @@
|
|||
import { acceptance } from "helpers/qunit-helpers";
|
||||
import { clearPopupMenuOptionsCallback } from "discourse/controllers/composer";
|
||||
import { Promise } from "rsvp";
|
||||
|
||||
acceptance("Rendering polls with pie charts - desktop", {
|
||||
loggedIn: true,
|
||||
settings: { poll_enabled: true, poll_groupable_user_fields: "something" },
|
||||
beforeEach() {
|
||||
clearPopupMenuOptionsCallback();
|
||||
},
|
||||
pretend(server, helper) {
|
||||
server.get("/polls/grouped_poll_results.json", () => {
|
||||
return new Promise(resolve => {
|
||||
resolve(
|
||||
helper.response({
|
||||
grouped_results: [
|
||||
{
|
||||
group: "Engineering",
|
||||
options: [
|
||||
{
|
||||
digest: "687a1ccf3c6a260f9aeeb7f68a1d463c",
|
||||
html: "This Is",
|
||||
votes: 1
|
||||
},
|
||||
{
|
||||
digest: "9377906763a1221d31d656ea0c4a4495",
|
||||
html: "A test for sure",
|
||||
votes: 1
|
||||
},
|
||||
{
|
||||
digest: "ecf47c65a85a0bb20029072b1b721977",
|
||||
html: "Why not give it some more",
|
||||
votes: 1
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
group: "Marketing",
|
||||
options: [
|
||||
{
|
||||
digest: "687a1ccf3c6a260f9aeeb7f68a1d463c",
|
||||
html: "This Is",
|
||||
votes: 1
|
||||
},
|
||||
{
|
||||
digest: "9377906763a1221d31d656ea0c4a4495",
|
||||
html: "A test for sure",
|
||||
votes: 1
|
||||
},
|
||||
{
|
||||
digest: "ecf47c65a85a0bb20029072b1b721977",
|
||||
html: "Why not give it some more",
|
||||
votes: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
settings: { poll_enabled: true, poll_groupable_user_fields: "something" }
|
||||
});
|
||||
|
||||
test("Polls", async assert => {
|
||||
test("Displays the pie chart", async assert => {
|
||||
await visit("/t/-/topic_with_pie_chart_poll");
|
||||
|
||||
const poll = find(".poll")[0];
|
||||
|
@ -90,39 +33,4 @@ test("Polls", async assert => {
|
|||
1,
|
||||
"Renders the chart div instead of bar container"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".poll-group-by-toggle").text(),
|
||||
"Show breakdown",
|
||||
"Shows the breakdown button when poll_groupable_user_fields is non-empty"
|
||||
);
|
||||
|
||||
await click(".poll-group-by-toggle:first");
|
||||
|
||||
assert.equal(
|
||||
find(".poll-group-by-toggle").text(),
|
||||
"Hide breakdown",
|
||||
"Shows the combine breakdown button after toggle is clicked"
|
||||
);
|
||||
|
||||
// Double click to make sure the state toggles back to combined view
|
||||
await click(".toggle-results:first");
|
||||
await click(".toggle-results:first");
|
||||
|
||||
assert.equal(
|
||||
find(".poll-group-by-toggle").text(),
|
||||
"Hide breakdown",
|
||||
"Returns to the grouped view, after toggling results shown"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
find(".poll-grouped-pie-container").length,
|
||||
2,
|
||||
"Renders a chart for each of the groups in group_results response"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
find(".poll-grouped-pie-container > canvas")[0].$chartjs,
|
||||
"$chartjs is defined on the pie charts"
|
||||
);
|
||||
});
|
||||
|
|
7
public/javascripts/chartjs-plugin-datalabels.min.js
vendored
Normal file
7
public/javascripts/chartjs-plugin-datalabels.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -623,6 +623,11 @@ chartjs-color@^2.1.0:
|
|||
chartjs-color-string "^0.6.0"
|
||||
color-convert "^1.9.3"
|
||||
|
||||
chartjs-plugin-datalabels@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/chartjs-plugin-datalabels/-/chartjs-plugin-datalabels-0.7.0.tgz#f72e44edb2db45ef68913e9320bcc50398a205e6"
|
||||
integrity sha512-PKVUX14nYhH0wcdCpgOoC39Gbzvn6cZ7O9n+bwc02yKD9FTnJ7/TSrBcfebmolFZp1Rcicr9xbT0a5HUbigS7g==
|
||||
|
||||
chrome-launcher@^0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/chrome-launcher/-/chrome-launcher-0.12.0.tgz#08db81ef0f7b283c331df2c350e780c38bd0ce3a"
|
||||
|
|
Loading…
Reference in New Issue
Block a user