marimo-pmprov 0.1.0__tar.gz
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.
- marimo_pmprov-0.1.0/.github/workflows/ci.yml +92 -0
- marimo_pmprov-0.1.0/.gitignore +10 -0
- marimo_pmprov-0.1.0/PKG-INFO +15 -0
- marimo_pmprov-0.1.0/README.md +130 -0
- marimo_pmprov-0.1.0/examples/demo_notebook.py +226 -0
- marimo_pmprov-0.1.0/frontend/build.mjs +22 -0
- marimo_pmprov-0.1.0/frontend/package-lock.json +496 -0
- marimo_pmprov-0.1.0/frontend/package.json +14 -0
- marimo_pmprov-0.1.0/frontend/src/components/artifact-badge.ts +20 -0
- marimo_pmprov-0.1.0/frontend/src/components/node-card.ts +87 -0
- marimo_pmprov-0.1.0/frontend/src/components/segmented-control.ts +16 -0
- marimo_pmprov-0.1.0/frontend/src/components/tag-chip.ts +9 -0
- marimo_pmprov-0.1.0/frontend/src/gutter.ts +105 -0
- marimo_pmprov-0.1.0/frontend/src/main.ts +236 -0
- marimo_pmprov-0.1.0/frontend/src/nav-state.ts +15 -0
- marimo_pmprov-0.1.0/frontend/src/picker.ts +103 -0
- marimo_pmprov-0.1.0/frontend/src/resolvers.ts +90 -0
- marimo_pmprov-0.1.0/frontend/src/theme.css +175 -0
- marimo_pmprov-0.1.0/frontend/src/types.ts +62 -0
- marimo_pmprov-0.1.0/frontend/src/views/config.ts +39 -0
- marimo_pmprov-0.1.0/frontend/src/views/curated.ts +180 -0
- marimo_pmprov-0.1.0/frontend/src/views/inspect.ts +78 -0
- marimo_pmprov-0.1.0/frontend/src/views/tree.ts +313 -0
- marimo_pmprov-0.1.0/frontend/tsconfig.json +11 -0
- marimo_pmprov-0.1.0/provenance_widget/__init__.py +1 -0
- marimo_pmprov-0.1.0/provenance_widget/display.py +24 -0
- marimo_pmprov-0.1.0/provenance_widget/interfaces.py +119 -0
- marimo_pmprov-0.1.0/provenance_widget/mock_source.py +207 -0
- marimo_pmprov-0.1.0/provenance_widget/pmprov_adapter.py +175 -0
- marimo_pmprov-0.1.0/provenance_widget/serialize.py +57 -0
- marimo_pmprov-0.1.0/provenance_widget/static/.gitkeep +0 -0
- marimo_pmprov-0.1.0/provenance_widget/static/widget.css +1 -0
- marimo_pmprov-0.1.0/provenance_widget/static/widget.js +2 -0
- marimo_pmprov-0.1.0/provenance_widget/widget.py +82 -0
- marimo_pmprov-0.1.0/pyproject.toml +20 -0
- marimo_pmprov-0.1.0/tests/__init__.py +0 -0
- marimo_pmprov-0.1.0/tests/test_display.py +20 -0
- marimo_pmprov-0.1.0/tests/test_interfaces.py +57 -0
- marimo_pmprov-0.1.0/tests/test_mock_source.py +37 -0
- marimo_pmprov-0.1.0/tests/test_serialize.py +34 -0
- marimo_pmprov-0.1.0/tests/test_widget.py +47 -0
- marimo_pmprov-0.1.0/uv.lock +1982 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
name: "CI"
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- name: Checkout โฌ๏ธ
|
|
16
|
+
uses: actions/checkout@v7.0.1
|
|
17
|
+
|
|
18
|
+
- name: Set up Python ๐
|
|
19
|
+
uses: actions/setup-python@v7.0.0
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.11"
|
|
22
|
+
|
|
23
|
+
- name: Install uv ๐ฆ
|
|
24
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
25
|
+
|
|
26
|
+
- name: Install dependencies ๐ฆ
|
|
27
|
+
run: uv sync --extra dev
|
|
28
|
+
|
|
29
|
+
- name: Run tests ๐งช
|
|
30
|
+
run: uv run pytest
|
|
31
|
+
|
|
32
|
+
build:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
needs: test
|
|
35
|
+
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
36
|
+
outputs:
|
|
37
|
+
already-published: ${{ steps.version-check.outputs.already-published }}
|
|
38
|
+
steps:
|
|
39
|
+
- name: Checkout โฌ๏ธ
|
|
40
|
+
uses: actions/checkout@v7.0.1
|
|
41
|
+
|
|
42
|
+
- name: Set up Python ๐
|
|
43
|
+
uses: actions/setup-python@v7.0.0
|
|
44
|
+
with:
|
|
45
|
+
python-version: "3.11"
|
|
46
|
+
|
|
47
|
+
- name: Install uv ๐ฆ
|
|
48
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
49
|
+
|
|
50
|
+
- name: Check if version already published ๐
|
|
51
|
+
id: version-check
|
|
52
|
+
run: |
|
|
53
|
+
NAME=$(python -c "import tomllib; d=tomllib.load(open('pyproject.toml','rb')); print(d['project']['name'])")
|
|
54
|
+
VERSION=$(python -c "import tomllib; d=tomllib.load(open('pyproject.toml','rb')); print(d['project']['version'])")
|
|
55
|
+
STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://pypi.org/pypi/$NAME/$VERSION/json)
|
|
56
|
+
if [ "$STATUS" = "200" ]; then
|
|
57
|
+
echo "already-published=true" >> $GITHUB_OUTPUT
|
|
58
|
+
else
|
|
59
|
+
echo "already-published=false" >> $GITHUB_OUTPUT
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
- name: Build ๐๏ธ
|
|
63
|
+
if: steps.version-check.outputs.already-published == 'false'
|
|
64
|
+
run: uv build
|
|
65
|
+
|
|
66
|
+
- name: Upload dist artifacts ๐ค
|
|
67
|
+
if: steps.version-check.outputs.already-published == 'false'
|
|
68
|
+
uses: actions/upload-artifact@v7.0.1
|
|
69
|
+
with:
|
|
70
|
+
name: dist
|
|
71
|
+
path: dist/
|
|
72
|
+
|
|
73
|
+
publish:
|
|
74
|
+
runs-on: ubuntu-latest
|
|
75
|
+
needs: build
|
|
76
|
+
if: needs.build.outputs.already-published == 'false'
|
|
77
|
+
environment: pypi
|
|
78
|
+
permissions:
|
|
79
|
+
contents: read
|
|
80
|
+
id-token: write
|
|
81
|
+
steps:
|
|
82
|
+
- name: Download dist artifacts ๐ฅ
|
|
83
|
+
uses: actions/download-artifact@v8.0.1
|
|
84
|
+
with:
|
|
85
|
+
name: dist
|
|
86
|
+
path: dist/
|
|
87
|
+
|
|
88
|
+
- name: Install uv ๐ฆ
|
|
89
|
+
uses: astral-sh/setup-uv@v9.0.0
|
|
90
|
+
|
|
91
|
+
- name: Publish to PyPI ๐
|
|
92
|
+
run: uv publish
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: marimo-pmprov
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A marimo anywidget for analytic provenance and annotation, decoupled from pmprov.
|
|
5
|
+
Requires-Python: >=3.11
|
|
6
|
+
Requires-Dist: anywidget>=0.9
|
|
7
|
+
Requires-Dist: marimo>=0.9
|
|
8
|
+
Requires-Dist: traitlets>=5.14
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: altair; extra == 'dev'
|
|
11
|
+
Requires-Dist: matplotlib; extra == 'dev'
|
|
12
|
+
Requires-Dist: pandas; extra == 'dev'
|
|
13
|
+
Requires-Dist: plotly; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
15
|
+
Requires-Dist: vl-convert-python; extra == 'dev'
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# marimo-pmprov
|
|
2
|
+
|
|
3
|
+
A marimo `anywidget` plugin that gives process-mining analysts a git-style
|
|
4
|
+
"Source Control" sidebar for analytic provenance and annotation: a curated
|
|
5
|
+
commit rail, a Miller-columns provenance tree with tag/artifact filters and
|
|
6
|
+
an inline inspector, and a read-only Reviewer mode with tag-based state
|
|
7
|
+
restore.
|
|
8
|
+
|
|
9
|
+
This package ships fully decoupled from the `pmprov` library โ it runs out
|
|
10
|
+
of the box against a `MockProvenanceSource` whose data is shaped exactly
|
|
11
|
+
like pmprov's real model (see `provenance_widget/interfaces.py`), so no
|
|
12
|
+
`pmprov` install is required to try it or to develop against it.
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install "marimo-pmprov @ git+https://github.com/alvarobernal2412/marimo-pmprov.git"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or, for local development:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/alvarobernal2412/marimo-pmprov.git
|
|
24
|
+
cd marimo-pmprov
|
|
25
|
+
pip install -e ".[dev]"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This installs the widget with its `MockProvenanceSource` demo backend. See
|
|
29
|
+
`examples/demo_notebook.py` for a full runnable notebook (run it with
|
|
30
|
+
`marimo edit examples/demo_notebook.py`).
|
|
31
|
+
|
|
32
|
+
## Quickstart
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
import marimo as mo
|
|
36
|
+
from provenance_widget.mock_source import MockProvenanceSource
|
|
37
|
+
from provenance_widget.widget import ProvenancePanel
|
|
38
|
+
|
|
39
|
+
panel = ProvenancePanel(source=MockProvenanceSource(), mode="student")
|
|
40
|
+
|
|
41
|
+
# mo.sidebar(...) must be the last bare expression in its own cell to render โ
|
|
42
|
+
# it cannot be assigned to a variable and displayed from a different cell.
|
|
43
|
+
mo.sidebar(panel.sidebar())
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The sidebar has three tabs:
|
|
47
|
+
|
|
48
|
+
- **curated** โ a chronological feed of committed annotations (oldest first,
|
|
49
|
+
newest appended at the bottom), plus a composer for writing new ones.
|
|
50
|
+
- **tree** โ a Miller-columns view of the full provenance history, with
|
|
51
|
+
filter chips for tags and linked artifacts (matching steps stay expanded,
|
|
52
|
+
the rest collapse to a single line) and an inspector pinned to the bottom
|
|
53
|
+
that opens automatically when you click a node.
|
|
54
|
+
- **config** โ switches between `student` (can annotate) and `reviewer`
|
|
55
|
+
(read-only, can request a restore to any tagged state) personas.
|
|
56
|
+
|
|
57
|
+
### Tagging notebook outputs so they can be picked
|
|
58
|
+
|
|
59
|
+
Any table or chart you want the sidebar's "Pick from notebook" picker (and
|
|
60
|
+
the hover "+" gutter affordance) to be able to target needs to be wrapped
|
|
61
|
+
with `show_table` / `show_chart`:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from provenance_widget.display import show_table, show_chart
|
|
65
|
+
|
|
66
|
+
# tables โ picking refines down to a specific cell or column automatically
|
|
67
|
+
annotated_table = show_table(
|
|
68
|
+
mo.ui.table(my_dataframe), artifact_id="art-event-log", artifact_name="event_log.csv"
|
|
69
|
+
)
|
|
70
|
+
annotated_table
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
# charts โ matplotlib figures work via mo.as_html; any renderer that
|
|
75
|
+
# produces an mo.Html works the same way (Altair, Plotly, etc.)
|
|
76
|
+
import matplotlib.pyplot as plt
|
|
77
|
+
|
|
78
|
+
fig, ax = plt.subplots()
|
|
79
|
+
my_dataframe.groupby("activity")["duration"].mean().plot.bar(ax=ax)
|
|
80
|
+
|
|
81
|
+
bar_chart = show_chart(mo.as_html(fig), artifact_id="art-duration-bar", artifact_name="Chart_DurationByActivity")
|
|
82
|
+
bar_chart
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`show_chart` works with any `mo.Html`-producing visualization, for example:
|
|
86
|
+
|
|
87
|
+
| Visualization | How to produce the `mo.Html` |
|
|
88
|
+
| --- | --- |
|
|
89
|
+
| Bar chart | `mo.as_html(fig)` from a matplotlib `ax.bar(...)` / `.plot.bar()` figure |
|
|
90
|
+
| Line chart | `mo.as_html(fig)` from a matplotlib `.plot.line()` figure |
|
|
91
|
+
| Scatter plot | `mo.as_html(fig)` from a matplotlib `ax.scatter(...)` figure |
|
|
92
|
+
| Histogram | `mo.as_html(fig)` from a matplotlib `ax.hist(...)` figure |
|
|
93
|
+
| Heatmap | `mo.as_html(fig)` from a matplotlib `ax.imshow(...)` figure |
|
|
94
|
+
|
|
95
|
+
See `examples/demo_notebook.py` for all five wired up against the same
|
|
96
|
+
dataset.
|
|
97
|
+
|
|
98
|
+
## Connecting a real pmprov-backed history
|
|
99
|
+
|
|
100
|
+
The widget only depends on the `ProvenanceSource` protocol
|
|
101
|
+
(`provenance_widget.interfaces.ProvenanceSource`) โ it never imports
|
|
102
|
+
`pmprov` directly. To back the sidebar with a real `pmprov.AnalysisHistory`
|
|
103
|
+
instead of mock data:
|
|
104
|
+
|
|
105
|
+
1. Implement a `PmprovAdapter` class with a `get_tree(self) -> ProvenanceTree`
|
|
106
|
+
method that translates `AnalysisHistory.list_states()` /
|
|
107
|
+
`ProvenanceTracker` data into the `ProvenanceTree` dataclasses in
|
|
108
|
+
`provenance_widget/interfaces.py` (field names already mirror pmprov's
|
|
109
|
+
model, per pmprov's `MODEL.md`). `Annotation`/`Tag` are new concepts not
|
|
110
|
+
present in pmprov โ the adapter is where you decide how they're sourced
|
|
111
|
+
or persisted against a real history.
|
|
112
|
+
2. Install `pmprov` alongside this package. PyPI doesn't allow published
|
|
113
|
+
packages to declare a direct VCS dependency, so there's no `pmprov`
|
|
114
|
+
extra โ install it separately:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
pip install marimo-pmprov "pmprov @ git+https://github.com/alvarobernal2412/pmprov.git@main"
|
|
118
|
+
```
|
|
119
|
+
3. Pass your adapter instead of `MockProvenanceSource` to `ProvenancePanel`.
|
|
120
|
+
|
|
121
|
+
## Development
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
pip install -e ".[dev]"
|
|
125
|
+
pytest
|
|
126
|
+
|
|
127
|
+
cd frontend
|
|
128
|
+
npm install
|
|
129
|
+
npm run build # or: npm run watch
|
|
130
|
+
```
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import marimo
|
|
2
|
+
|
|
3
|
+
__generated_with = "0.23.16"
|
|
4
|
+
app = marimo.App(width="full")
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@app.cell
|
|
8
|
+
def _():
|
|
9
|
+
import marimo as mo
|
|
10
|
+
import pandas as pd
|
|
11
|
+
import matplotlib
|
|
12
|
+
import altair as alt
|
|
13
|
+
import vl_convert as vlc
|
|
14
|
+
import plotly.express as px
|
|
15
|
+
|
|
16
|
+
matplotlib.use("Agg")
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
|
|
19
|
+
from provenance_widget.display import show_chart, show_table
|
|
20
|
+
from provenance_widget.mock_source import MockProvenanceSource
|
|
21
|
+
from provenance_widget.widget import ProvenancePanel
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
MockProvenanceSource,
|
|
25
|
+
ProvenancePanel,
|
|
26
|
+
alt,
|
|
27
|
+
mo,
|
|
28
|
+
pd,
|
|
29
|
+
plt,
|
|
30
|
+
px,
|
|
31
|
+
show_chart,
|
|
32
|
+
show_table,
|
|
33
|
+
vlc,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@app.cell
|
|
38
|
+
def _(MockProvenanceSource, ProvenancePanel):
|
|
39
|
+
# Persona (student/reviewer) is switched from the widget's own "config"
|
|
40
|
+
# tab, not a separate marimo control โ this is just the initial value.
|
|
41
|
+
panel = ProvenancePanel(source=MockProvenanceSource(), mode="student")
|
|
42
|
+
return (panel,)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@app.cell
|
|
46
|
+
def _(mo, panel):
|
|
47
|
+
# mo.sidebar(...) must be the last bare expression in this cell to render โ
|
|
48
|
+
# it cannot be assigned to a variable and displayed from a different cell.
|
|
49
|
+
mo.sidebar(panel.sidebar())
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.cell
|
|
54
|
+
def _(mo, pd, show_table):
|
|
55
|
+
event_log = pd.DataFrame({
|
|
56
|
+
"case:concept:name": ["c1", "c1", "c2"],
|
|
57
|
+
"concept:name": ["Register", "Approve", "Register"],
|
|
58
|
+
"duration": [12.5, 40.2, 8.1],
|
|
59
|
+
})
|
|
60
|
+
table = mo.ui.table(event_log)
|
|
61
|
+
annotated_table = show_table(table, artifact_id="art-event-log", artifact_name="event_log.csv")
|
|
62
|
+
annotated_table
|
|
63
|
+
return (event_log,)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@app.cell
|
|
67
|
+
def _(event_log, mo, plt, show_chart):
|
|
68
|
+
# Five different visualization types, each picker-targetable at
|
|
69
|
+
# "chart_selection" granularity (see provenance_widget/display.py).
|
|
70
|
+
|
|
71
|
+
bar_fig, bar_ax = plt.subplots(figsize=(4, 3))
|
|
72
|
+
event_log.groupby("concept:name")["duration"].mean().plot.bar(ax=bar_ax, color="#0EA5E9")
|
|
73
|
+
bar_ax.set_title("Mean duration by activity")
|
|
74
|
+
bar_ax.set_ylabel("hours")
|
|
75
|
+
bar_chart = show_chart(
|
|
76
|
+
mo.as_html(bar_fig), artifact_id="art-duration-bar", artifact_name="Chart_DurationByActivity"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
line_fig, line_ax = plt.subplots(figsize=(4, 3))
|
|
80
|
+
event_log["duration"].plot.line(ax=line_ax, color="#10B981", marker="o")
|
|
81
|
+
line_ax.set_title("Duration across events")
|
|
82
|
+
line_ax.set_ylabel("hours")
|
|
83
|
+
line_chart = show_chart(
|
|
84
|
+
mo.as_html(line_fig), artifact_id="art-duration-line", artifact_name="Chart_DurationTrend"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
scatter_fig, scatter_ax = plt.subplots(figsize=(4, 3))
|
|
88
|
+
scatter_ax.scatter(range(len(event_log)), event_log["duration"], c="#F59E0B")
|
|
89
|
+
scatter_ax.set_title("Duration per event index")
|
|
90
|
+
scatter_ax.set_ylabel("hours")
|
|
91
|
+
scatter_chart = show_chart(
|
|
92
|
+
mo.as_html(scatter_fig), artifact_id="art-duration-scatter", artifact_name="Chart_DurationScatter"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
hist_fig, hist_ax = plt.subplots(figsize=(4, 3))
|
|
96
|
+
hist_ax.hist(event_log["duration"], bins=5, color="#F43F5E")
|
|
97
|
+
hist_ax.set_title("Duration distribution")
|
|
98
|
+
hist_ax.set_xlabel("hours")
|
|
99
|
+
hist_chart = show_chart(
|
|
100
|
+
mo.as_html(hist_fig), artifact_id="art-duration-hist", artifact_name="Chart_DurationHistogram"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
heatmap_fig, heatmap_ax = plt.subplots(figsize=(4, 3))
|
|
104
|
+
pivot = event_log.pivot_table(
|
|
105
|
+
index="case:concept:name", columns="concept:name", values="duration", aggfunc="mean"
|
|
106
|
+
)
|
|
107
|
+
im = heatmap_ax.imshow(pivot.fillna(0), cmap="viridis", aspect="auto")
|
|
108
|
+
heatmap_ax.set_xticks(range(len(pivot.columns)))
|
|
109
|
+
heatmap_ax.set_xticklabels(pivot.columns, rotation=45, ha="right")
|
|
110
|
+
heatmap_ax.set_yticks(range(len(pivot.index)))
|
|
111
|
+
heatmap_ax.set_yticklabels(pivot.index)
|
|
112
|
+
heatmap_ax.set_title("Duration by case x activity")
|
|
113
|
+
heatmap_fig.colorbar(im, ax=heatmap_ax)
|
|
114
|
+
heatmap_chart = show_chart(
|
|
115
|
+
mo.as_html(heatmap_fig), artifact_id="art-duration-heatmap", artifact_name="Chart_DurationHeatmap"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
mo.vstack([
|
|
119
|
+
mo.hstack([bar_chart, line_chart], justify="start"),
|
|
120
|
+
mo.hstack([scatter_chart, hist_chart], justify="start"),
|
|
121
|
+
heatmap_chart,
|
|
122
|
+
])
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@app.cell
|
|
127
|
+
def _(alt, event_log, mo, show_chart, vlc):
|
|
128
|
+
# Unlike the matplotlib charts above (flat PNGs), Vega-Lite/Altair marks
|
|
129
|
+
# each bar with an aria-label carrying its data ("concept:name: Register;
|
|
130
|
+
# duration: 12.5; ..."), which the picker's "svg-mark" resolver
|
|
131
|
+
# (frontend/src/resolvers.ts) reads off directly โ no extra wiring needed.
|
|
132
|
+
#
|
|
133
|
+
# marimo's default mo.as_html(chart) embeds Vega-Lite as an interactive
|
|
134
|
+
# <canvas> (or a rasterized <img>), neither of which has per-mark DOM
|
|
135
|
+
# nodes to click on โ so here the chart is rendered straight to real
|
|
136
|
+
# inline SVG via vl-convert instead, which is what makes those
|
|
137
|
+
# aria-labeled <path> marks actually present in the page.
|
|
138
|
+
altair_chart = alt.Chart(event_log).mark_bar().encode(
|
|
139
|
+
x=alt.X(field="concept:name", type="nominal"),
|
|
140
|
+
y=alt.Y(field="duration", type="quantitative"),
|
|
141
|
+
color=alt.Color(field="case:concept:name", type="nominal"),
|
|
142
|
+
)
|
|
143
|
+
altair_svg = vlc.vegalite_to_svg(altair_chart.to_json())
|
|
144
|
+
annotated_altair_chart = show_chart(
|
|
145
|
+
mo.Html(altair_svg), artifact_id="art-duration-altair", artifact_name="Chart_DurationAltair"
|
|
146
|
+
)
|
|
147
|
+
annotated_altair_chart
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@app.cell
|
|
152
|
+
def _(mo, show_chart):
|
|
153
|
+
# A hand-authored SVG, tagged by hand rather than relying on a charting
|
|
154
|
+
# library's own accessibility output. This is the general-purpose escape
|
|
155
|
+
# hatch: any visualization โ a custom D3 diagram, a process map, a
|
|
156
|
+
# network graph โ can be picked down to individual elements by putting
|
|
157
|
+
# data-pw-detail (and optionally data-pw-granularity) directly on the
|
|
158
|
+
# SVG nodes. The picker's "svg-mark" resolver (resolvers.ts) looks for
|
|
159
|
+
# exactly that attribute, so no picker code changes were needed for this.
|
|
160
|
+
#
|
|
161
|
+
# Reuses artifact_id="art-process-map" โ the same artifact the "Baseline
|
|
162
|
+
# fitness" annotation in mock_source.py already references, so picking
|
|
163
|
+
# a node here and the tree's "Chart_ProcessMap" filter chip line up.
|
|
164
|
+
process_steps = ["Register", "Approve", "Archive"]
|
|
165
|
+
box_w, box_h, gap = 120, 48, 60
|
|
166
|
+
svg_width = len(process_steps) * box_w + (len(process_steps) - 1) * gap
|
|
167
|
+
svg_parts = [
|
|
168
|
+
f'<svg xmlns="http://www.w3.org/2000/svg" width="{svg_width}" height="{box_h + 20}" '
|
|
169
|
+
f'viewBox="0 0 {svg_width} {box_h + 20}">',
|
|
170
|
+
'<defs><marker id="pw-arrow" markerWidth="8" markerHeight="8" refX="6" refY="3" '
|
|
171
|
+
'orient="auto"><path d="M0,0 L6,3 L0,6 Z" fill="#64748B" /></marker></defs>',
|
|
172
|
+
]
|
|
173
|
+
for i, step in enumerate(process_steps):
|
|
174
|
+
x = i * (box_w + gap)
|
|
175
|
+
svg_parts.append(
|
|
176
|
+
f'<g data-pw-detail="{step}" data-pw-granularity="node" role="graphics-symbol" '
|
|
177
|
+
f'aria-label="Process step: {step}">'
|
|
178
|
+
f'<rect x="{x}" y="0" width="{box_w}" height="{box_h}" rx="6" '
|
|
179
|
+
f'fill="#EFF6FF" stroke="#0EA5E9" stroke-width="2" />'
|
|
180
|
+
f'<text x="{x + box_w / 2}" y="{box_h / 2}" text-anchor="middle" '
|
|
181
|
+
f'dominant-baseline="middle" font-family="sans-serif" font-size="13">{step}</text>'
|
|
182
|
+
f"</g>"
|
|
183
|
+
)
|
|
184
|
+
if i < len(process_steps) - 1:
|
|
185
|
+
line_x1, line_x2 = x + box_w, x + box_w + gap
|
|
186
|
+
svg_parts.append(
|
|
187
|
+
f'<line x1="{line_x1}" y1="{box_h / 2}" x2="{line_x2 - 8}" y2="{box_h / 2}" '
|
|
188
|
+
f'stroke="#64748B" stroke-width="2" marker-end="url(#pw-arrow)" />'
|
|
189
|
+
)
|
|
190
|
+
svg_parts.append("</svg>")
|
|
191
|
+
process_map_svg = "".join(svg_parts)
|
|
192
|
+
|
|
193
|
+
annotated_process_map = show_chart(
|
|
194
|
+
mo.Html(process_map_svg), artifact_id="art-process-map", artifact_name="Chart_ProcessMap"
|
|
195
|
+
)
|
|
196
|
+
annotated_process_map
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@app.cell
|
|
201
|
+
def _(event_log, mo, px, show_chart):
|
|
202
|
+
# A fourth chart library, for contrast: mo.ui.plotly renders inline
|
|
203
|
+
# (a <marimo-plotly> custom element โ not the CDN-loading <iframe> that
|
|
204
|
+
# mo.as_html(plotly_fig) produces), so at least "chart_selection"-level
|
|
205
|
+
# picking works out of the box like any other artifact. Plotly's own SVG
|
|
206
|
+
# marks don't carry the aria-label convention Vega-Lite does, so unlike
|
|
207
|
+
# the Altair chart above, per-bar picking isn't automatic here โ it's
|
|
208
|
+
# exactly the kind of case that would need its own resolver (see
|
|
209
|
+
# frontend/src/resolvers.ts) reading Plotly's "plotly_click" event data
|
|
210
|
+
# instead of guessing from the DOM.
|
|
211
|
+
plotly_fig = px.bar(
|
|
212
|
+
event_log.groupby("concept:name", as_index=False)["duration"].mean(),
|
|
213
|
+
x="concept:name",
|
|
214
|
+
y="duration",
|
|
215
|
+
color="concept:name",
|
|
216
|
+
title="Mean duration by activity (Plotly)",
|
|
217
|
+
)
|
|
218
|
+
annotated_plotly_chart = show_chart(
|
|
219
|
+
mo.ui.plotly(plotly_fig), artifact_id="art-duration-plotly", artifact_name="Chart_DurationPlotly"
|
|
220
|
+
)
|
|
221
|
+
annotated_plotly_chart
|
|
222
|
+
return
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
app.run()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as esbuild from "esbuild";
|
|
2
|
+
|
|
3
|
+
const watch = process.argv.includes("--watch");
|
|
4
|
+
|
|
5
|
+
const options = {
|
|
6
|
+
entryPoints: ["src/main.ts"],
|
|
7
|
+
bundle: true,
|
|
8
|
+
format: "esm",
|
|
9
|
+
outfile: "../provenance_widget/static/widget.js",
|
|
10
|
+
minify: !watch,
|
|
11
|
+
sourcemap: watch,
|
|
12
|
+
loader: { ".css": "css" },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
if (watch) {
|
|
16
|
+
const ctx = await esbuild.context(options);
|
|
17
|
+
await ctx.watch();
|
|
18
|
+
console.log("watching...");
|
|
19
|
+
} else {
|
|
20
|
+
await esbuild.build(options);
|
|
21
|
+
console.log("build complete");
|
|
22
|
+
}
|