KPI Card Grid
Store-driven metrics dashboard with multiple subscribing cards.
Live Demo
Updates every 3 seconds.
Source
HTML + JavaScript
<div data-component="kpi-grid">
<div class="kpi-grid" data-list="metrics">
<template>
<div class="kpi-card">
<div data-bind-style="{ color: color }"
data-bind="formattedValue"></div>
<div data-bind="label"></div>
<div data-bind-class="trend > 0 ? 'kpi-up' : 'kpi-down'"
data-bind="trendLabel"></div>
</div>
</template>
</div>
</div>
<script>
wildflower.component('kpi-grid', {
state: {
running: true,
metrics: [
{ label: 'Revenue', value: 48200, format: 'currency', color: '#6b996a', trend: 12.5 },
{ label: 'Users', value: 3842, format: 'number', color: '#3498db', trend: 8.2 },
{ label: 'Errors', value: 23, format: 'number', color: '#c0392b', trend: -15.1 },
{ label: 'Uptime', value: 99.97, format: 'percent', color: '#6b996a', trend: 0.1 }
]
},
computed: {
// Declaring the (m) parameter makes these item-level: the
// framework calls them per row with the list item.
formattedValue(m) {
if (m.format === 'currency') return '$' + m.value.toLocaleString();
if (m.format === 'percent') return m.value + '%';
return m.value.toLocaleString();
},
trendLabel(m) {
return (m.trend > 0 ? '▲ +' : '▼ ') + m.trend + '%';
}
},
init() {
this._interval = setInterval(() => {
if (!this.running) return;
this.metrics.forEach(m => {
const prev = m.value;
if (m.format === 'percent') {
const delta = m.value * (Math.random() * 0.04 - 0.02);
m.value = Math.min(100, Math.round((m.value + delta) * 100) / 100);
} else {
// Integer walk, at least +/-1 so small counts move.
let step = Math.round(m.value * (Math.random() * 0.04 - 0.02));
if (step === 0) step = Math.random() < 0.5 ? -1 : 1;
m.value = Math.max(0, m.value + step);
}
// Trend = real % change this tick, so the arrow matches the move.
m.trend = prev ? Math.round(((m.value - prev) / prev) * 1000) / 10 : 0;
});
}, 3000);
},
toggleUpdates() { this.running = !this.running; },
destroy() { clearInterval(this._interval); }
});
</script>
Key Points
data-bind-style="{ color: color }"applies inline styles from item data. Each metric gets its own colordata-bind-class="trend > 0 ? 'kpi-up' : 'kpi-down'"conditionally styles the trend indicator- Item-level computeds (
formattedValue,trendLabel) format data per list item; declaring a parameter (formattedValue(m)) is what tells the framework to call them per row with the item init()/destroy()lifecycle manages the update interval- Nested mutations (
m.value = ...) insideforEachpropagate reactively