From 18ac6003529ad1a861b56de52267a05202b19b9e Mon Sep 17 00:00:00 2001 From: Keegan George Date: Fri, 2 Aug 2024 09:40:27 -0700 Subject: [PATCH] DEV: Add stacked line chart mode (#28203) --- .../admin-report-stacked-line-chart.hbs | 3 + .../admin-report-stacked-line-chart.js | 179 ++++++++++++++++++ .../javascripts/admin/addon/models/report.js | 5 +- .../stylesheets/common/admin/admin_base.scss | 1 + .../admin_report_stacked_line_chart.scss | 5 + app/models/report.rb | 1 + 6 files changed, 193 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/admin/addon/components/admin-report-stacked-line-chart.hbs create mode 100644 app/assets/javascripts/admin/addon/components/admin-report-stacked-line-chart.js create mode 100644 app/assets/stylesheets/common/admin/admin_report_stacked_line_chart.scss diff --git a/app/assets/javascripts/admin/addon/components/admin-report-stacked-line-chart.hbs b/app/assets/javascripts/admin/addon/components/admin-report-stacked-line-chart.hbs new file mode 100644 index 00000000000..a5fb156ce96 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-report-stacked-line-chart.hbs @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/app/assets/javascripts/admin/addon/components/admin-report-stacked-line-chart.js b/app/assets/javascripts/admin/addon/components/admin-report-stacked-line-chart.js new file mode 100644 index 00000000000..fa1b09d8978 --- /dev/null +++ b/app/assets/javascripts/admin/addon/components/admin-report-stacked-line-chart.js @@ -0,0 +1,179 @@ +import Component from "@ember/component"; +import { schedule } from "@ember/runloop"; +import { classNames } from "@ember-decorators/component"; +import { number } from "discourse/lib/formatter"; +import loadScript from "discourse/lib/load-script"; +import discourseDebounce from "discourse-common/lib/debounce"; +import { makeArray } from "discourse-common/lib/helpers"; +import { bind } from "discourse-common/utils/decorators"; +import Report from "admin/models/report"; + +@classNames("admin-report-chart", "admin-report-stacked-line-chart") +export default class AdminReportStackedLineChart extends Component { + didInsertElement() { + super.didInsertElement(...arguments); + + window.addEventListener("resize", this._resizeHandler); + } + + willDestroyElement() { + super.willDestroyElement(...arguments); + + window.removeEventListener("resize", this._resizeHandler); + this._resetChart(); + } + + didReceiveAttrs() { + super.didReceiveAttrs(...arguments); + + discourseDebounce(this, this._scheduleChartRendering, 100); + } + + @bind + _resizeHandler() { + discourseDebounce(this, this._scheduleChartRendering, 500); + } + + _scheduleChartRendering() { + schedule("afterRender", () => { + if (!this.element) { + return; + } + + this._renderChart( + this.model, + this.element.querySelector(".chart-canvas") + ); + }); + } + + _renderChart(model, chartCanvas) { + if (!chartCanvas) { + return; + } + + const context = chartCanvas.getContext("2d"); + + const chartData = makeArray(model.chartData || model.data).map((cd) => { + return { + label: cd.label, + color: cd.color, + data: Report.collapse(model, cd.data), + }; + }); + + const data = { + labels: chartData[0].data.mapBy("x"), + datasets: chartData.map((cd) => { + return { + label: cd.label, + stack: "pageviews-stack", + data: cd.data, + fill: true, + backgroundColor: this._hexToRGBA(cd.color, 0.3), + borderColor: cd.color, + pointBackgroundColor: cd.color, + pointBorderColor: "#fff", + pointHoverBackgroundColor: "#fff", + pointHoverBorderColor: cd.color, + }; + }), + }; + + loadScript("/javascripts/Chart.min.js").then(() => { + this._resetChart(); + + this._chart = new window.Chart(context, this._buildChartConfig(data)); + }); + } + + _buildChartConfig(data) { + return { + type: "line", + data, + options: { + responsive: true, + maintainAspectRatio: false, + responsiveAnimationDuration: 0, + hover: { mode: "index" }, + animation: { + duration: 0, + }, + plugins: { + tooltip: { + mode: "index", + intersect: false, + callbacks: { + beforeFooter: (tooltipItem) => { + let total = 0; + tooltipItem.forEach( + (item) => (total += parseInt(item.parsed.y || 0, 10)) + ); + return `= ${total}`; + }, + title: (tooltipItem) => + moment(tooltipItem[0].label, "YYYY-MM-DD").format("LL"), + }, + }, + }, + + layout: { + padding: { + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + }, + scales: { + y: [ + { + stacked: true, + display: true, + ticks: { + callback: (label) => number(label), + sampleSize: 5, + maxRotation: 25, + minRotation: 25, + }, + }, + ], + x: [ + { + display: true, + gridLines: { display: false }, + type: "time", + time: { + unit: Report.unitForDatapoints(data.labels.length), + }, + ticks: { + sampleSize: 5, + maxRotation: 50, + minRotation: 50, + }, + }, + ], + }, + }, + }; + } + + _resetChart() { + this._chart?.destroy(); + this._chart = null; + } + + _hexToRGBA(hexCode, opacity) { + let hex = hexCode.replace("#", ""); + + if (hex.length === 3) { + hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2]; + } + + const r = parseInt(hex.substring(0, 2), 16), + g = parseInt(hex.substring(2, 4), 16), + b = parseInt(hex.substring(4, 6), 16); + + return `rgba(${r},${g},${b}, ${opacity})`; + } +} diff --git a/app/assets/javascripts/admin/addon/models/report.js b/app/assets/javascripts/admin/addon/models/report.js index 3e5a29ff056..09eb1a132b0 100644 --- a/app/assets/javascripts/admin/addon/models/report.js +++ b/app/assets/javascripts/admin/addon/models/report.js @@ -141,7 +141,10 @@ export default class Report extends EmberObject { .locale("en") .format("YYYY-MM-DD"); - if (report.modes[0] === "stacked_chart") { + if ( + report.modes[0] === "stacked_chart" || + report.modes[0] === "stacked_line_chart" + ) { report[filledField] = report[dataField].map((rep) => { return { req: rep.req, diff --git a/app/assets/stylesheets/common/admin/admin_base.scss b/app/assets/stylesheets/common/admin/admin_base.scss index d9eabe9f8da..51416738695 100644 --- a/app/assets/stylesheets/common/admin/admin_base.scss +++ b/app/assets/stylesheets/common/admin/admin_base.scss @@ -1074,6 +1074,7 @@ a.inline-editable-field { @import "common/admin/admin_report_chart"; @import "common/admin/admin_report_radar"; @import "common/admin/admin_report_stacked_chart"; +@import "common/admin/admin_report_stacked_line_chart"; @import "common/admin/admin_report_table"; @import "common/admin/admin_report_inline_table"; @import "common/admin/admin_intro"; diff --git a/app/assets/stylesheets/common/admin/admin_report_stacked_line_chart.scss b/app/assets/stylesheets/common/admin/admin_report_stacked_line_chart.scss new file mode 100644 index 00000000000..1b671705c3e --- /dev/null +++ b/app/assets/stylesheets/common/admin/admin_report_stacked_line_chart.scss @@ -0,0 +1,5 @@ +.admin-report-stacked-line-chart { + .chart-canvas-container { + height: 250px; + } +} diff --git a/app/models/report.rb b/app/models/report.rb index 613d4bc585c..cde970a0b62 100644 --- a/app/models/report.rb +++ b/app/models/report.rb @@ -428,6 +428,7 @@ class Report purple: "#721D8D", magenta: "#E84A5F", brown: "#8A6916", + yellow: "#FFCD56", } end