@ocelescope/exploration 0.10.1 → 0.11.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 +7 -7
- package/dist/{Dashboard-BQ_EzP8N.js → Dashboard-jE93H3NS.js} +253 -220
- package/dist/index.d.ts +0 -2
- package/dist/index.js +3 -5
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -6,13 +6,13 @@ backend, so an analysis is only ever one place in the code.
|
|
|
6
6
|
|
|
7
7
|
## Layout
|
|
8
8
|
|
|
9
|
-
| File
|
|
10
|
-
|
|
|
11
|
-
| `analyses.ts`
|
|
12
|
-
| `Dashboard.tsx` | The searchable analysis catalog and responsive card grid
|
|
13
|
-
| `Card.tsx`
|
|
14
|
-
| `Controls.tsx`
|
|
15
|
-
| `store.ts`
|
|
9
|
+
| File | Contents |
|
|
10
|
+
| --------------- | ------------------------------------------------------------ |
|
|
11
|
+
| `analyses.ts` | Every analysis: its question, parameters, SQL and chart |
|
|
12
|
+
| `Dashboard.tsx` | The searchable analysis catalog and responsive card grid |
|
|
13
|
+
| `Card.tsx` | One card: information, configuration, chart and export frame |
|
|
14
|
+
| `Controls.tsx` | The parameter inputs, and the log facts they offer |
|
|
15
|
+
| `store.ts` | The cards a reader has added, kept per OCEL in the browser |
|
|
16
16
|
|
|
17
17
|
To add an analysis, add an entry to `analyses.ts`.
|
|
18
18
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { ActivityPicker, EventAttributePicker, LineChart, ObjectAttributePicker, ObjectPicker, ObjectTypePicker, OcelChart, useCurrentOcel, useOcelQuery } from "@ocelescope/core";
|
|
2
|
-
import { ChartNoAxesCombinedIcon, Clock3Icon, Code2Icon, GitBranchIcon, InfoIcon, PlusIcon, SearchIcon, Settings2Icon, ShapesIcon, SlidersHorizontalIcon, Trash2Icon } from "lucide-react";
|
|
3
|
-
import { ActionIcon, Alert, Badge, Button, Code, Container, Divider, Group, HoverCard, Modal, MultiSelect, NumberInput, Paper, ScrollArea, SegmentedControl, Select, SimpleGrid, Stack, Text, TextInput, Textarea, ThemeIcon, Title, Tooltip, UnstyledButton } from "@mantine/core";
|
|
2
|
+
import { ChartNoAxesCombinedIcon, CheckIcon, Clock3Icon, Code2Icon, GitBranchIcon, InfoIcon, PlusIcon, SearchIcon, Settings2Icon, ShapesIcon, SlidersHorizontalIcon, Trash2Icon } from "lucide-react";
|
|
3
|
+
import { ActionIcon, Alert, Badge, Box, Button, Center, Code, Container, Divider, Group, HoverCard, Input, Modal, MultiSelect, NumberInput, Paper, ScrollArea, SegmentedControl, Select, SimpleGrid, Stack, Text, TextInput, Textarea, ThemeIcon, Title, Tooltip, UnstyledButton } from "@mantine/core";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
import { useEffect, useMemo, useState } from "react";
|
|
6
|
-
import { AsyncBoundary,
|
|
6
|
+
import { AsyncBoundary, ViewerExportFrame } from "@r4pm/components";
|
|
7
7
|
import { ValueType, useEventAttributes, useObjectAttributes } from "@ocelescope/api-base";
|
|
8
|
-
|
|
8
|
+
// src/analyses.ts
|
|
9
9
|
const ident = (name) => `"${String(name ?? "").replaceAll("\"", "\"\"")}"`;
|
|
10
10
|
const lit = (value) => `'${String(value ?? "").replaceAll("'", "''")}'`;
|
|
11
11
|
const int = (value, fallback) => Number.isFinite(Number(value)) ? Math.trunc(Number(value)) : fallback;
|
|
@@ -48,15 +48,30 @@ const counts = (source, limit = 25) => `WITH raw AS (${source}),
|
|
|
48
48
|
SELECT 'Missing', count(*), ${limit} + 2
|
|
49
49
|
FROM raw WHERE value IS NULL GROUP BY ALL HAVING count(*) > 0
|
|
50
50
|
ORDER BY rank`;
|
|
51
|
+
/** The column a histogram's bins start in, named after what they measure so
|
|
52
|
+
* the axis reads it - kept clear of the two columns beside it. */
|
|
53
|
+
const binColumn = (measure) => {
|
|
54
|
+
const name = String(measure ?? "value");
|
|
55
|
+
return name === "count" || name === "bin_end" ? `${name} ` : name;
|
|
56
|
+
};
|
|
57
|
+
/** How to draw what `histogram` returns. */
|
|
58
|
+
const histogramChart = (measure) => ({
|
|
59
|
+
type: "histogram",
|
|
60
|
+
x: binColumn(measure),
|
|
61
|
+
xEnd: "bin_end",
|
|
62
|
+
y: "count"
|
|
63
|
+
});
|
|
51
64
|
/**
|
|
52
|
-
* A histogram of `source`'s `value` column
|
|
65
|
+
* A histogram of `source`'s `value` column: one row per bin, with its lower
|
|
66
|
+
* edge in a column named after `measure`, its upper edge in `bin_end`.
|
|
53
67
|
*
|
|
54
68
|
* Bin width is Freedman-Diaconis (2·IQR/∛n), with Sturges' rule where the
|
|
55
69
|
* interquartile range is zero. Values beyond Tukey's outer fences are counted
|
|
56
|
-
* in
|
|
57
|
-
*
|
|
70
|
+
* in two open-ended bins - no lower edge, or no upper one - instead of
|
|
71
|
+
* stretching the bins, which is what left a long-tailed distribution as one
|
|
72
|
+
* tall bar and a row of empty ones.
|
|
58
73
|
*/
|
|
59
|
-
const histogram = (source) => `WITH raw AS (${source}),
|
|
74
|
+
const histogram = (source, measure) => `WITH raw AS (${source}),
|
|
60
75
|
spread AS (
|
|
61
76
|
SELECT quantile_cont(value, 0.25) AS q1, quantile_cont(value, 0.75) AS q3 FROM raw
|
|
62
77
|
),
|
|
@@ -81,16 +96,17 @@ const histogram = (source) => `WITH raw AS (${source}),
|
|
|
81
96
|
SELECT lo + width * least(floor((value - lo) / nullif(width, 0)), bins - 1) AS start, width
|
|
82
97
|
FROM raw, plan WHERE value BETWEEN lo AND hi
|
|
83
98
|
)
|
|
84
|
-
SELECT
|
|
99
|
+
SELECT start AS ${ident(binColumn(measure))}, start + width AS bin_end, count(*) AS count
|
|
85
100
|
FROM binned GROUP BY ALL
|
|
86
101
|
UNION ALL
|
|
87
|
-
SELECT
|
|
102
|
+
SELECT NULL, lo, count(*) FROM raw, plan WHERE value < lo GROUP BY ALL
|
|
88
103
|
UNION ALL
|
|
89
|
-
SELECT
|
|
90
|
-
ORDER BY
|
|
104
|
+
SELECT hi, NULL, count(*) FROM raw, plan WHERE value > hi GROUP BY ALL
|
|
105
|
+
ORDER BY 1 NULLS FIRST`;
|
|
91
106
|
/** Value counts or a histogram, whichever the attribute deserves. */
|
|
92
|
-
const distribution = (source, isNumeric) => isNumeric ? histogram(source) : counts(source);
|
|
93
|
-
|
|
107
|
+
const distribution = (source, attribute, isNumeric) => isNumeric ? histogram(source, attribute) : counts(source);
|
|
108
|
+
/** How to draw what `distribution` returns. */
|
|
109
|
+
const distributionChart = (attribute, isNumeric) => isNumeric ? histogramChart(attribute) : {
|
|
94
110
|
x: "bucket",
|
|
95
111
|
y: "count",
|
|
96
112
|
types: ["bar", "pie"]
|
|
@@ -133,7 +149,7 @@ const analyses = [
|
|
|
133
149
|
},
|
|
134
150
|
{
|
|
135
151
|
name: "series",
|
|
136
|
-
label: "Series
|
|
152
|
+
label: "Series",
|
|
137
153
|
kind: "column",
|
|
138
154
|
required: false
|
|
139
155
|
},
|
|
@@ -381,8 +397,8 @@ const analyses = [
|
|
|
381
397
|
kind: "eventAttribute"
|
|
382
398
|
}],
|
|
383
399
|
sql: ({ activity, attribute }, numeric) => distribution(`SELECT ${ident(attribute)} AS value FROM events
|
|
384
|
-
WHERE "ocel:activity" = ${lit(activity)}`, numeric(String(attribute))),
|
|
385
|
-
chart: () =>
|
|
400
|
+
WHERE "ocel:activity" = ${lit(activity)}`, attribute, numeric(String(attribute))),
|
|
401
|
+
chart: ({ attribute }, numeric) => distributionChart(attribute, numeric(String(attribute)))
|
|
386
402
|
},
|
|
387
403
|
{
|
|
388
404
|
id: "object-attribute-distribution",
|
|
@@ -418,8 +434,8 @@ const analyses = [
|
|
|
418
434
|
WHERE activity = ${lit(activity)} AND type = ${lit(object_type)}
|
|
419
435
|
)
|
|
420
436
|
SELECT changes.value FROM pairs
|
|
421
|
-
ASOF LEFT JOIN changes ON pairs.oid = changes.oid AND pairs.ts >= changes.ts`, numeric(String(attribute))),
|
|
422
|
-
chart: () =>
|
|
437
|
+
ASOF LEFT JOIN changes ON pairs.oid = changes.oid AND pairs.ts >= changes.ts`, attribute, numeric(String(attribute))),
|
|
438
|
+
chart: ({ attribute }, numeric) => distributionChart(attribute, numeric(String(attribute)))
|
|
423
439
|
},
|
|
424
440
|
{
|
|
425
441
|
id: "time-between-activities",
|
|
@@ -440,7 +456,7 @@ const analyses = [
|
|
|
440
456
|
},
|
|
441
457
|
{
|
|
442
458
|
name: "object_type",
|
|
443
|
-
label: "Object type
|
|
459
|
+
label: "Object type",
|
|
444
460
|
kind: "objectType",
|
|
445
461
|
required: false
|
|
446
462
|
},
|
|
@@ -466,13 +482,8 @@ const analyses = [
|
|
|
466
482
|
WINDOW w AS (PARTITION BY oid ORDER BY ts, eid)
|
|
467
483
|
)
|
|
468
484
|
SELECT epoch(next_ts - ts) / ${SECONDS[String(unit)] ?? 3600} AS value
|
|
469
|
-
FROM trace WHERE activity = ${lit(source)} AND next_activity = ${lit(target)}
|
|
470
|
-
chart: ({ unit }) => (
|
|
471
|
-
x: "bucket",
|
|
472
|
-
y: "count",
|
|
473
|
-
types: ["bar", "pie"],
|
|
474
|
-
title: `Time in ${unit ?? "hours"}`
|
|
475
|
-
})
|
|
485
|
+
FROM trace WHERE activity = ${lit(source)} AND next_activity = ${lit(target)}`, unit),
|
|
486
|
+
chart: ({ unit }) => histogramChart(unit)
|
|
476
487
|
},
|
|
477
488
|
{
|
|
478
489
|
id: "object-attribute-timeline",
|
|
@@ -503,11 +514,25 @@ const analyses = [
|
|
|
503
514
|
}
|
|
504
515
|
];
|
|
505
516
|
const findAnalysis = (id) => analyses.find((analysis) => analysis.id === id);
|
|
517
|
+
/**
|
|
518
|
+
* A card's values, with every parameter the reader has not set at its
|
|
519
|
+
* built-in value - so what a control shows is what the query runs with.
|
|
520
|
+
*/
|
|
521
|
+
const resolve = (analysis, values) => {
|
|
522
|
+
const resolved = {};
|
|
523
|
+
for (const param of analysis.params) {
|
|
524
|
+
const value = values[param.name] ?? param.default ?? param.options?.[0] ?? void 0;
|
|
525
|
+
if (value != null) resolved[param.name] = value;
|
|
526
|
+
}
|
|
527
|
+
return resolved;
|
|
528
|
+
};
|
|
529
|
+
const hasValue = (value) => value != null && (!Array.isArray(value) || value.length > 0);
|
|
506
530
|
/** Whether every required parameter has a value. */
|
|
507
|
-
const isConfigured = (analysis, values) =>
|
|
508
|
-
const
|
|
509
|
-
|
|
510
|
-
|
|
531
|
+
const isConfigured = (analysis, values) => {
|
|
532
|
+
const resolved = resolve(analysis, values);
|
|
533
|
+
return analysis.params.every((param) => param.required === false || hasValue(resolved[param.name]));
|
|
534
|
+
};
|
|
535
|
+
// src/Controls.tsx
|
|
511
536
|
/**
|
|
512
537
|
* The parameter inputs a card is configured with.
|
|
513
538
|
*
|
|
@@ -530,69 +555,110 @@ const useNumeric = (values) => {
|
|
|
530
555
|
const objects = useObjectAttributes(id, { names: type }, { query: { enabled: id != null && type.length > 0 } });
|
|
531
556
|
return (attribute) => [...events.data ?? [], ...objects.data ?? []].some((candidate) => candidate.name === attribute && BINNED.includes(candidate.type));
|
|
532
557
|
};
|
|
533
|
-
|
|
558
|
+
/** Whether a parameter narrows another: its choices depend on the activity. */
|
|
559
|
+
const followsActivity = (analysis, param) => param.kind === "eventAttribute" || param.kind === "objectType" && param.required !== false && analysis.params.some((other) => other.name === "activity");
|
|
560
|
+
/**
|
|
561
|
+
* The parameters to clear when one changes, because what they offer depends
|
|
562
|
+
* on it: an attribute on its activity or object type, an object type on the
|
|
563
|
+
* activity it is narrowed to.
|
|
564
|
+
*/
|
|
565
|
+
const dependents = (analysis, changed) => {
|
|
566
|
+
const stale = (param) => changed.name === "activity" && followsActivity(analysis, param) || changed.name === "object_type" && param.kind === "objectAttribute";
|
|
567
|
+
const names = analysis.params.filter(stale).map((param) => param.name);
|
|
568
|
+
return changed.name === "activity" && names.includes("object_type") ? [...names, ...analysis.params.filter((param) => param.kind === "objectAttribute").map((param) => param.name)] : names;
|
|
569
|
+
};
|
|
570
|
+
/**
|
|
571
|
+
* An analysis's parameters as numbered steps. A step shows a check once it
|
|
572
|
+
* has a value; the next one to fill is highlighted, and optional ones are
|
|
573
|
+
* dashed - so what is left to do is seen rather than read.
|
|
574
|
+
*/
|
|
575
|
+
const Fields = ({ analysis, values, onChange }) => {
|
|
576
|
+
const next = analysis.params.find((param) => param.required !== false && !hasValue(values[param.name]));
|
|
577
|
+
return /* @__PURE__ */ jsx(Stack, {
|
|
578
|
+
gap: "md",
|
|
579
|
+
children: analysis.params.map((param, index) => {
|
|
580
|
+
const done = hasValue(values[param.name]);
|
|
581
|
+
return /* @__PURE__ */ jsxs(Group, {
|
|
582
|
+
gap: "sm",
|
|
583
|
+
align: "flex-start",
|
|
584
|
+
wrap: "nowrap",
|
|
585
|
+
children: [/* @__PURE__ */ jsx(ThemeIcon, {
|
|
586
|
+
size: 22,
|
|
587
|
+
radius: "xl",
|
|
588
|
+
color: done ? "teal" : param === next ? "blue" : "gray",
|
|
589
|
+
variant: done || param === next ? "filled" : "outline",
|
|
590
|
+
style: {
|
|
591
|
+
fontSize: 11,
|
|
592
|
+
fontWeight: 600,
|
|
593
|
+
borderStyle: param.required === false ? "dashed" : void 0
|
|
594
|
+
},
|
|
595
|
+
children: done ? /* @__PURE__ */ jsx(CheckIcon, { size: 13 }) : index + 1
|
|
596
|
+
}), /* @__PURE__ */ jsx(Box, {
|
|
597
|
+
flex: 1,
|
|
598
|
+
miw: 0,
|
|
599
|
+
children: /* @__PURE__ */ jsx(Control, {
|
|
600
|
+
analysis,
|
|
601
|
+
param,
|
|
602
|
+
values,
|
|
603
|
+
onChange: (value) => onChange(param, value)
|
|
604
|
+
})
|
|
605
|
+
})]
|
|
606
|
+
}, param.name);
|
|
607
|
+
})
|
|
608
|
+
});
|
|
609
|
+
};
|
|
610
|
+
const Control = ({ analysis, param, values, onChange }) => {
|
|
534
611
|
const value = values[param.name];
|
|
612
|
+
const activity = typeof values.activity === "string" ? values.activity : void 0;
|
|
535
613
|
if (param.kind === "number") return /* @__PURE__ */ jsx(NumberInput, {
|
|
536
|
-
size: "xs",
|
|
537
614
|
label: param.label,
|
|
538
615
|
min: param.min,
|
|
539
616
|
max: param.max,
|
|
540
|
-
value: Number(value ??
|
|
617
|
+
value: Number(value ?? 0),
|
|
541
618
|
onChange: (next) => onChange(typeof next === "number" ? next : void 0)
|
|
542
619
|
});
|
|
543
|
-
if (param.kind === "
|
|
544
|
-
const selected = Array.isArray(value) ? value.map(String) : [];
|
|
545
|
-
return /* @__PURE__ */ jsxs(Stack, {
|
|
546
|
-
gap: 4,
|
|
547
|
-
children: [/* @__PURE__ */ jsx(ActivityPicker, {
|
|
548
|
-
label: param.label,
|
|
549
|
-
multiple: true,
|
|
550
|
-
value: selected,
|
|
551
|
-
onChange
|
|
552
|
-
}), /* @__PURE__ */ jsx(Text, {
|
|
553
|
-
size: "xs",
|
|
554
|
-
c: "dimmed",
|
|
555
|
-
children: "Leave empty to include all activities."
|
|
556
|
-
})]
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
if (param.kind === "choice") return /* @__PURE__ */ jsx(Select, {
|
|
560
|
-
size: "xs",
|
|
561
|
-
label: param.label,
|
|
562
|
-
data: [...param.options ?? []],
|
|
563
|
-
value: String(value ?? param.options?.[0] ?? ""),
|
|
564
|
-
onChange: (next) => onChange(next ?? void 0),
|
|
565
|
-
allowDeselect: false
|
|
566
|
-
});
|
|
567
|
-
if (param.kind === "text" || param.kind === "column") return /* @__PURE__ */ jsx(TextInput, {
|
|
620
|
+
if (param.kind === "choice") return /* @__PURE__ */ jsx(Input.Wrapper, {
|
|
568
621
|
label: param.label,
|
|
569
|
-
|
|
570
|
-
|
|
622
|
+
children: /* @__PURE__ */ jsx(SegmentedControl, {
|
|
623
|
+
fullWidth: true,
|
|
624
|
+
data: [...param.options ?? []],
|
|
625
|
+
value: String(value ?? ""),
|
|
626
|
+
onChange
|
|
627
|
+
})
|
|
571
628
|
});
|
|
572
|
-
if (param.kind === "
|
|
629
|
+
if (param.kind === "activities") return /* @__PURE__ */ jsx(ActivityPicker, {
|
|
573
630
|
label: param.label,
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
value: value
|
|
577
|
-
onChange
|
|
578
|
-
styles: { input: {
|
|
579
|
-
fontFamily: "monospace",
|
|
580
|
-
fontSize: 12
|
|
581
|
-
} }
|
|
631
|
+
placeholder: "All activities",
|
|
632
|
+
multiple: true,
|
|
633
|
+
value: Array.isArray(value) ? value.map(String) : [],
|
|
634
|
+
onChange
|
|
582
635
|
});
|
|
583
|
-
if (param.kind === "columns") return null;
|
|
584
636
|
const chosen = {
|
|
585
637
|
value: value == null ? null : String(value),
|
|
586
638
|
onChange
|
|
587
639
|
};
|
|
640
|
+
if (param.kind === "activity") return /* @__PURE__ */ jsx(ActivityPicker, {
|
|
641
|
+
label: param.label,
|
|
642
|
+
...chosen
|
|
643
|
+
});
|
|
644
|
+
if (param.kind === "objectType") {
|
|
645
|
+
const narrowed = followsActivity(analysis, param);
|
|
646
|
+
return /* @__PURE__ */ jsx(ObjectTypePicker, {
|
|
647
|
+
label: param.label,
|
|
648
|
+
activity: narrowed ? activity : void 0,
|
|
649
|
+
disabled: narrowed && activity === void 0,
|
|
650
|
+
...param.required === false && { placeholder: "All object types" },
|
|
651
|
+
...chosen
|
|
652
|
+
});
|
|
653
|
+
}
|
|
588
654
|
if (param.kind === "object") return /* @__PURE__ */ jsx(ObjectPicker, {
|
|
589
655
|
label: param.label,
|
|
590
656
|
...chosen
|
|
591
657
|
});
|
|
592
658
|
if (param.kind === "eventAttribute") return /* @__PURE__ */ jsx(EventAttributePicker, {
|
|
593
659
|
label: param.label,
|
|
594
|
-
activity
|
|
595
|
-
disabled:
|
|
660
|
+
activity,
|
|
661
|
+
disabled: activity === void 0,
|
|
596
662
|
...chosen
|
|
597
663
|
});
|
|
598
664
|
if (param.kind === "objectAttribute") return /* @__PURE__ */ jsx(ObjectAttributePicker, {
|
|
@@ -601,35 +667,20 @@ const Control = ({ param, values, onChange }) => {
|
|
|
601
667
|
disabled: values.object_type == null,
|
|
602
668
|
...chosen
|
|
603
669
|
});
|
|
604
|
-
if (param.kind === "activity") return /* @__PURE__ */ jsx(ActivityPicker, {
|
|
605
|
-
label: param.label,
|
|
606
|
-
...chosen
|
|
607
|
-
});
|
|
608
|
-
if (param.kind === "objectType") return /* @__PURE__ */ jsx(ObjectTypePicker, {
|
|
609
|
-
label: param.label,
|
|
610
|
-
...chosen
|
|
611
|
-
});
|
|
612
670
|
return null;
|
|
613
671
|
};
|
|
614
|
-
|
|
615
|
-
//#region src/Card.tsx
|
|
672
|
+
// src/Card.tsx
|
|
616
673
|
/** One analysis on the dashboard: its parameters, and the chart they produce. */
|
|
617
|
-
const AnalysisCard = ({ analysis, values, onChange, onRemove, configurationOpened, onConfigure, onCloseConfiguration }) => {
|
|
674
|
+
const AnalysisCard = ({ analysis, values: stored, onChange, onRemove, configurationOpened, onConfigure, onCloseConfiguration }) => {
|
|
675
|
+
const values = resolve(analysis, stored);
|
|
618
676
|
const numeric = useNumeric(values);
|
|
619
677
|
const configured = isConfigured(analysis, values);
|
|
620
678
|
const setParam = (param, value) => {
|
|
621
679
|
const next = {
|
|
622
|
-
...
|
|
680
|
+
...stored,
|
|
623
681
|
[param.name]: value
|
|
624
682
|
};
|
|
625
|
-
|
|
626
|
-
const dependent = analysis.params.find((candidate) => candidate.kind === "eventAttribute");
|
|
627
|
-
if (dependent) next[dependent.name] = void 0;
|
|
628
|
-
}
|
|
629
|
-
if (param.name === "object_type") {
|
|
630
|
-
const dependent = analysis.params.find((candidate) => candidate.kind === "objectAttribute");
|
|
631
|
-
if (dependent) next[dependent.name] = void 0;
|
|
632
|
-
}
|
|
683
|
+
for (const name of dependents(analysis, param)) next[name] = void 0;
|
|
633
684
|
onChange(next);
|
|
634
685
|
};
|
|
635
686
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Paper, {
|
|
@@ -656,25 +707,16 @@ const AnalysisCard = ({ analysis, values, onChange, onRemove, configurationOpene
|
|
|
656
707
|
children: [
|
|
657
708
|
/* @__PURE__ */ jsxs(Group, {
|
|
658
709
|
gap: "xs",
|
|
659
|
-
children: [
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
children: analysis.category
|
|
670
|
-
}),
|
|
671
|
-
!configured && /* @__PURE__ */ jsx(Badge, {
|
|
672
|
-
variant: "light",
|
|
673
|
-
color: "orange",
|
|
674
|
-
size: "xs",
|
|
675
|
-
children: "Configure first"
|
|
676
|
-
})
|
|
677
|
-
]
|
|
710
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
711
|
+
fw: 600,
|
|
712
|
+
size: "sm",
|
|
713
|
+
children: analysis.label
|
|
714
|
+
}), /* @__PURE__ */ jsx(Badge, {
|
|
715
|
+
variant: "light",
|
|
716
|
+
color: "gray",
|
|
717
|
+
size: "xs",
|
|
718
|
+
children: analysis.category
|
|
719
|
+
})]
|
|
678
720
|
}),
|
|
679
721
|
/* @__PURE__ */ jsx(Text, {
|
|
680
722
|
c: "dimmed",
|
|
@@ -763,57 +805,49 @@ const AnalysisCard = ({ analysis, values, onChange, onRemove, configurationOpene
|
|
|
763
805
|
}) : /* @__PURE__ */ jsx(OcelChart, {
|
|
764
806
|
height: "100%",
|
|
765
807
|
sql: analysis.sql(values, numeric),
|
|
766
|
-
...analysis.chart(values)
|
|
767
|
-
}) : /* @__PURE__ */ jsx(
|
|
768
|
-
|
|
769
|
-
|
|
808
|
+
...analysis.chart(values, numeric)
|
|
809
|
+
}) : /* @__PURE__ */ jsx(Center, {
|
|
810
|
+
h: "100%",
|
|
811
|
+
"data-export-ignore": true,
|
|
812
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
813
|
+
variant: "light",
|
|
814
|
+
leftSection: /* @__PURE__ */ jsx(Settings2Icon, { size: 16 }),
|
|
815
|
+
onClick: onConfigure,
|
|
816
|
+
children: "Configure"
|
|
817
|
+
})
|
|
770
818
|
})
|
|
771
819
|
})]
|
|
772
820
|
}), /* @__PURE__ */ jsx(Modal, {
|
|
773
821
|
opened: configurationOpened,
|
|
774
822
|
onClose: onCloseConfiguration,
|
|
775
|
-
title:
|
|
823
|
+
title: analysis.label,
|
|
776
824
|
size: "lg",
|
|
777
825
|
centered: true,
|
|
778
826
|
children: /* @__PURE__ */ jsxs(Stack, {
|
|
779
|
-
gap: "
|
|
780
|
-
children: [
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
onChange
|
|
792
|
-
}) : analysis.params.map((param) => /* @__PURE__ */ jsx(Control, {
|
|
793
|
-
param,
|
|
794
|
-
values,
|
|
795
|
-
onChange: (value) => setParam(param, value)
|
|
796
|
-
}, param.name))
|
|
797
|
-
})
|
|
798
|
-
}),
|
|
799
|
-
/* @__PURE__ */ jsxs(Group, {
|
|
800
|
-
justify: "space-between",
|
|
801
|
-
children: [/* @__PURE__ */ jsx(Text, {
|
|
802
|
-
size: "xs",
|
|
803
|
-
c: configured ? "teal" : "dimmed",
|
|
804
|
-
children: configured ? "Ready—the visualization is updating with these choices." : `Still needed: ${unset(analysis, values)}.`
|
|
805
|
-
}), /* @__PURE__ */ jsx(Button, {
|
|
806
|
-
onClick: onCloseConfiguration,
|
|
807
|
-
children: "Done"
|
|
808
|
-
})]
|
|
827
|
+
gap: "lg",
|
|
828
|
+
children: [/* @__PURE__ */ jsx(ScrollArea.Autosize, {
|
|
829
|
+
mah: "62vh",
|
|
830
|
+
type: "auto",
|
|
831
|
+
offsetScrollbars: true,
|
|
832
|
+
children: analysis.view === "custom-chart" ? /* @__PURE__ */ jsx(CustomChartControls, {
|
|
833
|
+
values,
|
|
834
|
+
onChange
|
|
835
|
+
}) : /* @__PURE__ */ jsx(Fields, {
|
|
836
|
+
analysis,
|
|
837
|
+
values,
|
|
838
|
+
onChange: setParam
|
|
809
839
|
})
|
|
810
|
-
|
|
840
|
+
}), /* @__PURE__ */ jsx(Group, {
|
|
841
|
+
justify: "flex-end",
|
|
842
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
843
|
+
disabled: !configured,
|
|
844
|
+
onClick: onCloseConfiguration,
|
|
845
|
+
children: "Done"
|
|
846
|
+
})
|
|
847
|
+
})]
|
|
811
848
|
})
|
|
812
849
|
})] });
|
|
813
850
|
};
|
|
814
|
-
/** The parameters still without a value, in words. */
|
|
815
|
-
const unset = (analysis, values) => analysis.params.filter((param) => param.required !== false && !hasValue(values[param.name])).map((param) => param.label.toLowerCase()).join(", ");
|
|
816
|
-
const hasValue = (value) => value != null && (!Array.isArray(value) || value.length > 0);
|
|
817
851
|
const summary = (analysis, values) => analysis.params.filter((param) => param.kind !== "sql" && values[param.name] != null).slice(0, 3).map((param) => {
|
|
818
852
|
const value = values[param.name];
|
|
819
853
|
const text = Array.isArray(value) ? value.length === 0 ? "All" : value.length <= 2 ? value.join(", ") : `${value.length} selected` : String(value);
|
|
@@ -948,8 +982,8 @@ const CustomChartControls = ({ values, onChange }) => {
|
|
|
948
982
|
})]
|
|
949
983
|
}),
|
|
950
984
|
/* @__PURE__ */ jsx(Select, {
|
|
951
|
-
label: "Series
|
|
952
|
-
|
|
985
|
+
label: "Series",
|
|
986
|
+
placeholder: "None",
|
|
953
987
|
data: columns,
|
|
954
988
|
value: values.series == null ? null : String(values.series),
|
|
955
989
|
clearable: true,
|
|
@@ -1016,14 +1050,13 @@ const Changes = ({ analysis, values }) => {
|
|
|
1016
1050
|
series: "attribute",
|
|
1017
1051
|
xRange: lifetime,
|
|
1018
1052
|
yAxes: "independent",
|
|
1019
|
-
|
|
1053
|
+
colorScope: "attribute",
|
|
1020
1054
|
step: true
|
|
1021
1055
|
});
|
|
1022
1056
|
}
|
|
1023
1057
|
});
|
|
1024
1058
|
};
|
|
1025
|
-
|
|
1026
|
-
//#region src/store.ts
|
|
1059
|
+
// src/store.ts
|
|
1027
1060
|
/** The dashboard a reader has built, kept per OCEL in browser storage. */
|
|
1028
1061
|
const key = (ocelId) => `ocelescope:exploration:${ocelId}`;
|
|
1029
1062
|
const useCards = (ocelId) => {
|
|
@@ -1046,12 +1079,14 @@ const useCards = (ocelId) => {
|
|
|
1046
1079
|
]);
|
|
1047
1080
|
return {
|
|
1048
1081
|
cards,
|
|
1082
|
+
/** False until the stored dashboard has been read. */
|
|
1083
|
+
loaded,
|
|
1049
1084
|
add: (analysis) => {
|
|
1050
1085
|
const id = globalThis.crypto.randomUUID();
|
|
1051
1086
|
setCards((current) => [...current, {
|
|
1052
1087
|
id,
|
|
1053
1088
|
analysis,
|
|
1054
|
-
values:
|
|
1089
|
+
values: {}
|
|
1055
1090
|
}]);
|
|
1056
1091
|
return id;
|
|
1057
1092
|
},
|
|
@@ -1062,15 +1097,6 @@ const useCards = (ocelId) => {
|
|
|
1062
1097
|
remove: (id) => setCards((current) => current.filter((card) => card.id !== id))
|
|
1063
1098
|
};
|
|
1064
1099
|
};
|
|
1065
|
-
/**
|
|
1066
|
-
* A new card starts with nothing chosen: which activity or object type a chart
|
|
1067
|
-
* is about is the reader's question to ask, and guessing it for them either
|
|
1068
|
-
* shows an arbitrary one or, across all of them at once, an unreadable chart.
|
|
1069
|
-
*
|
|
1070
|
-
* Only settings that are not names from the log - a bin count, a time unit -
|
|
1071
|
-
* carry their built-in value.
|
|
1072
|
-
*/
|
|
1073
|
-
const fixed = (analysis) => Object.fromEntries((findAnalysis(analysis)?.params ?? []).map((param) => [param.name, param.default ?? param.options?.[0]]).filter(([, value]) => value != null));
|
|
1074
1100
|
const read = (ocelId) => {
|
|
1075
1101
|
try {
|
|
1076
1102
|
const stored = JSON.parse(window.localStorage.getItem(key(ocelId)) ?? "[]");
|
|
@@ -1079,17 +1105,17 @@ const read = (ocelId) => {
|
|
|
1079
1105
|
return [];
|
|
1080
1106
|
}
|
|
1081
1107
|
};
|
|
1082
|
-
|
|
1083
|
-
//#region src/Dashboard.tsx
|
|
1108
|
+
// src/Dashboard.tsx
|
|
1084
1109
|
/** The exploration dashboard: choose a question, configure it, keep the result. */
|
|
1085
1110
|
const Dashboard = ({ ocelId }) => {
|
|
1086
|
-
const { cards, add, update, remove } = useCards(ocelId);
|
|
1111
|
+
const { cards, loaded, add, update, remove } = useCards(ocelId);
|
|
1087
1112
|
const [catalogOpen, setCatalogOpen] = useState(false);
|
|
1088
1113
|
const [configure, setConfigure] = useState();
|
|
1089
1114
|
const addAnalysis = (analysis) => {
|
|
1090
1115
|
const id = add(analysis);
|
|
1091
1116
|
setCatalogOpen(false);
|
|
1092
|
-
|
|
1117
|
+
const added = findAnalysis(analysis);
|
|
1118
|
+
if (added && !isConfigured(added, {})) setConfigure(id);
|
|
1093
1119
|
};
|
|
1094
1120
|
return /* @__PURE__ */ jsxs(Container, {
|
|
1095
1121
|
fluid: true,
|
|
@@ -1100,52 +1126,65 @@ const Dashboard = ({ ocelId }) => {
|
|
|
1100
1126
|
py: "md",
|
|
1101
1127
|
children: [/* @__PURE__ */ jsxs(Stack, {
|
|
1102
1128
|
gap: "lg",
|
|
1103
|
-
children: [
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1129
|
+
children: [
|
|
1130
|
+
/* @__PURE__ */ jsxs(Group, {
|
|
1131
|
+
align: "center",
|
|
1132
|
+
justify: "space-between",
|
|
1133
|
+
wrap: "wrap",
|
|
1134
|
+
children: [/* @__PURE__ */ jsx(Stack, {
|
|
1135
|
+
gap: 2,
|
|
1136
|
+
children: /* @__PURE__ */ jsxs(Group, {
|
|
1137
|
+
gap: "xs",
|
|
1138
|
+
children: [/* @__PURE__ */ jsx(Title, {
|
|
1139
|
+
order: 2,
|
|
1140
|
+
children: "Explore the log"
|
|
1141
|
+
}), cards.length > 0 && /* @__PURE__ */ jsxs(Badge, {
|
|
1142
|
+
variant: "light",
|
|
1143
|
+
color: "gray",
|
|
1144
|
+
radius: "sm",
|
|
1145
|
+
children: [
|
|
1146
|
+
cards.length,
|
|
1147
|
+
" ",
|
|
1148
|
+
cards.length === 1 ? "view" : "views"
|
|
1149
|
+
]
|
|
1150
|
+
})]
|
|
1151
|
+
})
|
|
1152
|
+
}), cards.length > 0 && /* @__PURE__ */ jsx(Button, {
|
|
1153
|
+
leftSection: /* @__PURE__ */ jsx(PlusIcon, { size: 16 }),
|
|
1154
|
+
onClick: () => setCatalogOpen(true),
|
|
1155
|
+
children: "Add visualization"
|
|
1156
|
+
})]
|
|
1157
|
+
}),
|
|
1158
|
+
loaded && cards.length === 0 && /* @__PURE__ */ jsx(Center, {
|
|
1159
|
+
h: "50vh",
|
|
1160
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
1161
|
+
size: "lg",
|
|
1162
|
+
variant: "light",
|
|
1163
|
+
leftSection: /* @__PURE__ */ jsx(PlusIcon, { size: 18 }),
|
|
1164
|
+
onClick: () => setCatalogOpen(true),
|
|
1165
|
+
children: "Add visualization"
|
|
1166
|
+
})
|
|
1167
|
+
}),
|
|
1168
|
+
/* @__PURE__ */ jsx(SimpleGrid, {
|
|
1169
|
+
cols: {
|
|
1170
|
+
base: 1,
|
|
1171
|
+
xl: 2
|
|
1172
|
+
},
|
|
1173
|
+
spacing: "md",
|
|
1174
|
+
children: cards.map((card) => {
|
|
1175
|
+
const analysis = findAnalysis(card.analysis);
|
|
1176
|
+
return analysis ? /* @__PURE__ */ jsx(AnalysisCard, {
|
|
1177
|
+
analysis,
|
|
1178
|
+
values: card.values,
|
|
1179
|
+
onChange: (values) => update(card.id, values),
|
|
1180
|
+
onRemove: () => remove(card.id),
|
|
1181
|
+
configurationOpened: configure === card.id,
|
|
1182
|
+
onConfigure: () => setConfigure(card.id),
|
|
1183
|
+
onCloseConfiguration: () => setConfigure(void 0)
|
|
1184
|
+
}, card.id) : null;
|
|
1124
1185
|
})
|
|
1125
|
-
}), /* @__PURE__ */ jsx(Button, {
|
|
1126
|
-
leftSection: /* @__PURE__ */ jsx(PlusIcon, { size: 16 }),
|
|
1127
|
-
onClick: () => setCatalogOpen(true),
|
|
1128
|
-
children: "Add visualization"
|
|
1129
|
-
})]
|
|
1130
|
-
}), /* @__PURE__ */ jsx(SimpleGrid, {
|
|
1131
|
-
cols: {
|
|
1132
|
-
base: 1,
|
|
1133
|
-
xl: 2
|
|
1134
|
-
},
|
|
1135
|
-
spacing: "md",
|
|
1136
|
-
children: cards.map((card) => {
|
|
1137
|
-
const analysis = findAnalysis(card.analysis);
|
|
1138
|
-
return analysis ? /* @__PURE__ */ jsx(AnalysisCard, {
|
|
1139
|
-
analysis,
|
|
1140
|
-
values: card.values,
|
|
1141
|
-
onChange: (values) => update(card.id, values),
|
|
1142
|
-
onRemove: () => remove(card.id),
|
|
1143
|
-
configurationOpened: configure === card.id,
|
|
1144
|
-
onConfigure: () => setConfigure(card.id),
|
|
1145
|
-
onCloseConfiguration: () => setConfigure(void 0)
|
|
1146
|
-
}, card.id) : null;
|
|
1147
1186
|
})
|
|
1148
|
-
|
|
1187
|
+
]
|
|
1149
1188
|
}), /* @__PURE__ */ jsx(AnalysisCatalog, {
|
|
1150
1189
|
opened: catalogOpen,
|
|
1151
1190
|
onClose: () => setCatalogOpen(false),
|
|
@@ -1277,16 +1316,10 @@ const CatalogItem = ({ analysis, onAdd }) => {
|
|
|
1277
1316
|
size: "xs",
|
|
1278
1317
|
lineClamp: 3,
|
|
1279
1318
|
children: analysis.question
|
|
1280
|
-
}),
|
|
1281
|
-
/* @__PURE__ */ jsx(Text, {
|
|
1282
|
-
size: "xs",
|
|
1283
|
-
c: analysis.params.length > 0 ? "dimmed" : "blue",
|
|
1284
|
-
children: analysis.params.length > 0 ? `${analysis.params.length} configuration ${analysis.params.length === 1 ? "choice" : "choices"}` : "Ready to view"
|
|
1285
1319
|
})
|
|
1286
1320
|
]
|
|
1287
1321
|
})
|
|
1288
1322
|
})
|
|
1289
1323
|
});
|
|
1290
1324
|
};
|
|
1291
|
-
//#endregion
|
|
1292
1325
|
export { Dashboard };
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -3,8 +3,8 @@ import { ChartNoAxesCombinedIcon } from "lucide-react";
|
|
|
3
3
|
import { LoadingOverlay } from "@mantine/core";
|
|
4
4
|
import dynamic from "next/dynamic";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
|
-
|
|
7
|
-
const Dashboard = dynamic(() => import("./Dashboard-
|
|
6
|
+
// src/pages/dashboard.tsx
|
|
7
|
+
const Dashboard = dynamic(() => import("./Dashboard-jE93H3NS.js").then((module) => module.Dashboard), { ssr: false });
|
|
8
8
|
const DashboardPage = () => {
|
|
9
9
|
const { id } = useCurrentOcel();
|
|
10
10
|
return id ? /* @__PURE__ */ jsx(Dashboard, { ocelId: id }, id) : /* @__PURE__ */ jsx(LoadingOverlay, { visible: true });
|
|
@@ -15,8 +15,7 @@ var dashboard_default = defineModuleRoute({
|
|
|
15
15
|
name: "dashboard",
|
|
16
16
|
requiresOcel: true
|
|
17
17
|
});
|
|
18
|
-
|
|
19
|
-
//#region src/index.ts
|
|
18
|
+
// src/index.ts
|
|
20
19
|
var src_default = defineModule({
|
|
21
20
|
name: "exploration",
|
|
22
21
|
description: "Explore the active OCEL through configurable visualizations.",
|
|
@@ -25,5 +24,4 @@ var src_default = defineModule({
|
|
|
25
24
|
routes: [dashboard_default],
|
|
26
25
|
icon: ChartNoAxesCombinedIcon
|
|
27
26
|
});
|
|
28
|
-
//#endregion
|
|
29
27
|
export { src_default as default };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ocelescope/exploration",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.11.0",
|
|
5
5
|
"description": "Interactive analytical dashboards for OCEL data",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -19,19 +19,19 @@
|
|
|
19
19
|
"dist"
|
|
20
20
|
],
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"@mantine/core": "^9.
|
|
23
|
-
"@ocelescope/api-base": "^0.
|
|
24
|
-
"@ocelescope/core": "^0.
|
|
25
|
-
"@r4pm/components": "^0.
|
|
26
|
-
"@tanstack/react-query": "^5.
|
|
27
|
-
"lucide-react": "^1.
|
|
28
|
-
"next": "16.3
|
|
29
|
-
"react": "^19.3.
|
|
22
|
+
"@mantine/core": "^9.7.4",
|
|
23
|
+
"@ocelescope/api-base": "^0.11.0",
|
|
24
|
+
"@ocelescope/core": "^0.11.0",
|
|
25
|
+
"@r4pm/components": "^0.4.0",
|
|
26
|
+
"@tanstack/react-query": "^5.105.0",
|
|
27
|
+
"lucide-react": "^1.63.0",
|
|
28
|
+
"next": "16.4.3",
|
|
29
|
+
"react": "^19.3.1"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@types/node": "^26.6.
|
|
32
|
+
"@types/node": "^26.6.5",
|
|
33
33
|
"@types/react": "^19.3.0",
|
|
34
|
-
"tsdown": "^0.23.
|
|
34
|
+
"tsdown": "^0.23.3",
|
|
35
35
|
"typescript": "^6.0.3"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|