FEATURE: allows to display charts by day/week/month (#10325)
This commit is contained in:
parent
bc11769118
commit
0c7eaa57b2
|
@ -8,6 +8,7 @@ export default Component.extend({
|
||||||
classNames: ["admin-report-chart"],
|
classNames: ["admin-report-chart"],
|
||||||
limit: 8,
|
limit: 8,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
options: null,
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
|
@ -49,7 +50,11 @@ export default Component.extend({
|
||||||
if (!chartCanvas) return;
|
if (!chartCanvas) return;
|
||||||
|
|
||||||
const context = chartCanvas.getContext("2d");
|
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(
|
const prevChartData = makeArray(
|
||||||
model.get("prevChartData") || model.get("prev_data")
|
model.get("prevChartData") || model.get("prev_data")
|
||||||
);
|
);
|
||||||
|
@ -91,11 +96,14 @@ export default Component.extend({
|
||||||
return;
|
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 {
|
return {
|
||||||
type: "line",
|
type: "line",
|
||||||
data,
|
data,
|
||||||
|
@ -144,7 +152,7 @@ export default Component.extend({
|
||||||
gridLines: { display: false },
|
gridLines: { display: false },
|
||||||
type: "time",
|
type: "time",
|
||||||
time: {
|
time: {
|
||||||
parser: "YYYY-MM-DD"
|
unit: this._unitForGrouping(options)
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
sampleSize: 5,
|
sampleSize: 5,
|
||||||
|
@ -163,5 +171,65 @@ export default Component.extend({
|
||||||
this._chart.destroy();
|
this._chart.destroy();
|
||||||
this._chart = null;
|
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";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -131,6 +131,18 @@ export default Component.extend({
|
||||||
return displayedModesLength > 1;
|
return displayedModesLength > 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@discourseComputed("currentMode")
|
||||||
|
isChartMode(currentMode) {
|
||||||
|
return currentMode === "chart";
|
||||||
|
},
|
||||||
|
|
||||||
|
@action
|
||||||
|
changeGrouping(grouping) {
|
||||||
|
this.send("refreshReport", {
|
||||||
|
chartGrouping: grouping
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
@discourseComputed("currentMode", "model.modes", "forcedModes")
|
@discourseComputed("currentMode", "model.modes", "forcedModes")
|
||||||
displayedModes(currentMode, reportModes, forcedModes) {
|
displayedModes(currentMode, reportModes, forcedModes) {
|
||||||
const modes = forcedModes ? forcedModes.split(",") : reportModes;
|
const modes = forcedModes ? forcedModes.split(",") : reportModes;
|
||||||
|
@ -184,6 +196,19 @@ export default Component.extend({
|
||||||
return reportKey;
|
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
|
@action
|
||||||
onChangeDateRange(range) {
|
onChangeDateRange(range) {
|
||||||
this.send("refreshReport", {
|
this.send("refreshReport", {
|
||||||
|
@ -211,6 +236,7 @@ export default Component.extend({
|
||||||
refreshReport(options = {}) {
|
refreshReport(options = {}) {
|
||||||
this.attrs.onRefresh({
|
this.attrs.onRefresh({
|
||||||
type: this.get("model.type"),
|
type: this.get("model.type"),
|
||||||
|
chartGrouping: options.chartGrouping,
|
||||||
startDate:
|
startDate:
|
||||||
typeof options.startDate === "undefined"
|
typeof options.startDate === "undefined"
|
||||||
? this.startDate
|
? this.startDate
|
||||||
|
@ -243,6 +269,10 @@ export default Component.extend({
|
||||||
@action
|
@action
|
||||||
changeMode(mode) {
|
changeMode(mode) {
|
||||||
this.set("currentMode", mode);
|
this.set("currentMode", mode);
|
||||||
|
|
||||||
|
this.send("refreshReport", {
|
||||||
|
chartGrouping: null
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
_computeReport() {
|
_computeReport() {
|
||||||
|
@ -364,7 +394,9 @@ export default Component.extend({
|
||||||
} else {
|
} else {
|
||||||
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
|
const chartOptions = JSON.parse(JSON.stringify(CHART_OPTIONS));
|
||||||
return EmberObject.create(
|
return EmberObject.create(
|
||||||
Object.assign(chartOptions, this.get("reportOptions.chart") || {})
|
Object.assign(chartOptions, this.get("reportOptions.chart") || {}, {
|
||||||
|
chartGrouping: this.get("reportOptions.chartGrouping")
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,10 +2,11 @@ import discourseComputed from "discourse-common/utils/decorators";
|
||||||
import Controller from "@ember/controller";
|
import Controller from "@ember/controller";
|
||||||
|
|
||||||
export default Controller.extend({
|
export default Controller.extend({
|
||||||
queryParams: ["start_date", "end_date", "filters"],
|
queryParams: ["start_date", "end_date", "filters", "chart_grouping"],
|
||||||
start_date: null,
|
start_date: null,
|
||||||
end_date: null,
|
end_date: null,
|
||||||
filters: null,
|
filters: null,
|
||||||
|
chart_grouping: null,
|
||||||
|
|
||||||
@discourseComputed("model.type")
|
@discourseComputed("model.type")
|
||||||
reportOptions(type) {
|
reportOptions(type) {
|
||||||
|
@ -15,6 +16,8 @@ export default Controller.extend({
|
||||||
options.table.limit = 10;
|
options.table.limit = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.chartGrouping = this.chart_grouping;
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,8 @@ export default DiscourseRoute.extend({
|
||||||
queryParams: {
|
queryParams: {
|
||||||
start_date: { refreshModel: true },
|
start_date: { refreshModel: true },
|
||||||
end_date: { refreshModel: true },
|
end_date: { refreshModel: true },
|
||||||
filters: { refreshModel: true }
|
filters: { refreshModel: true },
|
||||||
|
chart_grouping: { refreshModel: true }
|
||||||
},
|
},
|
||||||
|
|
||||||
model(params) {
|
model(params) {
|
||||||
|
@ -27,6 +28,9 @@ export default DiscourseRoute.extend({
|
||||||
.format("YYYY-MM-DD");
|
.format("YYYY-MM-DD");
|
||||||
delete params.end_date;
|
delete params.end_date;
|
||||||
|
|
||||||
|
params.chartGrouping = params.chart_grouping || "daily";
|
||||||
|
delete params.chart_grouping;
|
||||||
|
|
||||||
return params;
|
return params;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -57,6 +61,7 @@ export default DiscourseRoute.extend({
|
||||||
start_date: params.startDate
|
start_date: params.startDate
|
||||||
? params.startDate.toISOString(true).split("T")[0]
|
? params.startDate.toISOString(true).split("T")[0]
|
||||||
: null,
|
: null,
|
||||||
|
chart_grouping: params.chartGrouping,
|
||||||
filters: params.filters,
|
filters: params.filters,
|
||||||
end_date: params.endDate
|
end_date: params.endDate
|
||||||
? params.endDate.toISOString(true).split("T")[0]
|
? params.endDate.toISOString(true).split("T")[0]
|
||||||
|
|
|
@ -130,6 +130,18 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if isChartMode}}
|
||||||
|
<div class="chart-groupings">
|
||||||
|
{{#each chartGroupings as |chartGrouping|}}
|
||||||
|
{{d-button
|
||||||
|
label=chartGrouping.label
|
||||||
|
action=(action "changeGrouping" chartGrouping.id)
|
||||||
|
class=chartGrouping.class
|
||||||
|
}}
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
{{#if showDatesOptions}}
|
{{#if showDatesOptions}}
|
||||||
<div class="control">
|
<div class="control">
|
||||||
<span class="label">
|
<span class="label">
|
||||||
|
|
|
@ -140,6 +140,18 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-groupings {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, 1fr);
|
||||||
|
grid-gap: 0.5em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
|
||||||
|
.chart-grouping.active {
|
||||||
|
background: $tertiary;
|
||||||
|
color: $secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.control {
|
.control {
|
||||||
margin-bottom: 1em;
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
.admin-report-chart {
|
.admin-report-chart {
|
||||||
animation: fadein 2s;
|
animation: fadein 2s;
|
||||||
|
|
||||||
|
.chart-canvas-container {
|
||||||
|
position: relative;
|
||||||
|
height: 50vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3500,6 +3500,9 @@ en:
|
||||||
view_table: "table"
|
view_table: "table"
|
||||||
view_graph: "graph"
|
view_graph: "graph"
|
||||||
refresh_report: "Refresh Report"
|
refresh_report: "Refresh Report"
|
||||||
|
daily: Daily
|
||||||
|
monthly: Monthly
|
||||||
|
weekly: Weekly
|
||||||
dates: "Dates (UTC)"
|
dates: "Dates (UTC)"
|
||||||
groups: "All groups"
|
groups: "All groups"
|
||||||
disabled: "This report is disabled"
|
disabled: "This report is disabled"
|
||||||
|
@ -3630,7 +3633,6 @@ en:
|
||||||
optional_allowed_parameters: Allowed Parameters (optional)
|
optional_allowed_parameters: Allowed Parameters (optional)
|
||||||
any_parameter: (any parameter)
|
any_parameter: (any parameter)
|
||||||
|
|
||||||
|
|
||||||
web_hooks:
|
web_hooks:
|
||||||
title: "Webhooks"
|
title: "Webhooks"
|
||||||
none: "There are no webhooks right now."
|
none: "There are no webhooks right now."
|
||||||
|
|
Loading…
Reference in New Issue