Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
43234aa
add reference dashboard
MarcSkovMadsen Apr 11, 2025
882e924
ref dashboard
MarcSkovMadsen Apr 11, 2025
6025be1
indicators charts
MarcSkovMadsen Apr 11, 2025
f0a645f
table
MarcSkovMadsen Apr 11, 2025
6bd518c
refactor
MarcSkovMadsen Apr 12, 2025
adc6f0a
wip dashboard
MarcSkovMadsen Apr 13, 2025
9e15d7a
start timeline
MarcSkovMadsen Apr 14, 2025
0f1621c
Merge branch 'main' of https://github.com/panel-extensions/panel-mate…
MarcSkovMadsen Apr 20, 2025
8573adc
page update
MarcSkovMadsen Apr 20, 2025
90565b6
timeline
MarcSkovMadsen Apr 20, 2025
767f125
apply timeline
MarcSkovMadsen Apr 20, 2025
304027c
Timeline docs
MarcSkovMadsen Apr 21, 2025
230230f
fab trigger
MarcSkovMadsen Apr 21, 2025
8fd3cc5
add Icon and ChangeIndicator
MarcSkovMadsen Apr 21, 2025
d820ec3
add Drawer
MarcSkovMadsen Apr 21, 2025
a4bb0c1
use Drawer
MarcSkovMadsen Apr 21, 2025
c9613fa
use Drawer
MarcSkovMadsen Apr 21, 2025
912569e
merge with main
MarcSkovMadsen Apr 21, 2025
1d61de0
replace custom menu with List
MarcSkovMadsen Apr 21, 2025
bf5ebae
Fix and use use Fab
MarcSkovMadsen Apr 21, 2025
b78f8a9
notebook documentation
MarcSkovMadsen Apr 21, 2025
3d07877
clean up
MarcSkovMadsen Apr 21, 2025
4f13ca3
add notifications page
MarcSkovMadsen Apr 21, 2025
1c6aa3b
pre-commit
MarcSkovMadsen Apr 21, 2025
de43e1e
Minor cleanup
philippjfr Apr 22, 2025
28e2f81
merge origin main
MarcSkovMadsen Apr 23, 2025
5b2ba4a
panel version
MarcSkovMadsen Apr 23, 2025
4743bbf
merge main
MarcSkovMadsen Apr 26, 2025
4254cff
convert Timeline
MarcSkovMadsen Apr 26, 2025
efc43af
merge main
MarcSkovMadsen Apr 26, 2025
5130c49
ToggleTheme Switch
MarcSkovMadsen Apr 26, 2025
eadce47
more theme
MarcSkovMadsen Apr 26, 2025
db3fe25
Merge origin/main
philippjfr Apr 29, 2025
f5b4878
refactor
MarcSkovMadsen Apr 29, 2025
2077189
merge with main
MarcSkovMadsen Apr 29, 2025
762a691
merge into main
MarcSkovMadsen Apr 29, 2025
c6e2973
Merge origin/main
philippjfr May 5, 2025
bae5069
merge main
MarcSkovMadsen May 17, 2025
b79922a
color buttons
MarcSkovMadsen May 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 223 additions & 0 deletions examples/apps/dashboard/dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
# Inspiration: https://demos.creative-tim.com/material-dashboard/pages/dashboard
# docs: https://holoviz-dev.github.io/panel-material-ui/
# panel serve examples/apps/dashboard/*.py --dev
import panel_material_ui as pmu
import panel as pn
import param
import pandas as pd

from panel_material_ui import ChangeIndicator
from panel_material_ui import Icon

from shared.data import get_project_data
from shared.plots import (
get_website_views_config,
get_daily_sales_config,
get_completed_tasks_config,
)
from shared.components import create_menu, Timeline
from shared.page import create_page

my_theme = {
"palette": {
"primary": {
"main": "#FF5733",
# light, dark, and contrastText can be automatically computed
},
"secondary": {
"main": "#E0C2FF",
"light": "#F5EBFF", # optional
"dark": "#BA99D5", # optional
"contrastText": "#47008F", # optional
}
}
}


pn.extension("echarts")

page_title = f"""\
## Dashboard

Check the sales, value and bounce rate by country.\
"""

