<bmx-process-explorer>
Process mining in the browser: give it an event log - one row for each thing done, saying which case (an order, a claim, a ticket) it was done to, what was done and when, exported from an ERP, CRM, ticketing or workflow system - and it shows how the work really flows:
13 properties · 3 events · 15 methods · 7 parts
Example
Four hundred purchase orders at a fictional supplier. Choose an activity or a path for its figures, switch the map to Duration to see where orders wait, or press Replay.
Show markup
<bmx-process-explorer id="ex-pe" reference='{"path":["Create requisition","Approve requisition","Create purchase order","Send purchase order","Receive goods","Receive invoice","Match invoice","Pay invoice"],"optional":["Approve requisition"]}' style="--bmx-process-explorer-height: 32rem"></bmx-process-explorer>
<p class="note" id="ex-pe-out" role="status" style="margin: 12px 0 0">Four hundred purchase orders at a fictional supplier. Choose an activity or a path for its figures, switch the map to Duration to see where orders wait, or press Replay.</p>
<script type="module">
await customElements.whenDefined('bmx-process-explorer');
const pe = document.getElementById('ex-pe');
const out = document.getElementById('ex-pe-out');
// A purchase-to-pay log for Fernbrook Supplies, a fictional company, made up here with a seeded
// random generator so it reads the same every time: one row per thing done to a purchase order.
let seed = 7;
const rnd = () => (seed = (seed * 1664525 + 1013904223) >>> 0) / 4294967296;
const pick = list => list[Math.floor(rnd() * list.length)];
const H = 3600000;
const people = { buyer: ['Ana Ruiz', 'Tom Hale', 'Priya Nair'], approver: ['M. Okafor', 'J. Lindqvist'], stores: ['Goods-in A', 'Goods-in B'], ap: ['Payables 1', 'Payables 2'] };
const rows = [];
for (let i = 0; i < 400; i += 1) {
const order = `PO-${24001 + i}`;
const region = rnd() < 0.55 ? 'North' : 'South';
const amount = Math.round(200 + rnd() * rnd() * 18000);
let t = Date.UTC(2026, 0, 5, 8) + Math.floor(rnd() * 85 * 24) * H;
const step = (activity, who, waitHours) => {
t += Math.round(waitHours * H);
rows.push({ case: order, activity, timestamp: new Date(t).toISOString(), resource: who, region, amount: String(amount) });
};
step('Create requisition', pick(people.buyer), 0);
// Small orders skip approval (the reference marks it optional); a few big ones are turned down.
if (amount > 1000) {
step('Approve requisition', pick(people.approver), 4 + rnd() * 30 * (amount > 8000 ? 2 : 1));
if (rnd() < 0.06) {
step('Reject requisition', pick(people.approver), 1);
continue;
}
}
step('Create purchase order', pick(people.buyer), 1 + rnd() * 6);
if (rnd() < 0.14) {
step('Change purchase order', pick(people.buyer), 2 + rnd() * 20);
step('Approve requisition', pick(people.approver), 5 + rnd() * 30);
}
step('Send purchase order', pick(people.buyer), 0.5 + rnd() * 3);
const invoiceFirst = rnd() < 0.12;
if (invoiceFirst) step('Receive invoice', pick(people.ap), 24 + rnd() * 72);
step('Receive goods', pick(people.stores), 48 + rnd() * 120);
if (!invoiceFirst) step('Receive invoice', pick(people.ap), 2 + rnd() * 48);
step('Match invoice', pick(people.ap), 2 + rnd() * 24);
if (rnd() < 0.18) {
step('Hold invoice', pick(people.ap), 1);
step('Match invoice', pick(people.ap), 24 + rnd() * 96);
}
step('Pay invoice', pick(people.ap), 24 + rnd() * 200);
}
pe.log = rows;
pe.addEventListener('bmxSelect', e => {
const d = e.detail;
out.textContent = d.kind === 'activity' ? `bmxSelect: activity "${d.activity}".` : d.kind === 'path' ? `bmxSelect: path ${d.path.from} → ${d.path.to}, ${d.path.count} times.` : `bmxSelect: ${d.kind}.`;
});
pe.addEventListener('bmxFilterChange', e => {
out.textContent = `bmxFilterChange: ${e.detail.cases} cases left.`;
});
</script>
- the process map, discovered from the log: every activity and every
hand-over, drawn thicker the more cases take it, or coloured by how long
the wait is; sliders keep only the activities and paths that matter;
- an animated replay of the cases moving through the map over time;
- variants (the different ways cases went), ranked by how common;
- every case, with its timeline;
- conformance against the way cases are meant to go: skipped, extra, repeated and out-of-order steps, case by case;
- statistics: durations, cases started each week, the longest waits;
- filters by date, duration, activity, variant and any attribute;
- exports: the map as SVG or PNG, cases and variants as CSV.
<bmx-process-explorer id="pe" reference="Order → Approve → Ship → Invoice → Pay"></bmx-process-explorer>
<script>
pe.log = await (await fetch('orders.csv')).text(); // CSV, XES, JSON or an array of records
</script>
Everything runs in the page: the log never leaves it. The same engine is in
dist/node/bmx-process-explorer.mjs for a Worker or a server.
The map is keyboard-first: every activity is a button (arrow keys move to the nearest activity in that direction, Enter shows its figures, + and - zoom, 0 fits), and every figure the map draws is also in a table in the Statistics view.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
activityCoverage |
activity-coverage |
number |
100 |
The share of all events the activities kept cover, 1-100 (the busiest first). Mutable. |
allowOpen |
allow-open |
boolean |
true |
Offer the "Open a log" button. |
columns |
columns |
BmxProcessColumns | string |
— | Which columns hold the case, activity, time, end and resource (found from the header when not given). Object or JSON. |
direction |
direction |
BmxProcessDirection |
'down' |
Which way the map flows. Mutable. |
fileName |
file-name |
string |
'process' |
The name downloads are given (without the extension). |
filters |
filters |
BmxProcessFilters | string |
{} |
The filters applied (object or JSON). Mutable. |
label |
label |
string |
— | The element's accessible name. Default: "Process explorer". |
log |
log |
readonly Record<string, unknown>[] | string |
'' |
The event log: an array of records, or CSV, XES or JSON text. |
metric |
metric |
BmxProcessMetric |
'frequency' |
What the map shows: how often (frequency) or how long (duration). Mutable. |
pathCoverage |
path-coverage |
number |
100 |
The share of the path traffic drawn, 1-100 (the busiest first). Mutable. |
reference |
reference |
BmxProcessReference | readonly string[] | string |
— | The way cases are meant to go, to check them against: A → B → C, an array, or { path, optional } (object or JSON). |
strings |
strings |
Partial<BmxProcessExplorerStrings> | string |
— | Words to show instead of the English ones, as an object or JSON. |
view |
view |
BmxProcessView |
'map' |
The view shown. Mutable. |
Events
| Event | Detail | Description |
|---|---|---|
bmxFilterChange |
BmxProcessFilterDetail |
Fired when the filters change. |
bmxLoad |
BmxProcessLoadDetail |
Fired when a log has been read. |
bmxSelect |
BmxProcessSelectDetail |
Fired when an activity, path, variant or case is chosen. |
Methods
| Method | Signature | Description |
|---|---|---|
clearFilters |
clearFilters() => Promise<void> |
Removes every filter. |
download |
download(format?: BmxProcessExportFormat, fileName?: string) => Promise<void> |
Saves an export as a file. |
exportCsv |
exportCsv(kind?: "cases" | "variants") => Promise<string> |
The cases (with conformance, when there is a reference) or the variants, as CSV. |
exportSvg |
exportSvg() => Promise<string> |
The map as a stand-alone SVG document. |
fit |
fit() => Promise<void> |
Fits the map to the view. |
getCases |
getCases() => Promise<BmxProcessCase[]> |
The cases the filters leave. |
getConformance |
getConformance() => Promise<BmxConformanceSummary | undefined> |
The conformance of the cases the filters leave (needs reference). |
getMap |
getMap() => Promise<BmxProcessMap> |
The map (activities and paths) as drawn. |
getSummary |
getSummary() => Promise<BmxProcessSummary> |
The figures for the cases the filters leave. |
getVariants |
getVariants() => Promise<BmxProcessVariant[]> |
The variants of the cases the filters leave, most frequent first. |
pause |
pause() => Promise<void> |
Pauses the replay. |
play |
play() => Promise<void> |
Starts the replay (from the start when it has finished). |
seek |
seek(time: number) => Promise<void> |
Moves the replay to a moment (milliseconds). |
selectActivity |
selectActivity(name: string) => Promise<void> |
Chooses an activity on the map and shows its figures. |
setFilters |
setFilters(filters: BmxProcessFilters) => Promise<void> |
Replaces the filters. |
CSS shadow parts
| Part | Description |
|---|---|
details |
The details panel beside the map. |
edge |
A path on the map. |
node |
An activity on the map; also node-selected. |
stats |
The figures across the top. |
table |
A table (variants, cases, statistics). |
tabs |
The row of views. |
viewport |
The area the map is drawn in. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-process-explorer-background |
The map's background. |
--bmx-process-explorer-details-width |
The details panel's width. |
--bmx-process-explorer-height |
The map's height. |
--bmx-process-explorer-path |
The colour of paths. |
--bmx-process-explorer-radius |
Corner radius of the element. |
--bmx-process-explorer-token |
The colour of replay tokens. |