diff --git a/plots/wireframe-3d-basic/implementations/r/ggplot2.R b/plots/wireframe-3d-basic/implementations/r/ggplot2.R index 4fe4b0c448e..a5c945ae534 100644 --- a/plots/wireframe-3d-basic/implementations/r/ggplot2.R +++ b/plots/wireframe-3d-basic/implementations/r/ggplot2.R @@ -1,7 +1,7 @@ #' anyplot.ai #' wireframe-3d-basic: Basic 3D Wireframe Plot #' Library: ggplot2 3.5.1 | R 4.4.1 -#' Quality: 89/100 | Created: 2026-08-24 +#' Quality: 87/100 | Updated: 2026-09-10 library(ggplot2) library(ragg) @@ -18,7 +18,7 @@ BRAND <- "#009E73" # --- Camera: orthographic projection (elevation 30, azimuth 45) --------------- # ggplot2 has no 3D grammar, so the mesh is projected to 2D screen coordinates # ourselves (the same technique any static 3D renderer uses under the hood), -# then drawn with plain geom_path/geom_segment/geom_text. +# then drawn with plain geom_polygon/geom_segment/geom_text. elev <- 30 * pi / 180 azim <- 45 * pi / 180 @@ -39,21 +39,71 @@ up_axis <- c( z_lift <- 3.5 # visual height exaggeration so the shallow ripple reads clearly project_x <- function(x, y, z) x * right_axis[1] + y * right_axis[2] + z * z_lift * right_axis[3] project_y <- function(x, y, z) x * up_axis[1] + y * up_axis[2] + z * z_lift * up_axis[3] +depth_toward_camera <- function(x, y, z) x * view_dir[1] + y * view_dir[2] + z * z_lift * view_dir[3] # --- Data: ripple surface z = sin(sqrt(x^2 + y^2)) ----------------------------- -grid_n <- 15 +grid_n <- 30 x_vals <- seq(-6, 6, length.out = grid_n) y_vals <- seq(-6, 6, length.out = grid_n) - -surface <- expand.grid(x = x_vals, y = y_vals) -surface$z <- sin(sqrt(surface$x^2 + surface$y^2)) -surface$px <- project_x(surface$x, surface$y, surface$z) -surface$py <- project_y(surface$x, surface$y, surface$z) - -z_min <- min(surface$z) -z_max <- max(surface$z) -floor_z <- z_min - 0.3 -ceil_z <- z_max + 0.3 +z_fun <- function(x, y) sin(sqrt(x^2 + y^2)) + +z_range <- range(outer(x_vals, y_vals, z_fun)) +floor_z <- z_range[1] - 0.3 +ceil_z <- z_range[2] + 0.3 + +# --- Mesh quads with painter's-algorithm hidden-line removal ------------------ +# Each grid cell becomes a filled quad. Quads are drawn back-to-front (farthest +# from the camera first) with an opaque page-background fill, so nearer quads +# occlude the grid lines sitting behind them - the same trick base R's persp() +# uses instead of a real z-buffer. This lets the mesh resolution sit inside the +# spec's recommended 20x20-50x50 range without the interior crosshatching a +# flat semi-transparent wireframe produces. +n_cells <- (grid_n - 1)^2 +mesh <- data.frame( + quad_id = integer(n_cells * 4), + corner = integer(n_cells * 4), + px = numeric(n_cells * 4), + py = numeric(n_cells * 4) +) +quad_depth <- numeric(n_cells) + +row <- 1 +quad <- 1 +for (i in seq_len(grid_n - 1)) { + for (j in seq_len(grid_n - 1)) { + cx <- c(x_vals[i], x_vals[i + 1], x_vals[i + 1], x_vals[i]) + cy <- c(y_vals[j], y_vals[j], y_vals[j + 1], y_vals[j + 1]) + cz <- z_fun(cx, cy) + idx <- row:(row + 3) + mesh$quad_id[idx] <- quad + mesh$corner[idx] <- 1:4 + mesh$px[idx] <- project_x(cx, cy, cz) + mesh$py[idx] <- project_y(cx, cy, cz) + quad_depth[quad] <- mean(depth_toward_camera(cx, cy, cz)) + row <- row + 4 + quad <- quad + 1 + } +} + +# Farthest quad gets draw_rank 1 (painted first); nearest gets n_cells (painted +# last, on top). The fill itself must stay fully opaque for the occlusion to +# work - only the edge colour's alpha channel is faded with depth, as a subtle +# depth cue (bolder edges up close, softer far away). +draw_rank <- rank(quad_depth, ties.method = "first") +mesh$draw_rank <- draw_rank[mesh$quad_id] +fade <- 0.55 + 0.45 * (mesh$draw_rank - 1) / (n_cells - 1) +brand_rgb <- col2rgb(BRAND) / 255 +mesh$edge_color <- rgb(brand_rgb[1], brand_rgb[2], brand_rgb[3], alpha = fade) +mesh <- mesh[order(mesh$draw_rank, mesh$corner), ] + +# --- Floor reference plane (spatial grounding) --------------------------------- +floor_plane <- data.frame( + x = c(-6, 6, 6, -6), + y = c(-6, -6, 6, 6), + z = floor_z +) +floor_plane$px <- project_x(floor_plane$x, floor_plane$y, floor_plane$z) +floor_plane$py <- project_y(floor_plane$x, floor_plane$y, floor_plane$z) # --- Axis box: three edges meeting at the front-left-bottom corner ------------ axis_lines <- data.frame( @@ -74,12 +124,21 @@ y_breaks <- c(-6, -3, 0, 3, 6) z_breaks <- c(-1, 0, 1) ticks <- rbind( - data.frame(x = x_breaks, y = -9.6, z = floor_z, label = x_breaks), - data.frame(x = -9.6, y = y_breaks, z = floor_z, label = y_breaks) + data.frame(x = x_breaks, y = -9.6, z = floor_z, label = x_breaks, axis = "x"), + data.frame(x = -9.6, y = y_breaks, z = floor_z, label = y_breaks, axis = "y") ) ticks$px <- project_x(ticks$x, ticks$y, ticks$z) ticks$py <- project_y(ticks$x, ticks$y, ticks$z) +# The X-tick and Y-tick label columns sit on the mesh's near side, where the +# wireframe's screen footprint is widest, so a couple of low-value ticks +# ("-3"/"-6") land inside the mesh's silhouette instead of clearing it. Nudge +# each column sideways, away from the vertical Z axis, by a fixed screen +# offset (same lateral-offset trick used for z_ticks below) - harmless for +# the ticks that already clear the mesh, since it just adds margin. +tick_clearance <- 5.5 +ticks$px <- ticks$px + ifelse(ticks$axis == "x", tick_clearance, -tick_clearance) + # Z ticks sit on the vertical axis line itself; nudge the label text # (not the axis line) sideways into the open gap left of the mesh, well past # the Y-axis tick column so the two label groups don't merge into one line. @@ -98,10 +157,10 @@ axis_labels$py <- project_y(axis_labels$x, axis_labels$y, axis_labels$z) # --- Plot ----------------------------------------------------------------- p <- ggplot() + - geom_path(data = surface, aes(px, py, group = y), - color = BRAND, linewidth = 0.3, alpha = 0.35, lineend = "round") + - geom_path(data = surface, aes(px, py, group = x), - color = BRAND, linewidth = 0.3, alpha = 0.35, lineend = "round") + + geom_polygon(data = floor_plane, aes(px, py), + fill = NA, color = INK_SOFT, linewidth = 0.4, alpha = 0.4) + + geom_polygon(data = mesh, aes(px, py, group = draw_rank, colour = I(edge_color)), + fill = PAGE_BG, linewidth = 0.25) + geom_segment(data = axis_lines, aes(x = px, y = py, xend = pxend, yend = pyend), color = INK_SOFT, linewidth = 0.6) + geom_text(data = ticks, aes(px, py, label = label), diff --git a/plots/wireframe-3d-basic/metadata/r/ggplot2.yaml b/plots/wireframe-3d-basic/metadata/r/ggplot2.yaml index 78e3ebf59d3..226fa5ad44b 100644 --- a/plots/wireframe-3d-basic/metadata/r/ggplot2.yaml +++ b/plots/wireframe-3d-basic/metadata/r/ggplot2.yaml @@ -2,9 +2,9 @@ library: ggplot2 language: r specification_id: wireframe-3d-basic created: '2026-08-24T12:45:01Z' -updated: '2026-08-24T12:58:43Z' +updated: '2026-09-10T06:51:25Z' generated_by: claude-sonnet -workflow_run: 32727527087 +workflow_run: 34445087987 issue: 1015 language_version: 4.4.1 library_version: 3.5.1 @@ -12,52 +12,55 @@ preview_url_light: https://storage.googleapis.com/anyplot-images/plots/wireframe preview_url_dark: https://storage.googleapis.com/anyplot-images/plots/wireframe-3d-basic/r/ggplot2/plot-dark.png preview_html_light: null preview_html_dark: null -quality_score: 89 +quality_score: 87 review: strengths: - - Grid density and alpha reduced significantly (625->225 points, alpha 0.6->0.35) - per prior review feedback, producing a markedly cleaner wireframe read - - Z-axis and Y-axis tick label columns now clearly separated (horizontal offset - increased -7.6->-13), eliminating the ambiguous shared-column read flagged in - Review 1 - - Tick label font size bumped 2.8mm->3.3mm for better balance against the bold 3.6mm - axis titles - - Genuinely inventive hand-rolled orthographic camera projection (elevation 30deg - / azimuth 45deg) that works around ggplot2's lack of 3D grammar, matching the - spec's exact viewing angle request - - Clean KISS code, reproducible seed, only ggplot2 + ragg imported, theme-adaptive - chrome flips correctly between renders while the mesh color stays identical - - 'Full spec compliance: correct plot type, all required features present (grid - lines in both x and y directions, labeled X/Y/Z axes with tick marks), correct - mandated title format, no legend needed for a single series' + - 'Genuinely sophisticated 3D-in-2D engineering: manual perspective projection (elevation + 30°, azimuth 45°) combined with painter''s-algorithm hidden-line removal implemented + purely in ggplot2 grammar (geom_polygon + geom_segment + geom_text).' + - Grid resolution raised from 20x20 to 30x30 this attempt, now solidly mid-range + within the spec's recommended 20x20-50x50 window, with no legibility cost thanks + to the opaque-fill occlusion trick. + - Depth-based alpha fading on mesh edges adds a convincing depth cue without introducing + a second hue. + - Single-series brand green (#009E73) is pixel-identical between light and dark + renders; only chrome (background, axis lines, tick/axis labels) flips between + themes, with no dark-on-dark or light-on-light failures. + - All three axes clearly labeled (bold X/Y/Z) with tick marks; title matches the + required '{spec-id} · {language} · {library} · anyplot.ai' format exactly. + - Data (ripple function z = sin(sqrt(x²+y²))) matches the spec's own suggested example + with a sensible axis range and floor plane for spatial grounding. weaknesses: - - Interior of the mesh, especially near the two central ripple peaks, still shows - a moderately dense crosshatch of crossing lines (no depth-sorting/hidden-line - removal in the projection) - individual grid cells are hard to trace there even - though the overall dome/ripple silhouette reads clearly - - Grid resolution (15x15 = 225 points) is below the spec's recommended 20x20-50x50 - range; a defensible clarity trade-off given the no-occlusion projection technique, - but a distance-based alpha/color falloff would let resolution increase without - reintroducing clutter - - No back/top box edges or floor reference plane - leaves spatial grounding slightly - ambiguous (minor, carried over from Review 1, not a blocker) + - 'The X/Y tick-label overlap flagged in Attempt 1 is still present after repair, + confirmed by pixel-cropped close-up on both renders: the ''-6'' and ''-3'' labels + (mirrored on both sides of the Z axis) still have wireframe mesh strands crossing + directly through the glyphs. The applied fix (tick_clearance <- 5.5 px lateral + offset) was too small to clear the mesh''s silhouette, and bumping grid_n from + 20 to 30 in the same commit made the local mesh denser, so roughly the same number + of strands now cross the labels as before. Increase tick_clearance substantially + (try 15-20 instead of 5.5) and re-check against the denser 30x30 mesh specifically + at the ''-6''/''-3'' positions, since those are the values whose screen-projected + position lands deepest inside the mesh''s near-side footprint.' + - The '-6'/'-3' tick labels still read as floating mid-plot rather than clearly + outside the mesh silhouette, which softens the layout/canvas cleanliness slightly + (minor, secondary to the overlap itself). image_description: |- Light render (plot-light.png): - Background: Warm off-white matching #FAF8F1, not pure white. - Chrome: Title "wireframe-3d-basic · r · ggplot2 · anyplot.ai" centered at top in dark ink (#1A1A17), fully visible with no clipping. Bold "X"/"Y"/"Z" axis labels sit at the three box endpoints in the same dark ink. Muted grey-brown (#4A4A44) tick labels run along the two mirrored X/Y diagonals (-6, -3, 0, 3, 6) and up the vertical Z axis (-1, 0, 1); the Z-tick column is now clearly offset to the left, no longer sharing a screen column with the Y-tick labels. - Data: Single-series wireframe mesh entirely in brand green (#009E73), forming a dome/crown-shaped ripple surface with grid lines running in both x and y directions per spec. The overall topology (central peak, concentric dips and rises) reads clearly; the interior near the two central humps still shows a moderate crosshatch of crossing lines from the lack of hidden-line removal, though noticeably less cluttered than a denser 625-point mesh would be. - Legibility verdict: PASS - all text is readable against the light background, no light-on-light issues. + Background: Warm off-white, consistent with #FAF8F1 — not pure white, not dark. + Chrome: Title "wireframe-3d-basic · r · ggplot2 · anyplot.ai" centered at top in dark ink, clearly readable. Bold "X"/"Y"/"Z" axis labels and axis-box edges in dark ink/gray, all clearly legible. Floor-plane diamond outline faint but visible. Z-axis tick labels ("1", "0", "-1") sit cleanly clear of the mesh with no overlap (verified via crop). X/Y-axis tick labels ("-6", "-3", "0", "3", "6", mirrored on both sides of the Z axis) are readable, but the "-6" and "-3" instances have thin green mesh strands crossing directly through the glyphs (verified via cropped close-up) — text remains decipherable but the overlap is real. + Data: Single ripple surface z = sin(sqrt(x²+y²)) rendered as a green (#009E73) wireframe mesh of diamond-shaped quads with painter's-algorithm hidden-line removal (nearer quads occlude farther mesh lines via opaque page-background fill) and depth-based edge-opacity fading (bolder near camera, softer far away). This is the sole data color, correctly matching the Imprint first-series green. + Legibility verdict: PASS overall, with a caveat — the "-6"/"-3" tick label / mesh-line overlap (VQ-02) persists from Attempt 1 despite a repair attempt. Dark render (plot-dark.png): - Background: Warm near-black matching #1A1A17, not pure black. - Chrome: Same title, axis labels, and tick labels as the light render, now rendered in light ink (#F0EFE8) / soft grey (#B8B7B0) respectively - fully legible against the dark surface, no dark-on-dark failures anywhere. - Data: Mesh color is identical brand green (#009E73) to the light render - only chrome (background, ink) flipped, confirming correct theme-adaptive implementation. Same interior crosshatch density as the light render. - Legibility verdict: PASS - all text remains clearly readable, brand green reads well against the dark surface. + Background: Warm near-black, consistent with #1A1A17 — not pure black, not light. + Chrome: Same title, same layout. Axis-box lines, "X"/"Y"/"Z" labels, and all tick labels correctly flip to light gray/near-white with strong contrast against the dark background — no dark-on-dark failures detected anywhere. + Data: Wireframe mesh color is pixel-identical green (#009E73) to the light render — confirmed only chrome flipped, data color did not shift. + Legibility verdict: PASS overall, with the same "-6"/"-3" tick-label / mesh-line overlap caveat as the light render (confirmed via cropped close-up) — legibility itself still holds, but the overlap is unresolved. - Both renders pass the theme-readability check; data colors are identical between themes and only chrome flips as required. + Both renders viewed and analyzed above; a targeted pixel crop of the tick-label region was taken on both PNGs to confirm the overlap status precisely (screenshot evidence, not just visual estimate). criteria_checklist: visual_quality: - score: 27 + score: 23 max: 30 items: - id: VQ-01 @@ -65,77 +68,72 @@ review: score: 7 max: 8 passed: true - comment: Font sizes explicitly set (title 12pt, axis labels 3.6mm bold, tick - labels bumped 2.8mm->3.3mm); readable at full size in both themes + comment: All text readable in both themes; mesh lines cross through '-6'/'-3' + glyphs but do not obscure legibility - id: VQ-02 name: No Overlap - score: 6 + score: 2 max: 6 - passed: true - comment: Z-tick column now clearly separated from the Y-tick column after - the offset increase; no text collisions + passed: false + comment: X/Y tick labels ('-6'/'-3') still overlap mesh strands in both renders + — same issue flagged in Attempt 1; the applied tick_clearance=5.5 fix plus + the grid_n 20->30 density bump did not clear it - id: VQ-03 name: Element Visibility - score: 4 + score: 5 max: 6 passed: true - comment: Grid density and alpha halved from Review 1, improving legibility; - interior near the central ripple peaks still crosshatches due to no depth-sorting - in the projection + comment: Mesh density/line weight appropriate for the denser 30x30 grid - id: VQ-04 name: Color Accessibility score: 2 max: 2 passed: true - comment: Single-hue brand green against theme-adaptive neutral chrome, no - red-green reliance + comment: Single hue, CVD-safe - id: VQ-05 name: Layout & Canvas - score: 4 + score: 3 max: 4 passed: true - comment: Balanced margins, plot and axis box use a healthy share of the canvas, - nothing cut off + comment: No clipping, good margins; '-6'/'-3' tick labels float mid-plot rather + than clearly outside the mesh silhouette - id: VQ-06 name: Axis Labels & Title score: 2 max: 2 passed: true - comment: X/Y/Z bold labels plus numeric tick marks on all three axes; Z vs - Y tick ambiguity from Review 1 is fixed + comment: X/Y/Z labeled, title format exact - id: VQ-07 name: Palette Compliance score: 2 max: 2 passed: true - comment: 'First series is #009E73 in both renders; backgrounds and chrome - are theme-correct' + comment: Brand green sole data color, correct backgrounds, correct chrome + flip design_excellence: - score: 14 + score: 17 max: 20 items: - id: DE-01 name: Aesthetic Sophistication - score: 6 + score: 7 max: 8 passed: true - comment: Custom hand-rolled 3D camera projection is a genuinely sophisticated - solve; reduced mesh clutter lets the design read more clearly than Review - 1 + comment: Custom perspective projection + painter's-algorithm hidden-line removal + + depth-fade - id: DE-02 name: Visual Refinement - score: 4 + score: 5 max: 6 passed: true - comment: theme_void chrome is clean and minimal; lower grid density/alpha - improved refinement over Review 1's crosshatch texture + comment: theme_void chrome, subtle floor plane, generous whitespace - id: DE-03 name: Data Storytelling - score: 4 + score: 5 max: 6 passed: true - comment: Ripple shape and central focal point read more clearly now that density - is reduced, though no explicit emphasis/annotation is added + comment: Z-exaggeration and hidden-line removal create a clear, solid-looking + focal form spec_compliance: score: 15 max: 15 @@ -145,27 +143,26 @@ review: score: 5 max: 5 passed: true - comment: Correct 3D wireframe plot type, projected via custom camera + comment: Correct 3D wireframe - id: SC-02 name: Required Features score: 4 max: 4 passed: true - comment: Grid lines in both x/y directions, 3D perspective at elevation 30/azimuth - 45, all three axes labeled with tick marks + comment: Grid lines both directions, consistent line color, elev 30/azim 45, + labeled axes with ticks - id: SC-03 name: Data Mapping score: 3 max: 3 passed: true - comment: x/y/z correctly mapped through the projection; full data range visible + comment: X/Y/Z correctly mapped and projected - id: SC-04 name: Title & Legend score: 3 max: 3 passed: true - comment: Title matches the mandated format exactly; no legend needed for single - series + comment: Title matches required format exactly; no legend needed (single series) data_quality: score: 15 max: 15 @@ -175,39 +172,37 @@ review: score: 6 max: 6 passed: true - comment: Ripple surface shows multiple peaks/dips across the domain; grid_n=15 - is below the spec's 20-50 recommendation but a deliberate, documented legibility - trade-off + comment: grid_n now 30, solidly mid the spec's 20-50 recommended range (up + from 20, the previous review's ding) - id: DQ-02 name: Realistic Context score: 5 max: 5 passed: true - comment: Neutral mathematical ripple function z = sin(sqrt(x^2+y^2)), matches - the spec's own example + comment: Matches the spec's own suggested ripple example - id: DQ-03 name: Appropriate Scale score: 4 max: 4 passed: true - comment: Domain and z-range are mathematically correct for the chosen function + comment: Sensible axis ranges and z-exaggeration for legibility code_quality: - score: 10 + score: 9 max: 10 items: - id: CQ-01 name: KISS Structure - score: 3 + score: 2 max: 3 passed: true - comment: 'Linear script: tokens -> camera math -> data -> plot -> save, no - functions/classes' + comment: Small projection helper functions, justified by the manual 3D-projection + workaround ggplot2 requires - id: CQ-02 name: Reproducibility score: 2 max: 2 passed: true - comment: set.seed(42) + comment: set.seed(42) present (data itself is deterministic) - id: CQ-03 name: Clean Imports score: 2 @@ -219,15 +214,13 @@ review: score: 2 max: 2 passed: true - comment: Appropriate complexity for a hand-rolled 3D projection, no fake UI - or fabricated interactivity + comment: No fake UI/interactivity; appropriate complexity for the technique - id: CQ-05 name: Output & API score: 1 max: 1 passed: true - comment: Saves plot-{THEME}.png via ragg::agg_png, current ggplot2 API (linewidth, - not size) + comment: Saves plot-{THEME}.png via ragg::agg_png at the correct canvas size library_mastery: score: 8 max: 10 @@ -237,15 +230,15 @@ review: score: 4 max: 5 passed: true - comment: Idiomatic geom_path/geom_segment/geom_text layering and theme_void - base + comment: Composes geom_polygon/geom_segment/geom_text + theme_void idiomatically + for a scene ggplot2 has no native primitive for - id: LM-02 name: Distinctive Features score: 4 max: 5 passed: true - comment: Hand-rolled camera projection to work around ggplot2's lack of 3D - support is distinctive and hard to replicate trivially in another library + comment: Full painter's-algorithm hidden-line removal implemented purely in + ggplot2 grammar verdict: APPROVED impl_tags: dependencies: [] @@ -256,7 +249,8 @@ impl_tags: patterns: - data-generation - matrix-construction + - iteration-over-groups dataprep: [] styling: - - alpha-blending - minimal-chrome + - alpha-blending