indicators = pn.Row(
ChangeIndicator(
title="Today's Money", icon="weekend", value="$53,000", change_percent=55, since="since yesterday", sizing_mode="stretch_width"
),
ChangeIndicator(title="Today's Users", icon="person", value="2300", change_percent=3, since="than last month", sizing_mode="stretch_width"),
ChangeIndicator(title="Ads Views", icon="leaderboard", value="3,462", change_percent=-2, since="since yesterday", sizing_mode="stretch_width"),
ChangeIndicator(title="Sales", icon="weekend", value="$103,430", change_percent=5, since="since yesterday", sizing_mode="stretch_width"),
)


def last_update(message):
schedule = Icon(value="schedule", font_size="small", sizing_mode="fixed", width=20, height=25, margin=(10,5, 10, 5))
return pn.Row(schedule, pn.pane.HTML(message, sizing_mode="fixed", margin=(11,0)), sizing_mode="fixed")

def generate_company_html(row, image_width='40px'):
company_name=row["Company"]
company_image_url=row["CompanyImage"]
html = f'''
<div style="display: flex; align-items: center; gap: 10px;">
<img src="{company_image_url}" alt="{company_name}" style="width: {image_width}; height: auto;">
<h4>{company_name}</h4>
</div>
'''
return html

def generate_progress_bar(row):
completion_percent = int(round(float(row["Completion"]),0))
color = 'green' if completion_percent >=99.999 else 'blue'
html = f'''
<div style=\"width: 100%;\">
<div style=\"margin-bottom: 4px; font-size: 12px; text-align: left;\">{completion_percent}%</div>
<div style=\"width: 100%; background-color: #e0e0e0; border-radius: 4px; overflow: hidden; height: 5px;\">
<div style=\"width: {completion_percent}%; background-color: {color}; height: 100%;\"></div>
</div>
</div>
'''
return html

def generate_member_images(row, image_size='30px', overlap_offset='-10px'):
members = row["Members"]
images_html = ''
for idx, member in enumerate(members):
images_html += f'''
<img src=\"{member['Image']}\" title=\"{member['Name']}\"
style=\"
width: {image_size};
height: {image_size};
border-radius: 50%;
border: 2px solid white;
position: relative;
left: {idx * int(overlap_offset.replace('px', ''))}px;
z-index: {len(members)-idx};
transition: transform 0.2s;
\"
onmouseover=\"this.style.transform='scale(1.2)';this.style.zIndex='{len(members)+1}'\"
onmouseout=\"this.style.transform='scale(1)';this.style.zIndex='{len(members)-idx}'\"
>
'''
wrapper_html = f'''
<div style=\"display: flex; align-items: center;\">
{images_html}
</div>
'''
return wrapper_html

def to_styled_projects_table(data: pd.DataFrame):
data["Company"] = data.apply(generate_company_html, axis=1)
data["Completion"] = data.apply(generate_progress_bar, axis=1)
data["Members"] = data.apply(generate_member_images, axis=1)
data = data.drop(columns=["CompanyImage"])
styled_df = data.style
styled_df = (
styled_df.hide(axis="index")
.set_table_styles(
[
{
"selector": "th",
"props": [
("background-color", "white"),
("font-weight", "bold"),
("border-bottom", "1px solid black"),
("text-align", "left"),
],
},
{
"selector": "td",
"props": [
("padding", "10px"),
("background", "white"),
("border-top", "1px solid black"),
],
},
]
)
.set_properties(
**{"text-align": "left"},
)
)

return styled_df


with pn.config.set(sizing_mode="stretch_width"):
web_site_views = pmu.Paper(
"#### Website Views\n\nLast Campaign Performance",
pn.pane.ECharts(
get_website_views_config(),
height=250,
margin=(0, 5),
),
pn.Spacer(sizing_mode="stretch_height"),
pmu.Divider(margin=(10, 10, -5, 10)),
last_update("campaign sent 2 days ago"),
height=400, margin=10
)

daily_sales = pmu.Paper(
"#### Daily Sales\n\n**+15%** increase in todays sales",
pn.pane.ECharts(
get_daily_sales_config(),
height=250,
margin=(0, 5),
),
pn.Spacer(sizing_mode="stretch_height"),
pmu.Divider(margin=(10,10,-5,10)),
last_update("updated 4 min ago"),
margin=10,
height=400,
)
completed_tasks = pmu.Paper(
"#### Completed Tasks\n\nLast Campaign Performance",
pn.pane.ECharts(
get_completed_tasks_config(),
height=250,
margin=(0, 5),
),
pn.Spacer(sizing_mode="stretch_height"),
pmu.Divider(margin=(10,10,-5,10)),
last_update("just updated"),
margin=10,
height=400,
)

