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.
Files changed (42) hide show
  1. marimo_pmprov-0.1.0/.github/workflows/ci.yml +92 -0
  2. marimo_pmprov-0.1.0/.gitignore +10 -0
  3. marimo_pmprov-0.1.0/PKG-INFO +15 -0
  4. marimo_pmprov-0.1.0/README.md +130 -0
  5. marimo_pmprov-0.1.0/examples/demo_notebook.py +226 -0
  6. marimo_pmprov-0.1.0/frontend/build.mjs +22 -0
  7. marimo_pmprov-0.1.0/frontend/package-lock.json +496 -0
  8. marimo_pmprov-0.1.0/frontend/package.json +14 -0
  9. marimo_pmprov-0.1.0/frontend/src/components/artifact-badge.ts +20 -0
  10. marimo_pmprov-0.1.0/frontend/src/components/node-card.ts +87 -0
  11. marimo_pmprov-0.1.0/frontend/src/components/segmented-control.ts +16 -0
  12. marimo_pmprov-0.1.0/frontend/src/components/tag-chip.ts +9 -0
  13. marimo_pmprov-0.1.0/frontend/src/gutter.ts +105 -0
  14. marimo_pmprov-0.1.0/frontend/src/main.ts +236 -0
  15. marimo_pmprov-0.1.0/frontend/src/nav-state.ts +15 -0
  16. marimo_pmprov-0.1.0/frontend/src/picker.ts +103 -0
  17. marimo_pmprov-0.1.0/frontend/src/resolvers.ts +90 -0
  18. marimo_pmprov-0.1.0/frontend/src/theme.css +175 -0
  19. marimo_pmprov-0.1.0/frontend/src/types.ts +62 -0
  20. marimo_pmprov-0.1.0/frontend/src/views/config.ts +39 -0
  21. marimo_pmprov-0.1.0/frontend/src/views/curated.ts +180 -0
  22. marimo_pmprov-0.1.0/frontend/src/views/inspect.ts +78 -0
  23. marimo_pmprov-0.1.0/frontend/src/views/tree.ts +313 -0
  24. marimo_pmprov-0.1.0/frontend/tsconfig.json +11 -0
  25. marimo_pmprov-0.1.0/provenance_widget/__init__.py +1 -0
  26. marimo_pmprov-0.1.0/provenance_widget/display.py +24 -0
  27. marimo_pmprov-0.1.0/provenance_widget/interfaces.py +119 -0
  28. marimo_pmprov-0.1.0/provenance_widget/mock_source.py +207 -0
  29. marimo_pmprov-0.1.0/provenance_widget/pmprov_adapter.py +175 -0
  30. marimo_pmprov-0.1.0/provenance_widget/serialize.py +57 -0
  31. marimo_pmprov-0.1.0/provenance_widget/static/.gitkeep +0 -0
  32. marimo_pmprov-0.1.0/provenance_widget/static/widget.css +1 -0
  33. marimo_pmprov-0.1.0/provenance_widget/static/widget.js +2 -0
  34. marimo_pmprov-0.1.0/provenance_widget/widget.py +82 -0
  35. marimo_pmprov-0.1.0/pyproject.toml +20 -0
  36. marimo_pmprov-0.1.0/tests/__init__.py +0 -0
  37. marimo_pmprov-0.1.0/tests/test_display.py +20 -0
  38. marimo_pmprov-0.1.0/tests/test_interfaces.py +57 -0
  39. marimo_pmprov-0.1.0/tests/test_mock_source.py +37 -0
  40. marimo_pmprov-0.1.0/tests/test_serialize.py +34 -0
  41. marimo_pmprov-0.1.0/tests/test_widget.py +47 -0
  42. 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,10 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .pytest_cache/
4
+ node_modules/
5
+ dist/
6
+ *.egg-info/
7
+ .venv/
8
+ __marimo__/
9
+ docs/
10
+ project/
@@ -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
+ }