Skip to content

Commit 1a490f7

Browse files
add a html slot on the right side of a stat plugin (#7384)
## 📝 Summary <!-- Provide a concise summary of what this pull request is addressing. If this PR fixes any issues, list them here by number (e.g., Fixes #123). --> New API: ```python mo.stat( "$20", label="Marketing spend", slot=mo.md("Hello!"), ), ``` <img width="745" height="377" alt="image" src="https://github.com/user-attachments/assets/9266a5df-bc9c-478d-b664-a7fcdc48bfbb" /> ## 🔍 Description of Changes <!-- Detail the specific changes made in this pull request. Explain the problem addressed and how it was resolved. If applicable, provide before and after comparisons, screenshots, or any relevant details to help reviewers understand the changes easily. --> ## 📋 Checklist - [x] I have read the [contributor guidelines](https://github.com/marimo-team/marimo/blob/main/CONTRIBUTING.md). - [ ] For large changes, or changes that affect the public API: this change was discussed or approved through an issue, on [Discord](https://marimo.io/discord?ref=pr), or the community [discussions](https://github.com/marimo-team/marimo/discussions) (Please provide a link if applicable). - [x] I have added tests for the changes made. - [x] I have run the code and verified that it works as expected. --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 8a2dcff commit 1a490f7

File tree

6 files changed

+258
-38
lines changed

6 files changed

+258
-38
lines changed

docs/api/layouts/stat.md

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,69 @@
44

55
```python
66
@app.cell
7-
def __():
7+
def _():
88
active_users = mo.stat(
9-
value="1.2M",
10-
label="Active Users",
11-
caption="12k from last month",
9+
value="1.2M",
10+
label="Active Users",
11+
caption="12k from last month",
1212
direction="increase"
1313
)
1414

1515
revenue = mo.stat(
16-
value="$4.5M",
17-
label="Revenue",
18-
caption="8k from last quarter",
16+
value="$4.5M",
17+
label="Revenue",
18+
caption="8k from last quarter",
1919
direction="increase"
2020
)
21-
21+
2222
conversion = mo.stat(
23-
value="3.8",
24-
label="Conversion Rate",
25-
caption="0.5 from last week",
23+
value="3.8",
24+
label="Conversion Rate",
25+
caption="0.5 from last week",
2626
direction="decrease",
2727
)
28-
28+
2929
mo.hstack([active_users, revenue, conversion], justify="center", gap="2rem")
3030
return
3131
```
3232

33+
```python
34+
def _():
35+
import altair as alt
36+
import polars as pl
37+
38+
alt.renderers.set_embed_options(actions=False)
39+
40+
df = pl.DataFrame(
41+
{
42+
"revenue": [30, 20, 70, 45],
43+
"dates": ["01/01/2024", "01/03/2024", "01/06/2024", "01/09/2024"],
44+
}
45+
)
46+
47+
chart = (
48+
alt.Chart(df)
49+
.mark_line(interpolate="monotone")
50+
.encode(
51+
x=alt.X("dates", axis=None),
52+
y=alt.Y("revenue", axis=None),
53+
tooltip=["dates", "revenue"],
54+
)
55+
.properties(height=40, width=60, background="transparent")
56+
.configure_view(strokeWidth=0)
57+
)
58+
59+
mo.stat(
60+
value=df["revenue"][-1],
61+
label="Revenue",
62+
caption="QoQ Growth",
63+
direction="increase",
64+
bordered=True,
65+
slot=chart,
66+
)
67+
return
68+
```
69+
3370
///
3471

3572
::: marimo.stat

frontend/src/plugins/impl/vega/vega.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
@media (min-width: 500px) {
99
min-width: 300px;
1010
}
11+
12+
/* For vega embeds in slots, reset the styles to let the user set the width */
13+
@container style(--slot: true) {
14+
min-width: unset;
15+
}
1116
}
1217