plots = pn.Row(web_site_views, daily_sales, completed_tasks, sizing_mode="stretch_both")
project_table = pmu.Paper(
"#### Projects\n\n**30 done** this month",
pn.pane.DataFrame(to_styled_projects_table(get_project_data()), sizing_mode="stretch_width"),
margin=10,
height=550,
)
timeline_config = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually thinking about it, I would prefer not to ship Timeline for the time being. It's still a mui/lab component which means it's not stable and it's also not a must have imo.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The challenge I have is that I don't know how to get Icons working if its not a MaterialComponent.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be OK to introduce a .lab module with the same purpose as the Material UI lab concept? And then put the Timeline there?

Its a common thing. For example Streamlit has streamlit.experimental module.

Copy link
Collaborator Author

@MarcSkovMadsen MarcSkovMadsen Apr 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've moved the Timeline out of panel_material_ui to seperate example MaterialUIComponent. But now I have issues described in #190.

{"content_title": "$2400, Design changes", "content": "22 DEC 7:20 PM", "color": "success", "icon": "notifications", "disable_dot": True},
{"content_title": "New order #1832412", "content": "21 DEC 11 PM", "color": "error", "icon": "code", "disable_dot": True},
{"content_title": "Server payments for April", "content": "21 DEC 9:34 PM", "color": "primary", "icon": "shopping_cart", "disable_dot": True},
{"content_title": "New card added for order #4395133", "content": "20 DEC 2:20 AM", "color": "warning", "icon": "credit_card", "disable_dot": True},
{"content_title": "Unlock packages for development", "content": "18 DEC 4:54 AM", "color": "error", "icon": "key", "disable_dot": True},
{"content_title": "New order #9583120", "content": "17 DEC", "color": "dark", "icon": "payments", "disable_dot": True},
]
sx = {
"& .MuiTimelineItem-root:before": {
"flex": 0,
"padding-left": 10,
},
}
timeline_component = Timeline(object=timeline_config, sizing_mode="stretch_width", sx=sx)
timeline_component = pmu.Alert("The `Timeline` component does not work yet due to [#190](https://github.com/panel-extensions/panel-material-ui/issues/190).", alert_type="error", margin=10)
timeline = pmu.Paper(
"#### Orders overview\n\n**24%** this month",
timeline_component,
margin=10,
height=550,
)
table_timeline_row = pn.Row(project_table, timeline)

create_page(
name="Dashboard",
main=[page_title, indicators, plots, table_timeline_row]).servable(
title="Dashboard"
)
29 changes: 29 additions & 0 deletions examples/apps/dashboard/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import panel as pn
from shared.page import create_page
from panel_material_ui import Button, Alert
pn.extension(notifications=True)

with pn.config.set(sizing_mode="stretch_width"):
alert_card = pn.Column(
"""## Alerts

Notifications on this page use the `Alert` from Material UI. Read more details [here](https://mui.com/material-ui/react-alert/).
""",
*[Alert(title=f"A simple {severity} alert with an example link. Give it a click if you like.", severity=severity, variant="filled", closeable=True, margin=10) for severity in Alert.param.severity.objects],
max_width=1200,
)

success_notification = pn.widgets.Button(name="Success")
notifications_row = pn.Column(
"""## Notifications

Notifications on this page use the `SnackbarProvider` from notistack. Read more details [here](https://notistack.com/getting-started).""",
pn.Row(
Button(name="Error", color="error", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.error('This is an error notification.', duration=1000)),
Button(name="Info", color="info", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.info('This is an info notification.', duration=1000)),
Button(name="Success", color="success", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.success('This is a success notification.', duration=1000)),
Button(name="Warning", color="warning", sizing_mode="fixed", width=100, on_click=lambda e: pn.state.notifications.warning('This is a warning notification.', duration=1000)),
)
)

create_page("Notifications", main=[alert_card, notifications_row]).servable(title="Notifications")
59 changes: 59 additions & 0 deletions examples/apps/dashboard/shared/Timeline.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
Timeline as MUITimeline,
TimelineItem,
TimelineSeparator,
TimelineConnector,
TimelineContent,
TimelineOppositeContent,
TimelineDot,
} from "@mui/lab";
import {Icon} from "@mui/material"
import Typography from "@mui/material/Typography";

