Skip to content

Commit 8776ee6

Browse files
authored
feat(dashboard): add 6 new widget types (Phase B) (#34)
feat(dashboard): add 6 new widget types (Phase B)
2 parents 0ad7d77 + 02be3c6 commit 8776ee6

23 files changed

Lines changed: 1878 additions & 0 deletions

bridge/web/js/widgets.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ var Widgets = (function () {
1818
case "timeline": return renderTimeline(config, bodyEl);
1919
case "rawaxes": return renderRawAxes(config, bodyEl);
2020
case "group": return renderGroup(config, bodyEl);
21+
case "heatmap": return renderHeatmap(config, bodyEl);
22+
case "barchart": return renderBarChart(config, bodyEl);
23+
case "histogram":return renderHistogram(config, bodyEl);
24+
case "scatter": return renderScatter(config, bodyEl);
25+
case "image": return renderImage(config, bodyEl);
26+
case "multistatus": return renderMultiStatus(config, bodyEl);
2127
default:
2228
bodyEl.textContent = "Unknown widget type: " + type;
2329
}
@@ -322,6 +328,91 @@ var Widgets = (function () {
322328
});
323329
container.appendChild(content);
324330
}
331+
332+
/* --- Heatmap ------------------------------------------- */
333+
function renderHeatmap(cfg, el) {
334+
el.innerHTML = '<div class="placeholder-widget">' +
335+
'<div class="icon">&#x1F7E5;</div>' +
336+
'<div class="label">Heatmap: ' + (cfg.title || '') + '</div>' +
337+
'</div>';
338+
}
339+
340+
/* --- Bar Chart ----------------------------------------- */
341+
function renderBarChart(cfg, el) {
342+
el.innerHTML = '<div class="placeholder-widget">' +
343+
'<div class="icon">&#x1F4CA;</div>' +
344+
'<div class="label">Bar Chart: ' + (cfg.title || '') + '</div>' +
345+
'</div>';
346+
}
347+
348+
/* --- Histogram ----------------------------------------- */
349+
function renderHistogram(cfg, el) {
350+
el.innerHTML = '<div class="placeholder-widget">' +
351+
'<div class="icon">&#x1F4C8;</div>' +
352+
'<div class="label">Histogram: ' + (cfg.title || '') + '</div>' +
353+
'</div>';
354+
}
355+
356+
/* --- Scatter ------------------------------------------- */
357+
function renderScatter(cfg, el) {
358+
el.innerHTML = '<div class="placeholder-widget">' +
359+
'<div class="icon">&#x2022;</div>' +
360+
'<div class="label">Scatter: ' + (cfg.title || '') + '</div>' +
361+
'</div>';
362+
}
363+
364+
/* --- Image --------------------------------------------- */
365+
function renderImage(cfg, el) {
366+
if (cfg.file) {
367+
var img = document.createElement("img");
368+
img.src = cfg.file;
369+
img.alt = cfg.title || "Image";
370+
img.style.maxWidth = "100%";
371+
img.style.maxHeight = "100%";
372+
img.style.objectFit = "contain";
373+
el.appendChild(img);
374+
} else {
375+
el.innerHTML = '<div class="placeholder-widget">' +
376+
'<div class="icon">&#x1F5BC;</div>' +
377+
'<div class="label">Image: ' + (cfg.title || '') + '</div>' +
378+
'</div>';
379+
}
380+
}
381+
382+
/* --- Multi-Status -------------------------------------- */
383+
function renderMultiStatus(cfg, el) {
384+
var items = cfg.items || [];
385+
if (items.length === 0) {
386+
el.innerHTML = '<div class="placeholder-widget">' +
387+
'<div class="label">Multi-Status: ' + (cfg.title || '') + '</div>' +
388+
'</div>';
389+
return;
390+
}
391+
var grid = document.createElement("div");
392+
grid.style.display = "grid";
393+
grid.style.gridTemplateColumns = "repeat(auto-fill, minmax(80px, 1fr))";
394+
grid.style.gap = "6px";
395+
grid.style.padding = "8px";
396+
for (var i = 0; i < items.length; i++) {
397+
var item = items[i];
398+
var cell = document.createElement("div");
399+
cell.style.display = "flex";
400+
cell.style.alignItems = "center";
401+
cell.style.gap = "4px";
402+
var dot = document.createElement("span");
403+
dot.style.width = "10px";
404+
dot.style.height = "10px";
405+
dot.style.borderRadius = "50%";
406+
dot.style.display = "inline-block";
407+
dot.style.backgroundColor = item.color || "#888";
408+
cell.appendChild(dot);
409+
var lbl = document.createElement("span");
410+
lbl.style.fontSize = "0.8em";
411+
lbl.textContent = item.label || "";
412+
cell.appendChild(lbl);
413+
grid.appendChild(cell);
414+
}
415+
el.appendChild(grid);
325416
}
326417

327418
/* --- helpers ------------------------------------------- */

examples/example_dashboard_all_widgets.m

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
% Number/Gauge/Status: driven by sensor data (latest value / threshold check)
77
% Text/Table: static display (no interactivity needed)
88
% Timeline: derived from threshold violations
9+
% Heatmap: 2D colour grid (DataFcn returning matrix)
10+
% BarChart: categorical bar chart (DataFcn returning struct)
11+
% Histogram: sensor value distribution with optional normal fit
12+
% Scatter: two-sensor X-vs-Y correlation plot
13+
% Image: generated test pattern via ImageFcn
14+
% MultiStatus: per-sensor status grid (dot indicators)
915
%
1016
% Usage:
1117
% example_dashboard_all_widgets
@@ -247,11 +253,92 @@
247253
'Position', [1 19 24 3], ...
248254
'Events', events);
249255

