holoviz-mcp 0.0.1a0__py3-none-any.whl → 0.0.1a2__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.

Potentially problematic release.


This version of holoviz-mcp might be problematic. Click here for more details.

Files changed (37) hide show
  1. holoviz_mcp/__init__.py +18 -0
  2. holoviz_mcp/apps/__init__.py +1 -0
  3. holoviz_mcp/apps/configuration_viewer.py +116 -0
  4. holoviz_mcp/apps/search.py +314 -0
  5. holoviz_mcp/config/__init__.py +31 -0
  6. holoviz_mcp/config/config.yaml +167 -0
  7. holoviz_mcp/config/loader.py +308 -0
  8. holoviz_mcp/config/models.py +216 -0
  9. holoviz_mcp/config/resources/best-practices/hvplot.md +62 -0
  10. holoviz_mcp/config/resources/best-practices/panel-material-ui.md +318 -0
  11. holoviz_mcp/config/resources/best-practices/panel.md +294 -0
  12. holoviz_mcp/config/schema.json +203 -0
  13. holoviz_mcp/docs_mcp/__init__.py +1 -0
  14. holoviz_mcp/docs_mcp/data.py +963 -0
  15. holoviz_mcp/docs_mcp/models.py +21 -0
  16. holoviz_mcp/docs_mcp/pages_design.md +407 -0
  17. holoviz_mcp/docs_mcp/server.py +220 -0
  18. holoviz_mcp/hvplot_mcp/__init__.py +1 -0
  19. holoviz_mcp/hvplot_mcp/server.py +152 -0
  20. holoviz_mcp/panel_mcp/__init__.py +17 -0
  21. holoviz_mcp/panel_mcp/data.py +316 -0
  22. holoviz_mcp/panel_mcp/models.py +124 -0
  23. holoviz_mcp/panel_mcp/server.py +650 -0
  24. holoviz_mcp/py.typed +0 -0
  25. holoviz_mcp/serve.py +34 -0
  26. holoviz_mcp/server.py +77 -0
  27. holoviz_mcp/shared/__init__.py +1 -0
  28. holoviz_mcp/shared/extract_tools.py +74 -0
  29. holoviz_mcp-0.0.1a2.dist-info/METADATA +641 -0
  30. holoviz_mcp-0.0.1a2.dist-info/RECORD +33 -0
  31. {holoviz_mcp-0.0.1a0.dist-info → holoviz_mcp-0.0.1a2.dist-info}/WHEEL +1 -2
  32. holoviz_mcp-0.0.1a2.dist-info/entry_points.txt +4 -0
  33. holoviz_mcp-0.0.1a2.dist-info/licenses/LICENSE.txt +30 -0
  34. holoviz_mcp-0.0.1a0.dist-info/METADATA +0 -6
  35. holoviz_mcp-0.0.1a0.dist-info/RECORD +0 -5
  36. holoviz_mcp-0.0.1a0.dist-info/top_level.txt +0 -1
  37. main.py +0 -6
