Skip to content

Commit d503ef3

Browse files
committed
Polish example themes and add real screenshots
1 parent 824cac8 commit d503ef3

25 files changed

Lines changed: 1557 additions & 271 deletions

examples/custom-frontends/brutalist-lab/frontend/app.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { mountTheme } from "./shared-theme.js";
22

33
mountTheme({
44
title: document.querySelector("#title"),
5-
projectsEl: document.querySelector("#projects"),
6-
runsEl: document.querySelector("#runs"),
5+
projectSelect: document.querySelector("#project-select"),
6+
runSelect: document.querySelector("#run-select"),
77
metricsEl: document.querySelector("#metrics"),
8+
metricsSubtitle: document.querySelector("#metrics-subtitle"),
9+
projectSummary: document.querySelector("#project-summary"),
10+
runsCount: document.querySelector("#runs-count"),
11+
metricsCount: document.querySelector("#metrics-count"),
12+
selectedRunName: document.querySelector("#selected-run-name"),
813
});

examples/custom-frontends/brutalist-lab/frontend/index.html

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,36 @@
88
</head>
99
<body>
1010
<main class="shell">
11-
<header>
11+
<header class="hero">
1212
<p>BRUTALIST LAB</p>
1313
<h1 id="title">Loading project</h1>
14+
<p id="project-summary" class="summary">Punchy charts. Minimal chrome.</p>
1415
</header>
1516
<section class="columns">
16-
<div class="panel">
17-
<h2>Projects</h2>
18-
<div id="projects"></div>
19-
</div>
20-
<div class="panel">
21-
<h2>Runs</h2>
22-
<div id="runs"></div>
23-
</div>
24-
<div class="panel">
25-
<h2>Metrics</h2>
17+
<aside class="panel controls-panel">
18+
<h2>Controls</h2>
19+
<label class="field-label" for="project-select">Project</label>
20+
<select id="project-select" class="selector"></select>
21+
<label class="field-label" for="run-select">Run</label>
22+
<select id="run-select" class="selector"></select>
23+
<div class="stat-row">
24+
<div class="stat-card"><span>Runs</span><strong id="runs-count">0</strong></div>
25+
<div class="stat-card"><span>Metrics</span><strong id="metrics-count">0</strong></div>
26+
</div>
27+
<div class="run-badge">
28+
<span>Selected Run</span>
29+
<strong id="selected-run-name">Loading</strong>
30+
</div>
31+
</aside>
32+
<section class="panel metrics-panel">
33+
<div class="metrics-head">
34+
<div>
35+
<h2>Metrics</h2>
36+
<p id="metrics-subtitle" class="metrics-subtitle">Waiting for Trackio data.</p>
37+
</div>
38+
</div>
2639
<div id="metrics"></div>
27-
</div>
40+
</section>
2841
</section>
2942
</main>
3043
<script type="module" src="./app.js"></script>

examples/custom-frontends/brutalist-lab/frontend/shared-theme.js

Lines changed: 171 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,10 @@ async function api(name, payload = {}) {
1111
return json.data;
1212
}
1313

