DEV: Convert admin charts to glimmer/gjs (#28271)
This commit is contained in:
parent
fee8caf529
commit
5a8e7c5f29
|
@ -1,61 +0,0 @@
|
||||||
import Component from "@ember/component";
|
|
||||||
import { tagName } from "@ember-decorators/component";
|
|
||||||
import loadScript from "discourse/lib/load-script";
|
|
||||||
|
|
||||||
@tagName("canvas")
|
|
||||||
export default class AdminGraph extends Component {
|
|
||||||
type = "line";
|
|
||||||
|
|
||||||
refreshChart() {
|
|
||||||
const ctx = this.element.getContext("2d");
|
|
||||||
const model = this.model;
|
|
||||||
const rawData = this.get("model.data");
|
|
||||||
|
|
||||||
let data = {
|
|
||||||
labels: rawData.map((r) => r.x),
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
data: rawData.map((r) => r.y),
|
|
||||||
label: model.get("title"),
|
|
||||||
backgroundColor: `rgba(200,220,240,${this.type === "bar" ? 1 : 0.3})`,
|
|
||||||
borderColor: "#08C",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
type: this.type,
|
|
||||||
data,
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
plugins: {
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
title: (context) =>
|
|
||||||
moment(context[0].label, "YYYY-MM-DD").format("LL"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
scales: {
|
|
||||||
y: [
|
|
||||||
{
|
|
||||||
display: true,
|
|
||||||
ticks: {
|
|
||||||
stepSize: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
this._chart = new window.Chart(ctx, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
didInsertElement() {
|
|
||||||
super.didInsertElement(...arguments);
|
|
||||||
loadScript("/javascripts/Chart.min.js").then(() =>
|
|
||||||
this.refreshChart.apply(this)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,63 +1,19 @@
|
||||||
import Component from "@ember/component";
|
import Component from "@glimmer/component";
|
||||||
import { schedule } from "@ember/runloop";
|
|
||||||
import { classNames } from "@ember-decorators/component";
|
|
||||||
import { number } from "discourse/lib/formatter";
|
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 { makeArray } from "discourse-common/lib/helpers";
|
||||||
import { bind } from "discourse-common/utils/decorators";
|
|
||||||
import Report from "admin/models/report";
|
import Report from "admin/models/report";
|
||||||
|
import Chart from "./chart";
|
||||||
|
|
||||||
@classNames("admin-report-chart")
|
|
||||||
export default class AdminReportChart extends Component {
|
export default class AdminReportChart extends Component {
|
||||||
limit = 8;
|
get chartConfig() {
|
||||||
total = 0;
|
const { model, options } = this.args;
|
||||||
options = null;
|
|
||||||
|
|
||||||
didInsertElement() {
|
const chartData = Report.collapse(
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
_scheduleChartRendering() {
|
|
||||||
schedule("afterRender", () => {
|
|
||||||
this._renderChart(
|
|
||||||
this.model,
|
|
||||||
this.element && this.element.querySelector(".chart-canvas")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_renderChart(model, chartCanvas) {
|
|
||||||
if (!chartCanvas) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const context = chartCanvas.getContext("2d");
|
|
||||||
const chartData = this._applyChartGrouping(
|
|
||||||
model,
|
model,
|
||||||
makeArray(model.get("chartData") || model.get("data"), "weekly"),
|
makeArray(model.chartData || model.data, "weekly"),
|
||||||
this.options
|
options.chartGrouping
|
||||||
);
|
);
|
||||||
const prevChartData = makeArray(
|
const prevChartData = makeArray(model.prevChartData || model.prev_data);
|
||||||
model.get("prevChartData") || model.get("prev_data")
|
|
||||||
);
|
|
||||||
|
|
||||||
const labels = chartData.map((d) => d.x);
|
const labels = chartData.map((d) => d.x);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
|
@ -88,21 +44,6 @@ export default class AdminReportChart extends Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
|
||||||
this._resetChart();
|
|
||||||
|
|
||||||
if (!this.element) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._chart = new window.Chart(
|
|
||||||
context,
|
|
||||||
this._buildChartConfig(data, this.options)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildChartConfig(data, options) {
|
|
||||||
return {
|
return {
|
||||||
type: "line",
|
type: "line",
|
||||||
data,
|
data,
|
||||||
|
@ -164,19 +105,7 @@ export default class AdminReportChart extends Component {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
_resetChart() {
|
<template>
|
||||||
if (this._chart) {
|
<Chart @chartConfig={{this.chartConfig}} class="admin-report-chart" />
|
||||||
this._chart.destroy();
|
</template>
|
||||||
this._chart = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_applyChartGrouping(model, data, options) {
|
|
||||||
return Report.collapse(model, data, options.chartGrouping);
|
|
||||||
}
|
|
||||||
|
|
||||||
@bind
|
|
||||||
_resizeHandler() {
|
|
||||||
discourseDebounce(this, this._scheduleChartRendering, 500);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
<div class="chart-canvas-container">
|
|
||||||
<canvas class="chart-canvas"></canvas>
|
|
||||||
</div>
|
|
|
@ -0,0 +1,67 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { makeArray } from "discourse-common/lib/helpers";
|
||||||
|
import hexToRGBA from "admin/lib/hex-to-rgba";
|
||||||
|
import Report from "admin/models/report";
|
||||||
|
import Chart from "./chart";
|
||||||
|
|
||||||
|
export default class AdminReportRadar extends Component {
|
||||||
|
get chartConfig() {
|
||||||
|
const { model } = this.args;
|
||||||
|
|
||||||
|
const chartData = makeArray(model.chartData || model.data).map((cd) => ({
|
||||||
|
label: cd.label,
|
||||||
|
color: cd.color,
|
||||||
|
data: Report.collapse(model, cd.data),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
labels: chartData[0].data.mapBy("x"),
|
||||||
|
datasets: chartData.map((cd) => ({
|
||||||
|
label: cd.label,
|
||||||
|
data: cd.data.mapBy("y"),
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: hexToRGBA(cd.color, 0.3),
|
||||||
|
borderColor: cd.color,
|
||||||
|
pointBackgroundColor: cd.color,
|
||||||
|
pointBorderColor: "#fff",
|
||||||
|
pointHoverBackgroundColor: "#fff",
|
||||||
|
pointHoverBorderColor: cd.color,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "radar",
|
||||||
|
data,
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
responsiveAnimationDuration: 0,
|
||||||
|
hover: { mode: "index" },
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
mode: "index",
|
||||||
|
intersect: false,
|
||||||
|
callbacks: {
|
||||||
|
beforeFooter: (tooltipItem) => {
|
||||||
|
const total = tooltipItem.reduce(
|
||||||
|
(sum, item) => sum + parseInt(item.parsed.r || 0, 10)
|
||||||
|
);
|
||||||
|
return `= ${total}`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Chart
|
||||||
|
@chartConfig={{this.chartConfig}}
|
||||||
|
class="admin-report-chart admin-report-radar"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
<div class="chart-canvas-container">
|
|
||||||
<canvas class="chart-canvas"></canvas>
|
|
||||||
</div>
|
|
|
@ -1,137 +0,0 @@
|
||||||
import Component from "@ember/component";
|
|
||||||
import { schedule } from "@ember/runloop";
|
|
||||||
import { classNames } from "@ember-decorators/component";
|
|
||||||
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-radar")
|
|
||||||
export default class AdminReportRadar 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,
|
|
||||||
data: cd.data.mapBy("y"),
|
|
||||||
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: "radar",
|
|
||||||
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.r || 0, 10))
|
|
||||||
);
|
|
||||||
return `= ${total}`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
_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})`;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { number } from "discourse/lib/formatter";
|
||||||
|
import { makeArray } from "discourse-common/lib/helpers";
|
||||||
|
import Report from "admin/models/report";
|
||||||
|
import Chart from "./chart";
|
||||||
|
|
||||||
|
export default class AdminReportStackedChart extends Component {
|
||||||
|
get chartConfig() {
|
||||||
|
const { model } = this.args;
|
||||||
|
|
||||||
|
const chartData = makeArray(model.chartData || model.data).map((cd) => ({
|
||||||
|
label: cd.label,
|
||||||
|
color: cd.color,
|
||||||
|
data: Report.collapse(model, cd.data),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
labels: chartData[0].data.mapBy("x"),
|
||||||
|
datasets: chartData.map((cd) => ({
|
||||||
|
label: cd.label,
|
||||||
|
stack: "pageviews-stack",
|
||||||
|
data: cd.data,
|
||||||
|
backgroundColor: cd.color,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "bar",
|
||||||
|
data,
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
responsiveAnimationDuration: 0,
|
||||||
|
hover: { mode: "index" },
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
mode: "index",
|
||||||
|
intersect: false,
|
||||||
|
callbacks: {
|
||||||
|
beforeFooter: (tooltipItem) => {
|
||||||
|
const total = tooltipItem.reduce(
|
||||||
|
(sum, item) => sum + 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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Chart
|
||||||
|
@chartConfig={{this.chartConfig}}
|
||||||
|
class="admin-report-chart admin-report-stacked-chart"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
<div class="chart-canvas-container">
|
|
||||||
<canvas class="chart-canvas"></canvas>
|
|
||||||
</div>
|
|
|
@ -1,159 +0,0 @@
|
||||||
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-chart")
|
|
||||||
export default class AdminReportStackedChart 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,
|
|
||||||
backgroundColor: cd.color,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
loadScript("/javascripts/Chart.min.js").then(() => {
|
|
||||||
this._resetChart();
|
|
||||||
|
|
||||||
this._chart = new window.Chart(context, this._buildChartConfig(data));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
_buildChartConfig(data) {
|
|
||||||
return {
|
|
||||||
type: "bar",
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { number } from "discourse/lib/formatter";
|
||||||
|
import { makeArray } from "discourse-common/lib/helpers";
|
||||||
|
import hexToRGBA from "admin/lib/hex-to-rgba";
|
||||||
|
import Report from "admin/models/report";
|
||||||
|
import Chart from "./chart";
|
||||||
|
|
||||||
|
export default class AdminReportStackedLineChart extends Component {
|
||||||
|
get chartConfig() {
|
||||||
|
const { model } = this.args;
|
||||||
|
|
||||||
|
const chartData = makeArray(model.chartData || model.data).map((cd) => ({
|
||||||
|
label: cd.label,
|
||||||
|
color: cd.color,
|
||||||
|
data: Report.collapse(model, cd.data),
|
||||||
|
}));
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: chartData[0].data.mapBy("x"),
|
||||||
|
datasets: chartData.map((cd) => ({
|
||||||
|
label: cd.label,
|
||||||
|
stack: "pageviews-stack",
|
||||||
|
data: cd.data,
|
||||||
|
fill: true,
|
||||||
|
backgroundColor: hexToRGBA(cd.color, 0.3),
|
||||||
|
borderColor: cd.color,
|
||||||
|
pointBackgroundColor: cd.color,
|
||||||
|
pointBorderColor: "#fff",
|
||||||
|
pointHoverBackgroundColor: "#fff",
|
||||||
|
pointHoverBorderColor: cd.color,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
responsiveAnimationDuration: 0,
|
||||||
|
hover: { mode: "index" },
|
||||||
|
animation: {
|
||||||
|
duration: 0,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
mode: "index",
|
||||||
|
intersect: false,
|
||||||
|
callbacks: {
|
||||||
|
beforeFooter: (tooltipItem) => {
|
||||||
|
const total = tooltipItem.reduce(
|
||||||
|
(sum, item) => sum + parseInt(item.parsed.y || 0, 10),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
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(chartData[0].data.length),
|
||||||
|
},
|
||||||
|
ticks: {
|
||||||
|
sampleSize: 5,
|
||||||
|
maxRotation: 50,
|
||||||
|
minRotation: 50,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Chart
|
||||||
|
@chartConfig={{this.chartConfig}}
|
||||||
|
class="admin-report-chart admin-report-stacked-line-chart"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
<div class="chart-canvas-container">
|
|
||||||
<canvas class="chart-canvas"></canvas>
|
|
||||||
</div>
|
|
|
@ -1,179 +0,0 @@
|
||||||
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})`;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
import Component from "@glimmer/component";
|
||||||
|
import { modifier } from "ember-modifier";
|
||||||
|
import loadScript from "discourse/lib/load-script";
|
||||||
|
|
||||||
|
// args:
|
||||||
|
// chartConfig - object
|
||||||
|
export default class Chart extends Component {
|
||||||
|
renderChart = modifier((element) => {
|
||||||
|
loadScript("/javascripts/Chart.min.js").then(() => {
|
||||||
|
this.chart = new window.Chart(
|
||||||
|
element.getContext("2d"),
|
||||||
|
this.args.chartConfig
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => this.chart?.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div ...attributes>
|
||||||
|
<div class="chart-canvas-container">
|
||||||
|
<canvas {{this.renderChart}} class="chart-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
}
|
|
@ -22,4 +22,42 @@ export default class AdminSearchLogsTermController extends Controller {
|
||||||
name: I18n.t("admin.logs.search_logs.types.click_through_only"),
|
name: I18n.t("admin.logs.search_logs.types.click_through_only"),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
get chartConfig() {
|
||||||
|
return {
|
||||||
|
type: "bar",
|
||||||
|
data: {
|
||||||
|
labels: this.model.data.map((r) => r.x),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
data: this.model.data.map((r) => r.y),
|
||||||
|
label: this.model.title,
|
||||||
|
backgroundColor: "rgba(200,220,240,1)",
|
||||||
|
borderColor: "#08C",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
title: (context) =>
|
||||||
|
moment(context[0].label, "YYYY-MM-DD").format("LL"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
y: [
|
||||||
|
{
|
||||||
|
display: true,
|
||||||
|
ticks: {
|
||||||
|
stepSize: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
export default function 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);
|
||||||
|
const g = parseInt(hex.substring(2, 4), 16);
|
||||||
|
const b = parseInt(hex.substring(4, 6), 16);
|
||||||
|
|
||||||
|
return `rgba(${r},${g},${b}, ${opacity})`;
|
||||||
|
}
|
|
@ -16,7 +16,7 @@
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<ConditionalLoadingSpinner @condition={{this.refreshing}}>
|
<ConditionalLoadingSpinner @condition={{this.refreshing}}>
|
||||||
<AdminGraph @model={{this.model}} @type="bar" />
|
<Chart @chartConfig={{this.chartConfig}} />
|
||||||
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
<h2> {{i18n "admin.logs.search_logs.header_search_results"}} </h2>
|
<h2> {{i18n "admin.logs.search_logs.header_search_results"}} </h2>
|
||||||
|
|
|
@ -78,6 +78,7 @@
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
|
|
Loading…
Reference in New Issue