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