From cd7bd4ce77499c7bd2921145d9702be7c7e68b72 Mon Sep 17 00:00:00 2001 From: MarcSkovMadsen Date: Thu, 3 Jul 2025 04:50:01 +0000 Subject: [PATCH] altair vega --- doc/how_to/altair.md | 323 ++++++++++++++++++++ doc/how_to/vega.md | 486 +++++++++++++++++++++++++++++++ examples/apps/styling/altair.py | 163 +++++++++++ examples/apps/styling/altair2.py | 103 +++++++ 4 files changed, 1075 insertions(+) create mode 100644 doc/how_to/altair.md create mode 100644 doc/how_to/vega.md create mode 100644 examples/apps/styling/altair.py create mode 100644 examples/apps/styling/altair2.py diff --git a/doc/how_to/altair.md b/doc/how_to/altair.md new file mode 100644 index 00000000..e1570e5d --- /dev/null +++ b/doc/how_to/altair.md @@ -0,0 +1,323 @@ +# Altair and Vega-Lite + +Panel Material UI has integrated theming support for Altair and Vega-Lite plots. This means that the plots will automatically adapt to the active theme, including respecting the primary color, the font family, and toggling between dark and light mode. + +## Basic Theming + +To enable this behavior, you can either use the `Page` component or include a `ThemeToggle` in your app. Altair plots will automatically respect the current Material-UI theme. + +```{pyodide} +import panel as pn +import panel_material_ui as pmu +import altair as alt +from vega_datasets import data + +pn.extension("vega") + +df = data.cars() +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +chart = alt.Chart(df).mark_circle(size=60).encode( + x='Horsepower:Q', + y='Miles_per_Gallon:Q', + color='Origin:N', + tooltip=['Name', 'Origin', 'Horsepower', 'Miles_per_Gallon'] +).properties( + width=600, + height=400 +).interactive() + +pmu.Container( + toggle, + pn.pane.Vega(chart, sizing_mode="stretch_width"), + width_option="md" +).preview() +``` + +## Palettes & Color Schemes + +When visualizing categorical data, each color should be visibly distinct from all the other colors, not nearby in color space, to make each category separately visible. + +You can find existing Altair color schemes in the [Vega documentation](https://vega.github.io/vega/docs/schemes/) and use them with the `scale()` method. + +### Categorical Color Palettes + +If you want to create categorical color maps aligned with your Material theme, you can use the `pmu.theme.generate_palette` function. + +```{pyodide} +import altair as alt +import panel_material_ui as pmu +import panel as pn +from vega_datasets import data + +pn.extension("vega") + +df = data.iris() + +primary_color = "#0072B2" +colors = pmu.theme.generate_palette(primary_color) +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +chart = alt.Chart(df).mark_circle(size=100).encode( + x='petalWidth:Q', + y='petalLength:Q', + color=alt.Color('species:N').scale(range=colors), + tooltip=['species', 'petalWidth', 'petalLength'] +).properties( + width=600, + height=400 +).interactive() + +pmu.Container( + toggle, + pn.pane.Vega(chart, sizing_mode="stretch_width"), + theme_config={"palette": {"primary": {"main": primary_color}}}, + width_option="md" +).preview() +``` + +### Continuous Color Scales + +Similarly, you can use the `pmu.theme.linear_gradient` function to get a color scale aligned with your Material theme for continuous data. + +```{pyodide} +import altair as alt +import panel_material_ui as pmu +import panel as pn +from vega_datasets import data + +pn.extension("vega") + +df = data.cars() +primary_color = "#0072B2" +cmap = pmu.theme.linear_gradient("#ffffff", primary_color, n=10) +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +chart = alt.Chart(df).mark_circle(size=60).encode( + x='Horsepower:Q', + y='Miles_per_Gallon:Q', + color=alt.Color('Acceleration:Q').scale(range=cmap), + tooltip=['Name', 'Horsepower', 'Miles_per_Gallon', 'Acceleration'] +).properties( + width=600, + height=400 +).interactive() + +pmu.Container( + toggle, + pn.pane.Vega(chart, sizing_mode="stretch_width"), + theme_config={"palette": {"primary": {"main": primary_color}}}, + width_option="md" +).servable() +``` + +## Advanced Theming with Altair Themes + +Altair provides a powerful theming system that can be combined with Panel Material UI themes. You can use `alt.theme.enable()` to set global chart themes, or create custom themes that align with your Material design. + +### Using Built-in Altair Themes + +```{pyodide} +import altair as alt +import panel_material_ui as pmu +import panel as pn +from vega_datasets import data + +df = data.stocks() +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +@pn.depends(toggle.param.dark_theme) +def chart(dark_theme): + if dark_theme: + alt.theme.enable('dark') + else: + alt.theme.enable('powerbi') + + chart = alt.Chart(df).mark_line().encode( + x='date:T', + y='price:Q', + color='symbol:N' + ).properties( + width=600, + height=300 + ).interactive() + return chart + +pmu.Container( + toggle, + pn.pane.Vega(chart, sizing_mode="stretch_width"), + width_option="md" +).servable() +``` + +### Creating Custom Material-Aligned Themes + +You can create custom Altair themes that automatically align with your Material UI theme colors: + +```{pyodide} +import panel as pn +import panel_material_ui as pmu +import altair as alt +from vega_datasets import data +from typing import Optional + +pn.extension("vega") + +df = data.cars() +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +chart = alt.Chart(df).mark_circle(size=60).encode( + x='Horsepower:Q', + y='Miles_per_Gallon:Q', + color='Origin:N', + tooltip=['Name', 'Origin', 'Horsepower', 'Miles_per_Gallon'] +).properties( + width=600, + height=400 +) + +def configure( + chart: alt.Chart, + primary: str = "#1976d2", + primary_dark: str | None = None, + dark_theme: bool = True, +) -> alt.Chart: + """ + Configure Altair chart with Material UI theme colors. + + Parameters + ---------- + chart : alt.Chart + The Altair chart to configure + primary : str, default "#1976d2" + Primary color for chart elements + primary_dark : str, default "#115293" + Darker variant of primary color for accents + dark_theme : bool, default True + Whether to use dark theme styling + + Returns + ------- + alt.Chart + Configured chart with Material UI theming + """ + font = "Roboto, Helvetica, Arial, sans-serif" + font_color = "rgb(255, 255, 255)" if dark_theme else "rgba(0, 0, 0, 0.87)" + primary = primary_dark if (primary_dark and dark_theme) else primary + colors = pmu.theme.generate_palette(primary, n_colors=5) + return chart.configure( + font=font, + background="transparent" + ).configure_view( + stroke="transparent" + ).configure_mark( + color=primary, + filled=True, + tooltip=True, + ).configure_axis( + labelColor=font_color, + titleColor=font_color, + tickColor=font_color, + gridColor=font_color, + gridOpacity=0.33, + domainColor=font_color, + ).configure_legend( + labelColor=font_color, + titleColor=font_color, + ).configure_title( + color=font_color + ).configure_range( + # category=colors # Use generated palette for categorical colors + ).interactive() + +# Define theme colors +# orsted blue +primary_color = "#788A12" +primary_dark_color = "#E38DE6" + +pmu.Container( + toggle, + pn.pane.Vega( + pn.bind( + configure, + chart, + primary=primary_color, + primary_dark=primary_dark_color, + dark_theme=toggle.param.dark_theme, + ), + sizing_mode="stretch_width" + ), + """Lorum ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.""", + theme_config={ + "palette": { + "primary": { + "main": primary_color, + "dark": primary_dark_color + } + } + }, + width_option="md" +).servable() +``` + +## Interactive Charts with Material UI Controls + +You can combine Altair's interactivity with Panel Material UI components for a cohesive user experience: + +```{pyodide} +import altair as alt +import panel as pn +import panel_material_ui as pmui +from vega_datasets import data + +df = data.cars() + +color_picker = pmui.ColorPicker( + name="Primary Color", + value="#d219cb", + sizing_mode="stretch_width", +) + +def create_categorical_chart(color): + return alt.Chart(df).mark_circle(size=80).encode( + x='Horsepower:Q', + y='Miles_per_Gallon:Q', + color=alt.Color('Cylinders:O').scale( + range=pmui.theme.generate_palette(color, n_colors=5) + ), + tooltip=['Name', 'Origin', 'Horsepower', 'Miles_per_Gallon', 'Cylinders'] + ).properties( + width=600, height=400 + ).interactive() + +def create_theme_config(color): + return { + "palette": { + "primary": { + "main": color, + }, + } + } + +chart = pn.bind(create_categorical_chart, color=color_picker) +theme_config = pn.bind(create_theme_config, color=color_picker) + +pmui.Page( + title="Altair with Panel Material UI", + sidebar=[pn.pane.PNG("https://altair-viz.github.io/_static/altair-logo-light.png", align="center"), color_picker], + main=[pmui.Container(pn.pane.Vega(chart, sizing_mode="stretch_width"), width_option="lg", sx={"marginTop": "20px"})], + theme_config=theme_config, +).servable() +``` + +## Best Practices + +1. **Theme Synchronization**: When toggling the theme, your Altair charts will automatically adapt to light/dark mode changes. +2. **Consistent Color Schemes**: Use `pmu.theme.generate_palette()` and `pmu.theme.linear_gradient()` to ensure your Altair charts match your Material UI theme. +3. **Responsive Design**: Set chart properties like `width="container"` and use `sizing_mode="stretch_width"` in the Vega pane for responsive behavior. +4. **Tooltips**: Set tooltips with the `tooltip` encoding. +5. Make the axes interactive with the `.interactive()` method. +4. **Custom Themes**: Create reusable Altair themes that align with your Material Design system using `alt.theme.register()`. + + +Remember that Altair theme changes via `alt.theme.enable()` are global and will affect all subsequent charts unless explicitly changed or overridden. diff --git a/doc/how_to/vega.md b/doc/how_to/vega.md new file mode 100644 index 00000000..6745728c --- /dev/null +++ b/doc/how_to/vega.md @@ -0,0 +1,486 @@ +# Vega and Vega-Lite Specifications + +Panel Material UI has integrated theming support for Vega and Vega-Lite specifications when using the `Vega` pane. This means that your charts will automatically adapt to the active theme, including respecting the primary color, font family, and toggling between dark and light mode. + +## Basic Theming + +To enable this behavior, you can either use the `Page` component or include a `ThemeToggle` in your app. Vega specifications will automatically respect the current Material-UI theme. + +```{pyodide} +import panel as pn +import panel_material_ui as pmu + +pn.extension("vega") + +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +vega_spec = { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": {"url": "https://raw.githubusercontent.com/vega/vega/master/docs/data/cars.json"}, + "mark": {"type": "circle", "size": 60}, + "encoding": { + "x": {"field": "Horsepower", "type": "quantitative"}, + "y": {"field": "Miles_per_Gallon", "type": "quantitative"}, + "color": {"field": "Origin", "type": "nominal"}, + "tooltip": [ + {"field": "Name", "type": "nominal"}, + {"field": "Origin", "type": "nominal"}, + {"field": "Horsepower", "type": "quantitative"}, + {"field": "Miles_per_Gallon", "type": "quantitative"} + ] + }, + "width": 600, + "height": 400 +} + +pmu.Container( + toggle, + pn.pane.Vega(vega_spec, sizing_mode="stretch_width"), + width_option="md" +).preview() +``` + +## Palettes & Color Schemes + +When visualizing categorical data, each color should be visibly distinct from all the other colors, not nearby in color space, to make each category separately visible. + +You can find existing Vega color schemes in the [Vega documentation](https://vega.github.io/vega/docs/schemes/) and use them with the `scale` property. + +### Categorical Color Palettes + +If you want to create categorical color maps aligned with your Material theme, you can use the `pmu.theme.generate_palette` function and apply it directly to your Vega specification. + +```{pyodide} +import panel as pn +import panel_material_ui as pmu + +pn.extension("vega") + +primary_color = "#6200ea" +colors = pmu.theme.generate_palette(primary_color) +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +vega_spec = { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": {"url": "https://raw.githubusercontent.com/vega/vega/master/docs/data/iris.json"}, + "mark": {"type": "circle", "size": 100}, + "encoding": { + "x": {"field": "petalWidth", "type": "quantitative"}, + "y": {"field": "petalLength", "type": "quantitative"}, + "color": { + "field": "species", + "type": "nominal", + "scale": {"range": colors} + }, + "tooltip": [ + {"field": "species", "type": "nominal"}, + {"field": "petalWidth", "type": "quantitative"}, + {"field": "petalLength", "type": "quantitative"} + ] + }, + "width": 600, + "height": 400 +} + +pmu.Container( + toggle, + pn.pane.Vega(vega_spec, sizing_mode="stretch_width"), + theme_config={"palette": {"primary": {"main": primary_color}}}, + width_option="md" +).preview() +``` + +### Continuous Color Scales + +Similarly, you can use the `pmu.theme.linear_gradient` function to get a color scale aligned with your Material theme for continuous data. + +```{pyodide} +import panel as pn +import panel_material_ui as pmu + +pn.extension("vega") + +primary_color = "#6200ea" +cmap = pmu.theme.linear_gradient("#ffffff", primary_color, n=10) +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +vega_spec = { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": {"url": "https://raw.githubusercontent.com/vega/vega/master/docs/data/cars.json"}, + "mark": {"type": "circle", "size": 60}, + "encoding": { + "x": {"field": "Horsepower", "type": "quantitative"}, + "y": {"field": "Miles_per_Gallon", "type": "quantitative"}, + "color": { + "field": "Acceleration", + "type": "quantitative", + "scale": {"range": cmap} + }, + "tooltip": [ + {"field": "Name", "type": "nominal"}, + {"field": "Horsepower", "type": "quantitative"}, + {"field": "Miles_per_Gallon", "type": "quantitative"}, + {"field": "Acceleration", "type": "quantitative"} + ] + }, + "width": 600, + "height": 400 +} + +pmu.Container( + toggle, + pn.pane.Vega(vega_spec, sizing_mode="stretch_width"), + theme_config={"palette": {"primary": {"main": primary_color}}}, + width_option="md" +).preview() +``` + +## Advanced Theming with Configuration + +Vega-Lite provides a powerful configuration system that can be combined with Panel Material UI themes. You can add a `config` object to your specification to customize the appearance. + +### Using Built-in Vega Themes + +```{pyodide} +import panel as pn +import panel_material_ui as pmu + +pn.extension("vega") + +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +@pn.depends(toggle.param.dark_theme) +def create_themed_chart(dark_theme): + theme = "dark" if dark_theme else "default" + + vega_spec = { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": {"url": "https://raw.githubusercontent.com/vega/vega/master/docs/data/stocks.csv"}, + "mark": "line", + "encoding": { + "x": {"field": "date", "type": "temporal"}, + "y": {"field": "price", "type": "quantitative"}, + "color": {"field": "symbol", "type": "nominal"} + }, + "width": 600, + "height": 300, + "config": { + "theme": theme + } + } + return vega_spec + +pmu.Container( + toggle, + pn.pane.Vega(create_themed_chart, sizing_mode="stretch_width"), + width_option="md" +).preview() +``` + +### Creating Custom Material-Aligned Configurations + +You can create comprehensive Vega configurations that automatically align with your Material UI theme colors: + +```{pyodide} +import panel as pn +import panel_material_ui as pmu + +pn.extension("vega") + +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +def create_material_config(primary_color="#1976d2", dark_theme=False): + """Create a Vega-Lite config aligned with Material Design.""" + font_family = "Roboto, Helvetica, Arial, sans-serif" + colors = pmu.theme.generate_palette(primary_color) + + # Theme-specific colors + if dark_theme: + background = "transparent" + text_color = "white" + grid_color = "#555555" + domain_color = "#777777" + else: + background = "transparent" + text_color = "#424242" + grid_color = "#e0e0e0" + domain_color = "#bdbdbd" + + return { + "background": background, + "font": font_family, + "axis": { + "labelFont": font_family, + "labelFontSize": 12, + "labelColor": text_color, + "titleFont": font_family, + "titleFontSize": 14, + "titleColor": text_color, + "titleFontWeight": 500, + "grid": True, + "gridColor": grid_color, + "gridOpacity": 0.5, + "domain": False, + "ticks": False, + "labelPadding": 8, + "titlePadding": 16 + }, + "legend": { + "labelFont": font_family, + "labelFontSize": 12, + "labelColor": text_color, + "titleFont": font_family, + "titleFontSize": 14, + "titleColor": text_color, + "titleFontWeight": 500, + "symbolSize": 80, + "symbolStrokeWidth": 0, + "cornerRadius": 4 + }, + "title": { + "font": font_family, + "fontSize": 18, + "fontWeight": 500, + "color": text_color, + "anchor": "start", + "align": "left", + "offset": 20 + }, + "view": { + "stroke": "transparent", + "strokeWidth": 0 + }, + "range": { + "category": colors, + "diverging": pmu.theme.linear_gradient("#ffffff", primary_color, n=9), + "heatmap": pmu.theme.linear_gradient("#ffffff", primary_color, n=9), + "ordinal": colors, + "ramp": pmu.theme.linear_gradient("#ffffff", primary_color, n=9) + }, + "mark": { + "color": primary_color, + "tooltip": True + }, + "arc": {"fill": primary_color}, + "area": {"fill": primary_color, "fillOpacity": 0.7}, + "bar": {"fill": primary_color}, + "circle": {"fill": primary_color, "stroke": primary_color}, + "line": {"stroke": primary_color, "strokeWidth": 2}, + "point": {"fill": primary_color, "stroke": primary_color, "size": 60}, + "rect": {"fill": primary_color}, + "square": {"fill": primary_color}, + "tick": {"fill": primary_color}, + "trail": {"fill": primary_color} + } + +@pn.depends(toggle.param.dark_theme) +def create_configured_chart(dark_theme): + primary_color = "#e91e63" + config = create_material_config(primary_color, dark_theme) + + vega_spec = { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": {"url": "https://raw.githubusercontent.com/vega/vega/master/docs/data/cars.json"}, + "mark": {"type": "circle", "size": 60}, + "encoding": { + "x": {"field": "Horsepower", "type": "quantitative"}, + "y": {"field": "Miles_per_Gallon", "type": "quantitative"}, + "color": {"field": "Origin", "type": "nominal"}, + "tooltip": [ + {"field": "Name", "type": "nominal"}, + {"field": "Origin", "type": "nominal"}, + {"field": "Horsepower", "type": "quantitative"}, + {"field": "Miles_per_Gallon", "type": "quantitative"} + ] + }, + "width": 600, + "height": 400, + "config": config + } + return vega_spec + +primary_color = "#e91e63" + +pmu.Container( + toggle, + pn.pane.Vega(create_configured_chart, sizing_mode="stretch_width"), + theme_config={"palette": {"primary": {"main": primary_color}}}, + width_option="md" +).preview() +``` + +## Interactive Charts with Material UI Controls + +You can combine Vega's interactivity with Panel Material UI components for a cohesive user experience: + +```{pyodide} +import panel as pn +import panel_material_ui as pmu + +pn.extension("vega") + +# Material UI controls +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) +color_picker = pmu.ColorPicker( + name="Primary Color", + value="#1976d2", + width=200 +) + +origin_select = pmu.Select( + name="Origin Filter", + value="All", + options=["All", "Europe", "Japan", "USA"], + width=200 +) + +def create_interactive_chart(primary_color, origin_filter, dark_theme): + """Create an interactive Vega chart with Material UI theming.""" + colors = pmu.theme.generate_palette(primary_color) + config = create_material_config(primary_color, dark_theme) + + # Data filtering + data_url = "https://raw.githubusercontent.com/vega/vega/master/docs/data/cars.json" + transform = [] + if origin_filter != "All": + transform.append({"filter": f"datum.Origin == '{origin_filter}'"}) + + vega_spec = { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": {"url": data_url}, + "transform": transform, + "mark": {"type": "circle", "size": 80}, + "encoding": { + "x": {"field": "Horsepower", "type": "quantitative"}, + "y": {"field": "Miles_per_Gallon", "type": "quantitative"}, + "color": { + "field": "Cylinders", + "type": "ordinal", + "scale": {"range": colors} + }, + "tooltip": [ + {"field": "Name", "type": "nominal"}, + {"field": "Origin", "type": "nominal"}, + {"field": "Horsepower", "type": "quantitative"}, + {"field": "Miles_per_Gallon", "type": "quantitative"}, + {"field": "Cylinders", "type": "ordinal"} + ] + }, + "width": 600, + "height": 400, + "config": config + } + return vega_spec + +chart = pn.bind( + create_interactive_chart, + primary_color=color_picker, + origin_filter=origin_select, + dark_theme=toggle.param.dark_theme +) + +theme_config = pn.bind( + lambda color: {"palette": {"primary": {"main": color}}}, + color=color_picker +) + +pmu.Container( + pmu.Row(toggle, color_picker, origin_select, styles={"margin-bottom": "20px"}), + pn.pane.Vega(chart, sizing_mode="stretch_width"), + theme_config=theme_config, + width_option="lg" +).preview() +``` + +## Working with Selections + +The Vega pane supports Vega-Lite selections, which can be combined with Panel Material UI components for rich interactivity: + +```{pyodide} +import panel as pn +import panel_material_ui as pmu +import pandas as pd + +pn.extension("vega") + +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +# Create a Vega-Lite specification with brush selection +vega_spec = { + "$schema": "https://vega.github.io/schema/vega-lite/v5.json", + "data": {"url": "https://raw.githubusercontent.com/vega/vega/master/docs/data/cars.json"}, + "params": [{ + "name": "brush", + "select": {"type": "interval"} + }], + "mark": {"type": "circle", "size": 60}, + "encoding": { + "x": {"field": "Horsepower", "type": "quantitative"}, + "y": {"field": "Miles_per_Gallon", "type": "quantitative"}, + "color": { + "condition": { + "param": "brush", + "field": "Origin", + "type": "nominal" + }, + "value": "lightgray" + }, + "tooltip": [ + {"field": "Name", "type": "nominal"}, + {"field": "Origin", "type": "nominal"}, + {"field": "Horsepower", "type": "quantitative"}, + {"field": "Miles_per_Gallon", "type": "quantitative"} + ] + }, + "width": 600, + "height": 400, + "config": create_material_config("#1976d2", False) +} + +vega_pane = pn.pane.Vega(vega_spec, debounce=10, sizing_mode="stretch_width") + +def selection_info(selection): + """Display information about the current selection.""" + if not selection: + return pmu.Alert("No selection made. Drag to select points!", severity="info") + + info_text = "**Selection:**\\n" + for field, values in selection.items(): + info_text += f"- {field}: {values[0]:.2f} to {values[1]:.2f}\\n" + + return pmu.Alert(info_text, severity="success") + +info_panel = pn.bind(selection_info, vega_pane.selection.param.brush) + +pmu.Container( + toggle, + pmu.Row( + vega_pane, + pmu.Column( + pmu.Typography("## Selection Info", variant="h6"), + info_panel, + width_option="sm" + ), + styles={"gap": "20px"} + ), + width_option="xl" +).preview() +``` + +## Best Practices + +1. **Consistent Color Schemes**: Use `pmu.theme.generate_palette()` and `pmu.theme.linear_gradient()` to ensure your Vega charts match your Material UI theme. + +2. **Responsive Design**: Set chart properties like `"width": "container"` and use `sizing_mode="stretch_width"` in the Vega pane for responsive behavior. + +3. **Theme Synchronization**: When using `ThemeToggle`, create reactive functions that adapt the Vega configuration to light/dark mode changes. + +4. **Comprehensive Configuration**: Use the `config` object to create consistent theming across all chart elements including axes, legends, and marks. + +5. **Interactive Integration**: Combine Vega selections with Panel Material UI components for rich, themed dashboards. + +6. **Font Consistency**: Always specify the Material Design font stack in your configuration: `"Roboto, Helvetica, Arial, sans-serif"`. + +7. **Transparent Backgrounds**: Use `"background": "transparent"` to ensure charts integrate seamlessly with your Material UI containers. + +Remember that Vega-Lite configurations are applied at the specification level and will only affect the specific chart, making them ideal for fine-grained theming control. diff --git a/examples/apps/styling/altair.py b/examples/apps/styling/altair.py new file mode 100644 index 00000000..d70c85f6 --- /dev/null +++ b/examples/apps/styling/altair.py @@ -0,0 +1,163 @@ +import altair as alt +import panel_material_ui as pmu +from vega_datasets import data +import panel as pn + +pn.extension('vega') + +def create_material_theme(primary_color="#1976d2", font_family="Roboto, Helvetica, Arial", n_colors=10, enable=True): + """Create a custom Altair theme aligned with Material Design.""" + palette = pmu.theme.generate_palette(primary_color, n_colors=n_colors) + + # Generate color gradients for continuous scales + light_gradient = pmu.theme.linear_gradient("#ffffff", primary_color, n=256) + dark_theme = False + grid_color = "rgb(255, 255, 255, 0.67)" if dark_theme else "rgba(0, 0, 0, 0.33)" + + @alt.theme.register('material', enable=enable) + def material_theme(): + return { + "config": { + # View properties + "view": { + "continuousWidth": 600, + "continuousHeight": 400, + "stroke": "transparent", + "strokeWidth": 0 + }, + "background": "transparent", + + # Font settings + "font": font_family, + + # Mark defaults aligned with primary color + "mark": { + "color": primary_color, + "fill": primary_color, + "tooltip": True + }, + + # Specific mark types + "arc": {"fill": primary_color}, + "area": {"fill": primary_color, "fillOpacity": 0.7}, + "bar": {"fill": primary_color}, + "circle": {"fill": primary_color}, + "line": {"stroke": primary_color, "strokeWidth": 2}, + "point": {"fill": primary_color, "size": 60}, + "rect": {"fill": primary_color}, + "square": {"fill": primary_color}, + "tick": {"fill": primary_color}, + "trail": {"fill": primary_color}, + + # Axis styling + "axis": { + "labelFont": font_family, + "labelFontSize": 12, + "labelColor": "#666666", + "titleFont": font_family, + "titleFontSize": 14, + "titleColor": "#424242", + "titleFontWeight": 500, + "grid": True, + "gridColor": grid_color, + "gridOpacity": 0.5, + "domain": False, + "ticks": False, + "labelPadding": 8, + "titlePadding": 16 + }, + + # Legend styling + "legend": { + "labelFont": font_family, + "labelFontSize": 12, + "labelColor": "#666666", + "titleFont": font_family, + "titleFontSize": 14, + "titleColor": "#424242", + "titleFontWeight": 500, + "padding": 8, + "symbolSize": 80, + "symbolStrokeWidth": 0, + "cornerRadius": 4 + }, + + # Title styling + "title": { + "font": font_family, + "fontSize": 18, + "fontWeight": 500, + "color": "#212121", + "anchor": "start", + "align": "left", + "offset": 20 + }, + + # Header styling (for faceted charts) + "header": { + "titleFont": font_family, + "titleFontSize": 14, + "titleColor": "#424242", + "titleFontWeight": 500, + "labelFont": font_family, + "labelFontSize": 12, + "labelColor": "#666666" + }, + + # Color ranges + "range": { + "category": palette, + "diverging": light_gradient, + "heatmap": light_gradient, + "ordinal": palette, + "ramp": light_gradient + }, + + # Style definitions + "style": { + "guide-label": { + "font": font_family, + "fontSize": 12, + "fill": "#666666" + }, + "guide-title": { + "font": font_family, + "fontSize": 14, + "fill": "#424242", + "fontWeight": 500 + }, + "group-title": { + "font": font_family, + "fontSize": 16, + "fill": "#212121", + "fontWeight": 500 + } + } + } + } + + return material_theme + +# Create and apply the theme +primary_color = "#e91e63" +create_material_theme(primary_color, "Roboto, Helvetica, Arial") + +df = data.cars() +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +chart = alt.Chart(df).mark_circle(size=80).encode( + x='Horsepower:Q', + y='Miles_per_Gallon:Q', + color='Origin:N', + tooltip=['Name', 'Origin', 'Horsepower', 'Miles_per_Gallon'] +).properties( + width=600, + height=400 +).interactive() + +pmu.Container( + toggle, + pn.pane.Vega(chart, sizing_mode="stretch_width"), + theme_config={"palette": {"primary": {"main": primary_color}}}, + width_option="md" +).servable() diff --git a/examples/apps/styling/altair2.py b/examples/apps/styling/altair2.py new file mode 100644 index 00000000..433372d6 --- /dev/null +++ b/examples/apps/styling/altair2.py @@ -0,0 +1,103 @@ +import panel as pn +import panel_material_ui as pmu +import altair as alt +from vega_datasets import data +from typing import Optional + +pn.extension("vega") + +df = data.cars() +toggle = pmu.ThemeToggle(styles={"margin-left": "auto"}) + +chart = alt.Chart(df).mark_circle(size=60).encode( + x='Horsepower:Q', + y='Miles_per_Gallon:Q', + color='Origin:N', + tooltip=['Name', 'Origin', 'Horsepower', 'Miles_per_Gallon'] +).properties( + width=600, + height=400 +) + +def configure( + chart: alt.Chart, + primary: str = "#1976d2", + primary_dark: str | None = None, + dark_theme: bool = True, +) -> alt.Chart: + """ + Configure Altair chart with Material UI theme colors. + + Parameters + ---------- + chart : alt.Chart + The Altair chart to configure + primary : str, default "#1976d2" + Primary color for chart elements + primary_dark : str, default "#115293" + Darker variant of primary color for accents + dark_theme : bool, default True + Whether to use dark theme styling + + Returns + ------- + alt.Chart + Configured chart with Material UI theming + """ + font = "Roboto, Helvetica, Arial, sans-serif" + font_color = "rgb(255, 255, 255)" if dark_theme else "rgba(0, 0, 0, 0.87)" + primary = primary_dark if (primary_dark and dark_theme) else primary + colors = pmu.theme.generate_palette(primary, n_colors=5) + return chart.configure( + font=font, + background="transparent" + ).configure_view( + stroke="transparent" + ).configure_mark( + color=primary, + filled=True, + tooltip=True, + ).configure_axis( + labelColor=font_color, + titleColor=font_color, + tickColor=font_color, + gridColor=font_color, + gridOpacity=0.33, + domainColor=font_color, + ).configure_legend( + labelColor=font_color, + titleColor=font_color, + ).configure_title( + color=font_color + ).configure_range( + # category=colors # Use generated palette for categorical colors + ).interactive() + +# Define theme colors +# orsted blue +primary_color = "#788A12" +primary_dark_color = "#E38DE6" + +pmu.Container( + toggle, + pn.pane.Vega( + pn.bind( + configure, + chart, + primary=primary_color, + primary_dark=primary_dark_color, + dark_theme=toggle.param.dark_theme, + ), + sizing_mode="stretch_width" + ), + """Lorum ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.""", + theme_config={ + "palette": { + "primary": { + "main": primary_color, + "dark": primary_dark_color + } + } + }, + width_option="md" +).servable()