1318
.vega-embed > .chart-wrapper {

frontend/src/plugins/layout/StatPlugin.tsx

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ import { TriangleIcon } from "lucide-react";
44
import type { JSX } from "react";
55
import { useLocale } from "react-aria";
66
import { z } from "zod";
7+
import { getMimeValues } from "@/components/data-table/mime-cell";
78
import { cn } from "@/utils/cn";
9+
import { Logger } from "@/utils/Logger";
810
import { prettyNumber } from "@/utils/numbers";
11+
import { renderHTML } from "../core/RenderHTML";
912
import type {
1013
IStatelessPlugin,
1114
IStatelessPluginProps,
@@ -18,6 +21,7 @@ interface Data {
1821
bordered?: boolean;
1922
direction?: "increase" | "decrease";
2023
target_direction?: "increase" | "decrease";
24+
slot?: object;
2125
}
2226

2327
export class StatPlugin implements IStatelessPlugin<Data> {
@@ -30,6 +34,7 @@ export class StatPlugin implements IStatelessPlugin<Data> {
3034
bordered: z.boolean().default(false),
3135
direction: z.enum(["increase", "decrease"]).optional(),
3236
target_direction: z.enum(["increase", "decrease"]).default("increase"),
37+
slot: z.any().optional(),
3338
});
3439

3540
render({ data }: IStatelessPluginProps<Data>): JSX.Element {
@@ -44,6 +49,7 @@ export const StatComponent: React.FC<Data> = ({
4449
bordered,
4550
direction,
4651
target_direction,
52+
slot,
4753
}) => {
4854
const { locale } = useLocale();
4955

@@ -71,39 +77,53 @@ export const StatComponent: React.FC<Data> = ({
7177
const fillColor = onTarget ? "var(--grass-8)" : "var(--red-8)";
7278
const strokeColor = onTarget ? "var(--grass-9)" : "var(--red-9)";
7379

80+
const renderSlot = () => {
81+
const mimeValues = getMimeValues(slot);
82+
if (mimeValues?.[0]) {
83+
const { mimetype, data } = mimeValues[0];
84+
if (mimetype !== "text/html") {
85+
Logger.warn(`Expected text/html, got ${mimetype}`);
86+
}
87+
return renderHTML({ html: data, alwaysSanitizeHtml: true });
88+
}
89+
};
90+
7491
return (
7592
<div
7693
className={cn(
77-
"text-card-foreground",
94+
"text-card-foreground p-6",
7895
bordered && "rounded-xl border shadow bg-card",
7996
)}
8097
>
8198
{label && (
82-
<div className="p-6 flex flex-row items-center justify-between space-y-0 pb-2">
99+
<div className="flex flex-row items-center justify-between space-y-0 pb-2">
83100
<h3 className="tracking-tight text-sm font-medium">{label}</h3>
84101
</div>
85102
)}
86-
<div className="p-6 pt-0">
87-
<div className="text-2xl font-bold">{renderPrettyValue()}</div>
88-
{caption && (
89-
<p className="pt-1 text-xs text-muted-foreground flex align-center">
90-
{direction === "increase" && (
91-
<TriangleIcon
92-
className="w-4 h-4 mr-1 p-0.5"
93-
fill={fillColor}
94-
stroke={strokeColor}
95-
/>
96-
)}
97-
{direction === "decrease" && (
98-
<TriangleIcon
99-
className="w-4 h-4 mr-1 p-0.5 transform rotate-180"
100-
fill={fillColor}
101-
stroke={strokeColor}
102-
/>
103-
)}
104-
{caption}
105-
</p>
106-
)}
103+
<div className="pt-0 flex flex-row gap-3.5">
104+
<div>
105+
<div className="text-2xl font-bold">{renderPrettyValue()}</div>
106+
{caption && (
107+
<p className="pt-1 text-xs text-muted-foreground flex align-center whitespace-nowrap">
108+
{direction === "increase" && (
109+
<TriangleIcon
110+
className="w-4 h-4 mr-1 p-0.5"
111+
fill={fillColor}
112+
stroke={strokeColor}
113+
/>
114+
)}
115+
{direction === "decrease" && (
116+
<TriangleIcon
117+
className="w-4 h-4 mr-1 p-0.5 transform rotate-180"
118+
fill={fillColor}
119+
stroke={strokeColor}
120+
/>
121+
)}
122+
{caption}
123+
</p>
124+
)}
125+
</div>
126+
{slot && <div className="[--slot:true]">{renderSlot()}</div>}
107127
</div>
108128
</div>
109129
);

marimo/_plugins/stateless/stat.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
11
# Copyright 2024 Marimo. All rights reserved.
22
from __future__ import annotations
33

4-
from typing import Literal, Optional, Union
4+
from typing import Any, Literal, Optional, Union
55

6+
from marimo._loggers import marimo_logger
7+
from marimo._output.formatting import as_html
68
from marimo._output.hypertext import Html
79
from marimo._output.rich_help import mddoc
810
from marimo._plugins.core.web_component import build_stateless_plugin
911
from marimo._plugins.utils import remove_none_values
1012

13+
Logger = marimo_logger()
14+
1115

1216
@mddoc
1317
def stat(
@@ -17,6 +21,7 @@ def stat(
1721
direction: Optional[Literal["increase", "decrease"]] = None,
1822
bordered: bool = False,
1923
target_direction: Optional[Literal["increase", "decrease"]] = "increase",
24+
slot: Optional[Html] = None,
2025
) -> Html:
2126
"""Display a statistic.
2227
@@ -34,6 +39,7 @@ def stat(
3439
`increase` when higher values are better, or `decrease`
3540
when lower values are better. By default the target
3641
direction is `increase`.
42+
slot: an optional Html object to place beside the widget
3743
3844
3945
Returns:
@@ -50,7 +56,21 @@ def stat(
5056
"direction": direction,
5157
"bordered": bordered,
5258
"target_direction": target_direction,
59+
"slot": try_convert_to_html(slot),
5360
}
5461
),
5562
)
5663
)
64+
65+
66+
def try_convert_to_html(slot: Any) -> Optional[Html]:
67+
if slot is None:
68+
return None
69+
70+
try:
71+
return as_html(slot)
72+
except Exception as e:
73+
Logger.error(
74+
f"Error converting slot to Html: {e}. Please ensure it is a valid Html object."
75+
)
76+
return None

marimo/_smoke_tests/stats.py

Lines changed: 100 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
import marimo
44

5-
__generated_with = "0.15.5"
5+
__generated_with = "0.18.2"
66
app = marimo.App()
77

88

99
@app.cell
1010
def _():
1111
import marimo as mo
12-
return (mo,)
12+
import polars as pl
13+
import altair as alt
14+
return alt, mo, pl
1315

1416

1517
@app.cell
@@ -74,5 +76,101 @@ def _(mo):
7476
return
7577

7678

79+
@app.cell(hide_code=True)
80+
def _(alt, mo, pl):
81+
findata = pl.DataFrame(
82+
{
83+
"revenue": [30, 20, 70, 45, 68, 34, 87, 100],
84+
"dates": [
85+
"01/01/2024",
86+
"01/03/2024",
87+
"01/06/2024",
88+
"01/09/2024",
89+
"01/12/2024",
90+
"01/15/2024",
91+
"01/18/2024",
92+
"01/21/2024",
93+
],
94+
}
95+
)
96+
97+
alt.renderers.set_embed_options(actions=False)
98+
99+
100+
def create_chart(mark: str) -> alt.Chart:
101+
chart = alt.Chart(findata)
102+
if mark == "line":
103+
chart = chart.mark_line(interpolate="monotone")
104+
else:
105+
chart = chart.mark_bar()
106+
chart = (
107+
chart.encode(
108+
x=alt.X("dates", axis=None),
109+
y=alt.Y("revenue", axis=None),
110+
tooltip=["dates", "revenue"],
111+
)
112+
.properties(height=40, width=60, background="transparent")
113+
.configure_view(strokeWidth=0)
114+
)
115+
return chart
116+
117+
118+
hello_world = mo.Html("<h2><i>Hello, World</i></h2>")
119+
120+
_stats = [
121+
mo.stat(
122+
"$100",
123+
label="Revenue",
124+
caption="+ 10%",
125+
direction="increase",
126+
bordered=True,
127+
slot=create_chart("line"),
128+
),
129+
mo.stat(
130+
"$20",
131+
label="Marketing spend",
132+
caption="+ 10%",
133+
direction="increase",
134+
target_direction="decrease",
135+
slot=create_chart("bar"),
136+
),
137+
mo.stat(
138+
"$80",
139+
label="Profit",
140+
caption="- 10%",
141+
direction="decrease",
142+
bordered=True,
143+
slot="🚀🧑‍🚀💰",
144+
),
145+
]
146+
147+
_rich = [
148+
mo.stat(
149+
"$20",
150+
label="Marketing spend",
151+
caption="+ 10%",
152+
direction="increase",
153+
target_direction="decrease",
154+
slot=hello_world,
155+
),
156+
mo.stat(
157+
"2%",
158+
label="Churn",
159+
caption="- 2%",
160+
direction="decrease",
161+
bordered=True,
162+
target_direction="decrease",
163+
slot=_stats[0],
164+
),
165+
]
166+
167+
168+
_first = mo.hstack(_stats, widths="equal", gap=1)
169+
_second = mo.hstack(_rich, widths="equal", gap=1)
170+
171+
mo.vstack([_first, _second])
172+
return
173+
174+
77175
if __name__ == "__main__":
78176
app.run()

0 commit comments

Comments
 (0)