Improve dashboard readability and stress chart direction
This commit is contained in:
@@ -404,6 +404,60 @@ button {
|
|||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-detail {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
border-radius: 18px;
|
||||||
|
background: rgba(255, 255, 255, 0.08);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail__meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.18rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail__eyebrow {
|
||||||
|
color: rgba(239, 247, 255, 0.58);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail__meta strong {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail__label,
|
||||||
|
.calendar-detail__subtle {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail__score {
|
||||||
|
display: grid;
|
||||||
|
justify-items: end;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail__score span {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail__score small {
|
||||||
|
color: var(--muted);
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail__link {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
.calendar-legend {
|
.calendar-legend {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
@@ -479,6 +533,12 @@ button {
|
|||||||
stroke-width: 1.1;
|
stroke-width: 1.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-cell--selected {
|
||||||
|
stroke: rgba(255, 255, 255, 0.9);
|
||||||
|
stroke-width: 1.2;
|
||||||
|
filter: brightness(1.08);
|
||||||
|
}
|
||||||
|
|
||||||
.line-chart {
|
.line-chart {
|
||||||
min-height: 10.25rem;
|
min-height: 10.25rem;
|
||||||
}
|
}
|
||||||
@@ -1182,6 +1242,15 @@ input[type="range"] {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-detail {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-detail__score {
|
||||||
|
justify-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
.line-chart {
|
.line-chart {
|
||||||
min-height: 9.5rem;
|
min-height: 9.5rem;
|
||||||
}
|
}
|
||||||
|
|||||||
+117
-11
@@ -347,6 +347,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seriesName = container.dataset.series || "";
|
const seriesName = container.dataset.series || "";
|
||||||
|
const invertScale = seriesName === "stress";
|
||||||
const values = items.map(item => Number(item.value));
|
const values = items.map(item => Number(item.value));
|
||||||
const width = 760;
|
const width = 760;
|
||||||
const height = 196;
|
const height = 196;
|
||||||
@@ -377,7 +378,8 @@
|
|||||||
const only = items[0];
|
const only = items[0];
|
||||||
const chartTop = padding.top;
|
const chartTop = padding.top;
|
||||||
const chartBottom = height - padding.bottom;
|
const chartBottom = height - padding.bottom;
|
||||||
const ratio = (Number(only.value) - minValue) / Math.max(maxValue - minValue, 1);
|
const baseRatio = (Number(only.value) - minValue) / Math.max(maxValue - minValue, 1);
|
||||||
|
const ratio = invertScale ? (1 - baseRatio) : baseRatio;
|
||||||
const cx = width / 2;
|
const cx = width / 2;
|
||||||
const cy = chartTop + ((1 - ratio) * (chartBottom - chartTop));
|
const cy = chartTop + ((1 - ratio) * (chartBottom - chartTop));
|
||||||
|
|
||||||
@@ -398,7 +400,8 @@
|
|||||||
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) => {
|
||||||
const ratio = (Number(item.value) - minValue) / Math.max(maxValue - minValue, 1);
|
const baseRatio = (Number(item.value) - minValue) / Math.max(maxValue - minValue, 1);
|
||||||
|
const ratio = invertScale ? (1 - baseRatio) : baseRatio;
|
||||||
return {
|
return {
|
||||||
x: padding.left + (index * step),
|
x: padding.left + (index * step),
|
||||||
y: padding.top + ((1 - ratio) * (height - padding.top - padding.bottom)),
|
y: padding.top + ((1 - ratio) * (height - padding.top - padding.bottom)),
|
||||||
@@ -516,10 +519,49 @@
|
|||||||
return `rgba(112, 240, 182, ${0.38 + (ratio * 0.52)})`;
|
return `rgba(112, 240, 182, ${0.38 + (ratio * 0.52)})`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calendarRangeConfig() {
|
||||||
|
const viewport = window.innerWidth || container.clientWidth || 1200;
|
||||||
|
|
||||||
|
if (viewport <= 640) {
|
||||||
|
return {
|
||||||
|
rangeDays: 119,
|
||||||
|
label: "letzte 4 Monate",
|
||||||
|
cellSize: 14,
|
||||||
|
columnGap: 6,
|
||||||
|
rowGap: 6,
|
||||||
|
xOffset: 36,
|
||||||
|
yOffset: 24,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewport <= 980) {
|
||||||
|
return {
|
||||||
|
rangeDays: 181,
|
||||||
|
label: "letzte 6 Monate",
|
||||||
|
cellSize: 13,
|
||||||
|
columnGap: 5,
|
||||||
|
rowGap: 5,
|
||||||
|
xOffset: 36,
|
||||||
|
yOffset: 24,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
rangeDays: 364,
|
||||||
|
label: "letzte 12 Monate",
|
||||||
|
cellSize: 12,
|
||||||
|
columnGap: 5,
|
||||||
|
rowGap: 5,
|
||||||
|
xOffset: 34,
|
||||||
|
yOffset: 22,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const map = new Map(items.map(item => [item.date, item]));
|
const map = new Map(items.map(item => [item.date, item]));
|
||||||
|
const config = calendarRangeConfig();
|
||||||
const end = new Date();
|
const end = new Date();
|
||||||
const start = new Date(end);
|
const start = new Date(end);
|
||||||
start.setDate(end.getDate() - 364);
|
start.setDate(end.getDate() - config.rangeDays);
|
||||||
|
|
||||||
const gridStart = new Date(start);
|
const gridStart = new Date(start);
|
||||||
const startWeekday = gridStart.getDay();
|
const startWeekday = gridStart.getDay();
|
||||||
@@ -547,11 +589,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const totalWeeks = Math.floor((days.length - 1) / 7) + 1;
|
const totalWeeks = Math.floor((days.length - 1) / 7) + 1;
|
||||||
const cellSize = 12;
|
const cellSize = config.cellSize;
|
||||||
const baseCellGap = 5;
|
const baseCellGap = config.columnGap;
|
||||||
const verticalGap = 5;
|
const verticalGap = config.rowGap;
|
||||||
const xOffset = 34;
|
const xOffset = config.xOffset;
|
||||||
const yOffset = 22;
|
const yOffset = config.yOffset;
|
||||||
const gridHeight = (7 * cellSize) + (6 * verticalGap);
|
const gridHeight = (7 * cellSize) + (6 * verticalGap);
|
||||||
const height = yOffset + gridHeight + 8;
|
const height = yOffset + gridHeight + 8;
|
||||||
const rightPadding = 4;
|
const rightPadding = 4;
|
||||||
@@ -563,6 +605,9 @@
|
|||||||
: 0;
|
: 0;
|
||||||
const monthLabels = [];
|
const monthLabels = [];
|
||||||
let lastMonth = -1;
|
let lastMonth = -1;
|
||||||
|
const visibleEntries = days.filter(item => item.entry !== null);
|
||||||
|
const latestVisibleEntry = visibleEntries.length ? visibleEntries[visibleEntries.length - 1].entry : null;
|
||||||
|
let activeDate = latestVisibleEntry ? latestVisibleEntry.date : null;
|
||||||
|
|
||||||
const cells = days.map((item, index) => {
|
const cells = days.map((item, index) => {
|
||||||
const week = Math.floor(index / 7);
|
const week = Math.floor(index / 7);
|
||||||
@@ -571,7 +616,7 @@
|
|||||||
const y = yOffset + (row * (cellSize + verticalGap));
|
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) {
|
||||||
monthLabels.push({
|
monthLabels.push({
|
||||||
x,
|
x,
|
||||||
label: new Intl.DateTimeFormat("de-DE", { month: "short" }).format(new Date(`${item.date}T12:00:00`)),
|
label: new Intl.DateTimeFormat("de-DE", { month: "short" }).format(new Date(`${item.date}T12:00:00`)),
|
||||||
@@ -588,14 +633,38 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<a class="calendar-link" href="/track?date=${encodeURIComponent(item.date)}" aria-label="${title}">
|
<a class="calendar-link" data-date="${item.date}" href="/track?date=${encodeURIComponent(item.date)}" aria-label="${title}">
|
||||||
<rect class="calendar-cell calendar-cell--active" x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" rx="4" fill="${fill}"></rect>
|
<rect class="calendar-cell calendar-cell--active ${activeDate === item.date ? "calendar-cell--selected" : ""}" x="${x}" y="${y}" width="${cellSize}" height="${cellSize}" rx="4" fill="${fill}"></rect>
|
||||||
<title>${title}</title>
|
<title>${title}</title>
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
}).join("");
|
}).join("");
|
||||||
|
|
||||||
|
const detailMarkup = latestVisibleEntry === null
|
||||||
|
? `
|
||||||
|
<div class="calendar-detail__meta">
|
||||||
|
<span class="calendar-detail__eyebrow">${config.label}</span>
|
||||||
|
<strong>Noch kein getrackter Tag</strong>
|
||||||
|
<span class="calendar-detail__subtle">Sobald du Einträge speicherst, kannst du sie hier direkt auswählen.</span>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
<div class="calendar-detail__meta">
|
||||||
|
<span class="calendar-detail__eyebrow">${config.label}</span>
|
||||||
|
<strong data-calendar-date>${formatDateLabel(latestVisibleEntry.date)}</strong>
|
||||||
|
<span class="calendar-detail__label" data-calendar-label>${latestVisibleEntry.label}</span>
|
||||||
|
</div>
|
||||||
|
<div class="calendar-detail__score">
|
||||||
|
<span data-calendar-score>${formatNumber(Number(latestVisibleEntry.score))}</span>
|
||||||
|
<small>Punkte</small>
|
||||||
|
</div>
|
||||||
|
<a class="ghost-link calendar-detail__link" data-calendar-link href="/track?date=${encodeURIComponent(latestVisibleEntry.date)}">Tag öffnen</a>
|
||||||
|
`;
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
|
<div class="calendar-detail">
|
||||||
|
${detailMarkup}
|
||||||
|
</div>
|
||||||
<svg class="calendar-svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="Kalender">
|
<svg class="calendar-svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" role="img" aria-label="Kalender">
|
||||||
${monthLabels.map(item => `<text class="calendar-tooltip" x="${item.x}" y="14">${item.label}</text>`).join("")}
|
${monthLabels.map(item => `<text class="calendar-tooltip" x="${item.x}" y="14">${item.label}</text>`).join("")}
|
||||||
<text class="calendar-tooltip" x="0" y="34">Mo</text>
|
<text class="calendar-tooltip" x="0" y="34">Mo</text>
|
||||||
@@ -615,6 +684,43 @@
|
|||||||
<span>gut</span>
|
<span>gut</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const detailDate = container.querySelector("[data-calendar-date]");
|
||||||
|
const detailLabel = container.querySelector("[data-calendar-label]");
|
||||||
|
const detailScore = container.querySelector("[data-calendar-score]");
|
||||||
|
const detailLink = container.querySelector("[data-calendar-link]");
|
||||||
|
|
||||||
|
const setActive = date => {
|
||||||
|
const entry = map.get(date);
|
||||||
|
if (!entry || !detailDate || !detailLabel || !detailScore || !detailLink) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeDate = date;
|
||||||
|
detailDate.textContent = formatDateLabel(entry.date);
|
||||||
|
detailLabel.textContent = entry.label;
|
||||||
|
detailScore.textContent = formatNumber(Number(entry.score));
|
||||||
|
detailLink.href = `/track?date=${encodeURIComponent(entry.date)}`;
|
||||||
|
|
||||||
|
container.querySelectorAll(".calendar-cell--selected").forEach(cell => {
|
||||||
|
cell.classList.remove("calendar-cell--selected");
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeLink = container.querySelector(`.calendar-link[data-date="${date}"] .calendar-cell`);
|
||||||
|
if (activeLink) {
|
||||||
|
activeLink.classList.add("calendar-cell--selected");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
container.querySelectorAll(".calendar-link[data-date]").forEach(link => {
|
||||||
|
const date = link.dataset.date;
|
||||||
|
if (!date) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
link.addEventListener("mouseenter", () => setActive(date));
|
||||||
|
link.addEventListener("focus", () => setActive(date));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function initDashboardCharts() {
|
function initDashboardCharts() {
|
||||||
|
|||||||
@@ -68,7 +68,7 @@
|
|||||||
<p class="eyebrow">Belastung</p>
|
<p class="eyebrow">Belastung</p>
|
||||||
<h3>Stressverlauf</h3>
|
<h3>Stressverlauf</h3>
|
||||||
</div>
|
</div>
|
||||||
<span class="chart-chip chart-chip--warm">letzte 30 Einträge</span>
|
<span class="chart-chip chart-chip--warm">weniger ist besser</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="line-chart" data-chart-type="line" data-series="stress" data-payload="<?= e($chartPayload) ?>"></div>
|
<div class="line-chart" data-chart-type="line" data-series="stress" data-payload="<?= e($chartPayload) ?>"></div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
Reference in New Issue
Block a user