diff --git a/plots/candlestick-volume/implementations/python/letsplot.py b/plots/candlestick-volume/implementations/python/letsplot.py index 330bc88da4..2b32ac7445 100644 --- a/plots/candlestick-volume/implementations/python/letsplot.py +++ b/plots/candlestick-volume/implementations/python/letsplot.py @@ -1,9 +1,11 @@ -""" pyplots.ai +""" anyplot.ai candlestick-volume: Stock Candlestick Chart with Volume -Library: letsplot 4.8.2 | Python 3.13.11 -Quality: 91/100 | Created: 2025-12-31 +Library: letsplot 4.9.0 | Python 3.13.13 +Quality: 90/100 | Updated: 2026-05-16 """ +import os + import numpy as np import pandas as pd from lets_plot import * @@ -11,41 +13,40 @@ LetsPlot.setup_html() - -# Format volume as human-readable (e.g., 5.0M instead of 5000000) -def format_volume(val): - if val >= 1_000_000: - return f"{val / 1_000_000:.1f}M" - elif val >= 1_000: - return f"{val / 1_000:.0f}K" - return str(int(val)) - +THEME = os.getenv("ANYPLOT_THEME", "light") +PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17" +ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420" +INK = "#1A1A17" if THEME == "light" else "#F0EFE8" +INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0" # Data - 60 trading days of synthetic stock data np.random.seed(42) n_days = 60 dates = pd.date_range("2024-01-02", periods=n_days, freq="B") -# Generate realistic price movement with trend and volatility -returns = np.random.normal(0.001, 0.02, n_days) +# Generate realistic price movement with more dramatic reversals +returns = np.random.normal(0.001, 0.025, n_days) +returns[15] = -0.08 # Dramatic drop +returns[35] = 0.06 # Strong bounce +returns[45] = -0.05 # Another reversal close_prices = 150 * np.cumprod(1 + returns) # Generate OHLC from close prices open_prices = np.roll(close_prices, 1) open_prices[0] = 150 -high_prices = np.maximum(open_prices, close_prices) * (1 + np.abs(np.random.normal(0, 0.01, n_days))) -low_prices = np.minimum(open_prices, close_prices) * (1 - np.abs(np.random.normal(0, 0.01, n_days))) +high_prices = np.maximum(open_prices, close_prices) * (1 + np.abs(np.random.normal(0, 0.012, n_days))) +low_prices = np.minimum(open_prices, close_prices) * (1 - np.abs(np.random.normal(0, 0.012, n_days))) -# Generate volume with some correlation to price movement +# Generate volume with correlation to price movement base_volume = 5_000_000 volatility = np.abs(close_prices - open_prices) / open_prices volume = base_volume * (1 + volatility * 10 + np.random.uniform(-0.3, 0.3, n_days)) volume = volume.astype(int) -# Determine up/down days for coloring (shorter labels to avoid truncation) -direction = ["Up Day" if c >= o else "Down Day" for c, o in zip(close_prices, open_prices)] +# Determine up/down days for coloring +direction = ["Up" if c >= o else "Down" for c, o in zip(close_prices, open_prices)] -# Create date labels for x-axis (show every 10th trading day) +# Create date labels for x-axis (show every 10 trading days) date_labels = [d.strftime("%b %d") for d in dates] date_breaks = list(range(0, n_days, 10)) date_tick_labels = [date_labels[i] for i in date_breaks] @@ -64,69 +65,67 @@ def format_volume(val): } ) -# Colorblind-safe colors (blue for up, orange for down) -color_up = "#0077BB" -color_down = "#EE7733" +# Colorblind-safe colors (first series Okabe-Ito, second is orange) +color_up = "#009E73" +color_down = "#D55E00" + +# Create volume breaks and labels (inline formatting) +vol_min, vol_max = df["volume"].min(), df["volume"].max() +vol_breaks = [int(vol_min), int((vol_min + vol_max) / 2), int(vol_max)] +vol_labels = [f"{v / 1_000_000:.1f}M" if v >= 1_000_000 else f"{v / 1_000:.0f}K" for v in vol_breaks] + +# Theme-adaptive styling +anyplot_theme = theme( + plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG), + panel_background=element_rect(fill=PAGE_BG), + panel_grid_major=element_line(color=INK_SOFT, size=0.25), + panel_grid_minor=element_blank(), + axis_title=element_text(color=INK, size=20), + axis_text=element_text(color=INK_SOFT, size=16), + axis_line=element_line(color=INK_SOFT, size=0.3), + plot_title=element_text(color=INK, size=24), + legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT), + legend_text=element_text(color=INK_SOFT, size=16), + legend_title=element_text(color=INK, size=18), +) # Create candlestick chart (main pane) candle_plot = ( ggplot(df) - # Wicks (high-low lines) - + geom_segment(aes(x="date_idx", xend="date_idx", y="low", yend="high", color="direction"), size=1.0) + # Wicks (high-low lines) - thicker for visibility + + geom_segment(aes(x="date_idx", xend="date_idx", y="low", yend="high", color="direction"), size=1.5) # Bodies (open-close rectangles) - + geom_segment(aes(x="date_idx", xend="date_idx", y="open", yend="close", color="direction"), size=5.0) - + scale_color_manual(values={"Up Day": color_up, "Down Day": color_down}, name="Direction") + + geom_segment(aes(x="date_idx", xend="date_idx", y="open", yend="close", color="direction"), size=6.0) + + scale_color_manual(values={"Up": color_up, "Down": color_down}, name="Direction") + scale_x_continuous(breaks=date_breaks, labels=date_tick_labels) - + labs(title="candlestick-volume · letsplot · pyplots.ai", y="Price ($)", x="") - # Enable crosshair cursor for precise reading - + coord_cartesian() - + theme_minimal() + + labs(title="Stock Trading · candlestick-volume · letsplot · anyplot.ai", y="Price ($)", x="") + + anyplot_theme + theme( - plot_title=element_text(size=24), - axis_title_y=element_text(size=20), - axis_text_y=element_text(size=16), axis_text_x=element_blank(), - legend_position=[0.5, 0.98], + legend_position=[0.5, 0.95], legend_justification=[0.5, 1.0], legend_direction="horizontal", - legend_title=element_text(size=18), - legend_text=element_text(size=16), - panel_grid_major=element_line(color="#E5E7EB", size=0.5), - panel_grid_minor=element_blank(), - plot_margin=[40, 20, 5, 10], + plot_margin=[40, 20, 2, 10], ) + ggsize(1600, 630) ) -# Create volume breaks and labels for human-readable format -vol_min, vol_max = df["volume"].min(), df["volume"].max() -vol_breaks = [int(vol_min), int((vol_min + vol_max) / 2), int(vol_max)] -vol_labels = [format_volume(v) for v in vol_breaks] - # Volume chart (lower pane) volume_plot = ( ggplot(df) + geom_bar(aes(x="date_idx", y="volume", fill="direction"), stat="identity", width=0.8) - + scale_fill_manual(values={"Up Day": color_up, "Down Day": color_down}, name="Direction") + + scale_fill_manual(values={"Up": color_up, "Down": color_down}, name="Direction") + scale_x_continuous(breaks=date_breaks, labels=date_tick_labels) + scale_y_continuous(breaks=vol_breaks, labels=vol_labels) + labs(x="Date (2024)", y="Volume (shares)") - # Enable crosshair cursor for precise reading (interactive HTML) - + coord_cartesian() - + theme_minimal() - + theme( - axis_title=element_text(size=20), - axis_text=element_text(size=16), - legend_position="none", - panel_grid_major=element_line(color="#E5E7EB", size=0.5), - panel_grid_minor=element_blank(), - ) + + anyplot_theme + + theme(legend_position="none", plot_margin=[2, 20, 10, 10]) + ggsize(1600, 270) ) -# Use gggrid for dual-pane layout (replaces deprecated GGBunch) +# Use gggrid for dual-pane layout with tighter spacing combined = gggrid([candle_plot, volume_plot], ncol=1, heights=[0.7, 0.3]) -# Save outputs (path='' ensures files are saved in current directory) -ggsave(combined, "plot.png", scale=3, path=".") -ggsave(combined, "plot.html", path=".") +# Save outputs +ggsave(combined, f"plot-{THEME}.png", scale=3, path=".") +ggsave(combined, f"plot-{THEME}.html", path=".") diff --git a/plots/candlestick-volume/metadata/python/letsplot.yaml b/plots/candlestick-volume/metadata/python/letsplot.yaml index 3eb103a124..ea2dfaedb7 100644 --- a/plots/candlestick-volume/metadata/python/letsplot.yaml +++ b/plots/candlestick-volume/metadata/python/letsplot.yaml @@ -1,208 +1,246 @@ library: letsplot +language: python specification_id: candlestick-volume created: '2025-12-31T13:53:52Z' -updated: '2025-12-31T14:44:41Z' -generated_by: claude-opus-4-5-20251101 -workflow_run: 20620318739 +updated: '2026-05-16T05:41:41Z' +generated_by: claude-haiku +workflow_run: 25953910171 issue: 3068 -python_version: 3.13.11 -library_version: 4.8.2 -preview_url: https://storage.googleapis.com/anyplot-images/plots/candlestick-volume/letsplot/plot.png -preview_html: https://storage.googleapis.com/anyplot-images/plots/candlestick-volume/letsplot/plot.html -quality_score: 91 -impl_tags: - dependencies: [] - techniques: - - subplots - - html-export - patterns: - - data-generation - dataprep: - - time-series - styling: - - grid-styling +python_version: 3.13.13 +library_version: 4.9.0 +preview_url_light: https://storage.googleapis.com/anyplot-images/plots/candlestick-volume/python/letsplot/plot-light.png +preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/candlestick-volume/python/letsplot/plot-dark.png +preview_html_light: https://storage.googleapis.com/anyplot-images/plots/candlestick-volume/python/letsplot/plot-light.html +preview_html_dark: https://storage.googleapis.com/anyplot-images/plots/candlestick-volume/python/letsplot/plot-dark.html +quality_score: 90 review: strengths: - - Excellent dual-pane layout using gggrid with proper 70/30 height ratio as specified - - Colorblind-safe color scheme (blue/orange) applied consistently across both panes - - Human-readable volume labels (3.9M, 6.1M, 8.3M) improve readability - - Clean implementation of candlesticks using geom_segment for both wicks and bodies - - Proper ggplot2 grammar of graphics style leveraging lets-plot strengths - - Correct title format and descriptive axis labels with units + - Perfect visual quality across both light and dark themes with no readability issues + - Excellent use of letsplot's gggrid() for dual-pane layout with proper 70/30 spacing + - Comprehensive theme-adaptive styling with consistent INK and INK_SOFT token usage + - 'Realistic data demonstrating clear features: uptrend, reversal, recovery, and + volume correlation' + - Proper font sizing (24/20/16pt) with explicit configuration, not relying on defaults + - Candlestick visualization technically correct with distinct wicks and bodies + - Clean KISS code structure with proper reproducibility (np.random.seed) weaknesses: - - Helper function format_volume() violates KISS principle (code should be flat script) - - Crosshair cursor mentioned in comments but only available in HTML output, not - PNG - - Minor visual gap between the two panes could be tighter - image_description: The plot displays a professional dual-pane candlestick chart - with synchronized volume bars. The upper pane (~70% height) shows OHLC candlesticks - with blue color for "Up Day" and orange for "Down Day", depicting a stock declining - from ~$165 to ~$127 over 60 trading days (Jan-Mar 2024). Wicks are visible extending - from candlestick bodies. The lower pane (~30% height) shows corresponding volume - bars using the same color scheme, with human-readable y-axis labels (3.9M, 6.1M, - 8.3M). The title "candlestick-volume · letsplot · pyplots.ai" appears at top, - with a horizontal legend showing "Direction" with Up Day/Down Day entries. Both - panes share a common x-axis showing dates. Grid lines are subtle gray. The layout - is well-balanced with good canvas utilization. + - Design sophistication follows standard financial chart patterns rather than adding + unique visual storytelling + - Minimal data storytelling - plot displays data clearly but lacks strong visual + hierarchy or emphasis to guide viewer insights + - Limited use of visual emphasis techniques (color contrast, size variation, focal + points) to create narrative + image_description: |- + Light render (plot-light.png): + Background: Warm off-white (#FAF8F1), professional appearance + Chrome: Title "candlestick-volume · letsplot · anyplot.ai" in dark text (24pt), axis labels "Price ($)" and "Volume (shares)" in dark text (20pt), tick labels readable (16pt), date labels (Jan 02, Jan 16, etc.) with no overlap, horizontal legend at top center + Data: Teal green (#009E73) candlesticks for "Up" days, orange (#D55E00) for "Down" days, wicks and bodies clearly distinct, volume bars show same color scheme, 60-day series fits well without cramping + Legibility verdict: PASS - All text is dark and clearly readable against the warm light background + + Dark render (plot-dark.png): + Background: Warm near-black (#1A1A17), appropriate for dark theme + Chrome: Title in white text (light/readable), axis labels in light gray (INK_SOFT token), tick labels readable, date labels visible with proper contrast, no dark-on-dark text failures + Data: Candlestick colors identical to light render (#009E73 green, #D55E00 orange), volume bars maintain same colors - confirming data-color invariance across themes + Legibility verdict: PASS - All text is light and clearly readable against the dark background, no theme-adaptation failures criteria_checklist: visual_quality: - score: 36 - max: 40 + score: 30 + max: 30 items: - id: VQ-01 name: Text Legibility - score: 10 - max: 10 + score: 8 + max: 8 passed: true - comment: Title ~24pt, axis labels ~20pt, tick marks ~16pt, all perfectly readable + comment: All font sizes explicitly set (title 24pt, labels 20pt, ticks 16pt); + perfectly readable in both themes - id: VQ-02 name: No Overlap - score: 8 - max: 8 + score: 6 + max: 6 passed: true - comment: No overlapping text elements, date labels well-spaced + comment: Date labels, tick labels, legend all clearly separated with no collision - id: VQ-03 name: Element Visibility - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: Candlesticks are visible with appropriate sizing; wicks could be - slightly thicker for better visibility + comment: Candlestick wicks (1.5px) and bodies (6.0px) optimally sized for + 60-day density; volume bars clearly distinct - id: VQ-04 name: Color Accessibility - score: 5 - max: 5 + score: 2 + max: 2 passed: true - comment: Blue (#0077BB) and orange (#EE7733) are colorblind-safe + comment: 'Okabe-Ito palette (#009E73, #D55E00) is colorblind-safe with good + luminance contrast' - id: VQ-05 - name: Layout Balance + name: Layout & Canvas score: 4 - max: 5 + max: 4 passed: true - comment: Good 70/30 split as specified; slight gap between panes but acceptable + comment: Landscape 4800×2700px with excellent utilization; 70/30 pane split; + balanced margins - id: VQ-06 - name: Axis Labels + name: Axis Labels & Title score: 2 max: 2 passed: true - comment: '"Price ($)" and "Volume (shares)" with units, "Date (2024)" is descriptive' + comment: 'Descriptive labels with units: Price ($), Volume (shares), Date + (2024)' - id: VQ-07 - name: Grid & Legend - score: 0 + name: Palette Compliance + score: 2 max: 2 passed: true - comment: Grid is subtle, but legend positioning at top overlaps with data - area conceptually; minor grid alignment issue between panes + comment: 'Okabe-Ito colors correct; backgrounds #FAF8F1 light / #1A1A17 dark; + text colors theme-correct in both renders' + design_excellence: + score: 10 + max: 20 + items: + - id: DE-01 + name: Aesthetic Sophistication + score: 4 + max: 8 + passed: false + comment: Well-configured library default with intentional Okabe-Ito palette + and theme-adaptive design, but not exceptional or publication-ready + - id: DE-02 + name: Visual Refinement + score: 4 + max: 6 + passed: true + comment: Subtle grid, thoughtful legend positioning, generous margins, dual-pane + structure shows refinement + - id: DE-03 + name: Data Storytelling + score: 2 + max: 6 + passed: false + comment: Clear visual hierarchy (price dominant, volume supporting) but lacks + strong focal point or narrative emphasis spec_compliance: - score: 24 - max: 25 + score: 15 + max: 15 items: - id: SC-01 name: Plot Type - score: 8 - max: 8 - passed: true - comment: Correct candlestick chart with volume bars in dual-pane layout - - id: SC-02 - name: Data Mapping score: 5 max: 5 passed: true - comment: Date on X, OHLC prices correctly mapped, volume on lower pane - - id: SC-03 + comment: Correct candlestick chart with OHLC structure and volume bars in + dual-pane layout + - id: SC-02 name: Required Features score: 4 - max: 5 + max: 4 passed: true - comment: Has shared x-axis, color-coded volume, proper pane ratio; no crosshair - cursor in PNG (only available in HTML) - - id: SC-04 - name: Data Range + comment: 'All features present: candlesticks, volume bars, dual-pane, shared + x-axis, up/down coloring' + - id: SC-03 + name: Data Mapping score: 3 max: 3 passed: true - comment: All data visible, axes show complete range - - id: SC-05 - name: Legend Accuracy - score: 2 - max: 2 - passed: true - comment: '"Direction" legend with "Up Day"/"Down Day" is accurate' - - id: SC-06 - name: Title Format - score: 2 - max: 2 + comment: 'X-axis: date index with labels; Y-axes: price and volume correctly + mapped; all data visible' + - id: SC-04 + name: Title & Legend + score: 3 + max: 3 passed: true - comment: '"candlestick-volume · letsplot · pyplots.ai" is correct' + comment: Title format correct; legend labels match data categories data_quality: - score: 18 - max: 20 + score: 15 + max: 15 items: - id: DQ-01 name: Feature Coverage - score: 7 - max: 8 + score: 6 + max: 6 passed: true - comment: Shows both bullish (blue) and bearish (orange) candles, varying volume, - overall downtrend with some recovery; good mix but could show more dramatic - reversals + comment: Shows bullish/bearish candles, high/low volatility, uptrend/reversal/recovery, + volume-volatility correlation - id: DQ-02 name: Realistic Context - score: 7 - max: 7 + score: 5 + max: 5 passed: true - comment: Stock price data with realistic volatility and volume correlation + comment: Stock trading (neutral), 60 trading days (realistic), price range + $115-$170 (plausible), volume 3.9M-10.1M (realistic) - id: DQ-03 name: Appropriate Scale score: 4 - max: 5 + max: 4 passed: true - comment: Prices ($127-$165) and volumes (4M-8M) are realistic; volume format - is human-readable + comment: Price evolution realistic; volume correlates with volatility; all + values factually plausible code_quality: - score: 8 + score: 10 max: 10 items: - id: CQ-01 name: KISS Structure - score: 1 + score: 3 max: 3 passed: true - comment: Has helper function `format_volume()` which breaks KISS principle - (no functions/classes rule) + comment: 'Linear flow: Imports → Data → Theme → Plot → Save; no functions/classes' - id: CQ-02 name: Reproducibility - score: 3 - max: 3 + score: 2 + max: 2 passed: true - comment: Uses `np.random.seed(42)` + comment: np.random.seed(42) set; deterministic data generation - id: CQ-03 name: Clean Imports score: 2 max: 2 passed: true - comment: Only numpy, pandas, lets_plot used + comment: Only necessary imports; no extraneous dependencies - id: CQ-04 - name: No Deprecated API - score: 1 - max: 1 + name: Code Elegance + score: 2 + max: 2 passed: true - comment: Uses modern gggrid instead of deprecated GGBunch + comment: Clean, Pythonic, appropriate complexity; no fake functionality - id: CQ-05 - name: Output Correct + name: Output & API score: 1 max: 1 passed: true - comment: Saves as plot.png and plot.html - library_features: - score: 5 - max: 5 + comment: Correct output format (plot-{THEME}.png/html); current API + library_mastery: + score: 10 + max: 10 items: - - id: LF-01 - name: Uses distinctive library features + - id: LM-01 + name: Idiomatic Usage + score: 5 + max: 5 + passed: true + comment: Expert use of ggplot() + geom_*(), scale_manual(), theme(), gggrid(), + ggsave() + - id: LM-02 + name: Distinctive Features score: 5 max: 5 passed: true - comment: Uses gggrid for multi-panel layout, ggplot2 grammar, scale_color_manual, - theme customization, ggsave with scale parameter + comment: Uses gggrid() for dual-pane (distinctive to letsplot), theme-adaptive + element_*() styling, geom_segment() candlestick construction + score_caps: + applied: false + details: No defects trigger score caps verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - subplots + - manual-ticks + - custom-legend + patterns: + - data-generation + dataprep: + - cumulative-sum + styling: + - grid-styling