Skip to content

Commit ced6fd3

Browse files
committed
viewer: <rezolus-chart> Lit web component spike
Vendor lit@3.2.1 (16 KB self-contained ESM bundle) into src/viewer/assets/lib/lit/ and add a minimal <rezolus-chart> custom element that consumes the Plot descriptor shape produced by crates/dashboard/ with inline series data ([[timestamps_ms], [values]]). Scope is deliberately narrow — validates that a Lit component can wrap the existing echarts global, render inside a Shadow DOM root, and react to property updates. Future adapters (HTTP, WASM, SSE) will feed the same .plot property without changing the component contract. Demo at /lib/embed/demo.html mounts the component twice (synthetic single-series and an empty-data placeholder) against the existing lib/charts/echarts.min.js global. No build step introduced; everything loads as raw modules from /lib like the rest of the viewer assets. Not browser-verified in this sandbox (rezolus binary not built).
1 parent 0b37b74 commit ced6fd3

3 files changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8"/>
5+
<meta name="viewport" content="width=device-width, initial-scale=1"/>
6+
<title>rezolus-chart embed spike</title>
7+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
8+
<script src="/lib/charts/echarts.min.js"></script>
9+
<script type="module" src="./rezolus-chart.js"></script>
10+
<style>
11+
body {
12+
font-family: 'Inter', system-ui, sans-serif;
13+
max-width: 960px;
14+
margin: 2rem auto;
15+
padding: 0 1rem;
16+
color: #1a1a1a;
17+
}
18+
h1 { font-weight: 600; }
19+
h2 { font-weight: 600; font-size: 1.05rem; margin-top: 2rem; }
20+
p { line-height: 1.5; }
21+
code { background: #f3f4f6; padding: 1px 5px; border-radius: 3px; font-size: 0.92em; }
22+
rezolus-chart {
23+
background: #fff;
24+
border: 1px solid #e5e7eb;
25+
border-radius: 6px;
26+
padding: 12px 14px;
27+
box-sizing: border-box;
28+
}
29+
</style>
30+
</head>
31+
<body>
32+
<h1>&lt;rezolus-chart&gt; embed spike</h1>
33+
<p>Lit web component consuming a <code>Plot</code> descriptor (the shape
34+
produced by <code>crates/dashboard/</code>) with inline series data. The
35+
component lives in its own Shadow DOM, so this page's CSS does not leak
36+
into the chart and vice versa.</p>
37+
38+
<h2>Single series with synthetic data</h2>
39+
<rezolus-chart id="c1"></rezolus-chart>
40+
41+
<h2>Empty plot</h2>
42+
<rezolus-chart id="c2"></rezolus-chart>
43+
44+
<script type="module">
45+
const now = Date.now();
46+
const times = Array.from({ length: 180 }, (_, i) => now - (180 - i) * 1000);
47+
const values = times.map((_, i) =>
48+
55 + 22 * Math.sin(i / 9) + 8 * Math.sin(i / 2.3) + (Math.random() - 0.5) * 4
49+
);
50+
51+
document.getElementById('c1').plot = {
52+
data: [times, values],
53+
opts: {
54+
title: 'CPU Usage (synthetic)',
55+
id: 'cpu-usage-demo',
56+
type: 'gauge',
57+
format: { unit_system: 'percentage', y_axis_label: '%' },
58+
},
59+
};
60+
61+
document.getElementById('c2').plot = {
62+
data: [],
63+
opts: {
64+
title: 'No data available',
65+
id: 'empty-demo',
66+
type: 'gauge',
67+
format: {},
68+
},
69+
};
70+
</script>
71+
</body>
72+
</html>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// <rezolus-chart> — Lit web component that renders a single Plot
2+
// descriptor (the shape produced by crates/dashboard/) inside Shadow DOM.
3+
//
4+
// Spike scope: inline-series adapter only. The component accepts a `plot`
5+
// property whose `data` field is [[timestamps_ms], [values]]. Future
6+
// adapters (HTTP, WASM, SSE) will live alongside this file and feed the
7+
// same property contract.
8+
//
9+
// Requires echarts to be loaded as a global (see lib/charts/echarts.min.js).
10+
import { LitElement, html, css } from '../lit/lit.js';
11+
12+
class RezolusChart extends LitElement {
13+
static properties = {
14+
plot: { type: Object },
15+
};
16+
17+
static styles = css`
18+
:host {
19+
display: block;
20+
width: 100%;
21+
height: 280px;
22+
font-family: 'Inter', system-ui, sans-serif;
23+
color: #222;
24+
}
25+
.title {
26+
margin: 0 0 0.4rem;
27+
font-size: 14px;
28+
font-weight: 600;
29+
}
30+
.canvas {
31+
position: relative;
32+
width: 100%;
33+
height: calc(100% - 1.8rem);
34+
}
35+
.empty {
36+
display: flex;
37+
align-items: center;
38+
justify-content: center;
39+
height: 100%;
40+
color: #888;
41+
font-style: italic;
42+
font-size: 13px;
43+
}
44+
`;
45+
46+
constructor() {
47+
super();
48+
this.plot = null;
49+
this._chart = null;
50+
this._observer = null;
51+
}
52+
53+
firstUpdated() {
54+
const canvas = this.renderRoot.querySelector('.canvas');
55+
this._observer = new ResizeObserver(() => this._chart?.resize());
56+
this._observer.observe(canvas);
57+
this._render();
58+
}
59+
60+
updated(changed) {
61+
if (changed.has('plot')) this._render();
62+
}
63+
64+
disconnectedCallback() {
65+
super.disconnectedCallback();
66+
this._observer?.disconnect();
67+
this._chart?.dispose();
68+
this._chart = null;
69+
}
70+
71+
_render() {
72+
const canvas = this.renderRoot.querySelector('.canvas');
73+
if (!canvas) return;
74+
75+
const data = this.plot?.data;
76+
const hasData = Array.isArray(data) && data.length >= 2
77+
&& Array.isArray(data[0]) && data[0].length > 0;
78+
79+
if (!hasData) {
80+
this._chart?.dispose();
81+
this._chart = null;
82+
return;
83+
}
84+
85+
if (typeof window.echarts === 'undefined') {
86+
canvas.textContent = 'echarts not loaded';
87+
return;
88+
}
89+
90+
if (!this._chart) {
91+
this._chart = window.echarts.init(canvas, null, { renderer: 'canvas' });
92+
}
93+
94+
const [times, values] = data;
95+
const seriesData = times.map((t, i) => [t, values[i]]);
96+
const fmt = this.plot.opts?.format ?? {};
97+
98+
this._chart.setOption({
99+
grid: { left: 56, right: 16, top: 12, bottom: 28 },
100+
xAxis: { type: 'time' },
101+
yAxis: {
102+
type: fmt.log_scale ? 'log' : 'value',
103+
name: fmt.y_axis_label ?? '',
104+
nameTextStyle: { fontSize: 11 },
105+
},
106+
tooltip: { trigger: 'axis', appendToBody: false },
107+
series: [{
108+
name: this.plot.opts?.title ?? '',
109+
type: 'line',
110+
showSymbol: false,
111+
data: seriesData,
112+
}],
113+
}, { notMerge: true });
114+
}
115+
116+
render() {
117+
const title = this.plot?.opts?.title;
118+
const hasData = this.plot?.data?.[0]?.length > 0;
119+
return html`
120+
${title ? html`<h3 class="title">${title}</h3>` : ''}
121+
<div class="canvas">
122+
${!hasData ? html`<div class="empty">no data</div>` : ''}
123+
</div>
124+
`;
125+
}
126+
}
127+
128+
customElements.define('rezolus-chart', RezolusChart);
129+
130+
export { RezolusChart };

0 commit comments

Comments
 (0)