@elabs-ai/components-process 4.2.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/dist/core/index.d.ts +801 -3
- package/dist/core/index.js +1334 -0
- package/dist/core/index.js.map +1 -1
- package/dist/index.d.ts +1889 -34
- package/dist/index.js +5512 -196
- package/dist/index.js.map +1 -1
- package/dist/test/index.d.ts +223 -5
- package/dist/test/index.js +346 -191
- package/dist/test/index.js.map +1 -1
- package/package.json +14 -13
- package/src/__contract__/case-table.contract.test.tsx +49 -0
- package/src/__contract__/compare-kpi-strip.contract.test.tsx +49 -0
- package/src/__contract__/conformance-overlay.contract.test.tsx +49 -0
- package/src/__contract__/happy-path-editor.contract.test.tsx +49 -0
- package/src/__contract__/violation-list.contract.test.tsx +49 -0
- package/src/abstraction-controls/abstraction-controls-per-type.test.tsx +80 -0
- package/src/abstraction-controls/abstraction-controls.stories.tsx +43 -1
- package/src/abstraction-controls/abstraction-controls.tsx +198 -5
- package/src/case-table/case-table.stories.tsx +89 -0
- package/src/case-table/case-table.test.tsx +148 -0
- package/src/case-table/case-table.tsx +144 -0
- package/src/case-table/columns.ts +116 -0
- package/src/case-table/index.ts +11 -0
- package/src/case-timeline/case-timeline-model.test.ts +72 -0
- package/src/case-timeline/case-timeline-model.ts +112 -0
- package/src/case-timeline/case-timeline.stories.tsx +94 -0
- package/src/case-timeline/case-timeline.test.tsx +51 -0
- package/src/case-timeline/case-timeline.tsx +109 -0
- package/src/case-timeline/index.ts +9 -0
- package/src/conformance-overlay/conformance-fixture.ts +59 -0
- package/src/conformance-overlay/conformance-legend.tsx +109 -0
- package/src/conformance-overlay/conformance-overlay.stories.tsx +116 -0
- package/src/conformance-overlay/conformance-overlay.test.tsx +88 -0
- package/src/conformance-overlay/conformance-overlay.tsx +107 -0
- package/src/conformance-overlay/conformance-state.test.ts +79 -0
- package/src/conformance-overlay/conformance-state.ts +220 -0
- package/src/conformance-overlay/index.ts +4 -0
- package/src/core/activity-color-scale.test.ts +107 -0
- package/src/core/activity-color-scale.ts +133 -0
- package/src/core/adapters/ocel.test.ts +112 -0
- package/src/core/adapters/ocel.ts +359 -0
- package/src/core/adapters/xes.test.ts +293 -0
- package/src/core/adapters/xes.ts +384 -0
- package/src/core/cases-from-log.test.ts +72 -0
- package/src/core/cases-from-log.ts +85 -0
- package/src/core/conformance.test.ts +80 -0
- package/src/core/conformance.ts +91 -0
- package/src/core/diff-graphs.test.ts +151 -0
- package/src/core/diff-graphs.ts +118 -0
- package/src/core/discover-object-centric-graph.test.ts +94 -0
- package/src/core/discover-object-centric-graph.ts +296 -0
- package/src/core/fixtures/ocel-sample.ts +82 -0
- package/src/core/fixtures/sample.xes +68 -0
- package/src/core/index.ts +113 -0
- package/src/core/reference-model.test.ts +43 -0
- package/src/core/reference-model.ts +116 -0
- package/src/core/replay-timeline.test.ts +161 -0
- package/src/core/replay-timeline.ts +260 -0
- package/src/core/segments.test.ts +185 -0
- package/src/core/segments.ts +153 -0
- package/src/core/token-replay.test.ts +218 -0
- package/src/core/token-replay.ts +456 -0
- package/src/core/types.ts +2 -2
- package/src/dotted-chart/compute-dots.test.ts +176 -0
- package/src/dotted-chart/compute-dots.ts +241 -0
- package/src/dotted-chart/dotted-chart-labels.ts +93 -0
- package/src/dotted-chart/dotted-chart.stories.tsx +182 -0
- package/src/dotted-chart/dotted-chart.test.tsx +135 -0
- package/src/dotted-chart/dotted-chart.tsx +841 -0
- package/src/dotted-chart/index.ts +23 -0
- package/src/dotted-chart/use-element-size.ts +33 -0
- package/src/happy-path-editor/happy-path-editor-context.ts +81 -0
- package/src/happy-path-editor/happy-path-editor.stories.tsx +116 -0
- package/src/happy-path-editor/happy-path-editor.test.tsx +142 -0
- package/src/happy-path-editor/happy-path-editor.tsx +239 -0
- package/src/happy-path-editor/happy-path-step-node.tsx +175 -0
- package/src/happy-path-editor/index.ts +4 -0
- package/src/index.ts +51 -1
- package/src/performance-spectrum/aggregate-segments.test.ts +107 -0
- package/src/performance-spectrum/aggregate-segments.ts +174 -0
- package/src/performance-spectrum/index.ts +25 -0
- package/src/performance-spectrum/performance-spectrum-context.tsx +116 -0
- package/src/performance-spectrum/performance-spectrum.stories.tsx +128 -0
- package/src/performance-spectrum/performance-spectrum.test.tsx +190 -0
- package/src/performance-spectrum/performance-spectrum.tsx +870 -0
- package/src/process-compare/compare-kpi-strip.stories.tsx +48 -0
- package/src/process-compare/compare-kpi-strip.tsx +94 -0
- package/src/process-compare/compare-model.ts +83 -0
- package/src/process-compare/compare-side.tsx +42 -0
- package/src/process-compare/diff-to-graph.ts +104 -0
- package/src/process-compare/index.ts +23 -0
- package/src/process-compare/process-compare.stories.tsx +184 -0
- package/src/process-compare/process-compare.test.tsx +224 -0
- package/src/process-compare/process-compare.tsx +251 -0
- package/src/process-explorer.stories.tsx +1 -1
- package/src/process-filter-bar/index.ts +2 -0
- package/src/process-filter-bar/process-filter-bar.stories.tsx +156 -0
- package/src/process-filter-bar/process-filter-bar.test.tsx +201 -0
- package/src/process-filter-bar/process-filter-bar.tsx +167 -0
- package/src/process-kpi-strip/process-kpi-strip.stories.tsx +47 -0
- package/src/process-kpi-strip/process-kpi-strip.test.tsx +67 -0
- package/src/process-kpi-strip/process-kpi-strip.tsx +148 -8
- package/src/process-map/activity-accent.ts +25 -0
- package/src/process-map/index.ts +1 -0
- package/src/process-map/map-model.test.ts +16 -0
- package/src/process-map/map-model.ts +323 -1
- package/src/process-map/object-centric-map.test.tsx +132 -0
- package/src/process-map/process-activity-node.tsx +152 -14
- package/src/process-map/process-map-object-centric.stories.tsx +219 -0
- package/src/process-map/process-map.stories.tsx +64 -0
- package/src/process-map/process-map.tsx +240 -16
- package/src/process-map/process-transition-edge.test.tsx +47 -0
- package/src/process-map/process-transition-edge.tsx +134 -7
- package/src/process-map/use-process-layout.ts +30 -9
- package/src/process-replay/congestion-heat.tsx +107 -0
- package/src/process-replay/index.ts +14 -0
- package/src/process-replay/process-replay.stories.tsx +168 -0
- package/src/process-replay/process-replay.test.tsx +170 -0
- package/src/process-replay/process-replay.tsx +285 -0
- package/src/process-replay/replay-controls.tsx +147 -0
- package/src/process-replay/replay-format.ts +83 -0
- package/src/process-replay/replay-tokens-context.ts +30 -0
- package/src/process-replay/use-controllable-value.ts +30 -0
- package/src/templates-process-explorer.stories.tsx +1304 -0
- package/src/test/contract.test.ts +66 -0
- package/src/test/contract.ts +107 -6
- package/src/test/doubles.test.tsx +87 -1
- package/src/test/doubles.tsx +174 -3
- package/src/test/index.ts +25 -1
- package/src/use-process-explorer/use-process-explorer.test.ts +44 -0
- package/src/use-process-explorer/use-process-explorer.ts +34 -2
- package/src/variant-explorer/coverage-bar.tsx +36 -0
- package/src/variant-explorer/index.ts +16 -0
- package/src/variant-explorer/sequence-chips.tsx +103 -0
- package/src/variant-explorer/variant-explorer-model.ts +42 -0
- package/src/variant-explorer/variant-explorer.stories.tsx +226 -0
- package/src/variant-explorer/variant-explorer.test.tsx +302 -0
- package/src/variant-explorer/variant-explorer.tsx +567 -0
- package/src/variant-explorer/variant-row.tsx +137 -0
- package/src/violation-list/index.ts +2 -0
- package/src/violation-list/violation-list.stories.tsx +73 -0
- package/src/violation-list/violation-list.test.tsx +84 -0
- package/src/violation-list/violation-list.tsx +259 -0
|
@@ -33,13 +33,15 @@
|
|
|
33
33
|
* identical line height without showing misleading or duplicated copy to a sighted or
|
|
34
34
|
* screen-reader user.
|
|
35
35
|
*/
|
|
36
|
-
import { type HTMLAttributes } from "react";
|
|
36
|
+
import { type HTMLAttributes, type ReactNode } from "react";
|
|
37
37
|
import { CircleSlash2 } from "lucide-react";
|
|
38
|
-
import { MetricCard, type LocaleContextValue } from "@elabs-ai/components-ui";
|
|
38
|
+
import { MetricCard, Skeleton, type LocaleContextValue } from "@elabs-ai/components-ui";
|
|
39
|
+
import { cn } from "@elabs-ai/components-ui/lib/cn";
|
|
39
40
|
import { useLocale } from "@elabs-ai/components-ui";
|
|
40
41
|
import { MetricGrid } from "@elabs-ai/components-charts";
|
|
41
42
|
import { Sparkline } from "@elabs-ai/components-charts";
|
|
42
43
|
import { formatDurationMs } from "../process-map/map-model";
|
|
44
|
+
import type { ConformanceRatePoint, ConformanceResult } from "../core/conformance";
|
|
43
45
|
|
|
44
46
|
export interface ProcessKpiStripKpis {
|
|
45
47
|
cases: number;
|
|
@@ -53,11 +55,34 @@ export type ProcessKpiStripTrendKey = keyof ProcessKpiStripKpis | "conformance";
|
|
|
53
55
|
|
|
54
56
|
export interface ProcessKpiStripProps extends HTMLAttributes<HTMLDivElement> {
|
|
55
57
|
kpis: ProcessKpiStripKpis;
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
+
/**
|
|
59
|
+
* `0..1`, a replay result (RM-061's `tokenReplay`, whose `overallFitness` the tile
|
|
60
|
+
* prints — RM-062), or `null`/`undefined` when no conformance model has been fitted.
|
|
61
|
+
*/
|
|
62
|
+
conformance?: number | ConformanceResult | null;
|
|
58
63
|
/** Optional per-tile trend series (oldest -> newest), keyed by KPI. */
|
|
59
64
|
trends?: Partial<Record<ProcessKpiStripTrendKey, number[]>>;
|
|
65
|
+
/**
|
|
66
|
+
* Conformance rate over time (`conformanceRateSeries`), plotted as the conformance
|
|
67
|
+
* tile's sparkline — the rate-over-time reading (RM-062). Takes precedence over
|
|
68
|
+
* `trends.conformance`.
|
|
69
|
+
*/
|
|
70
|
+
conformanceSeries?: ConformanceRatePoint[];
|
|
60
71
|
loading?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* How the six numbers are laid out. `"grid"` (default) is the card grid a dashboard page
|
|
74
|
+
* opens with. `"inline"` is one dense ribbon — label, value and sparkline per cell, divided
|
|
75
|
+
* by hairlines, scrolling sideways when it runs out of room — for a workspace whose height
|
|
76
|
+
* belongs to the process map, not to its KPIs. Same numbers, same formats, same sparkline
|
|
77
|
+
* text alternatives in both.
|
|
78
|
+
*/
|
|
79
|
+
layout?: "grid" | "inline";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** The tile's fraction: a number as given, a replay result's `overallFitness`. */
|
|
83
|
+
function conformanceFraction(conformance: number | ConformanceResult | null | undefined) {
|
|
84
|
+
if (conformance === null || conformance === undefined) return undefined;
|
|
85
|
+
return typeof conformance === "number" ? conformance : conformance.overallFitness;
|
|
61
86
|
}
|
|
62
87
|
|
|
63
88
|
/** How a trend's two endpoints are formatted — mirrors each tile's own headline format. */
|
|
@@ -106,6 +131,7 @@ function trendVisual(
|
|
|
106
131
|
format: TrendValueFormat,
|
|
107
132
|
t: LocaleContextValue["t"],
|
|
108
133
|
formatNumber: LocaleContextValue["formatNumber"],
|
|
134
|
+
size?: { width: number; height: number },
|
|
109
135
|
) {
|
|
110
136
|
if (!values || values.length === 0) return undefined;
|
|
111
137
|
const first = values[0]!;
|
|
@@ -117,22 +143,136 @@ function trendVisual(
|
|
|
117
143
|
first: formatTrendValue(formatNumber, first, format),
|
|
118
144
|
last: formatTrendValue(formatNumber, last, format),
|
|
119
145
|
});
|
|
120
|
-
return <Sparkline values={values} label={label} />;
|
|
146
|
+
return <Sparkline values={values} label={label} {...size} />;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** One cell of the `layout="inline"` ribbon. */
|
|
150
|
+
function InlineKpi({
|
|
151
|
+
label,
|
|
152
|
+
value,
|
|
153
|
+
visual,
|
|
154
|
+
loading,
|
|
155
|
+
}: {
|
|
156
|
+
label: string;
|
|
157
|
+
value: ReactNode;
|
|
158
|
+
visual?: ReactNode;
|
|
159
|
+
loading: boolean;
|
|
160
|
+
}) {
|
|
161
|
+
return (
|
|
162
|
+
// A `div` inside a `<dl>` may hold nothing but `dt`/`dd` — so the cell is
|
|
163
|
+
// exactly those two, and the trend rides INSIDE the `dd` beside the number
|
|
164
|
+
// rather than as a third sibling.
|
|
165
|
+
<div data-slot="process-kpi-strip-cell" className="flex flex-1 flex-col px-4 py-2">
|
|
166
|
+
<dt className="text-meta whitespace-nowrap text-muted-foreground">{label}</dt>
|
|
167
|
+
<dd className="flex items-center justify-between gap-3">
|
|
168
|
+
<span className="text-subtitle font-semibold tabular-nums whitespace-nowrap">
|
|
169
|
+
{loading ? <Skeleton className="mt-1 h-5 w-14" /> : value}
|
|
170
|
+
</span>
|
|
171
|
+
{/* The trend is the first thing to go when the ribbon is short of room; the number stays. */}
|
|
172
|
+
{!loading && visual ? (
|
|
173
|
+
<span className="hidden shrink-0 text-muted-foreground @4xl:block">{visual}</span>
|
|
174
|
+
) : null}
|
|
175
|
+
</dd>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
121
178
|
}
|
|
122
179
|
|
|
123
180
|
export function ProcessKpiStrip({
|
|
124
181
|
kpis,
|
|
125
182
|
conformance,
|
|
126
183
|
trends,
|
|
184
|
+
conformanceSeries,
|
|
127
185
|
loading = false,
|
|
186
|
+
layout = "grid",
|
|
128
187
|
className,
|
|
129
188
|
...props
|
|
130
189
|
}: ProcessKpiStripProps) {
|
|
131
190
|
const { t, formatNumber } = useLocale();
|
|
132
191
|
|
|
133
|
-
const
|
|
192
|
+
const fraction = conformanceFraction(conformance);
|
|
193
|
+
const hasConformance = fraction !== undefined;
|
|
194
|
+
const conformanceTrend = conformanceSeries
|
|
195
|
+
? conformanceSeries.map((point) => point.fitness)
|
|
196
|
+
: trends?.conformance;
|
|
134
197
|
const conformanceHint = t("process.kpiStrip.conformanceUnavailableHint");
|
|
135
198
|
|
|
199
|
+
if (layout === "inline") {
|
|
200
|
+
const percent = (value: number) =>
|
|
201
|
+
formatNumber(value, { style: "percent", maximumFractionDigits: 1 });
|
|
202
|
+
const spark = (values: number[] | undefined, subject: string, format: TrendValueFormat) =>
|
|
203
|
+
trendVisual(values, subject, format, t, formatNumber, { width: 64, height: 24 });
|
|
204
|
+
return (
|
|
205
|
+
<div
|
|
206
|
+
data-slot="process-kpi-strip"
|
|
207
|
+
data-layout="inline"
|
|
208
|
+
role={loading ? "status" : undefined}
|
|
209
|
+
aria-busy={loading || undefined}
|
|
210
|
+
className={cn("@container overflow-x-auto", className)}
|
|
211
|
+
{...props}
|
|
212
|
+
>
|
|
213
|
+
<dl className="flex min-w-max divide-x divide-border">
|
|
214
|
+
<InlineKpi
|
|
215
|
+
label={t("process.kpiStrip.cases")}
|
|
216
|
+
value={formatNumber(kpis.cases)}
|
|
217
|
+
visual={spark(trends?.cases, t("process.kpiStrip.cases"), "count")}
|
|
218
|
+
loading={loading}
|
|
219
|
+
/>
|
|
220
|
+
<InlineKpi
|
|
221
|
+
label={t("process.kpiStrip.events")}
|
|
222
|
+
value={formatNumber(kpis.events)}
|
|
223
|
+
visual={spark(trends?.events, t("process.kpiStrip.events"), "count")}
|
|
224
|
+
loading={loading}
|
|
225
|
+
/>
|
|
226
|
+
<InlineKpi
|
|
227
|
+
label={t("process.kpiStrip.variants")}
|
|
228
|
+
value={formatNumber(kpis.variants)}
|
|
229
|
+
visual={spark(trends?.variants, t("process.kpiStrip.variants"), "count")}
|
|
230
|
+
loading={loading}
|
|
231
|
+
/>
|
|
232
|
+
<InlineKpi
|
|
233
|
+
label={t("process.kpiStrip.medianThroughput")}
|
|
234
|
+
value={formatDurationMs(kpis.medianThroughput)}
|
|
235
|
+
visual={spark(
|
|
236
|
+
trends?.medianThroughput,
|
|
237
|
+
t("process.kpiStrip.medianThroughput"),
|
|
238
|
+
"duration",
|
|
239
|
+
)}
|
|
240
|
+
loading={loading}
|
|
241
|
+
/>
|
|
242
|
+
<InlineKpi
|
|
243
|
+
label={t("process.kpiStrip.reworkRate")}
|
|
244
|
+
value={percent(kpis.reworkRate)}
|
|
245
|
+
visual={spark(trends?.reworkRate, t("process.kpiStrip.reworkRate"), "percent")}
|
|
246
|
+
loading={loading}
|
|
247
|
+
/>
|
|
248
|
+
<InlineKpi
|
|
249
|
+
label={t("process.kpiStrip.conformance")}
|
|
250
|
+
value={
|
|
251
|
+
hasConformance ? (
|
|
252
|
+
percent(fraction)
|
|
253
|
+
) : (
|
|
254
|
+
<span
|
|
255
|
+
data-slot="process-kpi-strip-conformance-unavailable"
|
|
256
|
+
title={conformanceHint}
|
|
257
|
+
className="inline-flex items-center gap-1.5 text-body font-normal text-muted-foreground"
|
|
258
|
+
>
|
|
259
|
+
<CircleSlash2 aria-hidden="true" className="size-4" />
|
|
260
|
+
{t("process.kpiStrip.conformanceUnavailable")}
|
|
261
|
+
</span>
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
visual={
|
|
265
|
+
hasConformance
|
|
266
|
+
? spark(conformanceTrend, t("process.kpiStrip.conformance"), "percent")
|
|
267
|
+
: undefined
|
|
268
|
+
}
|
|
269
|
+
loading={loading}
|
|
270
|
+
/>
|
|
271
|
+
</dl>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
136
276
|
return (
|
|
137
277
|
<div data-slot="process-kpi-strip" className={className} {...props}>
|
|
138
278
|
<MetricGrid columns={3} loading={loading}>
|
|
@@ -195,7 +335,7 @@ export function ProcessKpiStrip({
|
|
|
195
335
|
label={t("process.kpiStrip.conformance")}
|
|
196
336
|
value={
|
|
197
337
|
hasConformance ? (
|
|
198
|
-
|
|
338
|
+
fraction
|
|
199
339
|
) : (
|
|
200
340
|
<span
|
|
201
341
|
data-slot="process-kpi-strip-conformance-unavailable"
|
|
@@ -222,7 +362,7 @@ export function ProcessKpiStrip({
|
|
|
222
362
|
visual={
|
|
223
363
|
hasConformance
|
|
224
364
|
? trendVisual(
|
|
225
|
-
|
|
365
|
+
conformanceTrend,
|
|
226
366
|
t("process.kpiStrip.conformance"),
|
|
227
367
|
"percent",
|
|
228
368
|
t,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an activity's identity colour is painted (RM-054).
|
|
3
|
+
*
|
|
4
|
+
* The ONE place that turns an {@link ActivityColor} into CSS, shared by
|
|
5
|
+
* `ProcessActivityNode`'s accent swatch and `VariantExplorer`'s sequence chips — so the
|
|
6
|
+
* two views cannot drift into painting the same activity differently. Both endpoints are
|
|
7
|
+
* semantic tokens; no literal colour is authored.
|
|
8
|
+
*
|
|
9
|
+
* The shared "other" bucket is a diagonal hatch over a light wash of the same token, so
|
|
10
|
+
* it reads as "one of many" in greyscale too, never as a twelfth distinct activity.
|
|
11
|
+
*/
|
|
12
|
+
import type { CSSProperties } from "react";
|
|
13
|
+
import type { ActivityColor } from "../core/activity-color-scale";
|
|
14
|
+
|
|
15
|
+
/** Inline style for an accent swatch painted with `color`. */
|
|
16
|
+
export function activityAccentStyle(color: ActivityColor): CSSProperties {
|
|
17
|
+
const ink = `var(${color.token})`;
|
|
18
|
+
if (color.pattern === "other") {
|
|
19
|
+
return {
|
|
20
|
+
backgroundColor: `color-mix(in oklab, ${ink} 30%, transparent)`,
|
|
21
|
+
backgroundImage: `repeating-linear-gradient(135deg, ${ink} 0 1.5px, transparent 1.5px 3.5px)`,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return { backgroundColor: ink };
|
|
25
|
+
}
|
package/src/process-map/index.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* read, the layout hook, and the whole `map-model` vocabulary (the model is what keeps the
|
|
6
6
|
* canvas and the `tableView` twin printing identical numbers, so it is public on purpose).
|
|
7
7
|
*/
|
|
8
|
+
export * from "./activity-accent";
|
|
8
9
|
export * from "./map-model";
|
|
9
10
|
export * from "./process-map";
|
|
10
11
|
export * from "./process-map-context";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest";
|
|
2
2
|
import { computeEdgeWeightScale, DEFAULT_EDGE_WIDTH_RANGE } from "@elabs-ai/components-flow";
|
|
3
|
+
import { activityColorScale } from "../core/activity-color-scale";
|
|
3
4
|
import { discoverGraph } from "../core/discover-graph";
|
|
4
5
|
import { detectRework } from "../core/detect-rework";
|
|
5
6
|
import { generateSyntheticLog } from "../core/fixtures/synthetic-log";
|
|
@@ -324,3 +325,18 @@ describe("non-colour channels reach assistive technology", () => {
|
|
|
324
325
|
for (const edge of model.edges) expect(edge.ariaLabel).toBeTruthy();
|
|
325
326
|
});
|
|
326
327
|
});
|
|
328
|
+
|
|
329
|
+
describe("activity colour accent (RM-054)", () => {
|
|
330
|
+
const metric = { node: "absolute_case", edge: "absolute" } as const;
|
|
331
|
+
|
|
332
|
+
it("carries no accent when no colour scale is given — today's model, unchanged", () => {
|
|
333
|
+
const plain = buildProcessMapModel({ graph, metric });
|
|
334
|
+
for (const node of plain.nodes) expect("accent" in node.data).toBe(false);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("stamps each node with the scale's colour for its activity", () => {
|
|
338
|
+
const scale = activityColorScale(graph);
|
|
339
|
+
const coloured = buildProcessMapModel({ graph, metric, colorScale: scale });
|
|
340
|
+
for (const node of coloured.nodes) expect(node.data.accent).toEqual(scale.colorFor(node.id));
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -38,11 +38,14 @@
|
|
|
38
38
|
// bundle's tree-shaking mercy; naming the four modules the map actually needs takes it off
|
|
39
39
|
// the table entirely. Same package, so this is an ordinary relative import.
|
|
40
40
|
import type { Edge, Node } from "@xyflow/react";
|
|
41
|
+
import type { ActivityColor, ActivityColorScale } from "../core/activity-color-scale";
|
|
42
|
+
import type { ConformanceState } from "../conformance-overlay/conformance-state";
|
|
41
43
|
import { performanceValue } from "../core/aggregate-performance";
|
|
42
44
|
import { EDGE_KEY_SEPARATOR } from "../core/discover-graph";
|
|
43
45
|
import { minMax } from "../core/scale";
|
|
44
46
|
import type { ReworkStats } from "../core/detect-rework";
|
|
45
47
|
import type { FilterSpec } from "../core/filter-log";
|
|
48
|
+
import type { ObjectCentricGraph } from "../core/discover-object-centric-graph";
|
|
46
49
|
import type {
|
|
47
50
|
ActivityStats,
|
|
48
51
|
FrequencyMode,
|
|
@@ -192,6 +195,22 @@ export interface ProcessActivityNodeData extends Record<string, unknown> {
|
|
|
192
195
|
/** Repeat executions of this activity across the log; omitted when no rework data. */
|
|
193
196
|
reworkCount?: number;
|
|
194
197
|
selectionState: ProcessSelectionState;
|
|
198
|
+
/**
|
|
199
|
+
* The activity's identity colour from a shared `ActivityColorScale` (RM-054), painted as
|
|
200
|
+
* a small accent swatch — never the card fill, which would fight the metric reading.
|
|
201
|
+
* Absent when the map was given no `colorScale`.
|
|
202
|
+
*/
|
|
203
|
+
accent?: ActivityColor;
|
|
204
|
+
/**
|
|
205
|
+
* Where this activity sits against a reference model (RM-062). Set only when the map
|
|
206
|
+
* was given `conformance`; drives `data-conformance` plus its glyph and dash.
|
|
207
|
+
*/
|
|
208
|
+
conformance?: ConformanceState;
|
|
209
|
+
/**
|
|
210
|
+
* Object-centric — RM-066. One entry per object type this activity occurs in, in the
|
|
211
|
+
* graph's type order. Set only in object-centric mode; drives the per-type chips.
|
|
212
|
+
*/
|
|
213
|
+
objectTypes?: ProcessObjectTypeCount[];
|
|
195
214
|
}
|
|
196
215
|
|
|
197
216
|
/** `data` carried by every {@link ProcessMapEdge}. */
|
|
@@ -210,6 +229,32 @@ export interface ProcessTransitionEdgeData extends Record<string, unknown> {
|
|
|
210
229
|
isSelfLoop: boolean;
|
|
211
230
|
isBackEdge: boolean;
|
|
212
231
|
selectionState: ProcessSelectionState;
|
|
232
|
+
/**
|
|
233
|
+
* This transition's composed accessible name ({@link transitionAriaLabel}), duplicated
|
|
234
|
+
* here from the edge object's own `ariaLabel` (#354). React Flow's `EdgeWrapper` reads
|
|
235
|
+
* `ariaLabel` off the edge OBJECT for the outer, non-focusable `<g>` — it never forwards
|
|
236
|
+
* it to the edge COMPONENT via `EdgeProps` (`@xyflow/react`'s `EdgeProps` type has no
|
|
237
|
+
* `ariaLabel` field) — so `ProcessTransitionEdge`, which only receives `data`, has no
|
|
238
|
+
* other channel to reach it. Read this field, never recompute the name locally, or the
|
|
239
|
+
* canvas and the `TableView` twin (`transitionRows`) can drift apart. Always set by
|
|
240
|
+
* {@link buildProcessMapModel} — optional only because it is filled in a step after the
|
|
241
|
+
* rest of this object (it is itself derived from this object).
|
|
242
|
+
*/
|
|
243
|
+
ariaLabel?: string;
|
|
244
|
+
/**
|
|
245
|
+
* Where this transition sits against a reference model (RM-062). Set only when the map
|
|
246
|
+
* was given `conformance`; drives `data-conformance` plus its glyph and dash.
|
|
247
|
+
*/
|
|
248
|
+
conformance?: ConformanceState;
|
|
249
|
+
/**
|
|
250
|
+
* Object-centric — RM-066. The object type this edge belongs to; per-type edges between
|
|
251
|
+
* the same two activities are drawn side by side (see `parallelIndex`/`parallelCount`).
|
|
252
|
+
*/
|
|
253
|
+
objectType?: ProcessObjectTypeMark;
|
|
254
|
+
/** Object-centric — RM-066. This edge's slot among the per-type edges of its pair. */
|
|
255
|
+
parallelIndex?: number;
|
|
256
|
+
/** Object-centric — RM-066. How many per-type edges join this edge's pair. */
|
|
257
|
+
parallelCount?: number;
|
|
213
258
|
}
|
|
214
259
|
|
|
215
260
|
/** A process-map activity node. Register as `nodeTypes={{ "process-activity": … }}`. */
|
|
@@ -226,6 +271,8 @@ export interface ProcessActivityRow {
|
|
|
226
271
|
reworkCount?: number;
|
|
227
272
|
role: string;
|
|
228
273
|
selectionState: ProcessSelectionState;
|
|
274
|
+
/** Object-centric — RM-066. Per-type counts, as printed in the table twin. */
|
|
275
|
+
objectTypes?: string;
|
|
229
276
|
}
|
|
230
277
|
|
|
231
278
|
/** One row of the accessible `TableView` twin — transitions half. */
|
|
@@ -237,6 +284,8 @@ export interface ProcessTransitionRow {
|
|
|
237
284
|
secondaryLabel?: string;
|
|
238
285
|
shape: string;
|
|
239
286
|
selectionState: ProcessSelectionState;
|
|
287
|
+
/** Object-centric — RM-066. The object type's display name. */
|
|
288
|
+
objectType?: string;
|
|
240
289
|
}
|
|
241
290
|
|
|
242
291
|
/** Everything the canvas and the table are both rendered from. */
|
|
@@ -627,6 +676,11 @@ export interface BuildProcessMapModelOptions {
|
|
|
627
676
|
* every edge starts `isBackEdge: false` exactly as `discoverGraph` leaves it.
|
|
628
677
|
*/
|
|
629
678
|
backEdgeIds?: ReadonlySet<string>;
|
|
679
|
+
/**
|
|
680
|
+
* The shared activity colour scale (RM-054). When given, every node carries
|
|
681
|
+
* `data.accent`, so the map and `VariantExplorer` paint an activity identically.
|
|
682
|
+
*/
|
|
683
|
+
colorScale?: ActivityColorScale;
|
|
630
684
|
}
|
|
631
685
|
|
|
632
686
|
/**
|
|
@@ -640,6 +694,7 @@ export function buildProcessMapModel({
|
|
|
640
694
|
selection,
|
|
641
695
|
selectionStates,
|
|
642
696
|
backEdgeIds,
|
|
697
|
+
colorScale,
|
|
643
698
|
}: BuildProcessMapModelOptions): ProcessMapModel {
|
|
644
699
|
const neighbourhood = selectionNeighbourhood(graph, selection);
|
|
645
700
|
const denominators = processEdgeDenominators(graph.transitions);
|
|
@@ -688,6 +743,7 @@ export function buildProcessMapModel({
|
|
|
688
743
|
isEnd: activity.isEnd,
|
|
689
744
|
reworkCount,
|
|
690
745
|
selectionState,
|
|
746
|
+
...(colorScale ? { accent: colorScale.colorFor(activity.id) } : {}),
|
|
691
747
|
};
|
|
692
748
|
return {
|
|
693
749
|
id: activity.id,
|
|
@@ -741,13 +797,18 @@ export function buildProcessMapModel({
|
|
|
741
797
|
isBackEdge: backEdgeIds?.has(id) ?? transition.isBackEdge,
|
|
742
798
|
selectionState,
|
|
743
799
|
};
|
|
800
|
+
// Computed once, read from two places (#354): React Flow's own `EdgeWrapper` reads
|
|
801
|
+
// `ariaLabel` off THIS edge object for the outer, non-focusable `<g>`; `data.ariaLabel`
|
|
802
|
+
// is what `ProcessTransitionEdge` — which never sees this edge object, only `data` —
|
|
803
|
+
// folds onto the focused label pill. One computation, so the two can never drift.
|
|
804
|
+
data.ariaLabel = transitionAriaLabel(data, resolvedEdgeMetricLabel);
|
|
744
805
|
return {
|
|
745
806
|
id,
|
|
746
807
|
source: transition.source,
|
|
747
808
|
target: transition.target,
|
|
748
809
|
type: "process-transition",
|
|
749
810
|
data,
|
|
750
|
-
ariaLabel:
|
|
811
|
+
ariaLabel: data.ariaLabel,
|
|
751
812
|
};
|
|
752
813
|
});
|
|
753
814
|
|
|
@@ -871,3 +932,264 @@ export function transitionAriaLabel(data: ProcessTransitionEdgeData, metricLabel
|
|
|
871
932
|
if (data.selectionState !== "associated") parts.push(data.selectionState);
|
|
872
933
|
return parts.join(", ");
|
|
873
934
|
}
|
|
935
|
+
// ── Object-centric — RM-066 ──────────────────────────────────────────────────
|
|
936
|
+
|
|
937
|
+
/** How one object type is marked on the map: a name, a text code and a chart colour. */
|
|
938
|
+
export interface ProcessObjectTypeMark {
|
|
939
|
+
/** The object type's id, as keyed in the `ObjectCentricGraph`. */
|
|
940
|
+
type: string;
|
|
941
|
+
/** Display name. */
|
|
942
|
+
label: string;
|
|
943
|
+
/** Two-character code printed beside every swatch — the non-colour channel. */
|
|
944
|
+
code: string;
|
|
945
|
+
/** A categorical chart token from `objectTypeColorScale` (RM-054's scale, reused). */
|
|
946
|
+
color: ActivityColor;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** One object type's share of an activity node. */
|
|
950
|
+
export interface ProcessObjectTypeCount extends ProcessObjectTypeMark {
|
|
951
|
+
instances: number;
|
|
952
|
+
/** Distinct objects of this type the activity touches. */
|
|
953
|
+
cases: number;
|
|
954
|
+
/** The chip's accessible name, e.g. "order: 2 objects, 3 occurrences". */
|
|
955
|
+
ariaLabel: string;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/** Every string the object-centric mode composes. Override for a non-English locale. */
|
|
959
|
+
export interface ObjectCentricMapLabels {
|
|
960
|
+
/** A node chip's accessible name and table text. */
|
|
961
|
+
typeCount: (type: string, objects: number, occurrences: number) => string;
|
|
962
|
+
/** Prefixes a per-type edge's accessible name. */
|
|
963
|
+
edgePrefix: (type: string) => string;
|
|
964
|
+
/** Activity-table column header. */
|
|
965
|
+
columnObjectTypes: string;
|
|
966
|
+
/** Transition-table column header. */
|
|
967
|
+
columnObjectType: string;
|
|
968
|
+
/** The canvas legend's title. */
|
|
969
|
+
legendTitle: string;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/** English defaults for {@link ObjectCentricMapLabels}. */
|
|
973
|
+
export const OBJECT_CENTRIC_MAP_DEFAULT_LABELS: Readonly<ObjectCentricMapLabels> = Object.freeze({
|
|
974
|
+
typeCount: (type: string, objects: number, occurrences: number) =>
|
|
975
|
+
`${type}: ${objects} ${objects === 1 ? "object" : "objects"}, ${occurrences} ${
|
|
976
|
+
occurrences === 1 ? "occurrence" : "occurrences"
|
|
977
|
+
}`,
|
|
978
|
+
edgePrefix: (type: string) => `${type} flow`,
|
|
979
|
+
columnObjectTypes: "Object types",
|
|
980
|
+
columnObjectType: "Object type",
|
|
981
|
+
legendTitle: "Object types",
|
|
982
|
+
});
|
|
983
|
+
|
|
984
|
+
/** The id of one object type's edge: the merged edge id, the separator, the type. */
|
|
985
|
+
export function objectCentricEdgeId(source: string, target: string, type: string): string {
|
|
986
|
+
return `${processEdgeId(source, target)}${EDGE_KEY_SEPARATOR}${type}`;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** Inputs to {@link buildObjectCentricMapModel}. */
|
|
990
|
+
export interface BuildObjectCentricMapModelOptions extends BuildProcessMapModelOptions {
|
|
991
|
+
/**
|
|
992
|
+
* The object-centric graph `graph` was flattened from (`objectCentricProcessGraph`).
|
|
993
|
+
* Omit — or omit `objectTypeScale` — and this is exactly {@link buildProcessMapModel}.
|
|
994
|
+
*/
|
|
995
|
+
objectCentric?: ObjectCentricGraph;
|
|
996
|
+
/** `objectTypeColorScale(objectCentric)`, built once from the FULL graph. */
|
|
997
|
+
objectTypeScale?: ActivityColorScale;
|
|
998
|
+
objectCentricLabels?: ObjectCentricMapLabels;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* The object-centric process-map model: {@link buildProcessMapModel} over the flattened
|
|
1003
|
+
* graph, then
|
|
1004
|
+
*
|
|
1005
|
+
* - every node gains `data.objectTypes` (one chip per type, each with a text code and a
|
|
1006
|
+
* real accessible name folded into the node's own), and
|
|
1007
|
+
* - every merged edge is replaced by one edge PER OBJECT TYPE — its weight and printed
|
|
1008
|
+
* label read that type's own counts, its label is prefixed with the type's code, and its
|
|
1009
|
+
* stroke is the type's chart token. The edge-value colour ramp is never used here.
|
|
1010
|
+
*
|
|
1011
|
+
* A transition selection may name either a per-type edge or the merged pair; both select
|
|
1012
|
+
* the pair's neighbourhood, and only the named per-type edge reads `"selected"`.
|
|
1013
|
+
*/
|
|
1014
|
+
export function buildObjectCentricMapModel({
|
|
1015
|
+
objectCentric,
|
|
1016
|
+
objectTypeScale,
|
|
1017
|
+
objectCentricLabels = OBJECT_CENTRIC_MAP_DEFAULT_LABELS,
|
|
1018
|
+
...options
|
|
1019
|
+
}: BuildObjectCentricMapModelOptions): ProcessMapModel {
|
|
1020
|
+
if (!objectCentric || !objectTypeScale) return buildProcessMapModel(options);
|
|
1021
|
+
const labels = objectCentricLabels;
|
|
1022
|
+
const { metric } = options;
|
|
1023
|
+
|
|
1024
|
+
const typeEdges = new Map<string, Map<string, TransitionStats>>();
|
|
1025
|
+
const mergedIdOf = new Map<string, string>();
|
|
1026
|
+
for (const type of objectCentric.objectTypes) {
|
|
1027
|
+
for (const transition of objectCentric.transitionsByType[type] ?? []) {
|
|
1028
|
+
const mergedId = processEdgeId(transition.source, transition.target);
|
|
1029
|
+
mergedIdOf.set(objectCentricEdgeId(transition.source, transition.target, type), mergedId);
|
|
1030
|
+
let byType = typeEdges.get(mergedId);
|
|
1031
|
+
if (byType === undefined) {
|
|
1032
|
+
byType = new Map();
|
|
1033
|
+
typeEdges.set(mergedId, byType);
|
|
1034
|
+
}
|
|
1035
|
+
byType.set(type, transition);
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
const requested = options.selection;
|
|
1040
|
+
const typeSelectionId =
|
|
1041
|
+
requested?.kind === "transition" && mergedIdOf.has(requested.id) ? requested.id : undefined;
|
|
1042
|
+
const model = buildProcessMapModel({
|
|
1043
|
+
...options,
|
|
1044
|
+
selection: typeSelectionId
|
|
1045
|
+
? { kind: "transition", id: mergedIdOf.get(typeSelectionId) as string }
|
|
1046
|
+
: requested,
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
const marks = new Map<string, ProcessObjectTypeMark>(
|
|
1050
|
+
objectCentric.objectTypes.map((type) => [
|
|
1051
|
+
type,
|
|
1052
|
+
{
|
|
1053
|
+
type,
|
|
1054
|
+
label: objectTypeScale.labelFor(type),
|
|
1055
|
+
code: objectTypeScale.codeFor(type),
|
|
1056
|
+
color: objectTypeScale.colorFor(type),
|
|
1057
|
+
},
|
|
1058
|
+
]),
|
|
1059
|
+
);
|
|
1060
|
+
|
|
1061
|
+
const activityById = new Map(objectCentric.activities.map((a) => [a.id, a]));
|
|
1062
|
+
const nodes: ProcessMapNode[] = model.nodes.map((node) => {
|
|
1063
|
+
const activity = activityById.get(node.id);
|
|
1064
|
+
if (!activity) return node;
|
|
1065
|
+
const objectTypes: ProcessObjectTypeCount[] = [];
|
|
1066
|
+
for (const type of objectCentric.objectTypes) {
|
|
1067
|
+
const counts = activity.perType[type];
|
|
1068
|
+
const mark = marks.get(type);
|
|
1069
|
+
if (!counts || !mark) continue;
|
|
1070
|
+
objectTypes.push({
|
|
1071
|
+
...mark,
|
|
1072
|
+
instances: counts.instances,
|
|
1073
|
+
cases: counts.cases,
|
|
1074
|
+
ariaLabel: labels.typeCount(mark.label, counts.cases, counts.instances),
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
return {
|
|
1078
|
+
...node,
|
|
1079
|
+
data: { ...node.data, objectTypes },
|
|
1080
|
+
ariaLabel: [node.ariaLabel, ...objectTypes.map((entry) => entry.ariaLabel)].join(", "),
|
|
1081
|
+
};
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
const denominators = new Map(
|
|
1085
|
+
objectCentric.objectTypes.map((type) => [
|
|
1086
|
+
type,
|
|
1087
|
+
processEdgeDenominators(objectCentric.transitionsByType[type] ?? []),
|
|
1088
|
+
]),
|
|
1089
|
+
);
|
|
1090
|
+
|
|
1091
|
+
const edges: ProcessMapEdge[] = [];
|
|
1092
|
+
const values: number[] = [];
|
|
1093
|
+
for (const merged of model.edges) {
|
|
1094
|
+
const byType = typeEdges.get(merged.id);
|
|
1095
|
+
const mergedData = merged.data as ProcessTransitionEdgeData;
|
|
1096
|
+
if (!byType) continue;
|
|
1097
|
+
let parallelIndex = 0;
|
|
1098
|
+
for (const type of objectCentric.objectTypes) {
|
|
1099
|
+
const transition = byType.get(type);
|
|
1100
|
+
const mark = marks.get(type);
|
|
1101
|
+
const typeGraph = objectCentric.graphsByType[type];
|
|
1102
|
+
if (!transition || !mark || !typeGraph) continue;
|
|
1103
|
+
const typeDenominators = denominators.get(type) as ProcessEdgeDenominators;
|
|
1104
|
+
const id = objectCentricEdgeId(transition.source, transition.target, type);
|
|
1105
|
+
const value = transitionMetricValue(
|
|
1106
|
+
transition,
|
|
1107
|
+
metric.edge,
|
|
1108
|
+
typeGraph.totals,
|
|
1109
|
+
typeDenominators,
|
|
1110
|
+
);
|
|
1111
|
+
values.push(value);
|
|
1112
|
+
const selectionState: ProcessSelectionState =
|
|
1113
|
+
typeSelectionId !== undefined &&
|
|
1114
|
+
mergedData.selectionState === "selected" &&
|
|
1115
|
+
typeSelectionId !== id
|
|
1116
|
+
? "associated"
|
|
1117
|
+
: mergedData.selectionState;
|
|
1118
|
+
const data: ProcessTransitionEdgeData = {
|
|
1119
|
+
...mergedData,
|
|
1120
|
+
weight: value,
|
|
1121
|
+
value,
|
|
1122
|
+
label: `${mark.code} ${formatMetricValue(value, metric.edge)}`,
|
|
1123
|
+
secondaryLabel:
|
|
1124
|
+
metric.secondary === undefined
|
|
1125
|
+
? undefined
|
|
1126
|
+
: formatMetricValue(
|
|
1127
|
+
transitionMetricValue(
|
|
1128
|
+
transition,
|
|
1129
|
+
metric.secondary,
|
|
1130
|
+
typeGraph.totals,
|
|
1131
|
+
typeDenominators,
|
|
1132
|
+
),
|
|
1133
|
+
metric.secondary,
|
|
1134
|
+
),
|
|
1135
|
+
selectionState,
|
|
1136
|
+
objectType: mark,
|
|
1137
|
+
parallelIndex,
|
|
1138
|
+
parallelCount: byType.size,
|
|
1139
|
+
};
|
|
1140
|
+
data.ariaLabel = `${labels.edgePrefix(mark.label)}: ${transitionAriaLabel(
|
|
1141
|
+
data,
|
|
1142
|
+
model.edgeMetricLabel,
|
|
1143
|
+
)}`;
|
|
1144
|
+
edges.push({
|
|
1145
|
+
...merged,
|
|
1146
|
+
id,
|
|
1147
|
+
data,
|
|
1148
|
+
ariaLabel: data.ariaLabel,
|
|
1149
|
+
style: { stroke: `var(${mark.color.token})` },
|
|
1150
|
+
});
|
|
1151
|
+
parallelIndex += 1;
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const edgeDomain = values.length > 0 ? minMax(values) : model.edgeDomain;
|
|
1156
|
+
for (const edge of edges) (edge.data as ProcessTransitionEdgeData).valueDomain = edgeDomain;
|
|
1157
|
+
|
|
1158
|
+
const activityRows: ProcessActivityRow[] = model.activityRows.map((row, index) => {
|
|
1159
|
+
const objectTypes = nodes[index]?.data.objectTypes;
|
|
1160
|
+
return objectTypes
|
|
1161
|
+
? {
|
|
1162
|
+
...row,
|
|
1163
|
+
objectTypes: objectTypes.map((entry) => entry.ariaLabel).join("; "),
|
|
1164
|
+
}
|
|
1165
|
+
: row;
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
const transitionRows: ProcessTransitionRow[] = edges.map((edge) => {
|
|
1169
|
+
const data = edge.data as ProcessTransitionEdgeData;
|
|
1170
|
+
return {
|
|
1171
|
+
id: edge.id,
|
|
1172
|
+
source: data.source,
|
|
1173
|
+
target: data.target,
|
|
1174
|
+
primaryLabel: data.label,
|
|
1175
|
+
secondaryLabel: data.secondaryLabel,
|
|
1176
|
+
shape: transitionShape(data),
|
|
1177
|
+
selectionState: data.selectionState,
|
|
1178
|
+
objectType: data.objectType?.label,
|
|
1179
|
+
};
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
return {
|
|
1183
|
+
...model,
|
|
1184
|
+
nodes,
|
|
1185
|
+
edges,
|
|
1186
|
+
edgeDomain,
|
|
1187
|
+
activityRows,
|
|
1188
|
+
transitionRows,
|
|
1189
|
+
excludedCounts: {
|
|
1190
|
+
...model.excludedCounts,
|
|
1191
|
+
transitions: transitionRows.filter((row) => row.selectionState === "excluded").length,
|
|
1192
|
+
totalTransitions: transitionRows.length,
|
|
1193
|
+
},
|
|
1194
|
+
};
|
|
1195
|
+
}
|