Skip to content

Commit fb7dede

Browse files
committed
Only return a certain number of history entries, but allow zooming in.
Improve chart display. Fixes #44
1 parent af728dc commit fb7dede

File tree

3 files changed

+175
-83
lines changed

3 files changed

+175
-83
lines changed

express/backend/src/api/adapter.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ router.get("/api/adapter/:name/stats/history", async function (req, res) {
6060
latest: {},
6161
stable: {},
6262
};
63+
const counts: (AdapterVersions & { date: Date })[] = [];
6364
await rawStatistics
6465
.find()
6566
.project<Statistics>({
@@ -72,13 +73,28 @@ router.get("/api/adapter/:name/stats/history", async function (req, res) {
7273
.forEach((s) => {
7374
const stat = unescapeObjectKeys(s);
7475
if (stat.adapters[name]) {
75-
result.counts[stat.date] = {
76+
counts.push({
77+
date: new Date(stat.date),
7678
total: stat.adapters[name],
7779
versions: stat.versions[name],
78-
};
80+
});
7981
}
8082
});
8183

84+
// find all history entries in the given range
85+
const start = new Date((req.query.start as string) ?? 0);
86+
const end = new Date((req.query.end as string) ?? "2999-01-01");
87+
const filtered = counts.filter(
88+
({ date }) => date >= start && date <= end,
89+
);
90+
const minCounts = 100; // return at least 100 items
91+
const modulo = Math.max(1, Math.floor(filtered.length / minCounts));
92+
filtered
93+
.filter((_, i) => i % modulo === 0 || i === filtered.length - 1)
94+
.forEach(({ date, total, versions }) => {
95+
result.counts[date.toISOString()] = { total, versions };
96+
});
97+
8298
// the first version timestamp is wrong (except for new adapters)
8399
let hadFirstLatest = false;
84100
let hadFirstStable = false;
@@ -103,7 +119,19 @@ router.get("/api/adapter/:name/stats/history", async function (req, res) {
103119
}
104120
});
105121

106-
console.log(result);
122+
console.log(
123+
{
124+
name,
125+
start,
126+
end,
127+
total: counts.length,
128+
inRange: filtered.length,
129+
minCounts,
130+
modulo,
131+
returned: Object.keys(result.counts).length,
132+
},
133+
result,
134+
);
107135
if (Object.keys(result.counts).length === 0) {
108136
res.status(404).send("Adapter not found");
109137
return;

express/frontend/src/lib/ioBroker.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,24 @@ export async function getCurrentVersions(adapterName: string) {
167167
return result.data;
168168
}
169169

170-
export async function getStatisticsHistory(adapterName: string) {
171-
const result = await axios.get<AdapterStats>(
170+
export async function getStatisticsHistory(
171+
adapterName: string,
172+
start?: Date,
173+
end?: Date,
174+
) {
175+
const url = new URL(
172176
getApiUrl(`adapter/${uc(adapterName)}/stats/history`),
177+
document.location.origin,
173178
);
179+
if (start) {
180+
url.searchParams.set("start", start.toISOString());
181+
}
182+
183+
if (end) {
184+
url.searchParams.set("end", end.toISOString());
185+
}
186+
187+
const result = await axios.get<AdapterStats>(url.toString());
174188
return result.data;
175189
}
176190

express/frontend/src/tools/adapter/statistics/VersionHistory.tsx

Lines changed: 128 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
import { Box } from "@mui/material";
1+
import { Box, debounce } from "@mui/material";
2+
import { EChartsOption, SeriesOption } from "echarts";
23
import ReactECharts from "echarts-for-react";
34
import { useEffect, useState } from "react";
45
import { coerce } from "semver";
56
import sort from "semver/functions/sort";
67
import { useAdapterContext } from "../../../contexts/AdapterContext";
78
import { getStatisticsHistory } from "../../../lib/ioBroker";
89

9-
const chartDefaults = {
10+
const chartDefaults: EChartsOption = {
1011
title: {
1112
text: "Installed version history",
1213
},
@@ -40,7 +41,6 @@ const chartDefaults = {
4041
},
4142
{
4243
type: "inside",
43-
realtime: true,
4444
},
4545
],
4646
xAxis: [
@@ -56,89 +56,111 @@ const chartDefaults = {
5656
series: [],
5757
};
5858

59+
async function loadSeries(
60+
name: string,
61+
start?: Date,
62+
end?: Date,
63+
existing?: SeriesOption[],
64+
) {
65+
const stats = await getStatisticsHistory(name, start, end);
66+
const versions = new Set<string>();
67+
for (const date of Object.keys(stats.counts)) {
68+
Object.keys(stats.counts[date].versions)
69+
.map((v) => coerce(v))
70+
.filter((v) => !!v)
71+
.forEach((v) => versions.add(v!.version));
72+
}
73+
74+
const sortedVersions = Array.from(versions);
75+
sort(sortedVersions);
76+
const series = sortedVersions.map<SeriesOption>((v) => {
77+
const existingData = (existing?.find((e) => e.name === v)?.data ??
78+
[]) as [string, number][];
79+
const data = [...existingData];
80+
Object.keys(stats.counts)
81+
.map((date) => [date, stats.counts[date].versions[v] || 0] as const)
82+
.forEach(([date, value]) => {
83+
if (
84+
!existingData.some(
85+
([existingDate]) => existingDate === date,
86+
)
87+
) {
88+
data.push([date, value]);
89+
}
90+
});
91+
data.sort((a, b) => a[0].localeCompare(b[0]));
92+
return {
93+
name: v,
94+
type: "line",
95+
stack: name,
96+
showSymbol: false,
97+
areaStyle: {},
98+
emphasis: {
99+
focus: "series",
100+
},
101+
data,
102+
};
103+
});
104+
105+
// the "total" series is used also for version markers
106+
series.push({
107+
name: "Total",
108+
type: "line",
109+
symbol: "circle",
110+
showSymbol: false,
111+
lineStyle: { color: "black", width: 1.5 },
112+
itemStyle: { color: "black" },
113+
data: Object.keys(stats.counts).map((date) => [
114+
date,
115+
stats.counts[date].total,
116+
]),
117+
markLine: {
118+
data: [
119+
// all stable version markers
120+
...Object.keys(stats.stable).map((date) => [
121+
{
122+
name: `Stable\n${stats.stable[date]}`,
123+
xAxis: date,
124+
yAxis: 0,
125+
},
126+
{
127+
name: "end",
128+
xAxis: date,
129+
yAxis: "max",
130+
},
131+
]),
132+
// all latest that aren't in stable
133+
...Object.keys(stats.latest)
134+
.filter((date) => !stats.stable[date])
135+
.map((date) => [
136+
{
137+
name: `Latest\n${stats.latest[date]}`,
138+
xAxis: date,
139+
yAxis: 0,
140+
},
141+
{
142+
name: "end",
143+
xAxis: date,
144+
yAxis: "max",
145+
},
146+
]),
147+
] as SeriesOption["markLine"]["data"],
148+
},
149+
});
150+
151+
return series;
152+
}
153+
59154
export function VersionHistory() {
60155
const { name } = useAdapterContext();
61-
const [option, setOption] = useState<any>();
156+
const [option, setOption] = useState<EChartsOption>();
62157
const [showLoading, setShowLoading] = useState(true);
63158

64159
useEffect(() => {
65160
setOption(undefined);
66161
setShowLoading(true);
67162
const loadHistory = async () => {
68-
const stats = await getStatisticsHistory(name);
69-
const versions = new Set<string>();
70-
for (const date of Object.keys(stats.counts)) {
71-
Object.keys(stats.counts[date].versions)
72-
.map((v) => coerce(v))
73-
.filter((v) => !!v)
74-
.forEach((v) => versions.add(v!.version));
75-
}
76-
77-
const sortedVersions = Array.from(versions);
78-
sort(sortedVersions);
79-
const series: any[] = sortedVersions.map((v) => ({
80-
name: v,
81-
type: "line",
82-
stack: name,
83-
areaStyle: {},
84-
emphasis: {
85-
focus: "series",
86-
},
87-
data: Object.keys(stats.counts).map((date) => [
88-
date,
89-
stats.counts[date].versions[v] || 0,
90-
]),
91-
}));
92-
93-
// the "total" series is used also for version markers
94-
series.push({
95-
name: "Total",
96-
type: "line",
97-
symbol: "circle",
98-
lineStyle: { color: "black", width: 1.5 },
99-
itemStyle: { color: "black" },
100-
label: {
101-
show: true,
102-
position: "top",
103-
},
104-
data: Object.keys(stats.counts).map((date) => [
105-
date,
106-
stats.counts[date].total,
107-
]),
108-
markLine: {
109-
data: [
110-
// all stable version markers
111-
...Object.keys(stats.stable).map((date) => [
112-
{
113-
name: `Stable\n${stats.stable[date]}`,
114-
xAxis: date,
115-
yAxis: 0,
116-
},
117-
{
118-
name: "end",
119-
xAxis: date,
120-
yAxis: "max",
121-
},
122-
]),
123-
// all latest that aren't in stable
124-
...Object.keys(stats.latest)
125-
.filter((date) => !stats.stable[date])
126-
.map((date) => [
127-
{
128-
name: `Latest\n${stats.latest[date]}`,
129-
xAxis: date,
130-
yAxis: 0,
131-
},
132-
{
133-
name: "end",
134-
xAxis: date,
135-
yAxis: "max",
136-
},
137-
]),
138-
],
139-
},
140-
});
141-
163+
const series = await loadSeries(name);
142164
setShowLoading(false);
143165
setOption({
144166
...chartDefaults,
@@ -151,16 +173,44 @@ export function VersionHistory() {
151173
setOption(undefined);
152174
});
153175
}, [name]);
176+
154177
if (!option && !showLoading) {
155178
return null;
156179
}
180+
181+
async function onDataZoom(event: any, chart: any) {
182+
console.log("data zoom", event, chart);
183+
console.log("options", chart.getOption());
184+
185+
// according to https://github.com/apache/echarts/issues/17919#issuecomment-1316090464
186+
const extent = chart
187+
.getModel()
188+
.getComponent("xAxis", 0)
189+
.axis.scale.getExtent();
190+
console.log("extent", extent);
191+
192+
const series = await loadSeries(
193+
name,
194+
new Date(extent[0]),
195+
new Date(extent[1]),
196+
option?.series as SeriesOption[],
197+
);
198+
setOption({
199+
...chartDefaults,
200+
series,
201+
});
202+
}
203+
157204
return (
158205
<Box sx={{ marginTop: 2 }}>
159206
<ReactECharts
160207
style={{ height: "400px" }}
161208
loadingOption={{
162209
type: "default",
163210
}}
211+
onEvents={{
212+
datazoom: debounce(onDataZoom, 250),
213+
}}
164214
showLoading={showLoading}
165215
option={option || { ...chartDefaults }}
166216
/>

0 commit comments

Comments
 (0)