diff --git a/README.md b/README.md index 5ac5b72..2783052 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,7 @@ When you run this script, PyCharting will: Once you have your OHLC series, you pass additional series to `plot` in two different ways: + ```python +RHIZA_SKIP overlays = { "SMA_50": sma(close, 50), # rendered on top of price @@ -104,6 +105,7 @@ plot( Each subplot value can be a plain array (line), a dict with options, or a list of dicts for multi-series panels: + ```python +RHIZA_SKIP subplots = { # Simple line (default) @@ -136,6 +138,7 @@ Supported series types: `"line"` (default), `"bar"`, `"scatter"`. Each entry acc You can overlay buy/sell arrows on the price chart by passing a `trades` array aligned with your index. Values: `1` (buy), `-1` (sell), `0` (no trade). + ```python +RHIZA_SKIP import numpy as np @@ -171,6 +174,7 @@ The public API is intentionally small and focused. All functions are available f ### `plot` + ```python +RHIZA_SKIP from typing import Dict, Any, Optional, Union @@ -216,23 +220,35 @@ The returned dict includes: ### `stop_server` -```python +RHIZA_SKIP +```python from pycharting import stop_server stop_server() ``` +With no server running, it says so and does nothing: + +```result +ⓘ No active server to stop +``` + Stops the active chart server if it is running. This is useful in long‑running processes and demos to clean up after you are done exploring charts. ### `get_server_status` -```python +RHIZA_SKIP +```python from pycharting import get_server_status status = get_server_status() print(status) ``` +Before any chart has been plotted: + +```result +{'running': False, 'server_info': None, 'active_sessions': 0} +``` + Returns a small dict with: - `running`: whether the server is alive, diff --git a/src/pycharting/__init__.py b/src/pycharting/__init__.py index f09fdc7..c666a8f 100644 --- a/src/pycharting/__init__.py +++ b/src/pycharting/__init__.py @@ -11,26 +11,24 @@ - **Easy to Use:** Simple Python API similar to matplotlib or plotly. Usage: - The main entry point is the `plot` function. - - ```python - from pycharting import plot, stop_server - import numpy as np - - # Prepare your data (numpy arrays or pandas Series) - index = np.arange(100) - open_data = np.random.rand(100) + 100 - high_data = open_data + 1 - low_data = open_data - 1 - close_data = open_data + 0.5 - - # Create and open the chart - plot(index, open_data, high_data, low_data, close_data) - - # ... keep the script running if needed ... - # input("Press Enter to stop...") - # stop_server() - ``` + The public surface is three functions, re-exported at the top level: + + >>> import pycharting + >>> sorted(pycharting.__all__) + ['__version__', 'get_server_status', 'plot', 'stop_server'] + + The main entry point is `plot`. It starts a local server and opens a + browser, so the example below is illustrative rather than executed here: + + >>> import numpy as np + >>> from pycharting import plot, stop_server + >>> index = np.arange(100) + >>> open_data = np.random.rand(100) + 100 + >>> high_data = open_data + 1 + >>> low_data = open_data - 1 + >>> close_data = open_data + 0.5 + >>> plot(index, open_data, high_data, low_data, close_data) # doctest: +SKIP + >>> stop_server() # doctest: +SKIP Exports: - `plot`: Main function to create and display charts. diff --git a/src/pycharting/api/interface.py b/src/pycharting/api/interface.py index 528c005..4b14922 100644 --- a/src/pycharting/api/interface.py +++ b/src/pycharting/api/interface.py @@ -137,29 +137,33 @@ def plot( - `server_running`: Boolean indicating if the server is active. Example: - ```python - import numpy as np - from pycharting import plot - - # 1. Prepare Data - n = 1000 - index = np.arange(n) - close = np.cumsum(np.random.randn(n)) + 100 - - # 2. Simple Line Chart - plot(index, close=close) - - # 3. Candlestick Chart - open_p = close + np.random.randn(n) * 0.5 - high = np.maximum(open_p, close) + np.abs(np.random.randn(n)) - low = np.minimum(open_p, close) - np.abs(np.random.randn(n)) - - plot( - index, open_p, high, low, close, - overlays={"SMA 20": sma}, - session_id="my-analysis" - ) - ``` + Every call below starts a local server and opens a browser, so they are + shown rather than executed. The data preparation is real, though: + + >>> import numpy as np + >>> from pycharting import plot + >>> n = 1000 + >>> index = np.arange(n) + >>> close = np.cumsum(np.random.randn(n)) + 100 + >>> len(close) + 1000 + + A single series renders as a line chart: + + >>> plot(index, close=close) # doctest: +SKIP + + Supplying open/high/low as well renders candlesticks: + + >>> open_p = close + np.random.randn(n) * 0.5 + >>> high = np.maximum(open_p, close) + np.abs(np.random.randn(n)) + >>> low = np.minimum(open_p, close) - np.abs(np.random.randn(n)) + >>> bool(np.all(high >= np.maximum(open_p, close))) + True + >>> plot( + ... index, open_p, high, low, close, + ... overlays={"SMA 20": close}, + ... session_id="my-analysis", + ... ) # doctest: +SKIP """ global _active_server @@ -323,12 +327,11 @@ def stop_server() -> None: If no server is running, this function does nothing and prints a message. Example: - ```python - from pycharting import stop_server + Calling it when nothing is running is safe and simply reports so: - # ... after done with analysis ... - stop_server() - ``` + >>> from pycharting import stop_server + >>> stop_server() + ⓘ No active server to stop """ global _active_server @@ -352,13 +355,21 @@ def get_server_status() -> ServerStatus: - `active_sessions`: Count of currently loaded datasets/sessions. Example: - ```python - from pycharting import get_server_status + With no server started, the reported status is inert: + + >>> from pycharting import get_server_status + >>> status = get_server_status() + >>> sorted(status) + ['active_sessions', 'running', 'server_info'] + >>> status["running"] + False + >>> status["server_info"] is None + True + + Once a server is up, ``server_info`` carries its host and port: - status = get_server_status() - if status['running']: - print(f"Server running at {status['server_info']['url']}") - ``` + >>> if status["running"]: # doctest: +SKIP + ... print(f"Server running at {status['server_info']['url']}") """ global _active_server diff --git a/src/pycharting/core/server.py b/src/pycharting/core/server.py index f096fbd..7c5e3ea 100644 --- a/src/pycharting/core/server.py +++ b/src/pycharting/core/server.py @@ -68,13 +68,17 @@ def find_free_port(start_port: int | None = None, end_port: int | None = None) - RuntimeError: If no free port can be found in the requested range. Example: - ```python - # Let the OS pick a guaranteed-free ephemeral port. - port = find_free_port() + Let the OS pick a guaranteed-free ephemeral port: + + >>> port = find_free_port() + >>> port > 0 + True + + Scan a preferred range instead: - # Scan a preferred range instead. - port = find_free_port(8000, 8010) - ``` + >>> port = find_free_port(8000, 8010) + >>> 8000 <= port < 8010 + True """ # No range requested: let the OS hand out a guaranteed-unique ephemeral port. if start_port is None: @@ -253,13 +257,10 @@ def run_server( None: This function blocks until the server stops. Example: - ```python - # Run server on localhost, finding a free port automatically - run_server() + These block until the server stops, so they are not executed here. - # Run on a specific port - run_server(port=8080) - ``` + >>> run_server() # doctest: +SKIP + >>> run_server(port=8080) # doctest: +SKIP """ # Determine port if port is None: diff --git a/src/pycharting/data/ingestion.py b/src/pycharting/data/ingestion.py index 18448ef..6ab08d0 100644 --- a/src/pycharting/data/ingestion.py +++ b/src/pycharting/data/ingestion.py @@ -54,6 +54,36 @@ def validate_input( Raises: DataValidationError: If any validation check fails. + + Example: + A single series is mapped to ``close`` and rendered as a line chart, + leaving the other price fields unset: + + >>> import numpy as np + >>> result = validate_input(np.arange(3), close=np.array([10.0, 11.0, 12.0])) + >>> result["close"].tolist() + [10.0, 11.0, 12.0] + >>> result["open"] is None + True + + With open and close supplied, high and low are filled in from them: + + >>> result = validate_input( + ... np.arange(3), + ... open=np.array([10.0, 11.0, 12.0]), + ... close=np.array([11.0, 10.0, 13.0]), + ... ) + >>> result["high"].tolist() + [11.0, 11.0, 13.0] + >>> result["low"].tolist() + [10.0, 10.0, 12.0] + + A series whose length disagrees with the index is rejected: + + >>> validate_input(np.arange(3), close=np.array([1.0, 2.0])) + Traceback (most recent call last): + ... + pycharting.data.ingestion.DataValidationError: Close length (2) does not match index length (3) """ # Convert index to numpy array if needed if isinstance(index, (pd.Index, pd.Series)): @@ -234,7 +264,19 @@ def to_array(data: pd.Series | np.ndarray | list | None, name: str) -> np.ndarra class DataManager: - """High-performance data container and manager.""" + """High-performance data container and manager. + + Validates and normalizes the supplied series once on construction, then + serves arbitrary slices of them cheaply — one slice per viewport move. + + Example: + >>> import numpy as np + >>> dm = DataManager(np.arange(5), close=np.arange(5, dtype=float) * 10) + >>> dm.length + 5 + >>> dm.close.tolist() + [0.0, 10.0, 20.0, 30.0, 40.0] + """ def __init__( self, @@ -332,7 +374,30 @@ def get_chunk( start_index: int | None = None, end_index: int | None = None, ) -> dict[str, Any]: - """Return a JSON-serializable slice of the data between ``start_index`` and ``end_index``.""" + """Return a JSON-serializable slice of the data between ``start_index`` and ``end_index``. + + Indices are clamped to the available range, so an over-wide request + returns what exists rather than raising. + + Example: + >>> import numpy as np + >>> dm = DataManager(np.arange(5), close=np.arange(5, dtype=float)) + >>> chunk = dm.get_chunk(1, 3) + >>> chunk["index"] + [1, 2] + >>> chunk["close"] + [1.0, 2.0] + + Omitting both bounds returns the whole series: + + >>> len(dm.get_chunk()["index"]) + 5 + + An end index past the last bar is clamped: + + >>> dm.get_chunk(3, 99)["close"] + [3.0, 4.0] + """ # Handle default values if start_index is None: start_index = 0