@ocelescope/exploration 0.10.1 → 0.10.2
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.
|
@@ -1,9 +1,9 @@
|
|
|
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
|
//#region src/analyses.ts
|
|
9
9
|
const ident = (name) => `"${String(name ?? "").replaceAll("\"", "\"\"")}"`;
|
|
@@ -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,9 +514,24 @@ 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
|
|
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
|
+
};
|
|
509
535
|
//#endregion
|
|
510
536
|
//#region src/Controls.tsx
|
|
511
537
|
/**
|
|
@@ -530,69 +556,110 @@ const useNumeric = (values) => {
|
|
|
530
556
|
const objects = useObjectAttributes(id, { names: type }, { query: { enabled: id != null && type.length > 0 } });
|
|
531
557
|
return (attribute) => [...events.data ?? [], ...objects.data ?? []].some((candidate) => candidate.name === attribute && BINNED.includes(candidate.type));
|
|
532
558
|
};
|
|
533
|
-
|
|
559
|
+
/** Whether a parameter narrows another: its choices depend on the activity. */
|
|
560
|
+
const followsActivity = (analysis, param) => param.kind === "eventAttribute" || param.kind === "objectType" && param.required !== false && analysis.params.some((other) => other.name === "activity");
|
|
561
|
+
/**
|
|
562
|
+
* The parameters to clear when one changes, because what they offer depends
|
|
563
|
+
* on it: an attribute on its activity or object type, an object type on the
|
|
564
|
+
* activity it is narrowed to.
|
|
565
|
+
*/
|
|
566
|
+
const dependents = (analysis, changed) => {
|
|
567
|
+
const stale = (param) => changed.name === "activity" && followsActivity(analysis, param) || changed.name === "object_type" && param.kind === "objectAttribute";
|
|
568
|
+
const names = analysis.params.filter(stale).map((param) => param.name);
|
|
569
|
+
return changed.name === "activity" && names.includes("object_type") ? [...names, ...analysis.params.filter((param) => param.kind === "objectAttribute").map((param) => param.name)] : names;
|
|
570
|
+
};
|
|
571
|
+
/**
|
|
572
|
+
* An analysis's parameters as numbered steps. A step shows a check once it
|
|
573
|
+
* has a value; the next one to fill is highlighted, and optional ones are
|
|
574
|
+
* dashed - so what is left to do is seen rather than read.
|
|
575
|
+
*/
|
|
576
|
+
const Fields = ({ analysis, values, onChange }) => {
|
|
577
|
+
const next = analysis.params.find((param) => param.required !== false && !hasValue(values[param.name]));
|
|
578
|
+
return /* @__PURE__ */ jsx(Stack, {
|
|
579
|
+
gap: "md",
|
|
580
|
+
children: analysis.params.map((param, index) => {
|
|
581
|
+
const done = hasValue(values[param.name]);
|
|
582
|
+
return /* @__PURE__ */ jsxs(Group, {
|
|
583
|
+
gap: "sm",
|
|
584
|
+
align: "flex-start",
|
|
585
|
+
wrap: "nowrap",
|
|
586
|
+
children: [/* @__PURE__ */ jsx(ThemeIcon, {
|
|
587
|
+
size: 22,
|
|
588
|
+
radius: "xl",
|
|
589
|
+
color: done ? "teal" : param === next ? "blue" : "gray",
|
|
590
|
+
variant: done || param === next ? "filled" : "outline",
|
|
591
|
+
style: {
|
|
592
|
+
fontSize: 11,
|
|
593
|
+
fontWeight: 600,
|
|
594
|
+
borderStyle: param.required === false ? "dashed" : void 0
|
|
595
|
+
},
|
|
596
|
+
children: done ? /* @__PURE__ */ jsx(CheckIcon, { size: 13 }) : index + 1
|
|
597
|
+
}), /* @__PURE__ */ jsx(Box, {
|
|
598
|
+
flex: 1,
|
|
599
|
+
miw: 0,
|
|
600
|
+
children: /* @__PURE__ */ jsx(Control, {
|
|
601
|
+
analysis,
|
|
602
|
+
param,
|
|
603
|
+
values,
|
|
604
|
+
onChange: (value) => onChange(param, value)
|
|
605
|
+
})
|
|
606
|
+
})]
|
|
607
|
+
}, param.name);
|
|
608
|
+
})
|
|
609
|
+
});
|
|
610
|
+
};
|
|
611
|
+
const Control = ({ analysis, param, values, onChange }) => {
|
|
534
612
|
const value = values[param.name];
|
|
613
|
+
const activity = typeof values.activity === "string" ? values.activity : void 0;
|
|
535
614
|
if (param.kind === "number") return /* @__PURE__ */ jsx(NumberInput, {
|
|
536
|
-
size: "xs",
|
|
537
615
|
label: param.label,
|
|
538
616
|
min: param.min,
|
|
539
617
|
max: param.max,
|
|
540
|
-
value: Number(value ??
|
|
618
|
+
value: Number(value ?? 0),
|
|
541
619
|
onChange: (next) => onChange(typeof next === "number" ? next : void 0)
|
|
542
620
|
});
|
|
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, {
|
|
621
|
+
if (param.kind === "choice") return /* @__PURE__ */ jsx(Input.Wrapper, {
|
|
568
622
|
label: param.label,
|
|
569
|
-
|
|
570
|
-
|
|
623
|
+
children: /* @__PURE__ */ jsx(SegmentedControl, {
|
|
624
|
+
fullWidth: true,
|
|
625
|
+
data: [...param.options ?? []],
|
|
626
|
+
value: String(value ?? ""),
|
|
627
|
+
onChange
|
|
628
|
+
})
|
|
571
629
|
});
|
|
572
|
-
if (param.kind === "
|
|
630
|
+
if (param.kind === "activities") return /* @__PURE__ */ jsx(ActivityPicker, {
|
|
573
631
|
label: param.label,
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
value: value
|
|
577
|
-
onChange
|
|
578
|
-
styles: { input: {
|
|
579
|
-
fontFamily: "monospace",
|
|
580
|
-
fontSize: 12
|
|
581
|
-
} }
|
|
632
|
+
placeholder: "All activities",
|
|
633
|
+
multiple: true,
|
|
634
|
+
value: Array.isArray(value) ? value.map(String) : [],
|
|
635
|
+
onChange
|
|
582
636
|
});
|
|
583
|
-
if (param.kind === "columns") return null;
|
|
584
637
|
const chosen = {
|
|
585
638
|
value: value == null ? null : String(value),
|
|
586
639
|
onChange
|
|
587
640
|
};
|
|
641
|
+
if (param.kind === "activity") return /* @__PURE__ */ jsx(ActivityPicker, {
|
|
642
|
+
label: param.label,
|
|
643
|
+
...chosen
|
|
644
|
+
});
|
|
645
|
+
if (param.kind === "objectType") {
|
|
646
|
+
const narrowed = followsActivity(analysis, param);
|
|
647
|
+
return /* @__PURE__ */ jsx(ObjectTypePicker, {
|
|
648
|
+
label: param.label,
|
|
649
|
+
activity: narrowed ? activity : void 0,
|
|
650
|
+
disabled: narrowed && activity === void 0,
|
|
651
|
+
...param.required === false && { placeholder: "All object types" },
|
|
652
|
+
...chosen
|
|
653
|
+
});
|
|
654
|
+
}
|
|
588
655
|
if (param.kind === "object") return /* @__PURE__ */ jsx(ObjectPicker, {
|
|
589
656
|
label: param.label,
|
|
590
657
|
...chosen
|
|
591
658
|
});
|
|
592
659
|
if (param.kind === "eventAttribute") return /* @__PURE__ */ jsx(EventAttributePicker, {
|
|
593
660
|
label: param.label,
|
|
594
|
-
activity
|
|
595
|
-
disabled:
|
|
661
|
+
activity,
|
|
662
|
+
disabled: activity === void 0,
|
|
596
663
|
...chosen
|
|
597
664
|
});
|
|
598
665
|
if (param.kind === "objectAttribute") return /* @__PURE__ */ jsx(ObjectAttributePicker, {
|
|
@@ -601,35 +668,21 @@ const Control = ({ param, values, onChange }) => {
|
|
|
601
668
|
disabled: values.object_type == null,
|
|
602
669
|
...chosen
|
|
603
670
|
});
|
|
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
671
|
return null;
|
|
613
672
|
};
|
|
614
673
|
//#endregion
|
|
615
674
|
//#region src/Card.tsx
|
|
616
675
|
/** One analysis on the dashboard: its parameters, and the chart they produce. */
|
|
617
|
-
const AnalysisCard = ({ analysis, values, onChange, onRemove, configurationOpened, onConfigure, onCloseConfiguration }) => {
|
|
676
|
+
const AnalysisCard = ({ analysis, values: stored, onChange, onRemove, configurationOpened, onConfigure, onCloseConfiguration }) => {
|
|
677
|
+
const values = resolve(analysis, stored);
|
|
618
678
|
const numeric = useNumeric(values);
|
|
619
679
|
const configured = isConfigured(analysis, values);
|
|
620
680
|
const setParam = (param, value) => {
|
|
621
681
|
const next = {
|
|
622
|
-
...
|
|
682
|
+
...stored,
|
|
623
683
|
[param.name]: value
|
|
624
684
|
};
|
|
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
|
-
}
|
|
685
|
+
for (const name of dependents(analysis, param)) next[name] = void 0;
|
|
633
686
|
onChange(next);
|
|
634
687
|
};
|
|
635
688
|
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Paper, {
|
|
@@ -656,25 +709,16 @@ const AnalysisCard = ({ analysis, values, onChange, onRemove, configurationOpene
|
|
|
656
709
|
children: [
|
|
657
710
|
/* @__PURE__ */ jsxs(Group, {
|
|
658
711
|
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
|
-
]
|
|
712
|
+
children: [/* @__PURE__ */ jsx(Text, {
|
|
713
|
+
fw: 600,
|
|
714
|
+
size: "sm",
|
|
715
|
+
children: analysis.label
|
|
716
|
+
}), /* @__PURE__ */ jsx(Badge, {
|
|
717
|
+
variant: "light",
|
|
718
|
+
color: "gray",
|
|
719
|
+
size: "xs",
|
|
720
|
+
children: analysis.category
|
|
721
|
+
})]
|
|
678
722
|
}),
|
|
679
723
|
/* @__PURE__ */ jsx(Text, {
|
|
680
724
|
c: "dimmed",
|
|
@@ -763,57 +807,49 @@ const AnalysisCard = ({ analysis, values, onChange, onRemove, configurationOpene
|
|
|
763
807
|
}) : /* @__PURE__ */ jsx(OcelChart, {
|
|
764
808
|
height: "100%",
|
|
765
809
|
sql: analysis.sql(values, numeric),
|
|
766
|
-
...analysis.chart(values)
|
|
767
|
-
}) : /* @__PURE__ */ jsx(
|
|
768
|
-
|
|
769
|
-
|
|
810
|
+
...analysis.chart(values, numeric)
|
|
811
|
+
}) : /* @__PURE__ */ jsx(Center, {
|
|
812
|
+
h: "100%",
|
|
813
|
+
"data-export-ignore": true,
|
|
814
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
815
|
+
variant: "light",
|
|
816
|
+
leftSection: /* @__PURE__ */ jsx(Settings2Icon, { size: 16 }),
|
|
817
|
+
onClick: onConfigure,
|
|
818
|
+
children: "Configure"
|
|
819
|
+
})
|
|
770
820
|
})
|
|
771
821
|
})]
|
|
772
822
|
}), /* @__PURE__ */ jsx(Modal, {
|
|
773
823
|
opened: configurationOpened,
|
|
774
824
|
onClose: onCloseConfiguration,
|
|
775
|
-
title:
|
|
825
|
+
title: analysis.label,
|
|
776
826
|
size: "lg",
|
|
777
827
|
centered: true,
|
|
778
828
|
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
|
-
})]
|
|
829
|
+
gap: "lg",
|
|
830
|
+
children: [/* @__PURE__ */ jsx(ScrollArea.Autosize, {
|
|
831
|
+
mah: "62vh",
|
|
832
|
+
type: "auto",
|
|
833
|
+
offsetScrollbars: true,
|
|
834
|
+
children: analysis.view === "custom-chart" ? /* @__PURE__ */ jsx(CustomChartControls, {
|
|
835
|
+
values,
|
|
836
|
+
onChange
|
|
837
|
+
}) : /* @__PURE__ */ jsx(Fields, {
|
|
838
|
+
analysis,
|
|
839
|
+
values,
|
|
840
|
+
onChange: setParam
|
|
809
841
|
})
|
|
810
|
-
|
|
842
|
+
}), /* @__PURE__ */ jsx(Group, {
|
|
843
|
+
justify: "flex-end",
|
|
844
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
845
|
+
disabled: !configured,
|
|
846
|
+
onClick: onCloseConfiguration,
|
|
847
|
+
children: "Done"
|
|
848
|
+
})
|
|
849
|
+
})]
|
|
811
850
|
})
|
|
812
851
|
})] });
|
|
813
852
|
};
|
|
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
853
|
const summary = (analysis, values) => analysis.params.filter((param) => param.kind !== "sql" && values[param.name] != null).slice(0, 3).map((param) => {
|
|
818
854
|
const value = values[param.name];
|
|
819
855
|
const text = Array.isArray(value) ? value.length === 0 ? "All" : value.length <= 2 ? value.join(", ") : `${value.length} selected` : String(value);
|
|
@@ -948,8 +984,8 @@ const CustomChartControls = ({ values, onChange }) => {
|
|
|
948
984
|
})]
|
|
949
985
|
}),
|
|
950
986
|
/* @__PURE__ */ jsx(Select, {
|
|
951
|
-
label: "Series
|
|
952
|
-
|
|
987
|
+
label: "Series",
|
|
988
|
+
placeholder: "None",
|
|
953
989
|
data: columns,
|
|
954
990
|
value: values.series == null ? null : String(values.series),
|
|
955
991
|
clearable: true,
|
|
@@ -1016,7 +1052,7 @@ const Changes = ({ analysis, values }) => {
|
|
|
1016
1052
|
series: "attribute",
|
|
1017
1053
|
xRange: lifetime,
|
|
1018
1054
|
yAxes: "independent",
|
|
1019
|
-
|
|
1055
|
+
colorScope: "attribute",
|
|
1020
1056
|
step: true
|
|
1021
1057
|
});
|
|
1022
1058
|
}
|
|
@@ -1046,12 +1082,14 @@ const useCards = (ocelId) => {
|
|
|
1046
1082
|
]);
|
|
1047
1083
|
return {
|
|
1048
1084
|
cards,
|
|
1085
|
+
/** False until the stored dashboard has been read. */
|
|
1086
|
+
loaded,
|
|
1049
1087
|
add: (analysis) => {
|
|
1050
1088
|
const id = globalThis.crypto.randomUUID();
|
|
1051
1089
|
setCards((current) => [...current, {
|
|
1052
1090
|
id,
|
|
1053
1091
|
analysis,
|
|
1054
|
-
values:
|
|
1092
|
+
values: {}
|
|
1055
1093
|
}]);
|
|
1056
1094
|
return id;
|
|
1057
1095
|
},
|
|
@@ -1062,15 +1100,6 @@ const useCards = (ocelId) => {
|
|
|
1062
1100
|
remove: (id) => setCards((current) => current.filter((card) => card.id !== id))
|
|
1063
1101
|
};
|
|
1064
1102
|
};
|
|
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
1103
|
const read = (ocelId) => {
|
|
1075
1104
|
try {
|
|
1076
1105
|
const stored = JSON.parse(window.localStorage.getItem(key(ocelId)) ?? "[]");
|
|
@@ -1083,13 +1112,14 @@ const read = (ocelId) => {
|
|
|
1083
1112
|
//#region src/Dashboard.tsx
|
|
1084
1113
|
/** The exploration dashboard: choose a question, configure it, keep the result. */
|
|
1085
1114
|
const Dashboard = ({ ocelId }) => {
|
|
1086
|
-
const { cards, add, update, remove } = useCards(ocelId);
|
|
1115
|
+
const { cards, loaded, add, update, remove } = useCards(ocelId);
|
|
1087
1116
|
const [catalogOpen, setCatalogOpen] = useState(false);
|
|
1088
1117
|
const [configure, setConfigure] = useState();
|
|
1089
1118
|
const addAnalysis = (analysis) => {
|
|
1090
1119
|
const id = add(analysis);
|
|
1091
1120
|
setCatalogOpen(false);
|
|
1092
|
-
|
|
1121
|
+
const added = findAnalysis(analysis);
|
|
1122
|
+
if (added && !isConfigured(added, {})) setConfigure(id);
|
|
1093
1123
|
};
|
|
1094
1124
|
return /* @__PURE__ */ jsxs(Container, {
|
|
1095
1125
|
fluid: true,
|
|
@@ -1100,52 +1130,65 @@ const Dashboard = ({ ocelId }) => {
|
|
|
1100
1130
|
py: "md",
|
|
1101
1131
|
children: [/* @__PURE__ */ jsxs(Stack, {
|
|
1102
1132
|
gap: "lg",
|
|
1103
|
-
children: [
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1133
|
+
children: [
|
|
1134
|
+
/* @__PURE__ */ jsxs(Group, {
|
|
1135
|
+
align: "center",
|
|
1136
|
+
justify: "space-between",
|
|
1137
|
+
wrap: "wrap",
|
|
1138
|
+
children: [/* @__PURE__ */ jsx(Stack, {
|
|
1139
|
+
gap: 2,
|
|
1140
|
+
children: /* @__PURE__ */ jsxs(Group, {
|
|
1141
|
+
gap: "xs",
|
|
1142
|
+
children: [/* @__PURE__ */ jsx(Title, {
|
|
1143
|
+
order: 2,
|
|
1144
|
+
children: "Explore the log"
|
|
1145
|
+
}), cards.length > 0 && /* @__PURE__ */ jsxs(Badge, {
|
|
1146
|
+
variant: "light",
|
|
1147
|
+
color: "gray",
|
|
1148
|
+
radius: "sm",
|
|
1149
|
+
children: [
|
|
1150
|
+
cards.length,
|
|
1151
|
+
" ",
|
|
1152
|
+
cards.length === 1 ? "view" : "views"
|
|
1153
|
+
]
|
|
1154
|
+
})]
|
|
1155
|
+
})
|
|
1156
|
+
}), cards.length > 0 && /* @__PURE__ */ jsx(Button, {
|
|
1157
|
+
leftSection: /* @__PURE__ */ jsx(PlusIcon, { size: 16 }),
|
|
1158
|
+
onClick: () => setCatalogOpen(true),
|
|
1159
|
+
children: "Add visualization"
|
|
1160
|
+
})]
|
|
1161
|
+
}),
|
|
1162
|
+
loaded && cards.length === 0 && /* @__PURE__ */ jsx(Center, {
|
|
1163
|
+
h: "50vh",
|
|
1164
|
+
children: /* @__PURE__ */ jsx(Button, {
|
|
1165
|
+
size: "lg",
|
|
1166
|
+
variant: "light",
|
|
1167
|
+
leftSection: /* @__PURE__ */ jsx(PlusIcon, { size: 18 }),
|
|
1168
|
+
onClick: () => setCatalogOpen(true),
|
|
1169
|
+
children: "Add visualization"
|
|
1170
|
+
})
|
|
1171
|
+
}),
|
|
1172
|
+
/* @__PURE__ */ jsx(SimpleGrid, {
|
|
1173
|
+
cols: {
|
|
1174
|
+
base: 1,
|
|
1175
|
+
xl: 2
|
|
1176
|
+
},
|
|
1177
|
+
spacing: "md",
|
|
1178
|
+
children: cards.map((card) => {
|
|
1179
|
+
const analysis = findAnalysis(card.analysis);
|
|
1180
|
+
return analysis ? /* @__PURE__ */ jsx(AnalysisCard, {
|
|
1181
|
+
analysis,
|
|
1182
|
+
values: card.values,
|
|
1183
|
+
onChange: (values) => update(card.id, values),
|
|
1184
|
+
onRemove: () => remove(card.id),
|
|
1185
|
+
configurationOpened: configure === card.id,
|
|
1186
|
+
onConfigure: () => setConfigure(card.id),
|
|
1187
|
+
onCloseConfiguration: () => setConfigure(void 0)
|
|
1188
|
+
}, card.id) : null;
|
|
1124
1189
|
})
|
|
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
1190
|
})
|
|
1148
|
-
|
|
1191
|
+
]
|
|
1149
1192
|
}), /* @__PURE__ */ jsx(AnalysisCatalog, {
|
|
1150
1193
|
opened: catalogOpen,
|
|
1151
1194
|
onClose: () => setCatalogOpen(false),
|
|
@@ -1277,11 +1320,6 @@ const CatalogItem = ({ analysis, onAdd }) => {
|
|
|
1277
1320
|
size: "xs",
|
|
1278
1321
|
lineClamp: 3,
|
|
1279
1322
|
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
1323
|
})
|
|
1286
1324
|
]
|
|
1287
1325
|
})
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { LoadingOverlay } from "@mantine/core";
|
|
|
4
4
|
import dynamic from "next/dynamic";
|
|
5
5
|
import { jsx } from "react/jsx-runtime";
|
|
6
6
|
//#region src/pages/dashboard.tsx
|
|
7
|
-
const Dashboard = dynamic(() => import("./Dashboard-
|
|
7
|
+
const Dashboard = dynamic(() => import("./Dashboard-DlIf2ec5.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 });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ocelescope/exploration",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.10.
|
|
4
|
+
"version": "0.10.2",
|
|
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.10.
|
|
24
|
-
"@ocelescope/core": "^0.10.
|
|
22
|
+
"@mantine/core": "^9.7.2",
|
|
23
|
+
"@ocelescope/api-base": "^0.10.2",
|
|
24
|
+
"@ocelescope/core": "^0.10.2",
|
|
25
25
|
"@r4pm/components": "^0.3.2",
|
|
26
|
-
"@tanstack/react-query": "^5.
|
|
27
|
-
"lucide-react": "^1.
|
|
28
|
-
"next": "16.
|
|
26
|
+
"@tanstack/react-query": "^5.104.2",
|
|
27
|
+
"lucide-react": "^1.56.0",
|
|
28
|
+
"next": "16.4.0",
|
|
29
29
|
"react": "^19.3.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^26.6.1",
|
|
33
33
|
"@types/react": "^19.3.0",
|
|
34
|
-
"tsdown": "^0.23.
|
|
34
|
+
"tsdown": "^0.23.1",
|
|
35
35
|
"typescript": "^6.0.3"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|