export function render({model}) {
const [items] = model.useState("object")
const [position] = model.useState("position")
const [sx] = model.useState("sx")

return (
<MUITimeline position={position} sx={sx}>
{items.map((item, idx) => (
<TimelineItem key={idx}>
{(item.opposite !== undefined || item.opposite_title !== undefined) && (
<TimelineOppositeContent sx={item.icon ? {m: "auto 0"} : { }} align="right" variant="body2" color="text.secondary">
<Typography variant="h6" component="span">
{item.opposite_title}
</Typography>
<Typography>
{item.opposite}
</Typography>
</TimelineOppositeContent>
)}
<TimelineSeparator>
{(item.disable_dot && item.icon) ? (
<Icon sx={{margin: 1}} color={item.color || "grey"}>{item.icon}</Icon>
) : (
<TimelineDot
color={item.color || "grey"}
variant={item.variant || "filled"}
>
{item.icon !== undefined && (
<Icon sx={{margin: 1}}>{item.icon}</Icon>
)}
</TimelineDot>
)}
{idx < items.length - 1 && <TimelineConnector />}
</TimelineSeparator>
{(item.content !== undefined || item.content_title !== undefined) && (
<TimelineContent sx={item.icon ? {m: "auto 0"} : { }}>
<Typography variant="h6" component="span">
{item.content_title}
</Typography>
<Typography>{item.content}</Typography>
</TimelineContent>
)}
</TimelineItem>
))}
</MUITimeline>
)
}
Empty file.
78 changes: 78 additions & 0 deletions examples/apps/dashboard/shared/components.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import panel as pn
import panel_material_ui as pmu
from .config import PAGES
import param
import copy

def create_menu(selected: str, pages: list[tuple] = PAGES, button_color="primary"):
active = -1
for index, page in enumerate(pages):
if selected==page["label"]:
active=index

def get_pages(button_color):
pages2 = copy.deepcopy(pages)
for item in pages2:
item["color"]=button_color
return pages2
return pmu.List(items=pn.bind(get_pages, button_color), active=active, margin=(0,10), sizing_mode="stretch_width")

def create_card_with_jumbo_header(title: str, content: str):
return pmu.Paper(
pn.pane.Markdown(
title,
sizing_mode="stretch_width",
styles={
"background": "black",
"color": "white",
"border-radius": "5px",
"position": "relative",
"padding-left": "10px",
},
margin=10,
),
content,
margin=10,
)

class Timeline(pmu.MaterialUIComponent):
"""Material‑UI **Timeline** component for Panel.

References:

- https://mui.com/material-ui/react-timeline/

Example:

>>> config = [
... {"content_title": "Eat", "content": "Because you need strength", "opposite": "08:30", "color": "grey", "variant": "filled", "icon": "fastfood"},
... {"content_title": "Code", "content": "Because it's awesome!", "opposite": "09:00", "color": "primary", "variant": "filled", "icon": "laptop_mac"},
... {"content_title": "Sleep", "content": "Because you need rest", "opposite": "09:30", "color": "secondary", "variant": "outlined", "icon": "hotel"},
... {"content_title": "Repeat", "content": "Because this is the life you love!", "opposite": "11:00", "color": "success", "variant": "filled", "icon": "repeat"},
... ]
>>> Timeline(object=config, width=600)
""" # noqa: E501
object = param.List(default=[], doc="""
A list of dictionaries, each mapping directly onto a `TimelineItem` row.
Supported keys:

| key | type | default | description |
|-------------------|------|---------|-------------------------------------------------------------------------------------------------------------------|
| **content_title** | str | *None* | Header of `TimelineContent`. |
| **content** | str | *None* | Body of `TimelineContent`. |
| **opposite_title**| str | *None* | Header of `TimelineOppositeContent`. |
| **opposite** | str | *None* | Body of `TimelineOppositeContent`. |
| **color** | str | primary | Color prop of `TimelineDot`. |
| **variant** | str | filled | Variant prop of `TimelineDot` (filled/ outlined). |
| **icon** | str | *None* | Lowercase name of `Icon`. |
| **disable_dot** | bool | *None* | If `True`, the `Icon` is shown standalone and not inside the `TimelineDot`. |
""")

position = param.Selector(default="right", objects=[
'alternate-reverse',
'alternate',
'left',
'right',
], doc="""The position of the content/ opposite content.""")

_esm_base = "Timeline.jsx"
Loading
Loading