256+
% --- Row 22-27: Phase B widgets — Heatmap, BarChart, Histogram ---
257+
258+
% Heatmap: temperature binned by hour-of-day (24 cols) vs machine mode (3 rows)
259+
% Pre-compute mode at every sample to avoid calling valueAt N*24*3 times.
260+
tMode = zeros(1, N);
261+
for k = 1:N
262+
tMode(k) = scMode.valueAt(t(k));
263+
end
264+
hourBins = zeros(3, 24);
265+
for h = 0:23
266+
hourIdx = (floor(t / 3600) == h);
267+
for m = 0:2
268+
mIdx = hourIdx & (tMode == m);
269+
if any(mIdx)
270+
hourBins(m+1, h+1) = mean(temp(mIdx));
271+
end
272+
end
273+
end
274+
hourXLabels = cell(1, 24);
275+
for h = 0:23
276+
hourXLabels{h+1} = sprintf('%dh', h);
277+
end
278+
d.addWidget('heatmap', 'Title', 'Temp by Hour & Machine Mode', ...
279+
'Position', [1 22 14 6], ...
280+
'DataFcn', @() hourBins, ...
281+
'Colormap', 'jet', ...
282+
'XLabels', hourXLabels, ...
283+
'YLabels', {'Idle','Running','Maint'});
284+
285+
% BarChart: alarm counts by sensor tag
286+
alarmCounts = struct( ...
287+
'categories', {{'T-401','P-201','F-301'}}, ...
288+
'values', [sTemp.countViolations(), sPress.countViolations(), sFlow.countViolations()]);
289+
d.addWidget('barchart', 'Title', 'Violation Count by Sensor', ...
290+
'Position', [15 22 10 6], ...
291+
'DataFcn', @() alarmCounts, ...
292+
'Orientation', 'vertical');
293+
294+
% --- Row 28-33: Histogram, Scatter, Image, MultiStatus ---
295+
296+
% Histogram: temperature distribution with normal fit
297+
d.addWidget('histogram', 'Title', 'Temperature Distribution', ...
298+
'Position', [1 28 8 6], ...
299+
'Sensor', sTemp, ...
300+
'ShowNormalFit', true, ...
301+
'NumBins', 40);
302+
303+
% Scatter: temperature vs pressure, color-coded by flow rate
304+
d.addWidget('scatter', 'Title', 'Temp vs Pressure (color=Flow)', ...
305+
'Position', [9 28 8 6], ...
306+
'SensorX', sTemp, ...
307+
'SensorY', sPress, ...
308+
'SensorColor', sFlow, ...
309+
'MarkerSize', 4, ...
310+
'Colormap', 'parula');
311+
312+
% Image: procedural test pattern (no external file dependency)
313+
d.addWidget('image', 'Title', 'Process Diagram', ...
314+
'Position', [17 28 8 6], ...
315+
'ImageFcn', @() makePeaksThumb(), ...
316+
'Caption', 'Elevation map (peaks function)', ...
317+
'Scaling', 'fit');
318+
319+
% MultiStatus: all three process sensors at a glance
320+
d.addWidget('multistatus', 'Title', 'Sensor Status', ...
321+
'Position', [1 34 24 3], ...
322+
'Sensors', {sTemp, sPress, sFlow}, ...
323+
'Columns', 3, ...
324+
'IconStyle', 'dot', ...
325+
'ShowLabels', true);
326+
250327
%% Render
251328
d.render();
252329

