-
Couldn't load subscription status.
- Fork 13
Add reference dashboard #143
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
MarcSkovMadsen
wants to merge
39
commits into
main
Choose a base branch
from
enhancement/reference-example
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
43234aa
add reference dashboard
MarcSkovMadsen 882e924
ref dashboard
MarcSkovMadsen 6025be1
indicators charts
MarcSkovMadsen f0a645f
table
MarcSkovMadsen 6bd518c
refactor
MarcSkovMadsen adc6f0a
wip dashboard
MarcSkovMadsen 9e15d7a
start timeline
MarcSkovMadsen 0f1621c
Merge branch 'main' of https://github.com/panel-extensions/panel-mate…
MarcSkovMadsen 8573adc
page update
MarcSkovMadsen 90565b6
timeline
MarcSkovMadsen 767f125
apply timeline
MarcSkovMadsen 304027c
Timeline docs
MarcSkovMadsen 230230f
fab trigger
MarcSkovMadsen 8fd3cc5
add Icon and ChangeIndicator
MarcSkovMadsen d820ec3
add Drawer
MarcSkovMadsen a4bb0c1
use Drawer
MarcSkovMadsen c9613fa
use Drawer
MarcSkovMadsen 912569e
merge with main
MarcSkovMadsen 1d61de0
replace custom menu with List
MarcSkovMadsen bf5ebae
Fix and use use Fab
MarcSkovMadsen b78f8a9
notebook documentation
MarcSkovMadsen 3d07877
clean up
MarcSkovMadsen 4f13ca3
add notifications page
MarcSkovMadsen 1c6aa3b
pre-commit
MarcSkovMadsen de43e1e
Minor cleanup
philippjfr 28e2f81
merge origin main
MarcSkovMadsen 5b2ba4a
panel version
MarcSkovMadsen 4743bbf
merge main
MarcSkovMadsen 4254cff
convert Timeline
MarcSkovMadsen efc43af
merge main
MarcSkovMadsen 5130c49
ToggleTheme Switch
MarcSkovMadsen eadce47
more theme
MarcSkovMadsen db3fe25
Merge origin/main
philippjfr f5b4878
refactor
MarcSkovMadsen 2077189
merge with main
MarcSkovMadsen 762a691
merge into main
MarcSkovMadsen c6e2973
Merge origin/main
philippjfr bae5069
merge main
MarcSkovMadsen b79922a
color buttons
MarcSkovMadsen File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 = [ | ||
| {"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" | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
Timelinefor the time being. It's still amui/labcomponent which means it's not stable and it's also not a must have imo.There was a problem hiding this comment.
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.There was a problem hiding this comment.
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
.labmodule with the same purpose as the Material UI lab concept? And then put theTimelinethere?Its a common thing. For example Streamlit has
streamlit.experimentalmodule.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've moved the
Timelineout ofpanel_material_uito seperate exampleMaterialUIComponent. But now I have issues described in #190.