Skip to content

Commit 5c0f4d5

Browse files
authored
Merge pull request #3 from koaning/koaning/progress-bar-dark-mode
progress bar
2 parents 1ceca2b + 41d195d commit 5c0f4d5

4 files changed

Lines changed: 155 additions & 3 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ uv pip install mofresh
1717

1818
The goal of this project is to offer a few tools that make it easy for you to refresh charts in marimo. This can be useful during a PyTorch training loop where you might want to update a chart on every iteration, but there are many other use-cases for this too.
1919

20+
### Widgets
21+
22+
The library provides three widgets:
23+
24+
1. **`ImageRefreshWidget`** - Displays images that can be updated dynamically. Perfect for refreshing matplotlib plots or any image content.
25+
2. **`HTMLRefreshWidget`** - Renders HTML content that can be updated on the fly. Great for Altair charts, plotly visualizations, or custom HTML.
26+
3. **`ProgressBar`** - A modern progress bar with dark mode support. Ideal for tracking training loops or long-running operations.
27+
2028
## How it works
2129

2230
The trick to get updating charts to work is to leverage [anywidget](https://anywidget.dev/). These widgets have a loop that is independant of the marimo cells which means that you can update a chart even if the cell hasn't completed running. The goal of this library is to make it easy to use this pattern by giving you a few utilities.

demo.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import marimo
1515

16-
__generated_with = "0.13.6"
16+
__generated_with = "0.17.0"
1717
app = marimo.App(width="medium")
1818

1919

@@ -112,7 +112,7 @@ def _(mo):
112112
113113
This library can also deal with altair charts. This works by turning the chart into an SVG. This is a static representation that does not require any javascript to run, which means that we can apply a similar pattern as before!
114114
115-
> Due to a required dependency to convert the altair chart to SVG we cannot run the altair demo in WASM. This code will run just fine locally on your machine but currently breaks on the Github pages deployment.
115+
> Due to a required dependency to convert the altair chart to SVG we cannot run the altair demo in WASM. This code will run just fine locally on your machine but currently breaks on the Github pages deployment.
116116
"""
117117
)
118118
return
@@ -195,6 +195,37 @@ def _(html_widget, time):
195195
return
196196

197197

198+
@app.cell
199+
def _(mo):
200+
mo.md(r"""## Progress bars""")
201+
return
202+
203+
204+
@app.cell
205+
def _():
206+
from mofresh import ProgressBar
207+
208+
progress = ProgressBar(value=0, max_value=100)
209+
progress
210+
return (progress,)
211+
212+
213+
@app.cell
214+
def _(progress, random, time):
215+
from threading import Lock
216+
from collections import deque
217+
218+
def slow_task():
219+
"""Simulated task that takes time"""
220+
time.sleep(random.random())
221+
222+
progress.value = 0
223+
for _ in range(100):
224+
slow_task()
225+
progress.value += 1
226+
return
227+
228+
198229
@app.cell(hide_code=True)
199230
def _(mo):
200231
mo.md(r"""Enjoy!""")

mofresh/__init__.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,106 @@ def wrapper(*args, **kwargs):
8282
altair_chart = func(*args, **kwargs)
8383
return altair2svg(altair_chart)
8484
return wrapper
85+
86+
87+
class ProgressBar(anywidget.AnyWidget):
88+
_esm = """
89+
function render({ model, el }) {
90+
let getValue = () => model.get("value");
91+
let getMaxValue = () => model.get("max_value");
92+
93+
// Check for dark mode via marimo's body class or system preference
94+
const checkDarkMode = () => {
95+
return document.body.classList.contains('dark') ||
96+
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
97+
};
98+
99+
const container = document.createElement('div');
100+
container.style.width = '100%';
101+
container.style.marginBottom = '10px';
102+
container.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif';
103+
104+
// Label
105+
const label = document.createElement('div');
106+
label.style.marginBottom = '8px';
107+
label.style.fontSize = '13px';
108+
label.style.fontWeight = '500';
109+
110+
// Progress bar container
111+
const barContainer = document.createElement('div');
112+
barContainer.style.width = '100%';
113+
barContainer.style.height = '24px';
114+
barContainer.style.borderRadius = '12px';
115+
barContainer.style.overflow = 'hidden';
116+
117+
// Progress fill
118+
const fill = document.createElement('div');
119+
fill.style.height = '100%';
120+
fill.style.transition = 'width 0.4s cubic-bezier(0.4, 0, 0.2, 1)';
121+
fill.style.display = 'flex';
122+
fill.style.alignItems = 'center';
123+
fill.style.justifyContent = 'center';
124+
125+
// Percentage text
126+
const text = document.createElement('span');
127+
text.style.fontSize = '11px';
128+
text.style.fontWeight = '600';
129+
text.style.color = 'white';
130+
text.style.textShadow = '0 1px 2px rgba(0,0,0,0.3)';
131+
text.style.letterSpacing = '0.5px';
132+
133+
const applyTheme = () => {
134+
const isDarkMode = checkDarkMode();
135+
label.style.color = isDarkMode ? '#e0e0e0' : '#333';
136+
barContainer.style.backgroundColor = isDarkMode ? '#2a2a2a' : '#f0f0f0';
137+
barContainer.style.border = isDarkMode ? '1px solid #404040' : '1px solid #d0d0d0';
138+
barContainer.style.boxShadow = isDarkMode ? 'inset 0 1px 3px rgba(0,0,0,0.3)' : 'inset 0 1px 3px rgba(0,0,0,0.1)';
139+
fill.style.background = isDarkMode
140+
? 'linear-gradient(90deg, #5cb85c 0%, #4cae4c 100%)'
141+
: 'linear-gradient(90deg, #66d966 0%, #4caf50 100%)';
142+
fill.style.boxShadow = isDarkMode
143+
? '0 0 10px rgba(76, 174, 76, 0.3)'
144+
: '0 0 10px rgba(76, 175, 80, 0.3)';
145+
};
146+
147+
const updateDisplay = () => {
148+
const value = getValue();
149+
const max = getMaxValue();
150+
const percentage = max > 0 ? (value / max) * 100 : 0;
151+
152+
label.textContent = `Progress: ${value} / ${max}`;
153+
fill.style.width = percentage + '%';
154+
text.textContent = Math.round(percentage) + '%';
155+
156+
// Update text visibility based on bar width
157+
text.style.opacity = percentage > 10 ? '1' : '0';
158+
};
159+
160+
fill.appendChild(text);
161+
barContainer.appendChild(fill);
162+
container.appendChild(label);
163+
container.appendChild(barContainer);
164+
165+
applyTheme();
166+
updateDisplay();
167+
168+
model.on('change:value', updateDisplay);
169+
model.on('change:max_value', updateDisplay);
170+
171+
// Listen for dark mode changes (both system and marimo)
172+
if (window.matchMedia) {
173+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', applyTheme);
174+
}
175+
176+
// Watch for marimo dark mode class changes
177+
const observer = new MutationObserver(applyTheme);
178+
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
179+
180+
el.appendChild(container);
181+
}
182+
183+
export default { render };
184+
"""
185+
186+
value = traitlets.Int(0).tag(sync=True)
187+
max_value = traitlets.Int(100).tag(sync=True)

pyproject.toml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@
22
requires = ["hatchling"]
33
build-backend = "hatchling.build"
44

5+
[tool.hatch.build.targets.sdist]
6+
include = [
7+
"mofresh/",
8+
"README.md",
9+
"LICENSE",
10+
]
11+
12+
[tool.hatch.build.targets.wheel]
13+
packages = ["mofresh"]
14+
515
[project]
616
name = "mofresh"
7-
version = "0.2.2"
17+
version = "0.2.4"
818
authors = [
919
{ name = "Vincent Warmerdam" },
1020
]

0 commit comments

Comments
 (0)