Improve dashboard charts for sparse data and mobile layouts
This commit is contained in:
+45
-2
@@ -378,6 +378,7 @@ button {
|
|||||||
|
|
||||||
.dashboard-grid {
|
.dashboard-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-card--calendar,
|
.chart-card--calendar,
|
||||||
@@ -474,9 +475,12 @@ button {
|
|||||||
stroke-width: 1.1;
|
stroke-width: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-chart,
|
.line-chart {
|
||||||
|
min-height: 10.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.bar-chart {
|
.bar-chart {
|
||||||
min-height: 17rem;
|
min-height: 11rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-chart svg {
|
.line-chart svg {
|
||||||
@@ -499,16 +503,35 @@ button {
|
|||||||
stroke-width: 1.5;
|
stroke-width: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line-point--solo {
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-point-glow {
|
||||||
|
opacity: 0.16;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-axis {
|
.chart-axis {
|
||||||
stroke: rgba(255, 255, 255, 0.1);
|
stroke: rgba(255, 255, 255, 0.1);
|
||||||
stroke-width: 1;
|
stroke-width: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-guide {
|
||||||
|
opacity: 0.22;
|
||||||
|
stroke-width: 2;
|
||||||
|
}
|
||||||
|
|
||||||
.chart-label {
|
.chart-label {
|
||||||
fill: rgba(239, 247, 255, 0.65);
|
fill: rgba(239, 247, 255, 0.65);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-value {
|
||||||
|
fill: rgba(239, 247, 255, 0.9);
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.bar-chart svg {
|
.bar-chart svg {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -987,6 +1010,14 @@ input[type="range"] {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line-chart {
|
||||||
|
min-height: 9.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-chart {
|
||||||
|
min-height: 10rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
@@ -1011,4 +1042,16 @@ input[type="range"] {
|
|||||||
.hero-score {
|
.hero-score {
|
||||||
font-size: 2.8rem;
|
font-size: 2.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chart-card {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.line-chart {
|
||||||
|
min-height: 8.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bar-chart {
|
||||||
|
min-height: 9.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-13
@@ -1,5 +1,6 @@
|
|||||||
(function () {
|
(function () {
|
||||||
const textDecoder = new TextDecoder();
|
const textDecoder = new TextDecoder();
|
||||||
|
let dashboardResizeTimer = null;
|
||||||
|
|
||||||
function decodePayload(raw) {
|
function decodePayload(raw) {
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
@@ -282,11 +283,55 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const seriesName = container.dataset.series || "";
|
||||||
|
const values = items.map(item => Number(item.value));
|
||||||
const width = 760;
|
const width = 760;
|
||||||
const height = 220;
|
const height = 196;
|
||||||
const padding = { top: 18, right: 18, bottom: 38, left: 14 };
|
const padding = { top: 10, right: 18, bottom: 28, left: 14 };
|
||||||
const maxValue = Math.max(...items.map(item => Number(item.value)), 10);
|
let minValue = Math.min(...values);
|
||||||
const minValue = 0;
|
let maxValue = Math.max(...values);
|
||||||
|
|
||||||
|
if (seriesName === "mood" || seriesName === "stress") {
|
||||||
|
minValue = Math.max(1, minValue - 1.5);
|
||||||
|
maxValue = Math.min(10, maxValue + 1.5);
|
||||||
|
} else {
|
||||||
|
minValue = Math.max(0, minValue - 1);
|
||||||
|
maxValue = maxValue + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((maxValue - minValue) < 3) {
|
||||||
|
const center = (maxValue + minValue) / 2;
|
||||||
|
if (seriesName === "mood" || seriesName === "stress") {
|
||||||
|
minValue = Math.max(1, center - 1.5);
|
||||||
|
maxValue = Math.min(10, center + 1.5);
|
||||||
|
} else {
|
||||||
|
minValue = Math.max(0, center - 1.5);
|
||||||
|
maxValue = center + 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 1) {
|
||||||
|
const only = items[0];
|
||||||
|
const chartTop = padding.top;
|
||||||
|
const chartBottom = height - padding.bottom;
|
||||||
|
const ratio = (Number(only.value) - minValue) / Math.max(maxValue - minValue, 1);
|
||||||
|
const cx = width / 2;
|
||||||
|
const cy = chartTop + ((1 - ratio) * (chartBottom - chartTop));
|
||||||
|
|
||||||
|
container.innerHTML = `
|
||||||
|
<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="Verlauf">
|
||||||
|
<line class="chart-axis" x1="${width * 0.18}" y1="${chartBottom}" x2="${width * 0.82}" y2="${chartBottom}"></line>
|
||||||
|
<line class="chart-guide" x1="${cx}" y1="${cy + 10}" x2="${cx}" y2="${chartBottom}" stroke="${color}"></line>
|
||||||
|
<circle class="line-point line-point--solo" cx="${cx}" cy="${cy}" r="7" fill="${color}"></circle>
|
||||||
|
<circle class="line-point-glow" cx="${cx}" cy="${cy}" r="18" fill="${color}"></circle>
|
||||||
|
<text class="chart-value" x="${cx}" y="${Math.max(18, cy - 18)}" text-anchor="middle">${formatNumber(Number(only.value))}</text>
|
||||||
|
<text class="chart-label" x="${cx}" y="${height - 10}" text-anchor="middle">${formatDateLabel(only.date)}</text>
|
||||||
|
<title>${formatDateLabel(only.date)}: ${formatNumber(Number(only.value))}</title>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const step = items.length > 1 ? (width - padding.left - padding.right) / (items.length - 1) : 0;
|
const step = items.length > 1 ? (width - padding.left - padding.right) / (items.length - 1) : 0;
|
||||||
|
|
||||||
const points = items.map((item, index) => {
|
const points = items.map((item, index) => {
|
||||||
@@ -324,9 +369,9 @@
|
|||||||
const recent = items.slice(-18);
|
const recent = items.slice(-18);
|
||||||
const maxValue = Math.max(...recent.map(item => Number(item.value)), 1);
|
const maxValue = Math.max(...recent.map(item => Number(item.value)), 1);
|
||||||
const width = Math.max(recent.length * 34, 520);
|
const width = Math.max(recent.length * 34, 520);
|
||||||
const height = 220;
|
const height = 184;
|
||||||
const chartHeight = 150;
|
const chartHeight = 118;
|
||||||
const baseY = 176;
|
const baseY = 146;
|
||||||
|
|
||||||
const bars = recent.map((item, index) => {
|
const bars = recent.map((item, index) => {
|
||||||
const sport = Number(item.sport || 0);
|
const sport = Number(item.sport || 0);
|
||||||
@@ -349,7 +394,7 @@
|
|||||||
<title>${formatDateLabel(item.date)} · Sport ${sport} min</title>
|
<title>${formatDateLabel(item.date)} · Sport ${sport} min</title>
|
||||||
</rect>
|
</rect>
|
||||||
<text class="bar-value" x="${x + 9}" y="${baseY - chartHeight - 10}">${Math.round(total)}</text>
|
<text class="bar-value" x="${x + 9}" y="${baseY - chartHeight - 10}">${Math.round(total)}</text>
|
||||||
<text class="bar-label" x="${x + 9}" y="202" text-anchor="middle">${formatDateLabel(item.date)}</text>
|
<text class="bar-label" x="${x + 9}" y="172" text-anchor="middle">${formatDateLabel(item.date)}</text>
|
||||||
`;
|
`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
@@ -424,20 +469,27 @@
|
|||||||
|
|
||||||
const totalWeeks = Math.floor((days.length - 1) / 7) + 1;
|
const totalWeeks = Math.floor((days.length - 1) / 7) + 1;
|
||||||
const cellSize = 12;
|
const cellSize = 12;
|
||||||
const cellGap = 5;
|
const baseCellGap = 5;
|
||||||
|
const verticalGap = 5;
|
||||||
const xOffset = 34;
|
const xOffset = 34;
|
||||||
const yOffset = 22;
|
const yOffset = 22;
|
||||||
const gridHeight = (7 * cellSize) + (6 * cellGap);
|
const gridHeight = (7 * cellSize) + (6 * verticalGap);
|
||||||
const height = yOffset + gridHeight + 8;
|
const height = yOffset + gridHeight + 8;
|
||||||
const width = xOffset + ((totalWeeks - 1) * (cellSize + cellGap)) + cellSize + 12;
|
const rightPadding = 4;
|
||||||
|
const naturalWidth = xOffset + (totalWeeks * cellSize) + ((totalWeeks - 1) * baseCellGap) + rightPadding;
|
||||||
|
const availableWidth = Math.floor(container.clientWidth || 0);
|
||||||
|
const width = Math.max(naturalWidth, availableWidth);
|
||||||
|
const horizontalGap = totalWeeks > 1
|
||||||
|
? (width - xOffset - rightPadding - (totalWeeks * cellSize)) / (totalWeeks - 1)
|
||||||
|
: 0;
|
||||||
const monthLabels = [];
|
const monthLabels = [];
|
||||||
let lastMonth = -1;
|
let lastMonth = -1;
|
||||||
|
|
||||||
const cells = days.map((item, index) => {
|
const cells = days.map((item, index) => {
|
||||||
const week = Math.floor(index / 7);
|
const week = Math.floor(index / 7);
|
||||||
const row = item.weekday === 0 ? 6 : item.weekday - 1;
|
const row = item.weekday === 0 ? 6 : item.weekday - 1;
|
||||||
const x = xOffset + (week * (cellSize + cellGap));
|
const x = xOffset + (week * (cellSize + horizontalGap));
|
||||||
const y = yOffset + (row * (cellSize + cellGap));
|
const y = yOffset + (row * (cellSize + verticalGap));
|
||||||
const fill = calendarColor(item.entry);
|
const fill = calendarColor(item.entry);
|
||||||
|
|
||||||
if (item.day <= 7 && item.month !== lastMonth && item.entry !== null) {
|
if (item.day <= 7 && item.month !== lastMonth && item.entry !== null) {
|
||||||
@@ -511,6 +563,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
if (!document.querySelector("#calendar-heatmap")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.clearTimeout(dashboardResizeTimer);
|
||||||
|
dashboardResizeTimer = window.setTimeout(() => {
|
||||||
|
initDashboardCharts();
|
||||||
|
}, 120);
|
||||||
|
});
|
||||||
|
|
||||||
updateRangeOutputs();
|
updateRangeOutputs();
|
||||||
initHeaderDatePicker();
|
initHeaderDatePicker();
|
||||||
initTrackPreview();
|
initTrackPreview();
|
||||||
|
|||||||
Reference in New Issue
Block a user