14-
function createButton(project, selectedProject) {
15-
const button = document.createElement("button");
16-
button.className = project === selectedProject ? "active" : "";
17-
button.textContent = project;
18-
button.addEventListener("click", () => {
19-
const params = new URLSearchParams(window.location.search);
20-
params.set("project", project);
21-
window.location.search = params.toString();
22-
});
23-
return button;
14+
function updateProjectParam(project) {
15+
const params = new URLSearchParams(window.location.search);
16+
params.set("project", project);
17+
window.location.search = params.toString();
2418
}
2519

2620
function renderList(element, values, emptyLabel) {
@@ -32,7 +26,6 @@ function renderList(element, values, emptyLabel) {
3226
element.appendChild(item);
3327
return;
3428
}
35-
3629
for (const value of values) {
3730
const item = document.createElement("div");
3831
item.className = "item";
@@ -41,48 +34,189 @@ function renderList(element, values, emptyLabel) {
4134
}
4235
}
4336

44-
export async function mountTheme({ title, projectsEl, runsEl, metricsEl }) {
37+
function renderOptions(selectEl, options, selectedValue, labelForOption) {
38+
selectEl.innerHTML = "";
39+
for (const option of options) {
40+
const el = document.createElement("option");
41+
el.value = option.value;
42+
el.textContent = labelForOption(option);
43+
if (option.value === selectedValue) {
44+
el.selected = true;
45+
}
46+
selectEl.appendChild(el);
47+
}
48+
}
49+
50+
function isFiniteNumber(value) {
51+
return typeof value === "number" && Number.isFinite(value);
52+
}
53+
54+
function getChartableRows(rows) {
55+
return rows.filter((row) => isFiniteNumber(row.value));
56+
}
57+
58+
function isChartableSeries(rows) {
59+
return getChartableRows(rows).length >= 2;
60+
}
61+
62+
function formatValue(value) {
63+
if (!isFiniteNumber(value)) return String(value);
64+
if (Math.abs(value) >= 1000 || Math.abs(value) < 0.01) return value.toExponential(2);
65+
return value.toFixed(3);
66+
}
67+
68+
function buildPath(points) {
69+
return points.map(([x, y], index) => `${index === 0 ? "M" : "L"} ${x} ${y}`).join(" ");
70+
}
71+
72+
function buildAreaPath(points, height) {
73+
if (!points.length) return "";
74+
const line = buildPath(points);
75+
const [lastX] = points[points.length - 1];
76+
const [firstX] = points[0];
77+
return `${line} L ${lastX} ${height} L ${firstX} ${height} Z`;
78+
}
79+
80+
function renderMetricCard(metric, rows) {
81+
const chartableRows = getChartableRows(rows);
82+
const width = 360;
83+
const height = 140;
84+
const padding = 10;
85+
const values = chartableRows.map((row) => row.value);
86+
const min = Math.min(...values);
87+
const max = Math.max(...values);
88+
const span = max - min || 1;
89+
const points = chartableRows.map((row, index) => {
90+
const x = padding + (index / Math.max(chartableRows.length - 1, 1)) * (width - padding * 2);
91+
const y = height - padding - ((row.value - min) / span) * (height - padding * 2);
92+
return [x, y];
93+
});
94+
const latest = chartableRows[chartableRows.length - 1];
95+
const first = chartableRows[0];
96+
const skippedPoints = rows.length - chartableRows.length;
97+
98+
const card = document.createElement("article");
99+
card.className = "metric-card";
100+
card.innerHTML = `
101+
<h3>${metric}</h3>
102+
<div class="metric-meta">Latest ${formatValue(latest.value)} | ${chartableRows.length} plotted${skippedPoints ? ` | ${skippedPoints} skipped` : ""}</div>
103+
<div class="chart-frame">
104+
<svg viewBox="0 0 ${width} ${height}" role="img" aria-label="${metric} line chart">
105+
<line class="chart-axis" x1="${padding}" y1="${height - padding}" x2="${width - padding}" y2="${height - padding}"></line>
106+
<path class="chart-fill" d="${buildAreaPath(points, height - padding)}"></path>
107+
<path class="chart-line" d="${buildPath(points)}"></path>
108+
<circle class="chart-point" cx="${points[points.length - 1][0]}" cy="${points[points.length - 1][1]}" r="4"></circle>
109+
</svg>
110+
</div>
111+
<div class="metric-value">${formatValue(first.value)} -> ${formatValue(latest.value)}</div>
112+
`;
113+
return card;
114+
}
115+
116+
async function renderMetrics(element, project, run) {
117+
element.innerHTML = "";
118+
const metrics = await api("get_metrics_for_run", { project, run: run.name, run_id: run.id });
119+
if (!metrics.length) {
120+
renderList(element, [], "No metrics yet");
121+
return 0;
122+
}
123+
124+
const rowsByMetric = await Promise.all(
125+
metrics.slice(0, 12).map(async (metric) => ({
126+
metric,
127+
rows: await api("get_metric_values", {
128+
project,
129+
run: run.name,
130+
run_id: run.id,
131+
metric_name: metric,
132+
}),
133+
})),
134+
);
135+
136+
const chartable = rowsByMetric.filter((entry) => isChartableSeries(entry.rows));
137+
const other = rowsByMetric.filter((entry) => !isChartableSeries(entry.rows)).map((entry) => entry.metric);
138+
139+
if (chartable.length) {
140+
const grid = document.createElement("div");
141+
grid.className = "metric-grid";
142+
for (const entry of chartable.slice(0, 6)) grid.appendChild(renderMetricCard(entry.metric, entry.rows));
143+
element.appendChild(grid);
144+
} else {
145+
renderList(element, [], "No numeric metrics available for charting");
146+
}
147+
148+
if (other.length) {
149+
const extras = document.createElement("section");
150+
extras.className = "metric-extras";
151+
extras.innerHTML = "<h3>Other Logged Items</h3>";
152+
for (const metric of other) {
153+
const pill = document.createElement("span");
154+
pill.className = "metric-pill";
155+
pill.textContent = metric;
156+
extras.appendChild(pill);
157+
}
158+
element.appendChild(extras);
159+
}
160+
161+
return metrics.length;
162+
}
163+
164+
export async function mountTheme({
165+
title,
166+
projectSelect,
167+
runSelect,
168+
metricsEl,
169+
metricsSubtitle,
170+
projectSummary,
171+
runsCount,
172+
metricsCount,
173+
selectedRunName,
174+
}) {
45175
try {
46176
const projects = await api("get_all_projects");
47177
const params = new URLSearchParams(window.location.search);
48178
const selectedProject =
49-
params.get("project") && projects.includes(params.get("project"))
50-
? params.get("project")
51-
: projects[0];
52-
53-
title.textContent = selectedProject || "No projects";
54-
projectsEl.innerHTML = "";
55-
for (const project of projects) {
56-
projectsEl.appendChild(createButton(project, selectedProject));
57-
}
179+
params.get("project") && projects.includes(params.get("project")) ? params.get("project") : projects[0];
180+
181+
title.textContent = selectedProject || "No project";
182+
projectSummary.textContent = selectedProject ? "Punchy charts. Minimal chrome." : "Choose a project and run.";
183+
184+
renderOptions(projectSelect, projects.map((project) => ({ value: project })), selectedProject, (option) => option.value);
185+
projectSelect.onchange = () => updateProjectParam(projectSelect.value);
58186

59187
if (!selectedProject) {
60-
renderList(runsEl, [], "No runs yet");
61188
renderList(metricsEl, [], "No metrics yet");
189+
selectedRunName.textContent = "No run";
62190
return;
63191
}
64192

65193
const runs = await api("get_runs_for_project", { project: selectedProject });
66-
renderList(
67-
runsEl,
68-
runs.slice(0, 8).map((run) => run.name || "Unnamed run"),
69-
"No runs yet",
70-
);
71-
72-
const firstRun = runs[0]?.name;
73-
if (!firstRun) {
194+
runsCount.textContent = String(runs.length);
195+
if (!runs.length) {
196+
runSelect.innerHTML = "";
197+
metricsCount.textContent = "0";
198+
selectedRunName.textContent = "No run";
74199
renderList(metricsEl, [], "No metrics yet");
75200
return;
76201
}
77202

78-
const metrics = await api("get_metrics_for_run", {
79-
project: selectedProject,
80-
run: firstRun,
81-
});
82-
renderList(metricsEl, metrics.slice(0, 12), "No metrics yet");
203+
const paramsRunId = params.get("run_id");
204+
let selectedRun = runs.find((run) => run.id === paramsRunId) || runs[0];
205+
renderOptions(runSelect, runs.map((run) => ({ value: run.id, name: run.name || "Unnamed run" })), selectedRun.id, (option) => option.name);
206+
207+
const updateSelectedRun = async (runId) => {
208+
selectedRun = runs.find((run) => run.id === runId) || runs[0];
209+
runSelect.value = selectedRun.id;
210+
selectedRunName.textContent = selectedRun.name || "Unnamed run";
211+
metricsSubtitle.textContent = `Live plots for ${selectedRun.name || "the selected run"}.`;
212+
metricsCount.textContent = String(await renderMetrics(metricsEl, selectedProject, selectedRun));
213+
};
214+
215+
runSelect.onchange = () => updateSelectedRun(runSelect.value);
216+
await updateSelectedRun(selectedRun.id);
83217
} catch (error) {
84218
title.textContent = "Error";
85-
renderList(runsEl, [error.message], "Error");
86-
renderList(metricsEl, [], "Error");
219+
selectedRunName.textContent = "Error";
220+
renderList(metricsEl, [], error.message);
87221
}
88222
}

0 commit comments

Comments
 (0)