Build a Dashboard
-
Navigate to Dashboards → Dashboard Editor and create a new dashboard.
-
Configure it:
Field Value Name OEE and Top DowntimesDescription Understand what's driving your OEE Folder Downtime Dashboards(create the folder with the folder icon if it doesn't exist yet)Icon Optional Color Optional Enabled Enabled Private Optional — use private if only your user should see it; leave public/shared if the dashboard should be available to others -
Save the dashboard. The dashboard is now registered in Kanoa, and the design canvas becomes available for adding widgets.
Selector widgets define the context the analytics widgets use. This dashboard needs an asset selector and a date range selector.
- Select the full top row of the dashboard canvas. In the add widget popup, go to the Selectors section, choose Asset Selector, and add it to the top row. This lets the user choose which asset the dashboard analyzes.
- Select the row underneath the asset selector. In the Selectors section, choose Date Range Selector and add it. This date range drives the analytics widgets below it.
- Select a section of the canvas for the first chart. In the add widget popup, find the Asset OEE Bar Chart and add it. It shows OEE performance for the selected asset and time range.
- Select another section of the canvas. Find the Top Downtimes widget and add it. It shows the top downtime reasons for the selected asset and date range.
Each widget can receive view parameters that adjust how it behaves, which lets you reuse the same widget in different contexts. You type parameter values in, or pick them from a dropdown when the widget defines options for that parameter.
- Click the OEE widget and look at its View Params.
- Change the OEE metric parameter to focus the widget on a specific metric: enter
availabilityfor theoeeMetricparameter.
The widget then responds according to how it was designed.
For this dashboard, the analytics widgets should use the asset chosen in the Asset Selector, so leave the asset path as null. Conceptually:
{ "assetPath": null }
This means "use the current dashboard-selected asset." Alternatively, you can hardcode an asset path so a widget always shows one specific asset:
{ "assetPath": "CoreForge\\Philadelphia\\Machining\\Machine 1" }
Keep the asset path dynamic so the selector drives the dashboard.
- Switch from edit mode to view mode and confirm that:
- The dashboard layout looks correct
- The asset selector and date range selector work
- The OEE widget and the Top Downtimes widget respond to the selected filters
This validates that the dashboard works as an analytics tool, not just a static layout.
The dashboard editor supports different layouts for different screen sizes. The top of the editor shows the current layout mode, such as Desktop, Tablet, or Mobile, and each breakpoint can have its own widget arrangement. If only one breakpoint is configured, that layout is shown regardless of screen size.
On desktop, place the two analytics widgets side by side. On mobile, side by side is usually too cramped, so stack them vertically: switch to the mobile layout and place the widgets again in the mobile arrangement. This takes some rework, but it gives you much better control over the user experience.
Copy this script into the Ignition Designer Script Console (Tools → Script Console) and run it. It checks the Downtime Dashboards folder and the OEE and Top Downtimes dashboard (enabled, with a description), that the Desktop layout has the two selectors and the two analytics widgets side by side, that the OEE widget's oeeMetric is availability and both analytics widgets leave assetPath as null, and that the Mobile layout stacks the two analytics widgets. Each check prints PASS or FAIL with what to fix. It doesn't change anything.
Show the check script
# Kanoa Ops Training - check "Build a Dashboard" (Analytics, lesson 1)
#
# Run in the Ignition Designer Script Console (Tools > Script Console), in the
# project that inherits KANOA_SCRIPTS. It only reads; it changes nothing.
# Every check prints PASS or FAIL with what to fix, then a summary.
import re
DASHBOARD = 'OEE and Top Downtimes'
FOLDER = 'Downtime Dashboards'
SELECTORS = ['Asset Selector', 'Date Range Selector']
OEE = 'Asset OEE Bar Chart'
DOWNTIMES = 'Top Downtimes'
results = []
def check(ok, what, fix=None):
results.append(bool(ok))
if ok:
print 'PASS %s' % what
else:
print 'FAIL %s' % what
if fix:
print ' -> %s' % fix
def note(what):
print 'NOTE %s' % what
def field(row, *names):
for n in names:
if n in row and row[n] is not None:
return row[n]
return None
def text(v):
return '' if v is None else unicode(v).strip()
def key(v):
return ''.join(ch for ch in text(v).lower() if ch.isalnum())
def truthy(v):
return str(v).lower() in ('true', '1', 'yes')
def rows(data):
# Kanoa returns a dataset, a list of dictionaries, or None
if data is None:
return []
if hasattr(data, 'getRowCount'):
cols = list(data.getColumnNames())
return [dict((c, data.getValueAt(i, c)) for c in cols) for i in range(data.getRowCount())]
return [dict(r) for r in data]
def params(widget):
# viewParams is JSON, but can hold Python-style None/True from older widgets
raw = text(field(widget, 'viewParams'))
if not raw:
return {}
for candidate in (raw, raw.replace('None', 'null').replace('True', 'true').replace('False', 'false')):
try:
return system.util.jsonDecode(candidate) or {}
except:
pass
return {}
def span(widget, start, end):
a, b = field(widget, start), field(widget, end)
return (int(a), int(b)) if a is not None and b is not None else None
def overlaps(s1, s2):
# grid ranges are start-inclusive, end-exclusive (CSS grid lines)
return s1 is not None and s2 is not None and s1[0] < s2[1] and s2[0] < s1[1]
print '=' * 70
print 'Kanoa Ops Training - Build a Dashboard check'
print '=' * 70
# 1. Folder and dashboard
print '\n-- Dashboard'
folders = rows(system.kanoa.dashboard.getFolders({}))
folder = [f for f in folders if text(field(f, 'folderName')) == FOLDER]
near = [f for f in folders if key(field(f, 'folderName')) == key(FOLDER) and f not in folder]
if folder:
check(True, 'Folder %s exists' % FOLDER)
elif near:
check(False, 'Folder %s exists' % FOLDER, "Found '%s'. Rename it to %s." % (text(field(near[0], 'folderName')), FOLDER))
folder = near
else:
check(False, 'Folder %s exists' % FOLDER,
'In Dashboards > Dashboard Editor, click the folder icon and add a folder named %s.' % FOLDER)
folderIds = set(field(f, 'folderId') for f in folder)
dashboards = rows(system.kanoa.dashboard.getDashboards({}))
board = [d for d in dashboards if text(field(d, 'dashboardName')) == DASHBOARD]
if not board:
near = [d for d in dashboards if key(field(d, 'dashboardName')) == key(DASHBOARD)] or \
[d for d in dashboards if 'oee' in key(field(d, 'dashboardName')) and 'downtime' in key(field(d, 'dashboardName'))]
if near:
check(False, 'Dashboard %s exists' % DASHBOARD,
"Found '%s'. Rename it to %s." % (text(field(near[0], 'dashboardName')), DASHBOARD))
board = near
else:
check(False, 'Dashboard %s exists' % DASHBOARD,
'In Dashboards > Dashboard Editor, create a dashboard named %s and save it.' % DASHBOARD)
else:
check(True, 'Dashboard %s exists' % DASHBOARD)
if board:
d = board[0]
dashboardId = field(d, 'dashboardId')
if folderIds:
check(field(d, 'folderId') in folderIds, '%s is in the %s folder' % (DASHBOARD, FOLDER),
'Edit the dashboard and set its Folder to %s.' % FOLDER)
enabled = field(d, 'enabled')
if enabled is not None:
check(truthy(enabled), '%s is enabled' % DASHBOARD, 'Edit the dashboard and tick Enabled.')
check(text(field(d, 'dashboardDescription', 'description')),
'%s has a description' % DASHBOARD, "Edit the dashboard and set its Description, e.g. Understand what's driving your OEE.")
# 2. Widgets on each layout
widgets = rows(system.kanoa.dashboard.getDashboardWidgets({'dashboardId': dashboardId}))
widgets = [w for w in widgets if field(w, 'enabled') is None or truthy(field(w, 'enabled'))]
layouts = {}
for w in widgets:
layouts.setdefault(text(field(w, 'breakpointName')) or 'Desktop', []).append(w)
def find(layout, name):
return [w for w in layouts.get(layout, []) if key(name) in key(field(w, 'widgetName'))]
print '\n-- Desktop layout'
if not layouts:
check(False, '%s has widgets' % DASHBOARD, 'Open it in the Dashboard Editor and add the widgets from the lesson.')
desktop = 'Desktop' if 'Desktop' in layouts else (sorted(layouts)[0] if layouts else 'Desktop')
for name in SELECTORS + [OEE, DOWNTIMES]:
check(find(desktop, name), '%s layout has the %s widget' % (desktop, name),
'In the Dashboard Editor (%s layout), select an empty area of the canvas and add %s.' % (desktop, name))
oee, top = find(desktop, OEE), find(desktop, DOWNTIMES)
if oee:
p = params(oee[0])
metric = text(p.get('oeeMetric'))
check(metric.lower() == 'availability', 'OEE widget shows availability (oeeMetric)',
"oeeMetric is '%s'. Click the OEE widget and set oeeMetric to availability in its View Params." % (metric or 'not set'))
for w, name in [(oee, OEE), (top, DOWNTIMES)]:
if w:
p = params(w[0])
if 'assetPath' in p:
check(p.get('assetPath') in (None, ''), '%s follows the Asset Selector (assetPath is null)' % name,
"assetPath is '%s'. Set it to null so the Asset Selector drives this widget." % text(p.get('assetPath')))
if oee and top:
a, b = oee[0], top[0]
sideBySide = overlaps(span(a, 'rowStart', 'rowEnd'), span(b, 'rowStart', 'rowEnd')) and \
not overlaps(span(a, 'columnStart', 'columnEnd'), span(b, 'columnStart', 'columnEnd'))
check(sideBySide, 'On %s, the OEE and Top Downtimes widgets are side by side' % desktop,
'Place them next to each other on the same rows.')
# 3. Mobile layout: the two analytics widgets stacked
print '\n-- Mobile layout'
moOee, moTop = find('Mobile', OEE), find('Mobile', DOWNTIMES)
check(moOee and moTop, 'Mobile layout has the OEE and Top Downtimes widgets',
'At the top of the Dashboard Editor, switch to Mobile and place the widgets again for that layout.')
if moOee and moTop:
a, b = moOee[0], moTop[0]
stacked = not overlaps(span(a, 'rowStart', 'rowEnd'), span(b, 'rowStart', 'rowEnd'))
check(stacked, 'On Mobile, the OEE and Top Downtimes widgets are stacked',
'Place one above the other, each across the width of the Mobile layout.')
others = sorted(l for l in layouts if l not in (desktop, 'Mobile'))
if others:
note('Also laid out for: %s.' % ', '.join(others))
passed = results.count(True)
print '\n' + '=' * 70
print 'Result: %d of %d checks passed' % (passed, len(results))
if passed == len(results):
print 'All good. Your dashboard is built.'
else:
print 'Fix the FAIL items above, then run this again.'
print '=' * 70








