Example: Custom Component¶
Build a GaugeChart that renders on an HTML <canvas> using pure JavaScript —
no external charting library required. See examples/custom_component.py in the
repo for the full runnable file.
custom_component.py
import fluxui as ui
from fluxui.components.advanced import CustomComponent
class GaugeChart(CustomComponent):
"""
A semicircular gauge chart drawn on a <canvas> element.
No external scripts required — pure Canvas 2D API.
"""
def __init__(self, value, *, max_value: int = 100, label: str = "") -> None:
self._value = value # ReactiveVar[float] or plain number
self._max_value = max_value
self._label = label
def _resolve(self, v):
"""Unwrap a ReactiveVar if needed."""
return v.value if hasattr(v, "value") else v
def render(self) -> str: # (1) called every time the component re-renders
val = self._resolve(self._value)
return (
f'<div style="display:flex;flex-direction:column;align-items:center;'
f'padding:1rem;">'
f' <canvas width="200" height="120"'
f' data-value="{val}"'
f' data-max="{self._max_value}"'
f' data-label="{self._label}">'
f' </canvas>'
f'</div>'
)
def client_script(self) -> str: # (2) runs in the browser after render
return r"""
(function() {
var canvas = this.querySelector('canvas');
if (!canvas || !canvas.getContext) return;
var ctx = canvas.getContext('2d');
var value = parseFloat(canvas.dataset.value);
var max = parseFloat(canvas.dataset.max);
var label = canvas.dataset.label;
var pct = Math.min(1, Math.max(0, value / max));
var W = canvas.width, H = canvas.height;
var cx = W / 2, cy = H - 10, r = 75;
ctx.clearRect(0, 0, W, H);
// Background track
ctx.beginPath();
ctx.arc(cx, cy, r, Math.PI, 0, false);
ctx.lineWidth = 16;
ctx.strokeStyle = getComputedStyle(document.documentElement)
.getPropertyValue('--flux-border') || '#e0e0e0';
ctx.stroke();
// Value arc
if (pct > 0) {
ctx.beginPath();
ctx.arc(cx, cy, r, Math.PI, Math.PI + pct * Math.PI, false);
ctx.lineWidth = 16;
ctx.lineCap = 'round';
ctx.strokeStyle = pct > 0.75 ? '#5f8a5f'
: pct > 0.40 ? '#d4a44a'
: '#c0625a';
ctx.stroke();
}
// Value text
var fg = getComputedStyle(document.documentElement)
.getPropertyValue('--flux-text') || '#333';
ctx.fillStyle = fg;
ctx.font = 'bold 24px Inter, system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(Math.round(value), cx, cy - 14);
// Label text
ctx.font = '12px Inter, system-ui, sans-serif';
ctx.fillStyle = getComputedStyle(document.documentElement)
.getPropertyValue('--flux-text-muted') || '#888';
ctx.fillText(label, cx, cy + 4);
}).call(this);
"""
def scripts(self) -> list: # (3) no external scripts needed
return []
# ── Demo page ─────────────────────────────────────────────────────────────────
app = ui.App("Custom Component Demo")
@app.page("/")
def home(session):
gauge_value = ui.state(65)
dark_mode = ui.state(False)
async def toggle_theme(v: bool) -> None:
dark_mode.set(v)
await session.set_theme("dark" if v else "light")
with ui.Column(gap="var(--s-6)"):
with ui.Row(justify="space-between", align="center"):
ui.Heading("Custom Gauge Component", level=1)
ui.Switch("Dark mode", checked=dark_mode, on_change=toggle_theme)
ui.Text(
"This gauge is a CustomComponent subclass — it renders an HTML canvas "
"and draws on it with the Canvas 2D API via client_script().",
variant="muted",
)
with ui.Grid(cols=3, gap="var(--s-4)"):
GaugeChart(gauge_value, max_value=100, label="Score")
GaugeChart(lambda: gauge_value.value * 0.7, max_value=100, label="Efficiency")
GaugeChart(lambda: 100 - gauge_value.value, max_value=100, label="Headroom")
with ui.Card(title="Adjust value"):
ui.Slider(
label="Gauge value",
value=gauge_value,
on_change=gauge_value.set,
min=0,
max=100,
step=1,
)
ui.Text(lambda: f"Current value: {int(gauge_value.value)}", variant="caption")
with ui.Card(title="How it works"):
ui.Code(
"class GaugeChart(CustomComponent):\n"
" def render(self) -> str:\n"
" # Returns HTML with data-* attributes\n"
" ...\n\n"
" def client_script(self) -> str:\n"
" # JavaScript that runs after render()\n"
" # `this` = the component's root DOM element\n"
" ...\n\n"
" def scripts(self) -> list[str]:\n"
" # External <script> URLs (deduplicated across components)\n"
" return []",
language="python",
)
if __name__ == "__main__":
app.run(port=8000)
render()returns an HTML string. FluxUI wraps it in a<div data-flux-id="...">and sends it to the browser. Thedata-*attributes carry the value and label.client_script()returns a JavaScript string executed withthisbound to the component's root element. It reads thedata-*attributes and draws the arc on the canvas. Becauserender()is called again whengauge_valuechanges, the canvas is redrawn automatically.scripts()lists external script URLs to inject. FluxUI deduplicates them — if 3 gauges are on one page, the script is injected only once.
Run it¶
Drag the slider — all three gauges update in real time.