Skip to content

Commit a44bace

Browse files
committed
improve popularity chart for mobile and legend styling
1 parent 8017972 commit a44bace

1 file changed

Lines changed: 143 additions & 85 deletions

File tree

src/components/popularity-chart.ts

Lines changed: 143 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ interface ChartData {
33
datasets: { label: string; data: (number | null)[] }[];
44
}
55

6+
const isSmallScreen = window.matchMedia(
7+
"(pointer: coarse) and (max-width: 768px)",
8+
).matches;
9+
610
const colors = [
711
"#08c",
812
"#dc3545",
@@ -45,6 +49,112 @@ function getTooltipElement(chart: {
4549
return el;
4650
}
4751

52+
function showTooltip(el: HTMLDivElement, html: string, x: number, y: number) {
53+
el.innerHTML = html;
54+
el.style.opacity = "1";
55+
el.style.left = `${x}px`;
56+
el.style.top = `${y}px`;
57+
}
58+
59+
function barTooltipHandler({
60+
chart,
61+
tooltip,
62+
}: {
63+
chart: { canvas: HTMLCanvasElement };
64+
tooltip: {
65+
opacity: number;
66+
dataPoints: { raw: unknown }[];
67+
caretX: number;
68+
caretY: number;
69+
};
70+
}) {
71+
const el = getTooltipElement(chart);
72+
if (tooltip.opacity === 0) {
73+
el.style.opacity = "0";
74+
return;
75+
}
76+
const value = (tooltip.dataPoints[0].raw as number).toFixed(2);
77+
showTooltip(el, value, tooltip.caretX, tooltip.caretY);
78+
}
79+
80+
function lineTooltipHandler({
81+
chart,
82+
tooltip,
83+
}: {
84+
chart: { canvas: HTMLCanvasElement };
85+
tooltip: {
86+
opacity: number;
87+
title: string[];
88+
dataPoints: {
89+
raw: unknown;
90+
datasetIndex: number;
91+
dataset: { label?: string };
92+
}[];
93+
caretX: number;
94+
caretY: number;
95+
};
96+
}) {
97+
const el = getTooltipElement(chart);
98+
if (tooltip.opacity === 0) {
99+
el.style.opacity = "0";
100+
return;
101+
}
102+
103+
const rows = tooltip.dataPoints
104+
.map((item) => {
105+
const color = colors[item.datasetIndex % colors.length];
106+
return `<tr>
107+
<td style="color:${color}">&#9679;</td>
108+
<td>${item.dataset.label}</td>
109+
<td>${(item.raw as number).toFixed(2)}</td>
110+
</tr>`;
111+
})
112+
.join("");
113+
114+
showTooltip(
115+
el,
116+
`<div class="chart-tooltip-title">${renderYearMonth(tooltip.title[0])}</div><table>${rows}</table>`,
117+
tooltip.caretX,
118+
tooltip.caretY,
119+
);
120+
}
121+
122+
function generateLegendLabels(textColor: string, gridColor: string) {
123+
return (chart: {
124+
data: { datasets: { label?: string }[] };
125+
isDatasetVisible(index: number): boolean;
126+
}) => {
127+
return chart.data.datasets.map((ds, i) => {
128+
const hidden = !chart.isDatasetVisible(i);
129+
const color = colors[i % colors.length];
130+
return {
131+
text: ds.label ?? "",
132+
fontColor: hidden ? gridColor : textColor,
133+
fillStyle: hidden ? "transparent" : color,
134+
strokeStyle: hidden ? gridColor : color,
135+
lineWidth: 1,
136+
hidden: false,
137+
datasetIndex: i,
138+
};
139+
});
140+
};
141+
}
142+
143+
const legendPaddingPlugin = {
144+
id: "legendPadding",
145+
beforeInit(chart: { legend?: { fit(): void; height: number } }) {
146+
const legend = chart.legend;
147+
if (!legend) {
148+
return;
149+
}
150+
const originalFit = legend.fit.bind(legend);
151+
legend.fit = function () {
152+
originalFit();
153+
this.height += 16;
154+
};
155+
},
156+
};
157+
48158
class PopularityChart extends HTMLElement {
49159
connectedCallback() {
50160
const script = this.querySelector('script[type="application/json"]');
@@ -127,51 +237,24 @@ class PopularityChart extends HTMLElement {
127237
animation: false,
128238
maintainAspectRatio: false,
129239
plugins: {
130-
legend: {
131-
display: false,
132-
},
240+
legend: { display: false },
133241
tooltip: {
134242
enabled: false,
135-
external: ({ chart, tooltip }) => {
136-
const el = getTooltipElement(chart);
137-
138-
if (tooltip.opacity === 0) {
139-
el.style.opacity = "0";
140-
return;
141-
}
142-
143-
const item = tooltip.dataPoints[0];
144-
el.innerHTML = (item.raw as number).toFixed(2);
145-
el.style.opacity = "1";
146-
el.style.left = `${tooltip.caretX}px`;
147-
el.style.top = `${tooltip.caretY}px`;
148-
},
243+
external: isSmallScreen ? undefined : barTooltipHandler,
149244
},
150245
},
151246
scales: {
152247
x: {
153248
min: 0,
154249
max: 100,
155-
border: {
156-
color: gridColor,
157-
},
158-
grid: {
159-
color: gridColor,
160-
},
161-
ticks: {
162-
color: textColor,
163-
},
250+
border: { color: gridColor },
251+
grid: { color: gridColor },
252+
ticks: { color: textColor },
164253
},
165254
y: {
166-
border: {
167-
color: gridColor,
168-
},
169-
grid: {
170-
display: false,
171-
},
172-
ticks: {
173-
color: textColor,
174-
},
255+
border: { color: gridColor },
256+
grid: { display: false },
257+
ticks: { color: textColor },
175258
},
176259
},
177260
},
@@ -208,58 +291,41 @@ class PopularityChart extends HTMLElement {
208291
new Chart(canvas, {
209292
type: "line",
210293
data,
294+
plugins: [legendPaddingPlugin],
211295
options: {
212296
animation: false,
213297
maintainAspectRatio: false,
214-
interaction: {
215-
mode: "index",
216-
intersect: false,
217-
},
298+
interaction: isSmallScreen
299+
? undefined
300+
: { mode: "index" as const, intersect: false },
218301
plugins: {
219302
tooltip: {
220303
enabled: false,
221-
itemSort: (a, b) =>
222-
(b.raw as number) - (a.raw as number),
223-
external: ({ chart, tooltip }) => {
224-
const el = getTooltipElement(chart);
225-
226-
if (tooltip.opacity === 0) {
227-
el.style.opacity = "0";
228-
return;
229-
}
230-
231-
const rows = tooltip.dataPoints
232-
.map((item) => {
233-
const color =
234-
colors[
235-
item.datasetIndex % colors.length
236-
];
237-
return `<tr>
238-
<td style="color:${color}">&#9679;</td>
239-
<td>${item.dataset.label}</td>
240-
<td>${(item.raw as number).toFixed(2)}</td>
241-
</tr>`;
242-
})
243-
.join("");
244-
245-
el.innerHTML = `<div class="chart-tooltip-title">${renderYearMonth(tooltip.title[0])}</div><table>${rows}</table>`;
246-
el.style.opacity = "1";
247-
el.style.left = `${tooltip.caretX}px`;
248-
el.style.top = `${tooltip.caretY}px`;
249-
},
304+
itemSort: isSmallScreen
305+
? undefined
306+
: (a, b) => (b.raw as number) - (a.raw as number),
307+
external: isSmallScreen
308+
? undefined
309+
: lineTooltipHandler,
250310
},
251311
legend: {
312+
align: isSmallScreen ? "start" : "center",
252313
labels: {
253314
color: textColor,
315+
boxWidth: 11,
316+
boxHeight: 11,
317+
font: isSmallScreen ? { size: 11 } : undefined,
318+
generateLabels: generateLegendLabels(
319+
textColor,
320+
gridColor,
321+
),
254322
},
255323
},
256324
},
257325
normalized: true,
258326
scales: {
259327
x: {
260-
border: {
261-
color: gridColor,
262-
},
328+
border: { color: gridColor },
263329
ticks: {
264330
callback(val) {
265331
return renderYearMonth(
@@ -269,32 +335,24 @@ class PopularityChart extends HTMLElement {
269335
color: textColor,
270336
autoSkipPadding: 30,
271337
},
272-
grid: {
273-
display: false,
274-
color: gridColor,
275-
},
338+
grid: { display: false, color: gridColor },
276339
},
277340
y: {
278341
type: "linear",
279342
min: 0,
280-
border: {
281-
color: gridColor,
282-
},
283-
grid: {
284-
color: gridColor,
285-
},
286-
ticks: {
287-
color: textColor,
288-
},
343+
border: { color: gridColor },
344+
grid: { color: gridColor },
345+
ticks: { color: textColor },
289346
},
290347
},
291348
elements: {
292349
line: {
293350
borderColor: colors,
351+
borderWidth: isSmallScreen ? 1.5 : 3,
294352
},
295353
point: {
296354
radius: 0,
297-
hoverRadius: 4,
355+
hoverRadius: isSmallScreen ? 0 : 4,
298356
hoverBackgroundColor: textColor,
299357
},
300358
},

0 commit comments

Comments
 (0)