forked from b.peck/BAT
Add per-area alarm breakdown (Areas tab + AreaCard), disable sim tick
New area_breakdown() calc groups top-5 alarm sources by plant area for a new Areas tab / AreaCard component. Disabled AlarmSimTick so it stops generating data during dev. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"scope": "G",
|
||||
"version": 1,
|
||||
"restricted": false,
|
||||
"overridable": true,
|
||||
"files": [
|
||||
"view.json"
|
||||
],
|
||||
"attributes": {}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
{
|
||||
"custom": {},
|
||||
"params": {
|
||||
"area": null,
|
||||
"top": null,
|
||||
"total": null
|
||||
},
|
||||
"propConfig": {
|
||||
"params.area": {
|
||||
"paramDirection": "input",
|
||||
"persistent": false
|
||||
},
|
||||
"params.top": {
|
||||
"paramDirection": "input",
|
||||
"persistent": false
|
||||
},
|
||||
"params.total": {
|
||||
"paramDirection": "input",
|
||||
"persistent": false
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"defaultSize": {
|
||||
"height": 240,
|
||||
"width": 340
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"children": [
|
||||
{
|
||||
"children": [
|
||||
{
|
||||
"meta": {
|
||||
"name": "lblArea"
|
||||
},
|
||||
"position": {
|
||||
"grow": 1,
|
||||
"shrink": 1
|
||||
},
|
||||
"propConfig": {
|
||||
"props.text": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"path": "view.params.area"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\treturn value or ''",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
"type": "property"
|
||||
}
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"style": {
|
||||
"fontSize": "15px",
|
||||
"fontWeight": "bold",
|
||||
"overflow": "hidden",
|
||||
"textOverflow": "ellipsis",
|
||||
"whiteSpace": "nowrap"
|
||||
}
|
||||
},
|
||||
"type": "ia.display.label"
|
||||
},
|
||||
{
|
||||
"meta": {
|
||||
"name": "chipTotal"
|
||||
},
|
||||
"position": {
|
||||
"shrink": 0
|
||||
},
|
||||
"propConfig": {
|
||||
"props.text": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"path": "view.params.total"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn '%s activations' % PrimeControls.fmt.num(int(value or 0))\n\texcept:\n\t\treturn ''",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
"type": "property"
|
||||
}
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"style": {
|
||||
"classes": "PrimeControls/Chip/Flat",
|
||||
"padding": "2px 8px"
|
||||
}
|
||||
},
|
||||
"type": "ia.display.label"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"name": "headerRow"
|
||||
},
|
||||
"position": {
|
||||
"basis": "26px",
|
||||
"shrink": 0
|
||||
},
|
||||
"props": {
|
||||
"alignItems": "center",
|
||||
"style": {
|
||||
"gap": "8px"
|
||||
}
|
||||
},
|
||||
"type": "ia.container.flex"
|
||||
},
|
||||
{
|
||||
"meta": {
|
||||
"name": "repTop"
|
||||
},
|
||||
"position": {
|
||||
"grow": 1
|
||||
},
|
||||
"propConfig": {
|
||||
"props.instances": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"path": "view.params.top"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\tout = []\n\tfor r in (value or []):\n\t\ttry:\n\t\t\tout.append({'rank': int(r['rank']), 'source': r['source'], 'label': r['label'], 'count': int(r['count']), 'pct': float(r['pct'])})\n\t\texcept:\n\t\t\tpass\n\treturn out",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
"type": "property"
|
||||
}
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"direction": "column",
|
||||
"elementPosition": {
|
||||
"basis": "34px",
|
||||
"grow": 0,
|
||||
"shrink": 0
|
||||
},
|
||||
"path": "PrimeControls/Components/SourceRow",
|
||||
"style": {
|
||||
"gap": "4px",
|
||||
"overflow": "hidden"
|
||||
},
|
||||
"useDefaultViewHeight": false,
|
||||
"useDefaultViewWidth": false
|
||||
},
|
||||
"type": "ia.display.flex-repeater"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"name": "root"
|
||||
},
|
||||
"props": {
|
||||
"direction": "column",
|
||||
"style": {
|
||||
"classes": "PrimeControls/Card",
|
||||
"gap": "8px",
|
||||
"overflow": "hidden"
|
||||
}
|
||||
},
|
||||
"type": "ia.container.flex"
|
||||
}
|
||||
}
|
||||
@@ -128,6 +128,23 @@
|
||||
"path": "PrimeControls/Components/TabButton"
|
||||
},
|
||||
"type": "ia.display.view"
|
||||
},
|
||||
{
|
||||
"meta": {
|
||||
"name": "tabBtn4"
|
||||
},
|
||||
"position": {
|
||||
"basis": "140px",
|
||||
"shrink": 0
|
||||
},
|
||||
"props": {
|
||||
"params": {
|
||||
"index": 4,
|
||||
"title": "Areas"
|
||||
},
|
||||
"path": "PrimeControls/Components/TabButton"
|
||||
},
|
||||
"type": "ia.display.view"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
@@ -301,6 +318,36 @@
|
||||
"path": "PrimeControls/Tabs/Journal"
|
||||
},
|
||||
"type": "ia.display.view"
|
||||
},
|
||||
{
|
||||
"meta": {
|
||||
"name": "tabAreas"
|
||||
},
|
||||
"position": {
|
||||
"grow": 1
|
||||
},
|
||||
"propConfig": {
|
||||
"position.display": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"expression": "{session.custom.PrimeControls.ui.selectedTab} = 4"
|
||||
},
|
||||
"type": "expr"
|
||||
}
|
||||
},
|
||||
"props.params.bundle": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"path": "view.custom.bundle"
|
||||
},
|
||||
"type": "property"
|
||||
}
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"path": "PrimeControls/Tabs/Areas"
|
||||
},
|
||||
"type": "ia.display.view"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"scope": "G",
|
||||
"version": 1,
|
||||
"restricted": false,
|
||||
"overridable": true,
|
||||
"files": [
|
||||
"view.json"
|
||||
],
|
||||
"attributes": {}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
{
|
||||
"custom": {},
|
||||
"params": {
|
||||
"bundle": null
|
||||
},
|
||||
"propConfig": {
|
||||
"params.bundle": {
|
||||
"paramDirection": "input",
|
||||
"persistent": false
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"defaultSize": {
|
||||
"height": 700,
|
||||
"width": 1600
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"children": [
|
||||
{
|
||||
"meta": {
|
||||
"name": "lblTitle"
|
||||
},
|
||||
"position": {
|
||||
"shrink": 0
|
||||
},
|
||||
"props": {
|
||||
"style": {
|
||||
"classes": "PrimeControls/Text/Muted",
|
||||
"fontWeight": "bold",
|
||||
"letterSpacing": "1px"
|
||||
},
|
||||
"text": "TOP 5 ALARMS BY AREA"
|
||||
},
|
||||
"type": "ia.display.label"
|
||||
},
|
||||
{
|
||||
"meta": {
|
||||
"name": "repAreas"
|
||||
},
|
||||
"position": {
|
||||
"grow": 1
|
||||
},
|
||||
"propConfig": {
|
||||
"position.display": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"path": "view.params.bundle"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn len(value['areas_top'] or []) > 0\n\texcept:\n\t\treturn False",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
"type": "property"
|
||||
}
|
||||
},
|
||||
"props.instances": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"path": "view.params.bundle"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn [{'area': a['area'], 'total': a['total'], 'top': a['top']} for a in (value['areas_top'] or [])]\n\texcept:\n\t\treturn []",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
"type": "property"
|
||||
}
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"alignContent": "flex-start",
|
||||
"direction": "row",
|
||||
"elementPosition": {
|
||||
"basis": "340px",
|
||||
"grow": 1,
|
||||
"shrink": 0
|
||||
},
|
||||
"path": "PrimeControls/Components/AreaCard",
|
||||
"style": {
|
||||
"gap": "10px",
|
||||
"overflow": "auto"
|
||||
},
|
||||
"useDefaultViewHeight": false,
|
||||
"useDefaultViewWidth": false,
|
||||
"wrap": "wrap",
|
||||
"elementStyle": {
|
||||
"maxWidth": "460px"
|
||||
}
|
||||
},
|
||||
"type": "ia.display.flex-repeater"
|
||||
},
|
||||
{
|
||||
"meta": {
|
||||
"name": "emptyAreas"
|
||||
},
|
||||
"position": {
|
||||
"grow": 1
|
||||
},
|
||||
"propConfig": {
|
||||
"position.display": {
|
||||
"binding": {
|
||||
"config": {
|
||||
"path": "view.params.bundle"
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\treturn len(value['areas_top'] or []) == 0\n\texcept:\n\t\treturn True",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
"type": "property"
|
||||
}
|
||||
}
|
||||
},
|
||||
"props": {
|
||||
"params": {
|
||||
"icon": "material/layers_clear",
|
||||
"message": "No alarm activity in range"
|
||||
},
|
||||
"path": "PrimeControls/Components/EmptyState"
|
||||
},
|
||||
"type": "ia.display.view"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"name": "root"
|
||||
},
|
||||
"props": {
|
||||
"direction": "column",
|
||||
"style": {
|
||||
"gap": "8px",
|
||||
"padding": "10px"
|
||||
}
|
||||
},
|
||||
"type": "ia.container.flex"
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@
|
||||
},
|
||||
"transforms": [
|
||||
{
|
||||
"code": "\ttry:\n\t\trows = value['bundle']['pareto']['rows'] or []\n\texcept:\n\t\trows = []\n\ttry:\n\t\ttotal = value['bundle']['pareto']['total_activations'] or 0\n\texcept:\n\t\ttotal = 0\n\ttry:\n\t\tfocus = value['focus'] or ''\n\texcept:\n\t\tfocus = ''\n\thi = {'backgroundColor': 'rgba(59, 125, 216, 0.18)', 'fontWeight': 'bold'}\n\tpri = ['Diagnostic', 'Low', 'Medium', 'High', 'Critical']\n\tout = []\n\trank = 0\n\tfor r in rows[:10]:\n\t\ttry:\n\t\t\trank = rank + 1\n\t\t\tsrc = r['source'] or ''\n\t\t\tcnt = r['count'] or 0\n\t\t\td = {}\n\t\t\td['rank'] = rank\n\t\t\td['source'] = src\n\t\t\td['label'] = r['label'] or src\n\t\t\td['area'] = r['area'] or ''\n\t\t\td['count'] = cnt\n\t\t\tif total > 0:\n\t\t\t\td['share'] = round(cnt * 100.0 / total, 1)\n\t\t\telse:\n\t\t\t\td['share'] = 0\n\t\t\ttry:\n\t\t\t\td['cum'] = PrimeControls.fmt.pct(r['cum_pct'])\n\t\t\texcept:\n\t\t\t\td['cum'] = '--'\n\t\t\tif focus and src == focus:\n\t\t\t\tfor k in ('label', 'area', 'count', 'cum'):\n\t\t\t\t\td[k] = {'value': d[k], 'style': hi}\n\t\t\tpname = r['priority_name'] or ''\n\t\t\tif pname in pri:\n\t\t\t\td['priority'] = {'value': pname, 'style': {'classes': 'PrimeControls/PrioritySoft/' + pname}}\n\t\t\telse:\n\t\t\t\td['priority'] = pname\n\t\t\tout.append(d)\n\t\texcept:\n\t\t\tpass\n\treturn out",
|
||||
"code": "\ttry:\n\t\trows = value['bundle']['pareto']['rows'] or []\n\texcept:\n\t\trows = []\n\ttry:\n\t\ttotal = value['bundle']['pareto']['total_activations'] or 0\n\texcept:\n\t\ttotal = 0\n\ttry:\n\t\tfocus = value['focus'] or ''\n\texcept:\n\t\tfocus = ''\n\thi = {'backgroundColor': 'rgba(59, 125, 216, 0.18)', 'fontWeight': 'bold'}\n\tpri = ['Diagnostic', 'Low', 'Medium', 'High', 'Critical']\n\tout = []\n\trank = 0\n\tfor r in rows[:10]:\n\t\ttry:\n\t\t\trank = rank + 1\n\t\t\tsrc = r['source'] or ''\n\t\t\tcnt = r['count'] or 0\n\t\t\td = {}\n\t\t\td['rank'] = rank\n\t\t\td['source'] = src\n\t\t\td['label'] = r['label'] or src\n\t\t\td['area'] = r['area'] or ''\n\t\t\td['count'] = cnt\n\t\t\tif total > 0:\n\t\t\t\td['share'] = round(cnt * 100.0 / total, 1)\n\t\t\telse:\n\t\t\t\td['share'] = 0\n\t\t\ttry:\n\t\t\t\td['cum'] = PrimeControls.fmt.pct(r['cum_pct'])\n\t\t\texcept:\n\t\t\t\td['cum'] = '--'\n\t\t\tif focus and src == focus:\n\t\t\t\tfor k in ('label', 'area', 'count', 'cum', 'share'):\n\t\t\t\t\td[k] = {'value': d[k], 'style': hi}\n\t\t\tpname = r['priority_name'] or ''\n\t\t\tif pname in pri:\n\t\t\t\td['priority'] = {'value': pname, 'style': {'classes': 'PrimeControls/PrioritySoft/' + pname}}\n\t\t\telse:\n\t\t\t\td['priority'] = pname\n\t\t\tout.append(d)\n\t\texcept:\n\t\t\tpass\n\treturn out",
|
||||
"type": "script"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -536,6 +536,40 @@ def heatmap(instances, opts=None):
|
||||
"max_count": mx, "total": total}
|
||||
|
||||
|
||||
def area_breakdown(instances, opts=None, per_area=5):
|
||||
"""Top-N most active alarm sources per plant area.
|
||||
Returns [{area, total, top: [{rank, source, label, priority_name, count,
|
||||
pct}]}] sorted by area total desc; pct is share of the AREA's activations."""
|
||||
groups = {}
|
||||
for inst in instances or []:
|
||||
if inst.get("active_ms") is None:
|
||||
continue
|
||||
a = inst.get("area") or "unknown"
|
||||
if a not in groups:
|
||||
groups[a] = {"total": 0, "counts": {}, "refs": {}}
|
||||
g = groups[a]
|
||||
g["total"] += 1
|
||||
src = inst["source"]
|
||||
g["counts"][src] = g["counts"].get(src, 0) + 1
|
||||
if src not in g["refs"]:
|
||||
g["refs"][src] = inst
|
||||
out = []
|
||||
for a in groups:
|
||||
g = groups[a]
|
||||
ordered = sorted(g["counts"].items(),
|
||||
key=lambda kv: (-kv[1], g["refs"][kv[0]]["label"]))[:per_area]
|
||||
top = []
|
||||
for i in range(len(ordered)):
|
||||
src, c = ordered[i]
|
||||
ref = g["refs"][src]
|
||||
top.append({"rank": i + 1, "source": src, "label": ref["label"],
|
||||
"priority_name": ref["priority_name"], "count": c,
|
||||
"pct": 100.0 * c / g["total"] if g["total"] else 0.0})
|
||||
out.append({"area": a, "total": g["total"], "top": top})
|
||||
out.sort(key=lambda x: (-x["total"], x["area"]))
|
||||
return out
|
||||
|
||||
|
||||
def priority_distribution(instances, opts=None):
|
||||
o = opts_merged(opts)
|
||||
raw_counts = {}
|
||||
@@ -727,6 +761,7 @@ def _assemble(instances, active_now, shelved_count, start_ms, end_ms, now_ms,
|
||||
par = pareto(current, o)
|
||||
prio = priority_distribution(current, o)
|
||||
heat = heatmap(current, o)
|
||||
areas_top = area_breakdown(current, o)
|
||||
|
||||
# prior mirrors (cheap subset for deltas)
|
||||
p_start = start_ms - (end_ms - start_ms)
|
||||
@@ -800,6 +835,7 @@ def _assemble(instances, active_now, shelved_count, start_ms, end_ms, now_ms,
|
||||
"heatmap": heat,
|
||||
"mtta_mttr": mm,
|
||||
"pareto": par,
|
||||
"areas_top": areas_top,
|
||||
"top_sources": top_sources(par, 5),
|
||||
"chattering": chat,
|
||||
"fleeting": fleet,
|
||||
|
||||
@@ -10,6 +10,6 @@
|
||||
"sharedThread": false,
|
||||
"delay": 5000,
|
||||
"fixedDelay": true,
|
||||
"enabled": true
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user