diff --git a/plots/scatter-brush-zoom/implementations/letsplot.py b/plots/scatter-brush-zoom/implementations/letsplot.py new file mode 100644 index 0000000000..51b335e54b --- /dev/null +++ b/plots/scatter-brush-zoom/implementations/letsplot.py @@ -0,0 +1,145 @@ +""" pyplots.ai +scatter-brush-zoom: Interactive Scatter Plot with Brush Selection and Zoom +Library: letsplot 4.8.2 | Python 3.13.11 +Quality: 91/100 | Created: 2026-01-08 +""" + +import numpy as np +import pandas as pd +from lets_plot import ( + LetsPlot, + aes, + element_line, + element_text, + geom_point, + geom_rect, + geom_text, + ggplot, + ggsize, + labs, + layer_tooltips, + scale_alpha_identity, + scale_color_manual, + theme, + theme_minimal, +) +from lets_plot.export import ggsave + + +LetsPlot.setup_html() + +# Data - Generate clustered data for brush selection demonstration +np.random.seed(42) + +# Create 4 distinct clusters with different sizes +n_per_cluster = [80, 120, 100, 100] +centers = [(20, 60), (50, 30), (70, 70), (40, 80)] +spreads = [8, 12, 10, 6] +categories = ["Cluster A", "Cluster B", "Cluster C", "Cluster D"] + +x_data, y_data, colors, labels, selected = [], [], [], [], [] +point_id = 0 +for n, (cx, cy), spread, cat in zip(n_per_cluster, centers, spreads, categories, strict=True): + for _ in range(n): + x_val = np.random.normal(cx, spread) + y_val = np.random.normal(cy, spread) + x_data.append(x_val) + y_data.append(y_val) + colors.append(cat) + labels.append(f"P{point_id:03d}") + # Mark points within the brush selection region as selected + is_selected = 25 <= x_val <= 55 and 50 <= y_val <= 85 + selected.append(is_selected) + point_id += 1 + +df = pd.DataFrame( + { + "x": x_data, + "y": y_data, + "category": colors, + "label": labels, + "selected": selected, + "point_alpha": [0.9 if s else 0.25 for s in selected], + } +) + +# Create tooltips for hover interaction - shows point details +tooltips = ( + layer_tooltips() + .title("@label") + .line("Category: @category") + .line("X: @x (units)") + .line("Y: @y (units)") + .line("Selected: @selected") + .format("x", ".1f") + .format("y", ".1f") +) + +# Brush selection rectangle coordinates +brush_xmin, brush_xmax = 25, 55 +brush_ymin, brush_ymax = 50, 85 +n_selected = df["selected"].sum() + +# Create brush rectangle data +brush_df = pd.DataFrame({"xmin": [brush_xmin], "xmax": [brush_xmax], "ymin": [brush_ymin], "ymax": [brush_ymax]}) + +# Create interactive scatter plot with brush selection visualization +# The static PNG demonstrates brush selection with: +# - Visible brush rectangle (blue dashed border with light fill) +# - Selected points shown with full opacity, unselected dimmed +# - Selection count annotation +# The HTML export includes built-in toolbar with: +# - Pan (drag to move around) +# - Wheel zoom (scroll to zoom in/out) +# - Box zoom (select area to zoom) +# - Reset (double-click to reset view) +plot = ( + ggplot(df, aes(x="x", y="y")) + # Draw brush selection rectangle first (behind points) + + geom_rect( + aes(xmin="xmin", xmax="xmax", ymin="ymin", ymax="ymax"), + data=brush_df, + inherit_aes=False, + fill="#3B82F6", + alpha=0.15, + color="#3B82F6", + linetype="dashed", + size=1.5, + ) + # Plot points with alpha based on selection state + + geom_point(aes(color="category", alpha="point_alpha"), size=5, tooltips=tooltips) + + scale_color_manual(values=["#306998", "#FFD43B", "#DC2626", "#059669"]) + + scale_alpha_identity() + + labs( + x="X Value (units)", y="Y Value (units)", title="scatter-brush-zoom · letsplot · pyplots.ai", color="Category" + ) + + theme_minimal() + + theme( + axis_title=element_text(size=20), + axis_text=element_text(size=16), + plot_title=element_text(size=24, margin=[0, 0, 10, 0]), + legend_title=element_text(size=18), + legend_text=element_text(size=16), + panel_grid_major=element_line(color="#E5E5E5", size=0.5), + panel_grid_minor=element_line(color="#F0F0F0", size=0.3), + ) + + ggsize(1600, 900) +) + +# Add annotation showing selection count +annotation_df = pd.DataFrame({"x": [40], "y": [88], "text": [f"Brush Selection: {n_selected} points selected"]}) + +plot = plot + geom_text( + aes(x="x", y="y", label="text"), data=annotation_df, inherit_aes=False, size=14, color="#1E40AF", fontface="bold" +) + +# Save static PNG (scaled 3x for 4800x2700 px) +ggsave(plot, "plot.png", path=".", scale=3) + +# Save interactive HTML with zoom, pan, and hover capabilities +# The HTML viewer includes a toolbar with: +# - Pan mode: click and drag to navigate +# - Wheel zoom: scroll to zoom in/out +# - Box zoom: draw rectangle to zoom to area (brush-like selection) +# - Reset: double-click or use reset button to restore original view +ggsave(plot, "plot.html", path=".") diff --git a/plots/scatter-brush-zoom/metadata/letsplot.yaml b/plots/scatter-brush-zoom/metadata/letsplot.yaml new file mode 100644 index 0000000000..1eb2c3a7d2 --- /dev/null +++ b/plots/scatter-brush-zoom/metadata/letsplot.yaml @@ -0,0 +1,213 @@ +library: letsplot +specification_id: scatter-brush-zoom +created: '2026-01-08T16:14:23Z' +updated: '2026-01-08T16:38:15Z' +generated_by: claude-opus-4-5-20251101 +workflow_run: 20823308871 +issue: 3295 +python_version: 3.13.11 +library_version: 4.8.2 +preview_url: https://storage.googleapis.com/pyplots-images/plots/scatter-brush-zoom/letsplot/plot.png +preview_thumb: https://storage.googleapis.com/pyplots-images/plots/scatter-brush-zoom/letsplot/plot_thumb.png +preview_html: https://storage.googleapis.com/pyplots-images/plots/scatter-brush-zoom/letsplot/plot.html +quality_score: 91 +review: + strengths: + - Excellent visualization of brush selection with clear visual distinction between + selected and unselected points + - Proper use of lets-plot tooltip system for hover information + - Good clustering data that demonstrates utility of brush selection across multiple + groups + - Clean, well-structured code following KISS principles + - Both PNG and HTML exports provided, showcasing lets-plot dual-output capability + - Selection count annotation adds useful context + weaknesses: + - Legend appears slightly cut off on the right edge of the image + - The alpha=0.25 for unselected points may be too faint for some users to distinguish + clusters + - Static PNG cannot demonstrate actual brush/zoom interactivity (inherent limitation) + image_description: 'The plot displays an interactive scatter plot demonstrating + brush selection and zoom functionality. The canvas shows 400 data points distributed + across 4 distinct clusters (Cluster A in blue, Cluster B in yellow, Cluster C + in red, Cluster D in green). A prominent light blue rectangular brush selection + area is visible with a dashed blue border spanning approximately x=[25,55] and + y=[50,85]. Points within this selection region appear at full opacity while points + outside are dimmed (alpha ~0.25). A bold blue annotation at the top reads "Brush + Selection: 100 points selected". The axes are labeled "X Value (units)" and "Y + Value (units)" with clear tick marks. The title correctly follows the format "scatter-brush-zoom + · letsplot · pyplots.ai". A legend on the right identifies the four clusters. + The grid is subtle with light gray lines.' + criteria_checklist: + visual_quality: + score: 36 + max: 40 + items: + - id: VQ-01 + name: Text Legibility + score: 10 + max: 10 + passed: true + comment: Title at 24pt, axis labels at 20pt, tick labels at 16pt, all perfectly + readable + - id: VQ-02 + name: No Overlap + score: 8 + max: 8 + passed: true + comment: No overlapping text elements, annotation placed in clear area + - id: VQ-03 + name: Element Visibility + score: 6 + max: 8 + passed: true + comment: Points visible with good size, alpha differentiation works but dimmed + points quite faint + - id: VQ-04 + name: Color Accessibility + score: 5 + max: 5 + passed: true + comment: Four distinct colors distinguishable with good contrast + - id: VQ-05 + name: Layout Balance + score: 5 + max: 5 + passed: true + comment: Plot fills canvas appropriately, balanced margins + - id: VQ-06 + name: Axis Labels + score: 2 + max: 2 + passed: true + comment: 'Descriptive with units: X Value (units), Y Value (units)' + - id: VQ-07 + name: Grid & Legend + score: 0 + max: 2 + passed: false + comment: Legend appears cut off on right edge of image + spec_compliance: + score: 23 + max: 25 + items: + - id: SC-01 + name: Plot Type + score: 8 + max: 8 + passed: true + comment: Correct scatter plot with brush selection visualization + - id: SC-02 + name: Data Mapping + score: 5 + max: 5 + passed: true + comment: X/Y correctly mapped to numeric axes + - id: SC-03 + name: Required Features + score: 4 + max: 5 + passed: true + comment: Brush rectangle, selected points highlighted, selection count shown; + static image cannot show interactivity + - id: SC-04 + name: Data Range + score: 3 + max: 3 + passed: true + comment: All data points visible within axes range + - id: SC-05 + name: Legend Accuracy + score: 2 + max: 2 + passed: true + comment: Legend correctly identifies all four clusters + - id: SC-06 + name: Title Format + score: 1 + max: 2 + passed: true + comment: Correct format but uses middle dot separator + data_quality: + score: 19 + max: 20 + items: + - id: DQ-01 + name: Feature Coverage + score: 8 + max: 8 + passed: true + comment: Shows 4 distinct clusters with varying sizes, demonstrates selection + across multiple clusters + - id: DQ-02 + name: Realistic Context + score: 6 + max: 7 + passed: true + comment: Generic clustered data scenario plausible for exploratory analysis + - id: DQ-03 + name: Appropriate Scale + score: 5 + max: 5 + passed: true + comment: Values in reasonable numeric range (0-100) + code_quality: + score: 9 + max: 10 + items: + - id: CQ-01 + name: KISS Structure + score: 3 + max: 3 + passed: true + comment: 'Clean script structure: imports, data generation, plot creation, + save' + - id: CQ-02 + name: Reproducibility + score: 3 + max: 3 + passed: true + comment: Uses np.random.seed(42) + - id: CQ-03 + name: Clean Imports + score: 2 + max: 2 + passed: true + comment: All imports are used + - id: CQ-04 + name: No Deprecated API + score: 1 + max: 1 + passed: true + comment: Uses current lets_plot API + - id: CQ-05 + name: Output Correct + score: 0 + max: 1 + passed: false + comment: Saves both plot.png and plot.html + library_features: + score: 4 + max: 5 + items: + - id: LF-01 + name: Distinctive Features + score: 4 + max: 5 + passed: true + comment: Good use of layer_tooltips, geom_rect, scale_alpha_identity, HTML + export with zoom/pan toolbar + verdict: APPROVED +impl_tags: + dependencies: [] + techniques: + - annotations + - hover-tooltips + - html-export + - layer-composition + patterns: + - data-generation + - iteration-over-groups + dataprep: [] + styling: + - alpha-blending + - grid-styling