@@ -0,0 +1,308 @@
1
+ """Configuration loader for HoloViz MCP server."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any
9
+ from typing import Optional
10
+
11
+ import yaml
12
+ from pydantic import ValidationError
13
+
14
+ from .models import HoloVizMCPConfig
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class ConfigurationError(Exception):
20
+ """Raised when configuration cannot be loaded or is invalid."""
21
+
22
+
23
+ class ConfigLoader:
24
+ """Loads and manages HoloViz MCP configuration."""
25
+
26
+ def __init__(self, config: Optional[HoloVizMCPConfig] = None):
27
+ """Initialize configuration loader.
28
+
29
+ Args:
30
+ config: Pre-configured HoloVizMCPConfig with environment paths.
31
+ If None, loads paths from environment. Configuration will
32
+ still be loaded from files even if this is provided.
33
+ """
34
+ self._env_config = config
35
+ self._loaded_config: Optional[HoloVizMCPConfig] = None
36
+
37
+ def load_config(self) -> HoloVizMCPConfig:
38
+ """Load configuration from files and environment.
39
+
40
+ Returns
41
+ -------
42
+ Loaded configuration.
43
+
44
+ Raises
45
+ ------
46
+ ConfigurationError: If configuration cannot be loaded or is invalid.
47
+ """
48
+ if self._loaded_config is not None:
49
+ return self._loaded_config
50
+
51
+ # Get environment config (from parameter or environment)
52
+ if self._env_config is not None:
53
+ env_config = self._env_config
54
+ else:
55
+ env_config = HoloVizMCPConfig()
56
+
57
+ # Start with default configuration dict
58
+ config_dict = self._get_default_config()
59
+
60
+ # Load from default config file if it exists
61
+ default_config_file = env_config.default_dir / "config.yaml"
62
+ if default_config_file.exists():
63
+ try:
64
+ default_config = self._load_yaml_file(default_config_file)
65
+ config_dict = self._merge_configs(config_dict, default_config)
66
+ logger.info(f"Loaded default configuration from {default_config_file}")
67
+ except Exception as e:
68
+ logger.warning(f"Failed to load default config from {default_config_file}: {e}")
69
+
70
+ # Load from user config file if it exists
71
+ user_config_file = env_config.config_file_path()
72
+ if user_config_file.exists():
73
+ user_config = self._load_yaml_file(user_config_file)
74
+ # Filter out any unknown fields to prevent validation errors
75
+ user_config = self._filter_known_fields(user_config)
76
+ config_dict = self._merge_configs(config_dict, user_config)
77
+ logger.info(f"Loaded user configuration from {user_config_file}")
78
+
79
+ # Apply environment variable overrides
80
+ config_dict = self._apply_env_overrides(config_dict)
81
+
82
+ # Add the environment paths to the config dict
83
+ config_dict.update(
84
+ {
85
+ "user_dir": env_config.user_dir,
86
+ "default_dir": env_config.default_dir,
87
+ "repos_dir": env_config.repos_dir,
88
+ }
89
+ )
90
+
91
+ # Create the final configuration
92
+ try:
93
+ self._loaded_config = HoloVizMCPConfig(**config_dict)
94
+ except ValidationError as e:
95
+ raise ConfigurationError(f"Invalid configuration: {e}") from e
96
+
97
+ return self._loaded_config
98
+
99
+ def _filter_known_fields(self, config_dict: dict[str, Any]) -> dict[str, Any]:
100
+ """Filter out unknown fields that aren't part of the HoloVizMCPConfig schema.
101
+
102
+ This prevents validation errors when loading user config files that might
103
+ contain extra fields.
104
+ """
105
+ known_fields = {"server", "docs", "resources", "prompts", "user_dir", "default_dir", "repos_dir"}
106
+ return {k: v for k, v in config_dict.items() if k in known_fields}
107
+
108
+ def _get_default_config(self) -> dict[str, Any]:
109
+ """Get default configuration dictionary."""
110
+ return {
111
+ "server": {
112
+ "name": "holoviz-mcp",
113
+ "version": "1.0.0",
114
+ "description": "Model Context Protocol server for HoloViz ecosystem",
115
+ "log_level": "INFO",
116
+ "security": {"allow_code_execution": True},
117
+ },
118
+ "docs": {
119
+ "repositories": {}, # No more Python-side defaults!
120
+ "index_patterns": ["**/*.md", "**/*.rst", "**/*.txt"],
121
+ "exclude_patterns": ["**/node_modules/**", "**/.git/**", "**/build/**"],
122
+ "max_file_size": 1024 * 1024, # 1MB
123
+ "update_interval": 86400, # 24 hours
124
+ },
125
+ "resources": {"search_paths": []},
126
+ "prompts": {"search_paths": []},
127
+ }
128
+
129
+ def _load_yaml_file(self, file_path: Path) -> dict[str, Any]:
130
+ """Load YAML file safely.
131
+
132
+ Args:
133
+ file_path: Path to YAML file.
134
+
135
+ Returns
136
+ -------
137
+ Parsed YAML content.
138
+
139
+ Raises
140
+ ------
141
+ ConfigurationError: If file cannot be loaded or parsed.
142
+ """
143
+ try:
144
+ with open(file_path, "r", encoding="utf-8") as f:
145
+ content = yaml.safe_load(f)
146
+ if content is None:
147
+ return {}
148
+ if not isinstance(content, dict):
149
+ raise ConfigurationError(f"Configuration file must contain a YAML dictionary: {file_path}")
150
+ return content
151
+ except yaml.YAMLError as e:
152
+ raise ConfigurationError(f"Invalid YAML in {file_path}: {e}") from e
153
+ except Exception as e:
154
+ raise ConfigurationError(f"Failed to load {file_path}: {e}") from e
155
+
156
+ def _merge_configs(self, base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
157
+ """Merge two configuration dictionaries recursively.
158
+
159
+ Args:
160
+ base: Base configuration.
161
+ override: Override configuration.
162
+
163
+ Returns
164
+ -------
165
+ Merged configuration.
166
+ """
167
+ result = base.copy()
168
+
169
+ for key, value in override.items():
170
+ if key in result and isinstance(result[key], dict) and isinstance(value, dict):
171
+ result[key] = self._merge_configs(result[key], value)
172
+ else:
173
+ result[key] = value
174
+
175
+ return result
176
+
177
+ def _apply_env_overrides(self, config: dict[str, Any]) -> dict[str, Any]:
178
+ """Apply environment variable overrides to configuration.
179
+
180
+ Args:
181
+ config: Configuration dictionary.
182
+
183
+ Returns
184
+ -------
185
+ Configuration with environment overrides applied.
186
+ """
187
+ # Log level override
188
+ if "HOLOVIZ_MCP_LOG_LEVEL" in os.environ:
189
+ config.setdefault("server", {})["log_level"] = os.environ["HOLOVIZ_MCP_LOG_LEVEL"]
190
+
191
+ # Server name override
192
+ if "HOLOVIZ_MCP_SERVER_NAME" in os.environ:
193
+ config.setdefault("server", {})["name"] = os.environ["HOLOVIZ_MCP_SERVER_NAME"]
194
+
195
+ # Transport override
196
+ if "HOLOVIZ_MCP_TRANSPORT" in os.environ:
197
+ config.setdefault("server", {})["transport"] = os.environ["HOLOVIZ_MCP_TRANSPORT"]
198
+
199
+ # Telemetry override
200
+ if "ANONYMIZED_TELEMETRY" in os.environ:
201
+ config.setdefault("server", {})["anonymized_telemetry"] = os.environ["ANONYMIZED_TELEMETRY"].lower() in ("true", "1", "yes", "on")
202
+
203
+ # Jupyter proxy URL override
204
+ if "JUPYTER_SERVER_PROXY_URL" in os.environ:
205
+ config.setdefault("server", {})["jupyter_server_proxy_url"] = os.environ["JUPYTER_SERVER_PROXY_URL"]
206
+
207
+ # Security configuration override
208
+ if "HOLOVIZ_MCP_ALLOW_CODE_EXECUTION" in os.environ:
209
+ config.setdefault("server", {}).setdefault("security", {})["allow_code_execution"] = os.environ["HOLOVIZ_MCP_ALLOW_CODE_EXECUTION"].lower() in (
210
+ "true",
211
+ "1",
212
+ "yes",
213
+ "on",
214
+ )
215
+
216
+ return config
217
+
218
+ def get_repos_dir(self) -> Path:
219
+ """Get the repository download directory."""
220
+ config = self.load_config()
221
+ return config.repos_dir
222
+
223
+ def get_resources_dir(self) -> Path:
224
+ """Get the resources directory."""
225
+ config = self.load_config()
226
+ return config.resources_dir()
227
+
228
+ def get_prompts_dir(self) -> Path:
229
+ """Get the prompts directory."""
230
+ config = self.load_config()
231
+ return config.prompts_dir()
232
+
233
+ def get_best_practices_dir(self) -> Path:
234
+ """Get the best practices directory."""
235
+ config = self.load_config()
236
+ return config.best_practices_dir()
237
+
238
+ def create_default_user_config(self) -> None:
239
+ """Create a default user configuration file."""
240
+ config = self.load_config()
241
+ config_file = config.config_file_path()
242
+
243
+ # Create directories if they don't exist
244
+ config_file.parent.mkdir(parents=True, exist_ok=True)
245
+
246
+ # Don't overwrite existing config
247
+ if config_file.exists():
248
+ logger.info(f"Configuration file already exists: {config_file}")
249
+ return
250
+
251
+ # Create default configuration
252
+ template = {
253
+ "server": {"name": "holoviz-mcp", "log_level": "INFO", "security": {"allow_code_execution": True}},
254
+ "docs": {
255
+ "repositories": {
256
+ "example-repo": {
257
+ "url": "https://github.com/example/repo.git",
258
+ "branch": "main",
259
+ "folders": {"doc": {"url_path": ""}},
260
+ "base_url": "https://example.readthedocs.io",
261
+ "reference_patterns": ["doc/reference/**/*.md", "examples/reference/**/*.ipynb"],
262
+ }
263
+ }
264
+ },
265
+ "resources": {"search_paths": []},
266
+ "prompts": {"search_paths": []},
267
+ }
268
+
269
+ with open(config_file, "w", encoding="utf-8") as f:
270
+ yaml.dump(template, f, default_flow_style=False, sort_keys=False)
271
+
272
+ logger.info(f"Created default user configuration file: {config_file}")
273
+
274
+ def reload_config(self) -> HoloVizMCPConfig:
275
+ """Reload configuration from files.
276
+
277
+ Returns
278
+ -------
279
+ Reloaded configuration.
280
+ """
281
+ self._loaded_config = None
282
+ return self.load_config()
283
+
284
+ def clear_cache(self) -> None:
285
+ """Clear the cached configuration to force reload on next access."""
286
+ self._loaded_config = None
287
+
288
+
289
+ # Global configuration loader instance
290
+ _config_loader: Optional[ConfigLoader] = None
291
+
292
+
293
+ def get_config_loader() -> ConfigLoader:
294
+ """Get the global configuration loader instance."""
295
+ global _config_loader
296
+ if _config_loader is None:
297
+ _config_loader = ConfigLoader()
298
+ return _config_loader
299
+
300
+
301
+ def get_config() -> HoloVizMCPConfig:
302
+ """Get the current configuration."""
303
+ return get_config_loader().load_config()
304
+
305
+
306
+ def reload_config() -> HoloVizMCPConfig:
307
+ """Reload configuration from files."""
308
+ return get_config_loader().reload_config()
@@ -0,0 +1,216 @@
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 SecurityConfig(BaseModel):
138
+ """Security configuration for code execution."""
139
+
140
+ allow_code_execution: bool = Field(
141
+ default=True,
142
+ description="Allow LLM to execute arbitrary code. " "This can be dangerous and should only be enabled in trusted environments.",
143
+ )
144
+
145
+
146
+ class ServerConfig(BaseModel):
147
+ """Configuration for the MCP server."""
148
+
149
+ name: str = Field(default="holoviz-mcp", description="Server name")
150
+ version: str = Field(default="1.0.0", description="Server version")
151
+ description: str = Field(default="Model Context Protocol server for HoloViz ecosystem", description="Server description")
152
+ log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = Field(default="INFO", description="Logging level")
153
+ transport: Literal["stdio", "http"] = Field(default="stdio", description="Transport protocol for MCP communication")
154
+ anonymized_telemetry: bool = Field(default=False, description="Enable anonymized telemetry")
155
+ jupyter_server_proxy_url: str = Field(default="", description="Jupyter server proxy URL for Panel app integration")
156
+ security: SecurityConfig = Field(default_factory=SecurityConfig, description="Security configuration")
157
+ vector_db_path: Path = Field(
158
+ default_factory=lambda: (_holoviz_mcp_user_dir() / "vector_db" / "chroma").expanduser(), description="Path to the Chroma vector database."
159
+ )
160
+
161
+
162
+ class HoloVizMCPConfig(BaseModel):
163
+ """Main configuration for HoloViz MCP server."""
164
+
165
+ server: ServerConfig = Field(default_factory=ServerConfig)
166
+ docs: DocsConfig = Field(default_factory=DocsConfig)
167
+ resources: ResourceConfig = Field(default_factory=ResourceConfig)
168
+ prompts: PromptConfig = Field(default_factory=PromptConfig)
169
+
170
+ # Environment paths - merged from EnvironmentConfig with defaults
171
+ user_dir: Path = Field(default_factory=_holoviz_mcp_user_dir, description="User configuration directory")
172
+ default_dir: Path = Field(default_factory=_holoviz_mcp_default_dir, description="Default configuration directory")
173
+ repos_dir: Path = Field(default_factory=lambda: _holoviz_mcp_user_dir() / "repos", description="Repository download directory")
174
+
175
+ model_config = ConfigDict(extra="forbid", validate_assignment=True)
176
+
177
+ def config_file_path(self, location: Literal["user", "default"] = "user") -> Path:
178
+ """Get the path to the configuration file.
179
+
180
+ Args:
181
+ location: Whether to get user or default config file path
182
+ """
183
+ if location == "user":
184
+ return self.user_dir / "config.yaml"
185
+ else:
186
+ return self.default_dir / "config.yaml"
187
+
188
+ def resources_dir(self, location: Literal["user", "default"] = "user") -> Path:
189
+ """Get the path to the resources directory.
190
+
191
+ Args:
192
+ location: Whether to get user or default resources directory
193
+ """
194
+ if location == "user":
195
+ return self.user_dir / "config" / "resources"
196
+ else:
197
+ return self.default_dir / "resources"
198
+
199
+ def prompts_dir(self, location: Literal["user", "default"] = "user") -> Path:
200
+ """Get the path to the prompts directory.
201
+
202
+ Args:
203
+ location: Whether to get user or default prompts directory
204
+ """
205
+ if location == "user":
206
+ return self.user_dir / "config" / "prompts"
207
+ else:
208
+ return self.default_dir / "prompts"
209
+
210
+ def best_practices_dir(self, location: Literal["user", "default"] = "user") -> Path:
211
+ """Get the path to the best practices directory.
212
+
213
+ Args:
214
+ location: Whether to get user or default best practices directory
215
+ """
216
+ return self.resources_dir(location) / "best-practices"
@@ -0,0 +1,62 @@
1
+ # hvPlot
2
+
3
+ ## General Guidelines
4
+
5
+ - Always import hvplot for your data backend:
6
+
7
+ ```python
8
+ import hvplot.pandas # will add .hvplot namespace to Pandas dataframes
9
+ import hvplot.polars # will add .hvplot namespace to Polars dataframes
10
+ ...
11
+ ```
12
+
13
+ - Prefer Bokeh > Plotly > Matplotlib plotting backend for interactivity
14
+ - DO use bar charts over pie Charts. Pie charts are not supported.
15
+ - DO use NumeralTickFormatter and 'a' formatter for axis formatting:
16
+
17
+ | Input | Format String | Output |
18
+ | - | - | - |
19
+ | 1230974 | '0.0a' | 1.2m |
20
+ | 1460 | '0 a' | 1 k |
21
+ | -104000 | '0a' | -104k |
22
+
23
+ ## Developing
24
+
25
+ When developing a hvplot please serve it for development using Panel:
26
+
27
+ ```python
28
+ import pandas as pd
29
+ import hvplot.pandas # noqa
30
+ import panel as pn
31
+
32
+ import numpy as np
33
+
34
+ np.random.seed(42)
35
+ dates = pd.date_range("2022-08-01", periods=30, freq="B")
36
+ open_prices = np.cumsum(np.random.normal(100, 2, size=len(dates)))
37
+ high_prices = open_prices + np.random.uniform(1, 5, size=len(dates))
38
+ low_prices = open_prices - np.random.uniform(1, 5, size=len(dates))
39
+ close_prices = open_prices + np.random.uniform(-3, 3, size=len(dates))
40
+
41
+ data = pd.DataFrame({
42
+ "open": open_prices.round(2),
43
+ "high": high_prices.round(2),
44
+ "low": low_prices.round(2),
45
+ "close": close_prices.round(2),
46
+ }, index=dates)
47
+
48
+
49
+ # Create a scatter plot of date vs close price
50
+ scatter_plot = data.hvplot.scatter(x="index", y="close", grid=True, title="Close Price Scatter Plot", xlabel="Date", ylabel="Close Price")
51
+
52
+
53
+ # Create a Panel app
54
+ app = pn.Column("# Close Price Scatter Plot", scatter_plot)
55
+
56
+ if pn.state.served:
57
+ app.servable()
58
+ ```
59
+
60
+ ```bash
61
+ panel serve plot.py --dev
62
+ ```