253330
fprintf('Dashboard rendered with %d widgets.\n', numel(d.Widgets));
254331
fprintf('Sensors: T-401 (%d violations), P-201 (%d violations), F-301 (%d violations)\n', ...
255332
sTemp.countViolations(), sPress.countViolations(), sFlow.countViolations());
333+
fprintf('Phase B widgets added: heatmap, barchart, histogram, scatter, image, multistatus.\n');
256334
fprintf('Click "Edit" to enter GUI builder mode.\n');
257335
fprintf('Click "Save" to export as JSON, "Export" to generate .m script.\n');
336+
337+
%% ---- Helper function ----
338+
function img = makePeaksThumb()
339+
% Return a grayscale uint8 image derived from peaks(64).
340+
Z = peaks(64);
341+
Z = Z - min(Z(:));
342+
Z = Z ./ max(Z(:));
343+
img = uint8(round(Z * 255));
344+
end

examples/example_widget_barchart.m

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
%% BarChartWidget — All Configurations Demo
2+
% Demonstrates BarChartWidget with vertical and horizontal orientations,
3+
% DataFcn data sources, and a sensor-bound mode.
4+
%
5+
% BarChartWidget Properties:
6+
% DataFcn — function_handle returning a struct with fields:
7+
% categories — cell array of category labels
8+
% values — numeric vector of bar heights
9+
% or returning a plain numeric vector (no labels).
10+
% Sensor — Sensor object; Y vector used directly as bar values.
11+
% Orientation — 'vertical' (default) or 'horizontal'.
12+
% Stacked — stacked bars when multiple value columns (default false).
13+
% Title — widget title.
14+
% Position — [col row width height] on the 24-column grid.
15+
%
16+
% Usage:
17+
% example_widget_barchart
18+
19+
close all force;
20+
clear functions;
21+
projectRoot = fileparts(fileparts(mfilename('fullpath')));
22+
run(fullfile(projectRoot, 'install.m'));
23+
24+
%% 1. Define data sources
25+
26+
% --- Production output per shift (vertical bar chart) ---
27+
shiftData = struct( ...
28+
'categories', {{'Shift A', 'Shift B', 'Shift C', 'Shift D'}}, ...
29+
'values', [412, 387, 455, 398]);
30+
31+
% --- Alarm counts by sensor (horizontal — long category labels) ---
32+
alarmData = struct( ...
33+
'categories', {{'Temperature T-401', 'Pressure P-201', 'Flow F-301', ...
34+
'Level L-102', 'Vibration V-501'}}, ...
35+
'values', [8, 3, 12, 1, 5]);
36+
37+
% --- Weekly throughput — plain numeric, no labels ---
38+
weeklyValues = [1820 1975 1843 2010 1965 1750 1410];
39+
40+
% --- Sensor-bound: hourly flow averages (8 buckets) ---
41+
rng(42);
42+
N = 5000;
43+
t = linspace(0, 28800, N); % 8 hours in seconds
44+
sFlow = Sensor('F-301', 'Name', 'Flow Rate');
45+
sFlow.Units = 'L/min';
46+
sFlow.X = t;
47+
sFlow.Y = max(0, 120 + 20*sin(2*pi*t/3600) + randn(1,N)*5);
48+
sFlow.addThresholdRule(struct(), 140, ...
49+
'Direction', 'upper', 'Label', 'Hi Alarm', ...
50+
'Color', [1 0.2 0.2], 'LineStyle', '-');
51+
sFlow.resolve();
52+
53+
%% 2. Build dashboard
54+
d = DashboardEngine('Bar Chart Widget Demo');
55+
d.Theme = 'light';
56+
57+
% Row 1-5: Vertical — production by shift
58+
d.addWidget('barchart', 'Title', 'Production by Shift', ...
59+
'Position', [1 1 12 5], ...
60+
'DataFcn', @() shiftData, ...
61+
'Orientation', 'vertical');
62+
63+
% Row 1-5: Horizontal — alarm counts by sensor tag
64+
d.addWidget('barchart', 'Title', 'Alarm Count by Sensor', ...
65+
'Position', [13 1 12 5], ...
66+
'DataFcn', @() alarmData, ...
67+
'Orientation', 'horizontal');
68+
69+
% Row 6-10: Vertical, no labels — plain numeric vector
70+
d.addWidget('barchart', 'Title', 'Weekly Throughput (Mon-Sun)', ...
71+
'Position', [1 6 12 5], ...
72+
'DataFcn', @() weeklyValues, ...
73+
'Orientation', 'vertical');
74+
75+
% Row 6-10: Sensor-bound (uses Sensor.Y values directly as bar data)
76+
d.addWidget('barchart', 'Title', 'Flow Distribution (sensor)', ...
77+
'Position', [13 6 12 5], ...
78+
'Sensor', sFlow, ...
79+
'Orientation', 'vertical');
80+
81+
%% 3. Render
82+
d.render();
83+
fprintf('Dashboard rendered with %d bar chart widgets.\n', numel(d.Widgets));
84+
fprintf('Flow sensor %s: latest %.1f %s\n', ...
85+
sFlow.Key, sFlow.Y(end), sFlow.Units);

0 commit comments

Comments
 (0)