diff --git a/app/assets/javascripts/discourse/app/components/modal-tab.js b/app/assets/javascripts/discourse/app/components/modal-tab.js
index 3211fbbdd2..a798e22d6d 100644
--- a/app/assets/javascripts/discourse/app/components/modal-tab.js
+++ b/app/assets/javascripts/discourse/app/components/modal-tab.js
@@ -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);
+ }
}
});
diff --git a/app/assets/javascripts/discourse/app/lib/show-modal.js b/app/assets/javascripts/discourse/app/lib/show-modal.js
index 37c6b8efe3..6a6fb17a71 100644
--- a/app/assets/javascripts/discourse/app/lib/show-modal.js
+++ b/app/assets/javascripts/discourse/app/lib/show-modal.js
@@ -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(
diff --git a/app/assets/javascripts/discourse/app/mixins/modal-functionality.js b/app/assets/javascripts/discourse/app/mixins/modal-functionality.js
index 70b5bd010c..ac40b161b5 100644
--- a/app/assets/javascripts/discourse/app/mixins/modal-functionality.js
+++ b/app/assets/javascripts/discourse/app/mixins/modal-functionality.js
@@ -18,10 +18,6 @@ export default Mixin.create({
closeModal() {
this.modal.send("closeModal");
this.set("panels", []);
- },
-
- onSelectPanel(panel) {
- this.set("selectedPanel", panel);
}
}
});
diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss
index 178be0a01d..fc09f8990d 100644
--- a/app/assets/stylesheets/common/base/modal.scss
+++ b/app/assets/stylesheets/common/base/modal.scss
@@ -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;
diff --git a/lib/tasks/javascript.rake b/lib/tasks/javascript.rake
index c7c745242a..25392df903 100644
--- a/lib/tasks/javascript.rake
+++ b/lib/tasks/javascript.rake
@@ -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
diff --git a/package.json b/package.json
index 9ce74d66f8..e73fee2fbc 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/plugins/poll/assets/javascripts/components/poll-breakdown-chart.js.es6 b/plugins/poll/assets/javascripts/components/poll-breakdown-chart.js.es6
new file mode 100644
index 0000000000..ea9a34559d
--- /dev/null
+++ b/plugins/poll/assets/javascripts/components/poll-breakdown-chart.js.es6
@@ -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();
+ }
+});
diff --git a/plugins/poll/assets/javascripts/components/poll-breakdown-option.js.es6 b/plugins/poll/assets/javascripts/components/poll-breakdown-option.js.es6
new file mode 100644
index 0000000000..f1816b13f9
--- /dev/null
+++ b/plugins/poll/assets/javascripts/components/poll-breakdown-option.js.es6
@@ -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();
+ }
+ }
+});
diff --git a/plugins/poll/assets/javascripts/controllers/poll-breakdown.js.es6 b/plugins/poll/assets/javascripts/controllers/poll-breakdown.js.es6
new file mode 100644
index 0000000000..f5cb61bcba
--- /dev/null
+++ b/plugins/poll/assets/javascripts/controllers/poll-breakdown.js.es6
@@ -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);
+ }
+});
diff --git a/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6 b/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6
index 0dc7325150..2c0c4ff36e 100644
--- a/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6
+++ b/plugins/poll/assets/javascripts/controllers/poll-ui-builder.js.es6
@@ -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";
diff --git a/plugins/poll/assets/javascripts/discourse/templates/components/poll-breakdown-chart.hbs b/plugins/poll/assets/javascripts/discourse/templates/components/poll-breakdown-chart.hbs
new file mode 100644
index 0000000000..9e91bc41e7
--- /dev/null
+++ b/plugins/poll/assets/javascripts/discourse/templates/components/poll-breakdown-chart.hbs
@@ -0,0 +1,2 @@
+
+
diff --git a/plugins/poll/assets/javascripts/discourse/templates/components/poll-breakdown-option.hbs b/plugins/poll/assets/javascripts/discourse/templates/components/poll-breakdown-option.hbs
new file mode 100644
index 0000000000..b56106982e
--- /dev/null
+++ b/plugins/poll/assets/javascripts/discourse/templates/components/poll-breakdown-option.hbs
@@ -0,0 +1,17 @@
+
+
+
+
+ {{#if showPercentage}}
+ {{this.percent}}%
+ {{else}}
+ {{@option.votes}}
+ {{/if}}
+
+ {{{@option.html}}}
+
diff --git a/plugins/poll/assets/javascripts/discourse/templates/modal/poll-breakdown.hbs b/plugins/poll/assets/javascripts/discourse/templates/modal/poll-breakdown.hbs
new file mode 100644
index 0000000000..0cb26c08d5
--- /dev/null
+++ b/plugins/poll/assets/javascripts/discourse/templates/modal/poll-breakdown.hbs
@@ -0,0 +1,49 @@
+{{#d-modal-body title="poll.breakdown.title"}}
+
+
+
+
+
+
+ {{combo-box
+ content=this.groupableUserFields
+ value=this.groupedBy
+ nameProperty="label"
+ class="poll-breakdown-dropdown"
+ onChange=(action this.setGrouping)
+ }}
+
+
+
+ {{#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}}
+
+
+{{/d-modal-body}}
diff --git a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6 b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6
index d9da92d252..eae72fff0b 100644
--- a/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6
+++ b/plugins/poll/assets/javascripts/initializers/add-poll-ui-builder.js.es6
@@ -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", {
diff --git a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6 b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
index 13b209dd50..897d37eee5 100644
--- a/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
+++ b/plugins/poll/assets/javascripts/initializers/extend-for-poll.js.es6
@@ -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);
diff --git a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6 b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6
index 96f6f32003..acff44d246 100644
--- a/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6
+++ b/plugins/poll/assets/javascripts/widgets/discourse-poll.js.es6
@@ -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 = $(`${option.html}`);
@@ -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" }
+ ]
+ });
}
});
diff --git a/plugins/poll/assets/stylesheets/common/poll-breakdown.scss b/plugins/poll/assets/stylesheets/common/poll-breakdown.scss
new file mode 100644
index 0000000000..c178c7bd38
--- /dev/null
+++ b/plugins/poll/assets/stylesheets/common/poll-breakdown.scss
@@ -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;
+}
diff --git a/plugins/poll/assets/stylesheets/common/poll.scss b/plugins/poll/assets/stylesheets/common/poll.scss
index e99f0c4e2f..a40bb78917 100644
--- a/plugins/poll/assets/stylesheets/common/poll.scss
+++ b/plugins/poll/assets/stylesheets/common/poll.scss
@@ -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;
diff --git a/plugins/poll/assets/stylesheets/desktop/poll.scss b/plugins/poll/assets/stylesheets/desktop/poll.scss
index 436dc952bf..bbfffda269 100644
--- a/plugins/poll/assets/stylesheets/desktop/poll.scss
+++ b/plugins/poll/assets/stylesheets/desktop/poll.scss
@@ -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;
diff --git a/plugins/poll/assets/stylesheets/mobile/poll.scss b/plugins/poll/assets/stylesheets/mobile/poll.scss
index 596c036d8e..037dfb7584 100644
--- a/plugins/poll/assets/stylesheets/mobile/poll.scss
+++ b/plugins/poll/assets/stylesheets/mobile/poll.scss
@@ -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%;
- }
}
}
diff --git a/plugins/poll/config/locales/client.en.yml b/plugins/poll/config/locales/client.en.yml
index 03a736e69d..856009b504 100644
--- a/plugins/poll/config/locales/client.en.yml
+++ b/plugins/poll/config/locales/client.en.yml
@@ -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 %{timeLeft}."
age: "Closed %{age}"
+ 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."
diff --git a/plugins/poll/plugin.rb b/plugins/poll/plugin.rb
index 8331b8fca6..1dee522602 100644
--- a/plugins/poll/plugin.rb
+++ b/plugins/poll/plugin.rb
@@ -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
diff --git a/plugins/poll/test/javascripts/acceptance/poll-breakdown-test.js.es6 b/plugins/poll/test/javascripts/acceptance/poll-breakdown-test.js.es6
new file mode 100644
index 0000000000..4b76ad90c7
--- /dev/null
+++ b/plugins/poll/test/javascripts/acceptance/poll-breakdown-test.js.es6
@@ -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"
+ );
+});
diff --git a/plugins/poll/test/javascripts/acceptance/poll-pie-chart-test.js.es6 b/plugins/poll/test/javascripts/acceptance/poll-pie-chart-test.js.es6
index 379a9554ad..02696d2035 100644
--- a/plugins/poll/test/javascripts/acceptance/poll-pie-chart-test.js.es6
+++ b/plugins/poll/test/javascripts/acceptance/poll-pie-chart-test.js.es6
@@ -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"
- );
});
diff --git a/public/javascripts/chartjs-plugin-datalabels.min.js b/public/javascripts/chartjs-plugin-datalabels.min.js
new file mode 100644
index 0000000000..75eb420336
--- /dev/null
+++ b/public/javascripts/chartjs-plugin-datalabels.min.js
@@ -0,0 +1,7 @@
+/*!
+ * chartjs-plugin-datalabels v0.7.0
+ * https://chartjs-plugin-datalabels.netlify.com
+ * (c) 2019 Chart.js Contributors
+ * Released under the MIT license
+ */
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e(require("chart.js")):"function"==typeof define&&define.amd?define(["chart.js"],e):(t=t||self).ChartDataLabels=e(t.Chart)}(this,function(t){"use strict";var e=(t=t&&t.hasOwnProperty("default")?t.default:t).helpers,r=function(){if("undefined"!=typeof window){if(window.devicePixelRatio)return window.devicePixelRatio;var t=window.screen;if(t)return(t.deviceXDPI||1)/(t.logicalXDPI||1)}return 1}(),n={toTextLines:function(t){var r,n=[];for(t=[].concat(t);t.length;)"string"==typeof(r=t.pop())?n.unshift.apply(n,r.split("\n")):Array.isArray(r)?t.push.apply(t,r):e.isNullOrUndef(t)||n.unshift(""+r);return n},toFontString:function(t){return!t||e.isNullOrUndef(t.size)||e.isNullOrUndef(t.family)?null:(t.style?t.style+" ":"")+(t.weight?t.weight+" ":"")+t.size+"px "+t.family},textSize:function(t,e,r){var n,i=[].concat(e),o=i.length,a=t.font,l=0;for(t.font=r.string,n=0;nr.right&&(n|=l),er.bottom&&(n|=s),n}function d(t,e){var r,n,i=e.anchor,o=t;return e.clamp&&(o=function(t,e){for(var r,n,i,o=t.x0,d=t.y0,c=t.x1,h=t.y1,x=f(o,d,e),y=f(c,h,e);x|y&&!(x&y);)(r=x||y)&u?(n=o+(c-o)*(e.top-d)/(h-d),i=e.top):r&s?(n=o+(c-o)*(e.bottom-d)/(h-d),i=e.bottom):r&l?(i=d+(h-d)*(e.right-o)/(c-o),n=e.right):r&a&&(i=d+(h-d)*(e.left-o)/(c-o),n=e.left),r===x?x=f(o=n,d=i,e):y=f(c=n,h=i,e);return{x0:o,x1:c,y0:d,y1:h}}(o,e.area)),"start"===i?(r=o.x0,n=o.y0):"end"===i?(r=o.x1,n=o.y1):(r=(o.x0+o.x1)/2,n=(o.y0+o.y1)/2),function(t,e,r,n,i){switch(i){case"center":r=n=0;break;case"bottom":r=0,n=1;break;case"right":r=1,n=0;break;case"left":r=-1,n=0;break;case"top":r=0,n=-1;break;case"start":r=-r,n=-n;break;case"end":break;default:i*=Math.PI/180,r=Math.cos(i),n=Math.sin(i)}return{x:t,y:e,vx:r,vy:n}}(r,n,t.vx,t.vy,e.align)}var c={arc:function(t,e){var r=(t.startAngle+t.endAngle)/2,n=Math.cos(r),i=Math.sin(r),o=t.innerRadius,a=t.outerRadius;return d({x0:t.x+n*o,y0:t.y+i*o,x1:t.x+n*a,y1:t.y+i*a,vx:n,vy:i},e)},point:function(t,e){var r=i(t,e.origin),n=r.x*t.radius,o=r.y*t.radius;return d({x0:t.x-n,y0:t.y-o,x1:t.x+n,y1:t.y+o,vx:r.x,vy:r.y},e)},rect:function(t,e){var r=i(t,e.origin),n=t.x,o=t.y,a=0,l=0;return t.horizontal?(n=Math.min(t.x,t.base),a=Math.abs(t.base-t.x)):(o=Math.min(t.y,t.base),l=Math.abs(t.base-t.y)),d({x0:n,y0:o+l,x1:n+a,y1:o,vx:r.x,vy:r.y},e)},fallback:function(t,e){var r=i(t,e.origin);return d({x0:t.x,y0:t.y,x1:t.x,y1:t.y,vx:r.x,vy:r.y},e)}},h=t.helpers,x=n.rasterize;function y(t){var e=t._model.horizontal,r=t._scale||e&&t._xScale||t._yScale;if(!r)return null;if(void 0!==r.xCenter&&void 0!==r.yCenter)return{x:r.xCenter,y:r.yCenter};var n=r.getBasePixel();return e?{x:n,y:null}:{x:null,y:n}}function v(t,e,r){var n=t.shadowBlur,i=r.stroked,o=x(r.x),a=x(r.y),l=x(r.w);i&&t.strokeText(e,o,a,l),r.filled&&(n&&i&&(t.shadowBlur=0),t.fillText(e,o,a,l),n&&i&&(t.shadowBlur=n))}var _=function(t,e,r,n){var i=this;i._config=t,i._index=n,i._model=null,i._rects=null,i._ctx=e,i._el=r};h.extend(_.prototype,{_modelize:function(e,r,i,o){var a,l=this._index,s=h.options.resolve,u=n.parseFont(s([i.font,{}],o,l)),f=s([i.color,t.defaults.global.defaultFontColor],o,l);return{align:s([i.align,"center"],o,l),anchor:s([i.anchor,"center"],o,l),area:o.chart.chartArea,backgroundColor:s([i.backgroundColor,null],o,l),borderColor:s([i.borderColor,null],o,l),borderRadius:s([i.borderRadius,0],o,l),borderWidth:s([i.borderWidth,0],o,l),clamp:s([i.clamp,!1],o,l),clip:s([i.clip,!1],o,l),color:f,display:e,font:u,lines:r,offset:s([i.offset,0],o,l),opacity:s([i.opacity,1],o,l),origin:y(this._el),padding:h.options.toPadding(s([i.padding,0],o,l)),positioner:(a=this._el,a instanceof t.elements.Arc?c.arc:a instanceof t.elements.Point?c.point:a instanceof t.elements.Rectangle?c.rect:c.fallback),rotation:s([i.rotation,0],o,l)*(Math.PI/180),size:n.textSize(this._ctx,r,u),textAlign:s([i.textAlign,"start"],o,l),textShadowBlur:s([i.textShadowBlur,0],o,l),textShadowColor:s([i.textShadowColor,f],o,l),textStrokeColor:s([i.textStrokeColor,f],o,l),textStrokeWidth:s([i.textStrokeWidth,0],o,l)}},update:function(t){var e,r,i,o=this,a=null,l=null,s=o._index,u=o._config,f=h.options.resolve([u.display,!0],t,s);f&&(e=t.dataset.data[s],r=h.valueOrDefault(h.callback(u.formatter,[e,t]),e),(i=h.isNullOrUndef(r)?[]:n.toTextLines(r)).length&&(l=function(t){var e=t.borderWidth||0,r=t.padding,n=t.size.height,i=t.size.width,o=-i/2,a=-n/2;return{frame:{x:o-r.left-e,y:a-r.top-e,w:i+r.width+2*e,h:n+r.height+2*e},text:{x:o,y:a,w:i,h:n}}}(a=o._modelize(f,i,u,t)))),o._model=a,o._rects=l},geometry:function(){return this._rects?this._rects.frame:{}},rotation:function(){return this._model?this._model.rotation:0},visible:function(){return this._model&&this._model.opacity},model:function(){return this._model},draw:function(t,e){var r,i=t.ctx,o=this._model,a=this._rects;this.visible()&&(i.save(),o.clip&&(r=o.area,i.beginPath(),i.rect(r.left,r.top,r.right-r.left,r.bottom-r.top),i.clip()),i.globalAlpha=n.bound(0,o.opacity,1),i.translate(x(e.x),x(e.y)),i.rotate(o.rotation),function(t,e,r){var n=r.backgroundColor,i=r.borderColor,o=r.borderWidth;(n||i&&o)&&(t.beginPath(),h.canvas.roundedRect(t,x(e.x)+o/2,x(e.y)+o/2,x(e.w)-o,x(e.h)-o,r.borderRadius),t.closePath(),n&&(t.fillStyle=n,t.fill()),i&&o&&(t.strokeStyle=i,t.lineWidth=o,t.lineJoin="miter",t.stroke()))}(i,a.frame,o),function(t,e,r,n){var i,o=n.textAlign,a=n.color,l=!!a,s=n.font,u=e.length,f=n.textStrokeColor,d=n.textStrokeWidth,c=f&&d;if(u&&(l||c))for(r=function(t,e,r){var n=r.lineHeight,i=t.w,o=t.x;return"center"===e?o+=i/2:"end"!==e&&"right"!==e||(o+=i),{h:n,w:i,x:o,y:t.y+n/2}}(r,o,s),t.font=s.string,t.textAlign=o,t.textBaseline="middle",t.shadowBlur=n.textShadowBlur,t.shadowColor=n.textShadowColor,l&&(t.fillStyle=a),c&&(t.lineJoin="round",t.lineWidth=d,t.strokeStyle=f),i=0,u=e.length;ie.x+e.w+2||t.y>e.y+e.h+2)},intersects:function(t){var e,r,n,i=this._points(),o=t._points(),a=[k(i[0],i[1]),k(i[0],i[3])];for(this._rotation!==t._rotation&&a.push(k(o[0],o[1]),k(o[0],o[3])),e=0;e=0;--r)for(i=t[r].$layout,n=r-1;n>=0&&i._visible;--n)(o=t[n].$layout)._visible&&i._box.intersects(o._box)&&e(i,o)})(t,function(t,e){var r=t._hidable,n=e._hidable;r&&n||n?e._visible=!1:r&&(t._visible=!1)})}(t)},lookup:function(t,e){var r,n;for(r=t.length-1;r>=0;--r)if((n=t[r].$layout)&&n._visible&&n._box.contains(e))return t[r];return null},draw:function(t,e){var r,n,i,o,a,l;for(r=0,n=e.length;r