diff --git a/app/assets/javascripts/admin/components/admin-report-chart.js b/app/assets/javascripts/admin/components/admin-report-chart.js index 7af8ce04745..3232b03519d 100644 --- a/app/assets/javascripts/admin/components/admin-report-chart.js +++ b/app/assets/javascripts/admin/components/admin-report-chart.js @@ -8,6 +8,7 @@ export default Component.extend({ classNames: ["admin-report-chart"], limit: 8, total: 0, + options: null, init() { this._super(...arguments); @@ -49,7 +50,11 @@ export default Component.extend({ if (!chartCanvas) return; const context = chartCanvas.getContext("2d"); - const chartData = makeArray(model.get("chartData") || model.get("data")); + const chartData = this._applyChartGrouping( + model, + makeArray(model.get("chartData") || model.get("data"), "weekly"), + this.options + ); const prevChartData = makeArray( model.get("prevChartData") || model.get("prev_data") ); @@ -91,11 +96,14 @@ export default Component.extend({ return; } - this._chart = new window.Chart(context, this._buildChartConfig(data)); + this._chart = new window.Chart( + context, + this._buildChartConfig(data, this.options) + ); }); }, - _buildChartConfig(data) { + _buildChartConfig(data, options) { return { type: "line", data, @@ -144,7 +152,7 @@ export default Component.extend({ gridLines: { display: false }, type: "time", time: { - parser: "YYYY-MM-DD" + unit: this._unitForGrouping(options) }, ticks: { sampleSize: 5, @@ -163,5 +171,65 @@ export default Component.extend({ this._chart.destroy(); this._chart = null; } + }, + + _applyChartGrouping(model, data, options) { + if (!options.chartGrouping || options.chartGrouping === "daily") { + return data; + } + + if ( + options.chartGrouping === "weekly" || + options.chartGrouping === "monthly" + ) { + const isoKind = options.chartGrouping === "weekly" ? "isoWeek" : "month"; + const kind = options.chartGrouping === "weekly" ? "week" : "month"; + const startMoment = moment(model.start_date, "YYYY-MM-DD"); + + let currentIndex = 0; + let currentStart = startMoment.clone().startOf(isoKind); + let currentEnd = startMoment.clone().endOf(isoKind); + const transformedData = [ + { + x: currentStart.format("YYYY-MM-DD"), + y: 0 + } + ]; + + data.forEach(d => { + let date = moment(d.x, "YYYY-MM-DD"); + + if (!date.isBetween(currentStart, currentEnd)) { + currentIndex += 1; + currentStart = currentStart.add(1, kind).startOf(isoKind); + currentEnd = currentEnd.add(1, kind).endOf(isoKind); + } + + if (transformedData[currentIndex]) { + transformedData[currentIndex].y += d.y; + } else { + transformedData[currentIndex] = { + x: d.x, + y: d.y + }; + } + }); + + return transformedData; + } + + // ensure we return something if grouping is unknown + return data; + }, + + _unitForGrouping(options) { + switch (options.chartGrouping) { + case "monthly": + return "month"; + case "weekly": + return "week"; + default: + return "day"; + } } }); diff --git a/app/assets/javascripts/admin/components/admin-report.js b/app/assets/javascripts/admin/components/admin-report.js index e64e02f3ebb..234959f1753 100644 --- a/app/assets/javascripts/admin/components/admin-report.js +++ b/app/assets/javascripts/admin/components/admin-report.js @@ -131,6 +131,18 @@ export default Component.extend({ return displayedModesLength > 1; }, + @discourseComputed("currentMode") + isChartMode(currentMode) { + return currentMode === "chart"; + }, + + @action + changeGrouping(grouping) { + this.send("refreshReport", { + chartGrouping: grouping + }); + }, + @discourseComputed("currentMode", "model.modes", "forcedModes") displayedModes(currentMode, reportModes, forcedModes) { const modes = forcedModes ? forcedModes.split(",") : reportModes; @@ -184,6 +196,19 @@ export default Component.extend({ return reportKey; }, + @discourseComputed("reportOptions.chartGrouping") + chartGroupings(chartGrouping) { + chartGrouping = chartGrouping || "daily"; + + return ["daily", "weekly", "monthly"].map(id => { + return { + id, + label: `admin.dashboard.reports.${id}`, + class: `chart-grouping ${chartGrouping === id ? "active" : "inactive"}` + }; + }); + }, + @action onChangeDateRange(range) { this.send("refreshReport", { @@ -211,6 +236,7 @@ export default Component.extend({ refreshReport(options = {}) { this.attrs.onRefresh({ type: this.get("model.type"), + chartGrouping: options.chartGrouping, startDate: typeof options.startDate === "undefined" ? this.startDate @@ -243,6 +269,10 @@ export default Component.extend({ @action changeMode(mode) { this.set("currentMode", mode); + + this.send("refreshReport", { + chartGrouping: null + }); }, _computeReport() { @@ -364,7 +394,9 @@ export default Component.extend({ } else { const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS)); return EmberObject.create( - Object.assign(chartOptions, this.get("reportOptions.chart") || {}) + Object.assign(chartOptions, this.get("reportOptions.chart") || {}, { + chartGrouping: this.get("reportOptions.chartGrouping") + }) ); } }, diff --git a/app/assets/javascripts/admin/controllers/admin-reports-show.js b/app/assets/javascripts/admin/controllers/admin-reports-show.js index 6d302204ced..f3919e4bed8 100644 --- a/app/assets/javascripts/admin/controllers/admin-reports-show.js +++ b/app/assets/javascripts/admin/controllers/admin-reports-show.js @@ -2,10 +2,11 @@ import discourseComputed from "discourse-common/utils/decorators"; import Controller from "@ember/controller"; export default Controller.extend({ - queryParams: ["start_date", "end_date", "filters"], + queryParams: ["start_date", "end_date", "filters", "chart_grouping"], start_date: null, end_date: null, filters: null, + chart_grouping: null, @discourseComputed("model.type") reportOptions(type) { @@ -15,6 +16,8 @@ export default Controller.extend({ options.table.limit = 10; } + options.chartGrouping = this.chart_grouping; + return options; } }); diff --git a/app/assets/javascripts/admin/routes/admin-reports-show.js b/app/assets/javascripts/admin/routes/admin-reports-show.js index 3a8a5430a9b..234c30a039e 100644 --- a/app/assets/javascripts/admin/routes/admin-reports-show.js +++ b/app/assets/javascripts/admin/routes/admin-reports-show.js @@ -4,7 +4,8 @@ export default DiscourseRoute.extend({ queryParams: { start_date: { refreshModel: true }, end_date: { refreshModel: true }, - filters: { refreshModel: true } + filters: { refreshModel: true }, + chart_grouping: { refreshModel: true } }, model(params) { @@ -27,6 +28,9 @@ export default DiscourseRoute.extend({ .format("YYYY-MM-DD"); delete params.end_date; + params.chartGrouping = params.chart_grouping || "daily"; + delete params.chart_grouping; + return params; }, @@ -57,6 +61,7 @@ export default DiscourseRoute.extend({ start_date: params.startDate ? params.startDate.toISOString(true).split("T")[0] : null, + chart_grouping: params.chartGrouping, filters: params.filters, end_date: params.endDate ? params.endDate.toISOString(true).split("T")[0] diff --git a/app/assets/javascripts/admin/templates/components/admin-report.hbs b/app/assets/javascripts/admin/templates/components/admin-report.hbs index be075dbd2d0..c2d6334de93 100644 --- a/app/assets/javascripts/admin/templates/components/admin-report.hbs +++ b/app/assets/javascripts/admin/templates/components/admin-report.hbs @@ -130,6 +130,18 @@ {{/if}} + {{#if isChartMode}} +