holoviz-mcp 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- holoviz_mcp/__init__.py +18 -0
- holoviz_mcp/apps/__init__.py +1 -0
- holoviz_mcp/apps/configuration_viewer.py +116 -0
- holoviz_mcp/apps/holoviz_get_best_practices.py +173 -0
- holoviz_mcp/apps/holoviz_search.py +319 -0
- holoviz_mcp/apps/hvplot_get_docstring.py +255 -0
- holoviz_mcp/apps/hvplot_get_signature.py +252 -0
- holoviz_mcp/apps/hvplot_list_plot_types.py +83 -0
- holoviz_mcp/apps/panel_get_component.py +496 -0
- holoviz_mcp/apps/panel_get_component_parameters.py +467 -0
- holoviz_mcp/apps/panel_list_components.py +311 -0
- holoviz_mcp/apps/panel_list_packages.py +71 -0
- holoviz_mcp/apps/panel_search_components.py +312 -0
- holoviz_mcp/cli.py +75 -0
- holoviz_mcp/client.py +94 -0
- holoviz_mcp/config/__init__.py +29 -0
- holoviz_mcp/config/config.yaml +178 -0
- holoviz_mcp/config/loader.py +316 -0
- holoviz_mcp/config/models.py +208 -0
- holoviz_mcp/config/resources/best-practices/holoviews.md +423 -0
- holoviz_mcp/config/resources/best-practices/hvplot.md +465 -0
- holoviz_mcp/config/resources/best-practices/panel-material-ui.md +318 -0
- holoviz_mcp/config/resources/best-practices/panel.md +562 -0
- holoviz_mcp/config/schema.json +228 -0
- holoviz_mcp/holoviz_mcp/__init__.py +1 -0
- holoviz_mcp/holoviz_mcp/data.py +970 -0
- holoviz_mcp/holoviz_mcp/models.py +21 -0
- holoviz_mcp/holoviz_mcp/pages_design.md +407 -0
- holoviz_mcp/holoviz_mcp/server.py +220 -0
- holoviz_mcp/hvplot_mcp/__init__.py +1 -0
- holoviz_mcp/hvplot_mcp/server.py +146 -0
- holoviz_mcp/panel_mcp/__init__.py +17 -0
- holoviz_mcp/panel_mcp/data.py +319 -0
- holoviz_mcp/panel_mcp/models.py +124 -0
- holoviz_mcp/panel_mcp/server.py +443 -0
- holoviz_mcp/py.typed +0 -0
- holoviz_mcp/serve.py +36 -0
- holoviz_mcp/server.py +86 -0
- holoviz_mcp/shared/__init__.py +1 -0
- holoviz_mcp/shared/extract_tools.py +74 -0
- holoviz_mcp/thumbnails/configuration_viewer.png +0 -0
- holoviz_mcp/thumbnails/holoviz_get_best_practices.png +0 -0
- holoviz_mcp/thumbnails/holoviz_search.png +0 -0
- holoviz_mcp/thumbnails/hvplot_get_docstring.png +0 -0
- holoviz_mcp/thumbnails/hvplot_get_signature.png +0 -0
- holoviz_mcp/thumbnails/hvplot_list_plot_types.png +0 -0
- holoviz_mcp/thumbnails/panel_get_component.png +0 -0
- holoviz_mcp/thumbnails/panel_get_component_parameters.png +0 -0
- holoviz_mcp/thumbnails/panel_list_components.png +0 -0
- holoviz_mcp/thumbnails/panel_list_packages.png +0 -0
- holoviz_mcp/thumbnails/panel_search_components.png +0 -0
- holoviz_mcp-0.4.0.dist-info/METADATA +216 -0
- holoviz_mcp-0.4.0.dist-info/RECORD +56 -0
- holoviz_mcp-0.4.0.dist-info/WHEEL +4 -0
- holoviz_mcp-0.4.0.dist-info/entry_points.txt +2 -0
- holoviz_mcp-0.4.0.dist-info/licenses/LICENSE.txt +30 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Configuration models for HoloViz MCP server."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Literal
|
|
8
|
+
from typing import Optional
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
from pydantic import AnyHttpUrl
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
from pydantic import ConfigDict
|
|
14
|
+
from pydantic import Field
|
|
15
|
+
from pydantic import PositiveInt
|
|
16
|
+
from pydantic import field_validator
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _holoviz_mcp_user_dir() -> Path:
|
|
20
|
+
"""Get the default user directory for HoloViz MCP."""
|
|
21
|
+
return Path(os.environ.get("HOLOVIZ_MCP_USER_DIR", Path.home() / ".holoviz-mcp"))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _holoviz_mcp_default_dir() -> Path:
|
|
25
|
+
"""Get the default configuration directory for HoloViz MCP."""
|
|
26
|
+
return Path(os.environ.get("HOLOVIZ_MCP_DEFAULT_DIR", Path(__file__).parent.parent / "config"))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class FolderConfig(BaseModel):
|
|
30
|
+
"""Configuration for a folder within a repository."""
|
|
31
|
+
|
|
32
|
+
url_path: str = Field(default="", description="URL path mapping for this folder (e.g., '' for root, '/reference' for reference docs)")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class GitRepository(BaseModel):
|
|
36
|
+
"""Configuration for a Git repository."""
|
|
37
|
+
|
|
38
|
+
url: AnyHttpUrl = Field(..., description="Git repository URL")
|
|
39
|
+
branch: Optional[str] = Field(default=None, description="Git branch to use")
|
|
40
|
+
tag: Optional[str] = Field(default=None, description="Git tag to use (e.g., '1.7.2')")
|
|
41
|
+
commit: Optional[str] = Field(default=None, description="Git commit hash to use")
|
|
42
|
+
folders: Union[list[str], dict[str, FolderConfig]] = Field(
|
|
43
|
+
default_factory=lambda: {"doc": FolderConfig()},
|
|
44
|
+
description="Folders to index within the repository. Can be a list of folder names or a dict mapping folder names to FolderConfig objects",
|
|
45
|
+
)
|
|
46
|
+
base_url: AnyHttpUrl = Field(..., description="Base URL for documentation links")
|
|
47
|
+
url_transform: Literal["holoviz", "plotly", "datashader"] = Field(
|
|
48
|
+
default="holoviz",
|
|
49
|
+
description="""How to transform file path into URL:
|
|
50
|
+
|
|
51
|
+
- holoViz transform suffix to .html: filename.md -> filename.html
|
|
52
|
+
- plotly transform suffix to /: filename.md -> filename/
|
|
53
|
+
- datashader removes leading index and transform suffix to .html: 01_filename.md -> filename.html
|
|
54
|
+
""",
|
|
55
|
+
)
|
|
56
|
+
reference_patterns: list[str] = Field(
|
|
57
|
+
default_factory=lambda: ["examples/reference/**/*.md", "examples/reference/**/*.ipynb"], description="Glob patterns for reference documentation files"
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@field_validator("tag")
|
|
61
|
+
@classmethod
|
|
62
|
+
def validate_tag(cls, v):
|
|
63
|
+
"""Validate git tag format, allowing both 'v1.2.3' and '1.2.3' formats."""
|
|
64
|
+
if v is not None and v.startswith("v"):
|
|
65
|
+
# Allow tags like 'v1.7.2' but also suggest plain version numbers
|
|
66
|
+
pass
|
|
67
|
+
return v
|
|
68
|
+
|
|
69
|
+
@field_validator("folders")
|
|
70
|
+
@classmethod
|
|
71
|
+
def validate_folders(cls, v):
|
|
72
|
+
"""Validate and normalize folders configuration."""
|
|
73
|
+
if isinstance(v, list):
|
|
74
|
+
# Convert list to dict format for backward compatibility
|
|
75
|
+
return {folder: FolderConfig() for folder in v}
|
|
76
|
+
elif isinstance(v, dict):
|
|
77
|
+
# Ensure all values are FolderConfig objects
|
|
78
|
+
result = {}
|
|
79
|
+
for folder, config in v.items():
|
|
80
|
+
if isinstance(config, dict):
|
|
81
|
+
result[folder] = FolderConfig(**config)
|
|
82
|
+
elif isinstance(config, FolderConfig):
|
|
83
|
+
result[folder] = config
|
|
84
|
+
else:
|
|
85
|
+
raise ValueError(f"Invalid folder config for '{folder}': must be dict or FolderConfig")
|
|
86
|
+
return result
|
|
87
|
+
else:
|
|
88
|
+
raise ValueError("folders must be a list or dict")
|
|
89
|
+
|
|
90
|
+
def get_folder_names(self) -> list[str]:
|
|
91
|
+
"""Get list of folder names for backward compatibility."""
|
|
92
|
+
if isinstance(self.folders, dict):
|
|
93
|
+
return list(self.folders.keys())
|
|
94
|
+
return self.folders
|
|
95
|
+
|
|
96
|
+
def get_folder_url_path(self, folder_name: str) -> str:
|
|
97
|
+
"""Get URL path for a specific folder."""
|
|
98
|
+
if isinstance(self.folders, dict):
|
|
99
|
+
folder_config = self.folders.get(folder_name)
|
|
100
|
+
if folder_config:
|
|
101
|
+
return folder_config.url_path
|
|
102
|
+
return ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class DocsConfig(BaseModel):
|
|
106
|
+
"""Configuration for documentation repositories."""
|
|
107
|
+
|
|
108
|
+
repositories: dict[str, GitRepository] = Field(default_factory=dict, description="Dictionary mapping package names to repository configurations")
|
|
109
|
+
index_patterns: list[str] = Field(
|
|
110
|
+
default_factory=lambda: ["**/*.md", "**/*.rst", "**/*.txt"], description="File patterns to include when indexing documentation"
|
|
111
|
+
)
|
|
112
|
+
exclude_patterns: list[str] = Field(
|
|
113
|
+
default_factory=lambda: ["**/node_modules/**", "**/.git/**", "**/build/**"], description="File patterns to exclude when indexing documentation"
|
|
114
|
+
)
|
|
115
|
+
max_file_size: PositiveInt = Field(
|
|
116
|
+
default=1024 * 1024, # 1MB
|
|
117
|
+
description="Maximum file size in bytes to index",
|
|
118
|
+
)
|
|
119
|
+
update_interval: PositiveInt = Field(
|
|
120
|
+
default=86400, # 24 hours
|
|
121
|
+
description="How often to check for updates in seconds",
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ResourceConfig(BaseModel):
|
|
126
|
+
"""Configuration for resources (best practices, etc.)."""
|
|
127
|
+
|
|
128
|
+
search_paths: list[Path] = Field(default_factory=list, description="Additional paths to search for resources")
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class PromptConfig(BaseModel):
|
|
132
|
+
"""Configuration for prompts."""
|
|
133
|
+
|
|
134
|
+
search_paths: list[Path] = Field(default_factory=list, description="Additional paths to search for prompts")
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class ServerConfig(BaseModel):
|
|
138
|
+
"""Configuration for the MCP server."""
|
|
139
|
+
|
|
140
|
+
name: str = Field(default="holoviz-mcp", description="Server name")
|
|
141
|
+
version: str = Field(default="1.0.0", description="Server version")
|
|
142
|
+
description: str = Field(default="Model Context Protocol server for HoloViz ecosystem", description="Server description")
|
|
143
|
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(default="INFO", description="Logging level")
|
|
144
|
+
transport: Literal["stdio", "http"] = Field(default="stdio", description="Transport protocol for MCP communication")
|
|
145
|
+
host: str = Field(default="127.0.0.1", description="Host address to bind to when using HTTP transport (use 0.0.0.0 for Docker)")
|
|
146
|
+
port: int = Field(default=8000, description="Port to bind to when using HTTP transport")
|
|
147
|
+
anonymized_telemetry: bool = Field(default=False, description="Enable anonymized telemetry")
|
|
148
|
+
jupyter_server_proxy_url: str = Field(default="", description="Jupyter server proxy URL for Panel app integration")
|
|
149
|
+
vector_db_path: Path = Field(
|
|
150
|
+
default_factory=lambda: (_holoviz_mcp_user_dir() / "vector_db" / "chroma").expanduser(), description="Path to the Chroma vector database."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class HoloVizMCPConfig(BaseModel):
|
|
155
|
+
"""Main configuration for HoloViz MCP server."""
|
|
156
|
+
|
|
157
|
+
server: ServerConfig = Field(default_factory=ServerConfig)
|
|
158
|
+
docs: DocsConfig = Field(default_factory=DocsConfig)
|
|
159
|
+
resources: ResourceConfig = Field(default_factory=ResourceConfig)
|
|
160
|
+
prompts: PromptConfig = Field(default_factory=PromptConfig)
|
|
161
|
+
|
|
162
|
+
# Environment paths - merged from EnvironmentConfig with defaults
|
|
163
|
+
user_dir: Path = Field(default_factory=_holoviz_mcp_user_dir, description="User configuration directory")
|
|
164
|
+
default_dir: Path = Field(default_factory=_holoviz_mcp_default_dir, description="Default configuration directory")
|
|
165
|
+
repos_dir: Path = Field(default_factory=lambda: _holoviz_mcp_user_dir() / "repos", description="Repository download directory")
|
|
166
|
+
|
|
167
|
+
model_config = ConfigDict(extra="forbid", validate_assignment=True)
|
|
168
|
+
|
|
169
|
+
def config_file_path(self, location: Literal["user", "default"] = "user") -> Path:
|
|
170
|
+
"""Get the path to the configuration file.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
location: Whether to get user or default config file path
|
|
174
|
+
"""
|
|
175
|
+
if location == "user":
|
|
176
|
+
return self.user_dir / "config.yaml"
|
|
177
|
+
else:
|
|
178
|
+
return self.default_dir / "config.yaml"
|
|
179
|
+
|
|
180
|
+
def resources_dir(self, location: Literal["user", "default"] = "user") -> Path:
|
|
181
|
+
"""Get the path to the resources directory.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
location: Whether to get user or default resources directory
|
|
185
|
+
"""
|
|
186
|
+
if location == "user":
|
|
187
|
+
return self.user_dir / "config" / "resources"
|
|
188
|
+
else:
|
|
189
|
+
return self.default_dir / "resources"
|
|
190
|
+
|
|
191
|
+
def prompts_dir(self, location: Literal["user", "default"] = "user") -> Path:
|
|
192
|
+
"""Get the path to the prompts directory.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
location: Whether to get user or default prompts directory
|
|
196
|
+
"""
|
|
197
|
+
if location == "user":
|
|
198
|
+
return self.user_dir / "config" / "prompts"
|
|
199
|
+
else:
|
|
200
|
+
return self.default_dir / "prompts"
|
|
201
|
+
|
|
202
|
+
def best_practices_dir(self, location: Literal["user", "default"] = "user") -> Path:
|
|
203
|
+
"""Get the path to the best practices directory.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
location: Whether to get user or default best practices directory
|
|
207
|
+
"""
|
|
208
|
+
return self.resources_dir(location) / "best-practices"
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: holoviews-development
|
|
3
|
+
description: HoloViews provides a powerful set of features for interactive, complex and publication-quality visualizations.
|
|
4
|
+
metadata:
|
|
5
|
+
version: "1.0.0"
|
|
6
|
+
author: holoviz
|
|
7
|
+
category: data-visualization
|
|
8
|
+
difficulty: intermediate
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# hvPlot Development Skill
|
|
13
|
+
|
|
14
|
+
This document provides best practices for developing plots and charts with HoloViz HoloViews in notebooks and .py files.
|
|
15
|
+
|
|
16
|
+
Please develop as an **Expert Python Developer** developing advanced data-driven, analytics and testable data visualisations, dashboards and applications would do. Keep the code short, concise, documented, testable and professional.
|
|
17
|
+
|
|
18
|
+
## Dependencies
|
|
19
|
+
|
|
20
|
+
Core dependencies provided with the `holoviews` Python package:
|
|
21
|
+
|
|
22
|
+
- **holoviews**: Declarative data visualization library with composable elements. Best for: complex multi-layered plots, advanced interactivity (linked brushing, selection), when you need fine control over plot composition, scientific visualizations. More powerful but steeper learning curve than hvPlot. hvPlot is built upon holoviews.
|
|
23
|
+
- **colorcet**: Perceptually uniform colormaps
|
|
24
|
+
- **panel**: Provides widgets and layouts enabling tool, dashboard and data app development.
|
|
25
|
+
- **param**: A declarative approach to creating classes with typed, validated, and documented parameters. Fundamental to the reactive programming model of hvPlot and the rest of the HoloViz ecosystem.
|
|
26
|
+
- **pandas**: Industry-standard DataFrame library for tabular data. Best for: data cleaning, transformation, time series analysis, datasets that fit in memory. The default choice for most data work.
|
|
27
|
+
|
|
28
|
+
Optional dependencies from the HoloViz Ecosystem:
|
|
29
|
+
|
|
30
|
+
- **hvplot**: Easy to use plotting library with Pandas `.plot` like API. Built on top of HoloViews.
|
|
31
|
+
- **datashader**: Renders large datasets (millions+ points) into images for visualization. Best for: big data visualization, geospatial datasets, scatter plots with millions of points, heatmaps of dense data. Requires hvPlot or HoloViews as frontend.
|
|
32
|
+
- **geoviews**: Geographic data visualization with map projections and tile sources. Best for: geographic/geospatial plots, map-based dashboards, when you need coordinate systems and projections. Built on HoloViews, works seamlessly with hvPlot.
|
|
33
|
+
- **holoviz-mcp**: Model Context Protocol server for HoloViz ecosystem. Provides access to detailed documentation, component search and best practices.
|
|
34
|
+
- **hvsampledata**: Shared datasets for the HoloViz projects.
|
|
35
|
+
|
|
36
|
+
Optional dependencies from the wider PyData Ecosystem:
|
|
37
|
+
|
|
38
|
+
- **dask**: Parallel computing library for scaling Pandas DataFrames beyond memory. Best for: processing datasets larger than RAM, parallel computation across multiple cores/machines, lazy evaluation workflows.
|
|
39
|
+
- **duckdb**: High-performance analytical SQL database. Best for: fast SQL queries on DataFrames, aggregations on large datasets, when you need SQL interface, OLAP-style analytics. Much faster than Pandas for analytical queries.
|
|
40
|
+
- **matplotlib**: Low-level, highly customizable plotting library. Best for: publication-quality static plots, fine-grained control over every aspect of visualization, scientific plots, when you need pixel-perfect control.
|
|
41
|
+
- **Plotly**: Interactive, publication-quality visualization library. Best for: 3D plots, complex interactive charts, animations, when you need hover tooltips and interactivity. Works well with Dash and Panel.
|
|
42
|
+
- **polars**: Modern, fast DataFrame library written in Rust. Best for: high-performance data processing, datasets that fit in memory but need speed, when you need lazy evaluation, better memory efficiency than Pandas.
|
|
43
|
+
- **xarray**: N-dimensional labeled arrays and datasets. Best for: multidimensional scientific data (climate, satellite imagery), data with multiple dimensions and coordinates, NetCDF/HDF5 files, geospatial raster data.
|
|
44
|
+
|
|
45
|
+
## Installation for Development
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install holoviews hvsampledata panel watchfiles
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
For development in .py files DO always include watchfiles for Panel hotreload.
|
|
52
|
+
|
|
53
|
+
## Earthquake Sample Data
|
|
54
|
+
|
|
55
|
+
In the example below we will use the `earthquakes` sample data:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import hvsampledata
|
|
59
|
+
|
|
60
|
+
hvsampledata.earthquakes("pandas")
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```text
|
|
64
|
+
Tabular record of earthquake events from the USGS Earthquake Catalog that provides detailed
|
|
65
|
+
information including parameters such as time, location as latitude/longitude coordinates
|
|
66
|
+
and place name, depth, and magnitude. The dataset contains 596 events.
|
|
67
|
+
|
|
68
|
+
Note: The columns `depth_class` and `mag_class` were created by categorizing numerical values from
|
|
69
|
+
the `depth` and `mag` columns in the original dataset using custom-defined binning:
|
|
70
|
+
|
|
71
|
+
Depth Classification
|
|
72
|
+
|
|
73
|
+
| depth | depth_class |
|
|
74
|
+
|-----------|--------------|
|
|
75
|
+
| Below 70 | Shallow |
|
|
76
|
+
| 70 - 300 | Intermediate |
|
|
77
|
+
| Above 300 | Deep |
|
|
78
|
+
|
|
79
|
+
Magnitude Classification
|
|
80
|
+
|
|
81
|
+
| mag | mag_class |
|
|
82
|
+
|-------------|-----------|
|
|
83
|
+
| 3.9 - <4.9 | Light |
|
|
84
|
+
| 4.9 - <5.9 | Moderate |
|
|
85
|
+
| 5.9 - <6.9 | Strong |
|
|
86
|
+
| 6.9 - <7.9 | Major |
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
Schema
|
|
90
|
+
------
|
|
91
|
+
| name | type | description |
|
|
92
|
+
|:------------|:-----------|:--------------------------------------------------------------------|
|
|
93
|
+
| time | datetime | UTC Time when the event occurred. |
|
|
94
|
+
| lat | float | Decimal degrees latitude. Negative values for southern latitudes. |
|
|
95
|
+
| lon | float | Decimal degrees longitude. Negative values for western longitudes |
|
|
96
|
+
| depth | float | Depth of the event in kilometers. |
|
|
97
|
+
| depth_class | category | The depth category derived from the depth column. |
|
|
98
|
+
| mag | float | The magnitude for the event. |
|
|
99
|
+
| mag_class | category | The magnitude category derived from the mag column. |
|
|
100
|
+
| place | string | Textual description of named geographic region near to the event. |
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Reference Data Exploration Example
|
|
104
|
+
|
|
105
|
+
Below is a simple reference example for data exploration.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
import hvsampledata
|
|
109
|
+
# DO import panel if working in .py files
|
|
110
|
+
import panel as pn
|
|
111
|
+
# Do importing hvplot.pandas to add .hvplot namespace to Pandas DataFrames and Series
|
|
112
|
+
import holoviews as hv
|
|
113
|
+
|
|
114
|
+
# DO always run pn.extension() to load panel javascript extensions
|
|
115
|
+
pn.extension()
|
|
116
|
+
|
|
117
|
+
# Do keep the extraction, transformation and plotting of data clearly separate
|
|
118
|
+
# Extract: earthquakes sample data
|
|
119
|
+
data = hvsampledata.earthquakes("pandas")
|
|
120
|
+
|
|
121
|
+
# Transform: Group by mag_class and count occurrences
|
|
122
|
+
mag_class_counts = data.groupby('mag_class').size().reset_index(name='counts')
|
|
123
|
+
|
|
124
|
+
# Plot: counts by mag_class
|
|
125
|
+
plot = hv.Bars(mag_class_counts, kdims='mag_class', vdims='counts').opts(title='Earthquake Counts by Magnitude Class')
|
|
126
|
+
# If working in notebook DO output to plot:
|
|
127
|
+
plot
|
|
128
|
+
# Else if working in .py file DO:
|
|
129
|
+
# DO provide a method to serve the app with `panel serve`
|
|
130
|
+
if pn.state.served:
|
|
131
|
+
# DO remember to add .servable to the panel components you want to serve with the app
|
|
132
|
+
pn.panel(plot, sizing_mode="stretch_both").servable()
|
|
133
|
+
# DON'T provide a `if __name__ == "__main__":` method to serve the app with `python`
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
If working in a .py file DO serve the plot with hotreload:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
panel serve path/to/file.py --dev --show
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
DONT serve with `python path_to_this_file.py`.
|
|
143
|
+
|
|
144
|
+
## Reference Publication Quality Bar Chart
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
# ============================================================================
|
|
148
|
+
# Publication-Quality Bar Chart - HoloViews Best Practices Example
|
|
149
|
+
# ============================================================================
|
|
150
|
+
# Demonstrates:
|
|
151
|
+
# - Data extraction, transformation, and visualization separation
|
|
152
|
+
# - Custom Bokeh themes for consistent styling
|
|
153
|
+
# - Interactive tooltips with formatted data
|
|
154
|
+
# - Text annotations on bars
|
|
155
|
+
# - Professional fonts, grids, and axis formatting
|
|
156
|
+
# - Panel integration for web serving
|
|
157
|
+
# ============================================================================
|
|
158
|
+
|
|
159
|
+
import hvsampledata
|
|
160
|
+
import panel as pn
|
|
161
|
+
from bokeh.models.formatters import NumeralTickFormatter
|
|
162
|
+
from bokeh.themes import Theme
|
|
163
|
+
|
|
164
|
+
import holoviews as hv
|
|
165
|
+
from holoviews.plotting.bokeh import ElementPlot
|
|
166
|
+
|
|
167
|
+
# ============================================================================
|
|
168
|
+
# BOKEH THEME SETUP - Define global styling
|
|
169
|
+
# ============================================================================
|
|
170
|
+
|
|
171
|
+
ACCENT_COLOR = '#007ACC' # Professional blue
|
|
172
|
+
FONT_FAMILY = 'Roboto'
|
|
173
|
+
|
|
174
|
+
def create_bokeh_theme(font_family=FONT_FAMILY, accent_color=ACCENT_COLOR):
|
|
175
|
+
"""Create custom theme with specified font. Default: Roboto"""
|
|
176
|
+
return Theme(json={
|
|
177
|
+
'attrs': {
|
|
178
|
+
'Title': {
|
|
179
|
+
'text_font': font_family,
|
|
180
|
+
'text_font_size': '16pt',
|
|
181
|
+
'text_font_style': 'bold'
|
|
182
|
+
},
|
|
183
|
+
'Axis': {
|
|
184
|
+
'axis_label_text_font': font_family,
|
|
185
|
+
'axis_label_text_font_size': '12pt',
|
|
186
|
+
'axis_label_text_font_style': 'bold',
|
|
187
|
+
'major_label_text_font': font_family,
|
|
188
|
+
'major_label_text_font_size': '10pt',
|
|
189
|
+
'major_tick_line_color': "black", # Remove tick marks
|
|
190
|
+
'minor_tick_line_color': None
|
|
191
|
+
},
|
|
192
|
+
'Plot': {
|
|
193
|
+
'background_fill_color': '#fafafa',
|
|
194
|
+
'border_fill_color': '#fafafa'
|
|
195
|
+
},
|
|
196
|
+
'Legend': {
|
|
197
|
+
'label_text_font': font_family,
|
|
198
|
+
'label_text_font_size': '10pt'
|
|
199
|
+
},
|
|
200
|
+
'Toolbar': {
|
|
201
|
+
"autohide": True,
|
|
202
|
+
"logo": None,
|
|
203
|
+
"stylesheets": [
|
|
204
|
+
f"""
|
|
205
|
+
.bk-OnOffButton.bk-active{{
|
|
206
|
+
border-color: {accent_color} !important;
|
|
207
|
+
}}
|
|
208
|
+
"""
|
|
209
|
+
]
|
|
210
|
+
},
|
|
211
|
+
# Does not work via Theme, so added here for reference purposes until I figure out how to do it
|
|
212
|
+
'Tooltip': {
|
|
213
|
+
"stylesheets": [f"""
|
|
214
|
+
.bk-tooltip-row-label {{
|
|
215
|
+
color: {ACCENT_COLOR} !important;
|
|
216
|
+
}}"""]
|
|
217
|
+
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
# Apply theme globally - affects all plots
|
|
223
|
+
hv.renderer('bokeh').theme = create_bokeh_theme()
|
|
224
|
+
|
|
225
|
+
# ============================================================================
|
|
226
|
+
# HOLOVIEWS OPTS SETUP - Define global configuration
|
|
227
|
+
# ============================================================================
|
|
228
|
+
|
|
229
|
+
GLOBAL_BACKEND_OPTS={
|
|
230
|
+
'plot.xgrid.visible': False, # Only horizontal grid lines
|
|
231
|
+
'plot.ygrid.visible': True,
|
|
232
|
+
'plot.ygrid.grid_line_color': "black",
|
|
233
|
+
'plot.ygrid.grid_line_alpha': 0.1,
|
|
234
|
+
'plot.min_border_left': 80, # Add padding on left (for y-axis label)
|
|
235
|
+
'plot.min_border_bottom': 80, # Add padding on bottom (for x-axis label)
|
|
236
|
+
'plot.min_border_right': 30, # Add padding on right
|
|
237
|
+
'plot.min_border_top': 80, # Add padding on top
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
ElementPlot.param.backend_opts.default = GLOBAL_BACKEND_OPTS
|
|
241
|
+
ElementPlot.param.yformatter.default = NumeralTickFormatter(format='0a') # 1k, ...
|
|
242
|
+
|
|
243
|
+
hv.opts.defaults(
|
|
244
|
+
hv.opts.Bars(
|
|
245
|
+
color=ACCENT_COLOR, # Professional blue
|
|
246
|
+
line_color=None, # Remove bar borders
|
|
247
|
+
),
|
|
248
|
+
hv.opts.Labels(
|
|
249
|
+
text_baseline='bottom',
|
|
250
|
+
text_font_size='11pt',
|
|
251
|
+
text_font_style='normal',
|
|
252
|
+
text_color='#333333',
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
hv.Cycle.default_cycles["default_colors"] = [ACCENT_COLOR, '#00948A', '#7E59BD', '#FFA20C', '#DA4341', '#D6F1FF', '#DAF5F4', '#F0E8FF', '#FFF8EA', '#FFF1EA', '#001142', '#003336', '#290031', '#371F00', '#3A0C13']
|
|
256
|
+
|
|
257
|
+
# ============================================================================
|
|
258
|
+
# DATA PIPELINE - Separate extraction, transformation, and plotting
|
|
259
|
+
# ============================================================================
|
|
260
|
+
|
|
261
|
+
def get_earthquake_data():
|
|
262
|
+
"""Extract raw earthquake data from sample dataset"""
|
|
263
|
+
return hvsampledata.earthquakes("pandas")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def aggregate_by_magnitude(earthquake_data):
|
|
267
|
+
"""Transform: Group earthquakes by magnitude class with statistics"""
|
|
268
|
+
# Aggregate: count events and calculate average depth per magnitude class
|
|
269
|
+
aggregated = (
|
|
270
|
+
earthquake_data
|
|
271
|
+
.groupby('mag_class', observed=True)
|
|
272
|
+
.agg({'mag': 'count', 'depth': 'mean'})
|
|
273
|
+
.reset_index()
|
|
274
|
+
.rename(columns={'mag': 'event_count', 'depth': 'avg_depth'})
|
|
275
|
+
.sort_values('event_count', ascending=False)
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
# Add percentage column for tooltips
|
|
279
|
+
aggregated['percentage'] = (
|
|
280
|
+
aggregated['event_count'] / aggregated['event_count'].sum() * 100
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
return aggregated
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def create_bar_chart(aggregated_data):
|
|
287
|
+
"""Create publication-quality bar chart with labels and tooltips"""
|
|
288
|
+
default_tools=['save']
|
|
289
|
+
|
|
290
|
+
# Main bar chart with professional styling
|
|
291
|
+
bar_chart = hv.Bars(aggregated_data, kdims='mag_class', vdims=['event_count', 'percentage', 'avg_depth']).opts(
|
|
292
|
+
# Titles and labels
|
|
293
|
+
title='Earthquake Distribution by Magnitude',
|
|
294
|
+
xlabel='Magnitude',
|
|
295
|
+
ylabel='Number of Events',
|
|
296
|
+
|
|
297
|
+
# Interactivity
|
|
298
|
+
# hover_cols = ["mag_class", "event_count", "percentage", "avg_depth"],
|
|
299
|
+
hover_tooltips=[
|
|
300
|
+
('Magnitude', '@mag_class'),
|
|
301
|
+
('Events', '@event_count{0,0}'), # Format: 1,234
|
|
302
|
+
('Percentage', '@percentage{0 a}%'), # Format: 45%
|
|
303
|
+
('Avg Depth', '@avg_depth{0f} km') # Format: 99 km
|
|
304
|
+
],
|
|
305
|
+
default_tools=default_tools
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Add text labels above bars
|
|
309
|
+
labels_data = aggregated_data.copy()
|
|
310
|
+
labels_data['label_y'] = labels_data['event_count'] + 20 # Offset above bars
|
|
311
|
+
|
|
312
|
+
text_labels = hv.Labels(labels_data, kdims=['mag_class', 'label_y'], vdims=['event_count', 'percentage', 'avg_depth']).opts(
|
|
313
|
+
hover_tooltips=[
|
|
314
|
+
('Magnitude', '@mag_class'),
|
|
315
|
+
('Events', '@event_count{0,0}'), # Format: 1,234
|
|
316
|
+
# tooltips below do currently not work on Labels
|
|
317
|
+
# ('Percentage', '@percentage{0 a}%'), # Format: 45%
|
|
318
|
+
# ('Avg Depth', '@avg_depth{0f} km'), # Format: 99 km
|
|
319
|
+
],
|
|
320
|
+
default_tools=default_tools
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Overlay: bar chart * text labels
|
|
324
|
+
return bar_chart * text_labels
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def create_plot():
|
|
328
|
+
"""Main function: Extract → Transform → Plot"""
|
|
329
|
+
# Extract: Get raw data
|
|
330
|
+
earthquake_data = get_earthquake_data()
|
|
331
|
+
|
|
332
|
+
# Transform: Aggregate and calculate statistics
|
|
333
|
+
aggregated = aggregate_by_magnitude(earthquake_data)
|
|
334
|
+
|
|
335
|
+
# Visualize: Create publication-quality chart
|
|
336
|
+
chart = create_bar_chart(aggregated)
|
|
337
|
+
|
|
338
|
+
return chart
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
# ============================================================================
|
|
342
|
+
# PANEL APP SETUP
|
|
343
|
+
# ============================================================================
|
|
344
|
+
|
|
345
|
+
# Serve the chart when running with Panel
|
|
346
|
+
if pn.state.served:
|
|
347
|
+
# Load Panel JavaScript extensions
|
|
348
|
+
pn.extension()
|
|
349
|
+
|
|
350
|
+
# Apply custom Bokeh theme (override the global theme)
|
|
351
|
+
# Create and serve the chart
|
|
352
|
+
plot = create_plot()
|
|
353
|
+
pn.panel(plot, sizing_mode="stretch_both", margin=25).servable()
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
## General Instructions
|
|
357
|
+
|
|
358
|
+
- In a notebook always run `hv.extension()` to load any Javascript dependencies.
|
|
359
|
+
|
|
360
|
+
```python
|
|
361
|
+
import holoviews as hv
|
|
362
|
+
|
|
363
|
+
hv.extension()
|
|
364
|
+
...
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
- Prefer Bokeh > Plotly > Matplotlib plotting backend for interactivity
|
|
368
|
+
- DO use bar charts over pie Charts. Pie charts are not supported.
|
|
369
|
+
- DO use NumeralTickFormatter and 'a' formatter for easy axis formatting:
|
|
370
|
+
|
|
371
|
+
```python
|
|
372
|
+
from bokeh.models.formatters import NumeralTickFormatter
|
|
373
|
+
|
|
374
|
+
plot.opts(
|
|
375
|
+
yformatter=NumeralTickFormatter(format='0.00a'), # Format as 1.00M, 2.50M, etc.
|
|
376
|
+
)
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
| Input | Format String | Output |
|
|
381
|
+
| - | - | - |
|
|
382
|
+
| 1230974 | '0.0a' | 1.2m |
|
|
383
|
+
| 1460 | '0 a' | 1 k |
|
|
384
|
+
| -104000 | '0a' | -104k |
|
|
385
|
+
|
|
386
|
+
### Recommended Plot Types
|
|
387
|
+
|
|
388
|
+
Curve - Line plots for time series and continuous data
|
|
389
|
+
Scatter - Scatter plots for exploring relationships between variables
|
|
390
|
+
Bars - Bar charts for categorical comparisons
|
|
391
|
+
Histogram - Histograms for distribution analysis
|
|
392
|
+
Area - Area plots for stacked or filled visualizations
|
|
393
|
+
|
|
394
|
+
## Workflows
|
|
395
|
+
|
|
396
|
+
### Lookup additional information
|
|
397
|
+
|
|
398
|
+
- If the HoloViz MCP server is available DO use the HoloViz MCP server to access relevant documentation (`holoviz_search`), list of plot types available (`hvplot_list_plot_types`), and detailed docstrings (`hvplot_get_docstring`).
|
|
399
|
+
- If the HoloViz MCP server is not available, DO search the web. For example searching the hvplot website for `streaming` related information via https://hvplot.holoviz.org/en/docs/latest/search.html?q=streaming url.
|
|
400
|
+
|
|
401
|
+
### Test the app with pytest
|
|
402
|
+
|
|
403
|
+
DO add tests to the `tests` folder and run them with `pytest tests/path/to/test_file.py`.
|
|
404
|
+
|
|
405
|
+
- DO separate data extraction and transformation from plotting code.
|
|
406
|
+
- DO fix any test errors and rerun the tests
|
|
407
|
+
- DO run the tests and fix errors before displaying or serving the plots
|
|
408
|
+
|
|
409
|
+
### Serve the plot with panel serve
|
|
410
|
+
|
|
411
|
+
DO always start and keep running a development server `panel serve path_to_file.py --dev --show` with hot reload while developing!
|
|
412
|
+
|
|
413
|
+
- Due to `--show` flag, a browser tab will automatically open showing your app.
|
|
414
|
+
- Due to `--dev` flag, the panel server and app will automatically reload if you change the code.
|
|
415
|
+
- The app will be served at http://localhost:5006/.
|
|
416
|
+
- DO make sure the correct virtual environment is activated before serving the app.
|
|
417
|
+
- DO fix any errors that show up in the terminal. Consider adding new tests to ensure they don't happen again.
|
|
418
|
+
- DON'T stop or restart the server after changing the code. The app will automatically reload.
|
|
419
|
+
- If you see 'Cannot start Bokeh server, port 5006 is already in use' in the terminal, DO serve the app on another port with `--port {port-number}` flag.
|
|
420
|
+
- DO remind the user to test the plot on multiple screen sizes (desktop, tablet, mobile)
|
|
421
|
+
- DON'T use legacy `--autoreload` flag
|
|
422
|
+
- DON't run `python path_to_file.py` to test or serve the app.
|
|
423
|
+
- DO use `pn.Column, pn.Tabs, pn.Accordion` to layout multiple plots
|