ggsketch 2.0.0
CRAN release: 2026-07-17
The 2.0 series turns ggsketch from a line style into a drawing-medium simulator. This first piece is the engine that makes it possible.
Chicklet charts. New
geom_sketch_chicklet()– a hand-drawn take onggchicklet::geom_chicklet(). It stacks rounded “pill” segments with a smallsegment_gapbetween them and a solid fill by default; addcoord_flip()for the classic horizontal layout. Built ongeom_sketch_col(), so it inherits every fill style, drawing medium, and sketch parameter.Repelled labels escape panel corners. The
repel_layout()solver now picks its separation axis by the room left inside the panel bounds, so labels pressed into a corner fan out along the edge instead of being clamped back on top of each other (previously e.g. three overlapping boxes in a scatter’s corner).Calendar axes read as calendars.
geom_sketch_calendar()now labels the y axis with weekday names (honouringweek_start) and the x axis with month names instead of raw grid numbers; setlabels = FALSEto supply your own scales.Saner default hachure pitch. When
hachure_gapisNULL,sketch_polygon_grob()now picks the gap at draw time in device inches – 15% of the shape’s smaller drawn extent, clamped to [0.04, 0.4] – instead of geoms guessing in data units. Wide flat rectangles (Gantt/funnel/pyramid bars, long tiles) no longer degenerate into a few huge strokes escaping the outline, andgeom_sketch_gantt()/funnel()/pyramid()drop their fixed 0.12 workaround pitch. An explicithachure_gapis honoured unchanged, but default-gap output shifts slightly.Rect geoms bend under nonlinear coords.
geom_sketch_rect(),geom_sketch_tile(), andgeom_sketch_col()/bar()now densify their boundaries before the coordinate transform, so bars undercoord_polar()/coord_sketch_polar()curve into proper wedges and rings instead of straight-edged quads.Radar legend fix.
geom_sketch_radar()no longer leaks anNAkey into a mapped discretefillscale, so its colour and fill legends merge into one (previously two legends, one with a spuriousNAentry).Edge labels are never clipped.
geom_sketch_bump(),geom_sketch_arc_diagram(),geom_sketch_parallel(), andgeom_sketch_dendrogram()now reserve panel room for their end/leaf/axis labels (and the dendrogram’s rotated labels hang cleanly away from the tree), so nothing is cut off at the plot edge undertheme_void().The lightest engraved region always draws.
scale_tone_continuous()’s default band is nowc(0.15, 0.95)(wasc(0.1, 0.95)), keeping the faintest mapped tone above the engraving ladder’s blank-paper threshold – previously the lightestgeom_sketch_shade()region could disappear entirely.Alluvial strata gaps.
geom_sketch_alluvial()gainsstratum_gap(default 0.02): a small vertical gap between adjacent strata so roughened boxes never overlap at their shared edge;0restores flush stacking.Variable-width strokes (Layer 1 + Layer 2).
stroke_ribbon()builds a hand-drawn stroke as a filled polygon ribbon offset around a centreline, so a line can taper to a point, swell with pressure, or vary like a broad calligraphic nib – effectsgridcannot produce with a constant-lwdpolyline.stroke_profile()supplies ready-made width profiles ("taper_in","taper_out","belly","flat"), andsketch_stroke_grob()renders a path as variable-width ribbons, re-roughening the centreline at device resolution (with round or butt caps, optional width jitter for a dry edge, and anib_anglefor calligraphy). This is the foundation the forthcoming media (ink, brush, pencil, charcoal, calligraphy) are built on.Drawing media on the path geoms. A new
mediumargument selects how a line is laid down:"pen"(the default – the classic constant-width double stroke, unchanged), or"ink","brush","pencil","charcoal","marker","crayon", which render through the variable-width engine – tapered ink, brushy belly swells, grainy multi-pass pencil/charcoal, translucent marker, waxy crayon. Available ongeom_sketch_line(),geom_sketch_path(),geom_sketch_segment(), andgeom_sketch_step();sketch_media()lists the options. Existing plots are unaffected (the default stays"pen").Three more drawing media.
"fountain_pen"(a crisp, wet line that pools slightly at the ends),"ballpoint"(a thin, even, faintly skipping stroke), and"pastel"(a broad, soft, grainy and translucent mark, lighter than charcoal) join themediumfamily and thescale_medium_discrete()palette.Waterfall charts.
geom_sketch_waterfall()floats each step’s delta (y) from the running total before it to the running total after it across the categories (x), with dotted hand-drawn connectors carrying the level over the gaps. Bars colour by direction (fill_increase/fill_decrease/fill_total, or mapfilltoafter_stat(change)yourself), an optionalmeasure = "total"aesthetic draws closing-total bars from zero, and anyfill_styleworks (including"watercolor").Gantt / timeline charts.
geom_sketch_gantt()draws one roughened bar per task fromx(start) toxend(end) on a discretey– the whiteboard project-planning look. An optionalprogressaesthetic (0-1) overlays a slimmer, darker solid bar over the completed fraction; bars take rounded corners (corner_radius), dates on x and anyfill_style.Funnel and population-pyramid charts.
geom_sketch_funnel()centres one bar per stage on zero with width equal to the stage’s value, so the shrinking bars read as drop-off, with translucent trapezoid connectors carrying each stage into the next.geom_sketch_pyramid()draws back-to-back bars mirrored about zero by a two-levelsideaesthetic – the population pyramid. Both take everyfill_style.Font-aware export helper.
ggsketch_save()is a drop-inggplot2::ggsave()wrapper that picks a device able to see fonts registered withregister_sketch_font(): ragg for raster formats, svglite for SVG,cairo_pdffor PDF (the basepdfdevice misses registered fonts), and it warns PostScript users towards PDF – ending the “my handwriting font disappeared in the saved file” footgun.One-call style presets.
sketch_style()bundles a paper ground, a qualitative colour/fill palette tuned to it and (on ggplot2 >= 4.0) matching default geom ink into a single+-able object:p + sketch_style("chalkboard"). Presets:"notebook","chalkboard","blueprint","field_notes"and"graphite";sketch_styles()lists them, andpalette = FALSEkeeps your own scales.Highlighter swipes and hand-drawn underlines.
annotate_sketch_highlight()lays a wide translucent chisel-tip band (the new highlighter medium) over a region of interest in one call, andannotate_sketch_underline()draws a quick wobbly underline (strokes > 1re-draws it for an emphatic scrawl).Chalk and highlighter media.
medium = "chalk"draws a broad, dry, flat-ended stroke with a faint halo of settled dust either side of the line – made fortheme_sketch(paper = "chalkboard")with a light stroke colour.medium = "highlighter"lays a single very translucent chisel-tip band with crisp edges, so a swipe drawn under (or over) a pen line reads as a fluorescent emphasis rather than a line of its own.Airbrush / spray medium. A new
medium = "spray"renders a line as a soft cloud of dots scattered around the centreline with no hard outline – the spray-can / airbrush look. Powered by a new Layer-1 samplerspray_scatter()(Gaussian perpendicular offset, density falloff and feathered edges) and a Layer-2sketch_spray_grob()that re-roughens and scatters at device resolution. Selectable per group viaaes(medium = )like the other media.Paper / canvas grounds.
theme_sketch(paper = )paints a simulated paper texture behind the data:"notebook"(blue rules + red margin),"graph","dotted","aged"(warm ground with soft blotches),"blueprint"and"chalkboard"(dark grounds that flip the text light), and"kraft". The ruling is spaced in physical inches so it looks right on any panel aspect, and everything is drawn as vector primitives, so it reproduces on every device.element_sketch_paper()is the underlying panel-background element (usable directly intheme()), andsketch_papers()lists the grounds. The default"none"leaves the theme unchanged.Watercolour fill. A new
fill_style = "watercolor"paints a region with stacked translucent washes instead of stroked fill lines: where the jittered boundary copies overlap the colour deepens toward the interior, the irregular edges feather like pigment bleeding into wet paper, and a scatter of darker specks reads as pigment granulation. Overlapping shapes blend wet-on-wet. It works on every polygon-fill geom (geom_sketch_polygon(),geom_sketch_area(),geom_sketch_col()/bar(),geom_sketch_rect()/tile(),geom_sketch_violin(),geom_sketch_boxplot(),geom_sketch_ridgeline(), …), on the ellipse/circle geoms (geom_sketch_circle(), themark_circle/mark_ellipsefamily), and on the hole-aware band geoms (geom_sketch_contour_filled(),geom_sketch_density_2d_filled()) where the washes respect holes via a new Layer-1watercolor_wash_multi(). Powered by a new Layer-1watercolor_wash(). Everything is vector, so it reproduces on every device.Watercolour media physics: wet-on-wet bleed and ink-into-paper grain. Two couplings make the washes behave more like real pigment. Where two watercolour regions overlap, the pigments now mingle: a new Layer-1
wash_bleed()samples the shared area and lays down soft specks tinted with the blended colour, so the overlap reads as mixed paint (toggle withoptions(ggsketch.wash_bleed =)). And the wash edge now feathers along the paper tooth:watercolor_wash()gains agrainargument that wicks the boundary in coherent capillary channels, andtheme_sketch(paper = )couples it automatically through a newpaper_grain()(smooth notebook/graph grounds wick little; aged and kraft wick a lot).grain = 0is the historical look and draws no extra randomness, so existing seeds reproduce exactly.mediumis now a mappable aesthetic. On the path-like geoms (geom_sketch_line(),geom_sketch_path(),geom_sketch_segment(),geom_sketch_step()) you can map a discrete variable withaes(medium = )to draw each group (or each segment) in a different medium, andscale_medium_discrete()chooses which media the levels map to. The legend keys render in their own medium. Settingmediumas a constant still works exactly as before, and the default is unchanged ("pen").pressureis a mappable aesthetic. Ongeom_sketch_line()/geom_sketch_path(), mapaes(pressure = )to a variable to make the stroke swell and thin along the line, like a pen pressed harder in places. The line then renders through the variable-widthstroke_ribbon()engine even under the defaultmedium = "pen", and combines with any non-penmedium (the width profiles multiply). Values are rescaled by the newscale_pressure_continuous()(default bandc(0.2, 1.6)); wrap inI()for raw multipliers. Per-vertex pressure is carried as an interpolated arc-length profile, so it stays correct when the centreline is re-roughened at device resolution.The sketch colour palette now interpolates.
scale_colour_sketch()/scale_fill_sketch()(andsketch_palette()) used to recycle once a factor had more than eight levels. They now interpolate the eight ink-on-paper anchors into one distinct colour per level via acolorRampPalette()ramp, so the discrete scales keep working for large factors and quasi-continuous use. The first eight colours are unchanged, so small plots look identical; passinterpolate = FALSEfor the old recycling behaviour.coord_sketch(). A drop-in replacement forcoord_cartesian()that draws the frame hand-drawn – the panel gridlines and axis ticks become roughened sketch grobs – under any theme, not onlytheme_sketch(). It reuses ggplot2’s own gridline and axis layout and only swaps how those elements are drawn, so limits, expansion, and clipping behave exactly likecoord_cartesian().rough_grid/rough_tickstoggle each element; combine withtheme_sketch(rough_frame = TRUE)to roughen the panel border too.coord_sketch_polar(). The polar companion tocoord_sketch()– a drop-in replacement forcoord_polar()that draws the circular grid hand-drawn (wobbly concentric rings and radial spokes), so rose / circular bar charts get a frame that matches the marks under any theme. Likecoord_sketch()it reuses ggplot2’s polar layout and only swaps how the grid is drawn (theta,start,directionbehave as incoord_polar()).New chart families:
geom_sketch_dumbbell()andgeom_sketch_slope(). Two hand-drawn comparison charts.geom_sketch_dumbbell()draws a roughened connector fromxtoxendon a sharedy, capped with a sketch dot at each end (separatecolour_x/colour_xend), for showing the gap between two paired values per category.geom_sketch_slope()draws one roughened line pergroupacross an ordered x with a dot at each vertex – a sketch slopegraph for before/after rank changes. Both reuse the existing stroke and point grobs, so they need no new dependencies.New chart family:
geom_sketch_beeswarm(). A hand-drawn beeswarm (dot-strip) plot – at each categoricalxthe points are nudged sideways so they no longer overlap, so the width of the swarm reads as the local density ofy. The offset is computed deterministically in data space (no device metrics, fully reproducible) and the dots are drawn withgeom_sketch_point(), so they keep the usual wobble.widthwidens the swarm;binwidth/nbinscontrol theyrows. No new dependencies (cf.ggbeeswarm::geom_beeswarm()).New chart family:
geom_sketch_ridgeline(). A hand-drawn ridgeline (joyplot) – a kernel density ofxfor each category on a discretey, raised to its own baseline so the ridges overlap and the changing shape of the distribution is easy to compare.scalesets how far the tallest ridge rises (values above 1 overlap),rel_min_heighttrims the tails, and the fill honours everyfill_styleincluding"watercolor". Ridges are drawn back-to-front so nearer ones sit on top. Powered by a newStatSketchDensityRidges; no new dependencies (cf.ggridges::geom_density_ridges()).New chart family:
geom_sketch_waffle(). A hand-drawn waffle chart – a square grid (default 10x10 = 100 cells) where the number of cells of each colour reads as that category’s share of the whole. Map the category tofilland (for summarised data) a count toweight; cells are tallied by a newStatSketchWaffle(largest-remainder rounding) and drawn withgeom_sketch_tile(), so they take the roughened outline and anyfill_style(including"watercolor"). No new dependencies (cf.waffle::geom_waffle()).New chart family:
geom_sketch_treemap(). A hand-drawn treemap – nested rectangles tiling a square, each with area proportional to a value, so the biggest categories take the most space. Map the value toarea, the category tofill, and optionallylabelto write a name in each tile. Rectangles are placed by a new Layer-1 squarified algorithm (treemap_layout(), exported) and drawn with the roughened rect look, so they take anyfill_style. No new dependencies (cf.treemapify::geom_treemap()).New chart family:
geom_sketch_streamgraph(). A hand-drawn streamgraph (ThemeRiver) – a stacked area whose baseline floats so the coloured bands flow around a moving centre. Mapx,y, andfill; a newStatSketchStreamstacks the values and offsets the baseline (offset = "silhouette"centred,"zero"for a normal stacked area, or"wiggle"to minimise the slope), and each band is drawn as a roughened ribbon (anyfill_style, including"watercolor"). No new dependencies (cf.ggstream::geom_stream()).New chart family:
geom_sketch_calendar(). A hand-drawn calendar heatmap in the GitHub-contributions style – one roughened tile per day, laid out as weeks (columns) and weekdays (rows), coloured by a value. Mapdateandfill; a newStatSketchCalendarbuilds the week/weekday grid (week_startchooses Sunday or Monday on top), and tiles are drawn withgeom_sketch_tile()(defaultfill_style = "solid"so the gradient reads). Facet on a year column for multiple years. No new dependencies.Flagship: hand-drawn networks.
geom_sketch_node()andgeom_sketch_edge()draw a graph the sketch way – roughened node markers with optional handwriting labels, and roughened (optionally curved, viacurvature) edges.sketch_graph()is the bridge: give it a two-column edge data frame (or anigraphobject) and it returns ready-to-plotnodesandedgesframes, positioning nodes withforce_layout(), a from-scratch pure-R Fruchterman-Reingold force-directed layout – so the whole flagship needs no graph dependency.igraphis an optional, guarded convenience for ingestion only; extra edge/node columns are carried through as attributes you can map. The layout is seeded and reproducible and never disturbs the global RNG.Flagship: hand-drawn maps (
sf).geom_sketch_sf()is a sketch take onggplot2::geom_sf(): it roughens the boundaries of simple-features geometry in one call –(MULTI)POLYGONfeatures get a hole-aware hachure (or anyfill_style),(MULTI)LINESTRINGfeatures become sketch paths, and(MULTI)POINTfeatures become sketch points. It extracts coordinates from thesfobject up front and draws ordinary sketch layers, so it sidestepscoord_sf()/stat_sf()and plots in planar coordinates – pre-project lon/lat data withsf::st_transform()for a faithful map.sfis an optional, guarded dependency (Suggests); it is only needed when you call this geom.Radar / spider charts.
geom_sketch_radar()draws each series as a closed roughened polygon over evenly spaced angular axes, behind a hand-drawn web of concentric rings, radial spokes and axis labels. Mapaxis,valueandgroup(pluscolour/fill); values are scaled to a common outer ring (rmax) and each polygon takes anyfill_style, including"watercolor". Like the pie geoms it lives in its own square space – pair withcoord_equal(). No new dependencies (cf.ggradar,fmsb::radarchart()).Chord diagrams.
geom_sketch_chord()lays nodes on a circle, each given a rim arc proportional to its total flow, and draws every weighted relation as a ribbon whose ends are sub-arcs joined by curves through the centre. Give it an edge table and thefrom,toandvaluecolumns; ribbons are coloured by source node (add [scale_fill_sketch()] or any fill scale), take anyfill_style, and self-loops are dropped. Pair withcoord_equal(). No new dependencies (cf.circlize::chordDiagram()).Alluvial / Sankey diagrams.
geom_sketch_alluvial()draws two or more categorical axes as stacks of strata, joined by flows whose thickness is the frequency of each category combination. Give it a wide data frame, theaxescolumns (in order), an optionalvalueweight, and an optionalfillcolumn (default: the first axis); flows get raised-cosine edges and anyfill_style. It draws in ordinary x/y space – pair withtheme_void(). No new dependencies (cf.ggalluvial,ggsankey).Motion.
animate_sketch()animates a sketch plot two ways.type = "boil"(default) re-renders it while shifting every roughening seed per frame, so the whole drawing shimmers and re-draws itself like a hand-animated cel (the “boiling line” effect); the shift rides on a new globalggsketch.seed_jitteroption thatresolve_seed()adds to every resolved seed – explicit or inherited – so a plot boils with no change to its code, and frame 1 reproduces the static render exactly.type = "draw_on"instead reveals the finished drawing progressively, as if a hand were drawing it on. The reveal can be a straight wipe (direction"lr"/"rl"/"bt"/"tb"), a diagonal wipe ("diag"), or a circular iris opening from the centre ("radial"), and its pacing across frames follows aneasingcurve ("linear","ease_in","ease_out","ease_in_out"). Frames are stitched into a GIF whengifskiormagickis installed (guarded Suggests, no new hard dependency); otherwise the frame paths are returned. Fully reproducible fromseed.rough_framenow reaches facet strips and the colourbar.theme_sketch(rough_frame = TRUE)already drew the gridlines, panel border and axis ticks hand-drawn; it now also roughens facet strip backgrounds and the continuous-scale colourbar frame and ticks, so a faceted or colour-mapped plot’s whole frame matches the marks. These reuse the existingelement_sketch_rect()/element_sketch_line()elements (which work directly asstrip.background,legend.frameandlegend.ticksin any theme), so no new function or fragile facet/guide subclass is involved.Two new tonal fills:
"stipple"and"pencil_shade".fill_style = "stipple"scatters fine rough dots at random interior points (a pointillist tone with no visible lattice, unlike the grid-aligned"dots");fill_style = "pencil_shade"lays trimmed graphite strokes plus a sparser cross set at a small angle for soft directional shading. Both work on every polygon-fill geom and respect holes on the band geoms (via new multi-ring variants). Density / texture followhachure_gapandroughnessas usual.Ink-into-paper grain now reaches the line media too. The paper-tooth coupling that feathers watercolour washes (
theme_sketch(paper = )) now also roughens the variable-width stroke media (ink, brush, pencil, charcoal, …): on a toothy ground ("aged","kraft") their centreline and wet edge pick up extra grain, so a drawing reads consistently across line and fill. A no-op on plain ground, so existing plots are unchanged.Repelled labels (the sketch answer to ggrepel).
geom_sketch_text_repel()andgeom_sketch_label_repel()place text / boxed labels near their points but nudged apart so they no longer overlap each other or cover the data, each tied back to its anchor by a hand-drawn leader. A new Layer-1 force solverrepel_layout()does the placement (run in device inches at draw time, so the repulsion is even on any panel aspect): overlapping boxes shove apart, boxes slide off the points they cover, and a weak spring keeps each label near what it names. The boxes and leaders are roughened like the rest of ggsketch; the glyphs stay a handwriting font (ADR-0007). No new dependency (cf.ggrepel).Callout leader routing.
geom_sketch_callout()(andannotate_sketch_callout()) gainleader =:"straight"(default),"elbow"(horizontal then vertical, flowchart style) or"curved"(a bowed Bezier, sized bycurvature). The arrowhead re-orients to the leader’s end tangent for every route. Backed by a new Layer-1leader_path().Arrowhead vocabulary. Arrows and callout leaders now take a
head =argument choosing the head style:"triangle_open"(the classic two-stroke V),"triangle_filled","barb"(swept-back harpoon barbs),"fishtail"(a forked swallowtail),"dot"(a blob) or"bar"(a perpendicular tick), andgeom_sketch_arrow()gainsends =("last"/"first"/"both") for double-headed arrows. One Layer-1 generatorarrowhead()(listed bysketch_arrowheads()) drivesgeom_sketch_arrow(),annotate_sketch_arrow()andgeom_sketch_callout()alike. The oldarrow_type("open"/"closed") still works and maps onto the new styles.Faster boil animations (frame caching).
animate_sketch(type = "boil")now computes the plot’s build (statistics, scales, layout) once and only re-tables / re-draws it per frame, instead of rebuilding the whole plot every frame – the boil only ever varied at draw time. Output is unchanged (frame 1 still equals the static render); heavier plots (stats, large data, contours) see the biggest speed-up.gganimate bridge.
boil_gganimate()renders a gganimate animation – any ggsketch plot plus atransition_*()– so that, on top of gganimate’s data tweening (bars growing, points flying between states, a line drawing itself alongx), the hand-drawn lines boil: the roughening seed steps once per frame as gganimate draws it, so the marks shimmer while the data moves. It is the moving-data companion toanimate_sketch()(which boils a static plot), keyed to a per-frame counter so frame 1 is un-boiled and the run reproduces. gganimate is an optional Suggests; frames are stitched with the samegifski/magickbackend, or returned as paths.Parallel-coordinates plots.
geom_sketch_parallel()draws several numeric columns as vertical axes and every observation as a roughened polyline crossing them at its values. Give it theaxescolumns (in order); axes scale independently to a common height by default, andcolourmaps a column to the lines. No new dependencies (cf.GGally::ggparcoord(),MASS::parcoord()).Mosaic plots.
geom_sketch_mosaic()splits the unit square into columns by the marginal counts of one categorical variable, then each column vertically by the conditional counts of a second, so every roughened tile’s area is the joint frequency. Give itx,yand an optionalvalueweight; colour byy(default) orx. No new dependencies (cf.graphics::mosaicplot(),ggmosaic).Sunburst charts.
geom_sketch_sunburst()draws a hierarchy as nested rings of roughened annular sectors: the columns inlevelsdefine the hierarchy from the inner root ring outward, and each deeper ring splits its parent’s angular span by the children’s summedvalue, so a child always nests inside its parent. Colour by the top-level ancestor (fill_by = "root", the classic look), by each node, or by ring. Pair withcoord_equal()+theme_void(). No new dependencies (cf.sunburstR,plotly).Arc diagrams.
geom_sketch_arc_diagram()lays nodes along a horizontal line and draws every weighted relation as a roughened semicircle arching over (or under,side = "bottom") the axis between its endpoints - a linear cousin of the chord diagram. Edges are coloured by source and their width scales withvalue; node points scale with incident weight. No new dependencies (cf.arcdiagram,ggraph).Bump / ranking charts.
geom_sketch_bump()draws each series’ rank at every time point and joins it across adjacent times with smooth roughened curves, so a crossing reads as one series overtaking another. Give it long data withx(time),group(series) andvalue(ranked within each time); colour by series and label the ends. No new dependencies (cf.ggbump).Dendrograms.
geom_sketch_dendrogram()draws a hierarchical-clustering tree with the right-angle elbows roughened into a hand-drawn wobble. Pass a readystats::hclust()object or a numeric data frame (a tree is then computed viahclust(dist(...)), withmethod/distancecontrols). Four orientations (up/down/left/right). Pure base stats; no new dependencies (cf.ggdendro).Marimekko charts.
geom_sketch_marimekko()draws variable-width stacked bars: each column’s width is one category’s (x) share of the total and the stacked segments are a second category’s (fill) shares, so every tile’s area is the joint value. The value-weighted, segment-coloured cousin of the mosaic plot (and reuses its layout); shows each column’s width percent on top. No new dependencies.Coxcomb / Nightingale rose charts.
geom_sketch_rose()gives each category an equal angular wedge whose radius encodesvalue; witharea_true = TRUEthe sector area (radius square-root) encodes value, as in Florence Nightingale’s mortality roses. An optionalfillcategory stacks radially within each wedge. Pair withcoord_equal()+theme_void(). No new dependencies.-
Engraving: tonal shading by line density. A new module shades by the density of hand-drawn hatch lines, the way an etcher or banknote engraver builds a gradient – light areas stay near-blank, shadows accumulate dense cross-hatch. Unlike the fill-pattern packages (which tile a motif), the tone is computed from geometry.
geom_sketch_engrave()shades anx/y/zsurface (highz= dark);geom_sketch_shade()shades each polygon region with a uniform density set by atoneaesthetic, so a mapped value reads as darkness. Powered by a new Layer-1 engine (engrave_fill()/engrave_ladder(), exposed at Layer 1) that lays down a ladder of hatch layers and keeps each only where the tone reaches its threshold, reusing the hole-aware scan-line. A pitch floor (min_gap_in) keeps the darkest tones from exploding into a runaway number of strokes.-
scale_tone_continuous()(aliasscale_engrave()) rescales a mapped variable to a legible tone band, soaes(tone = z)works the wayscale_size()does for size; wrap inI()to use raw tone. -
geom_sketch_engrave()takes anx/y/zgrid directly, or shades raw points through a density stat, e.g.geom_sketch_engrave(stat = "density_2d", contour = FALSE, aes(z = after_stat(density))).
-
Dot plots.
geom_sketch_dotplot()draws a hand-drawn Wilkinson-style dot plot: the data are binned alongxand one roughened circular dot per observation is stacked upward in each bin. Dots stay round on any panel aspect (the diameter is taken from the bin width and converted to device inches at draw time). The sketch analogue ofggplot2::geom_dotplot().Filled contour and 2-D density bands.
geom_sketch_contour_filled()andgeom_sketch_density_2d_filled()fill the bands between contour levels (not just the lines). Each band is a region that may contain holes (the next level up, cut out); a new hole-aware scan-line filler (hachure_fill_multi()/sketch_fill_multi(), exposed at Layer 1) keeps the holes empty, sofill_style = "hachure"and"cross_hatch"work on multi-ring regions as well as"solid". This completes the filled*_filledfamily planned since 1.3.0.Bounding marks.
geom_sketch_mark_circle(),geom_sketch_mark_ellipse(), andgeom_sketch_mark_rect()draw a roughened bounding shape around each group of points – the sketch analogues ofggforce::geom_mark_circle()/geom_mark_ellipse()/geom_mark_rect(), completing the mark family started bygeom_sketch_mark_hull(). Shade them with a mappedfillor leave them outline-only.Lollipop charts.
geom_sketch_lollipop()draws a roughened stem from abaselineto each value, capped with a sketch point – a tidy alternative to bars for ranked or sparse values (cf.ggalt::geom_lollipop()). Sethorizontal = TRUEfor horizontal stems.Empirical CDF.
geom_sketch_ecdf()draws the empirical cumulative distribution as a hand-drawn stairstep (the sketch analogue ofggplot2::stat_ecdf()).Content-aware arrows.
geom_sketch_arrow()(and the one-offannotate_sketch_arrow()) draw a hand-drawn arrow from(x, y)to(xend, yend)with an optional handwriting label. They are “content-aware”: the shaft curvature defaults to an automatic, pleasing bow whose side follows the direction of travel; the roughened arrowhead orients itself to the curve’s end tangent, so it always points at the target however the shaft bends; and the label justifies itself away from the target so it never sits under the shaft.arrow_type = "closed"gives a filled rough arrowhead.Callouts.
geom_sketch_callout()(andannotate_sketch_callout()) draw a handwriting label in a roughened rounded box, optionally with a leader arrow to a target. The box auto-sizes to the label and the leader leaves from the box edge nearest the target – a sketch speech-bubble / callout.Hull marks.
geom_sketch_mark_hull()draws a roughened convex hull around each group of points (the sketch analogue ofggforce::geom_mark_hull()), for circling or grouping clusters; shade it with afillor leave it outline-only.Hand-drawn pie and donut charts.
geom_sketch_pie()draws a sketchy pie (one slice per row, sized by theamountaesthetic and coloured byfill);geom_sketch_donut()is the same with a hole. Slices stay circular on any panel shape (radii are taken from the smaller panel dimension and assembled in device space), so they look right withoutcoord_fixed(). Slices are solid by default, with a rough edge; pass anyfill_style("hachure", …) to hatch them instead.Arc geometry (Layer 1). New
rough_arc()roughens an elliptical arc into open hand-drawn strokes (the open-arc sibling ofrough_ellipse()), and a newsketch_wedge_grob()draws roughened pie/donut sectors. These are the arc sampler that powers the pie/donut geoms.Rounded rectangles and bars.
geom_sketch_rect(),geom_sketch_tile(), andgeom_sketch_col()/geom_sketch_bar()gain acorner_radiusargument (a fraction[0, 1]of each half-side) for rounded corners. Default0keeps sharp corners, so existing plots render identically.
ggsketch 1.6.0
-
roughnessis now a mappable aesthetic on the per-shape geoms. Followinggeom_sketch_point(), you can maproughnessper shape withaes(roughness = )(rescaled byscale_roughness_continuous()) or set a constant on:geom_sketch_col()/geom_sketch_bar(),geom_sketch_rect()/geom_sketch_tile(),geom_sketch_circle()/geom_sketch_ellipse(),geom_sketch_segment()/geom_sketch_spoke(), and the point-basedgeom_sketch_jitter()/geom_sketch_count().sketch_ellipse_grob()now takes a per-shape roughness vector. Defaults are unchanged, so existing plots render identically. Path-like geoms (line, path, area, density, smooth, step, reference lines, …) keeproughnessas a layer parameter, since per-row roughness is ill-defined for a single path. -
Independent fill roughness and seed. Every fill-bearing geom now accepts
fill_roughnessandfill_seed, so the fill texture can be controlled separately from the outline. Previously the fill roughness was hardwired to a fraction of the outline’s (roughness * 0.5for polygons,* 0.4for ellipses) and shared the outline’s seed. The defaults are unchanged (NULLkeeps the historical coupling), so existing plots render identically; set the values for a scratchier fill under a clean edge, or to reshuffle the fill pattern without moving the outline. Exposed onsketch_polygon_grob()/sketch_ellipse_grob()and threaded throughgeom_sketch_col()/bar(),rect()/tile(),polygon(),area()/ribbon(),density(),violin(),circle()/ellipse(),crossbar(),boxplot(),hex(), andsmooth().
ggsketch 1.5.0
Annotation toolkit (first piece), roughness-as-an-aesthetic, and a real solid fill.
-
geom_sketch_bracket()draws a hand-drawn significance / comparison bracket spanningxmintoxmaxat heighty, with short end tips and an optional handwritinglabel(e.g. a p-value or “n.s.”) centred above. The sketch counterpart of aggsignifbracket, for marking pairwise comparisons on boxplots, bars, and violins. -
roughnessis now a mappable aesthetic ongeom_sketch_point(). Map it to a variable (aes(roughness = z)) so each point wobbles more or less, or set a constant. A mapped variable is rescaled to a legible roughness band by the newscale_roughness_continuous()(default rangec(0.01, 0.75)), applied automatically just likescale_size(); wrap values inI()to use them as raw roughness. Rolling the mappable treatment out to the other geoms is planned.
Bug fixes
-
fill_style = "solid"now actually paints the shape with its fill colour instead of leaving the interior empty. Previously “solid” drew the outline only, so the fill colour was computed and then never used and the shape stayed transparent (most visible on solid bars/columns). The fill follows the roughened boundary so the hand-drawn edge is kept. Shapes with no fill (fill = NA) stay outline-only as before. -
geom_sketch_boxplot()now defaults tofill = NA, so the box stays outline-only (as it effectively was before solid fill started painting). Pass afillfor a solid box, orfill_style = "hachure"with afillfor a shaded one. -
theme_sketch(base_family = )now defaults togetOption("ggsketch.base_family", ""), sooptions(ggsketch.base_family = "auto")makes every sketch plot’s text (titles, axes, legend) use a handwriting font, not only the labels drawn bygeom_sketch_text()/geom_sketch_bracket().
ggsketch 1.4.0
The frame can now be roughened too, plus a matching colour palette, a new fill style, and reproducible fonts.
-
Rough theme elements.
element_sketch_line()andelement_sketch_rect()render gridlines, panel borders, and axis ticks as roughened sketch grobs.theme_sketch(rough_frame = TRUE)turns them on so the frame matches the marks. Works on ggplot2 3.5 and 4.0 (S3element_grobmethods; sketch params stored as attributes that survive theme merging). -
Sketch colour scales.
scale_colour_sketch()/scale_fill_sketch()use a qualitative palette (sketch_palette()) chosen to suit the hand-drawn look; the*_sketch_c()variants give a continuous blue gradient. -
New fill style
"scribble"— one continuous winding stroke that overshoots the boundary, like scribbling to fill a shape. Available everywhere afill_styleis accepted. -
Reproducible fonts.
register_sketch_font()registers a handwriting font file (via systemfonts) so the result reproduces on any machine or CI runner without a system install; the font resolver now also finds registered fonts.
Bug fixes
-
geom_sketch_point()(andgeom_sketch_circle()/geom_sketch_ellipse()) now draw each mark in its own colour when the colour/fill aesthetic varies within a single group — e.g. a continuous scale such asscale_colour_sketch_c(). Previously every mark took the first point’s colour, so a gradient looked flat. - The double-stroke outline now shares its vertices across passes, so it reads as one hand-drawn line gone over twice rather than two parallel lines. Previously each pass jittered the vertices independently and the strokes could drift apart on short or flat edges (most visible on outlines like violins).
-
geom_sketch_boxplot()draws the median with much less bowing so the thick median line reads as one firm line rather than a bowed lens. - The handwriting-font resolver (
base_family = "auto",geom_sketch_text()) now handles variable fonts. A face such as Caveat that ships as a variable font cannot be drawn by name on ragg/svglite (the device falls back to the default), so the resolver pins a renderable instance automatically. This is why a handwriting face now shows up out of the box, with no manualregister_sketch_font()call needed.
ggsketch 1.3.0
New geoms (Tier 3, first batch — 2-D density and text):
-
geom_sketch_contour()— hand-drawn contour lines of a surface (viastat_contour). -
geom_sketch_density2d()/geom_sketch_density_2d()— 2-D kernel-density contour lines (viastat_density_2d). -
geom_sketch_hex()— hexagonal binning heatmap (needs the optionalhexbinpackage). -
geom_sketch_text()andgeom_sketch_label()— text in a handwriting font (the sketch of text is the font, not roughened glyphs). They now honournudge_x/nudge_ylikeggplot2::geom_text(), and the font resolver falls back to handwriting faces preinstalled on Windows/macOS (Segoe Print, Ink Free, Bradley Hand, Chalkboard, Comic Sans MS, …) so a sketchy face is found even when Caveat is not installed. Use a font-aware device (ragg, svglite, cairo) to render the handwriting font; the base GDIpng()device may fall back to the default family.
Still to come in Tier 3: filled contour / 2-D density bands (*_filled, which need multi-ring hole-aware hachure) and geom_sketch_dotplot().
ggsketch 1.2.0
New geoms (Tier 2 coverage — more stats and connectors; reuse existing grobs, one new optional dependency):
-
geom_sketch_count()— points sized by overplot count (viastat_sum). -
geom_sketch_function()— sketch a function curve (viastat_function). -
geom_sketch_qq()andgeom_sketch_qq_line()— quantile-quantile points and reference line. -
geom_sketch_quantile()— quantile regression lines (needs the optionalquantregpackage). -
geom_sketch_rug()— marginal ticks along the panel edges (sides). -
geom_sketch_spoke()— segments from(x, y)byangle+radius. -
geom_sketch_curve()— hand-drawn curved connector (quadratic Bezier). -
geom_sketch_bin2d()/geom_sketch_bin_2d()— rectangular 2-D bin heatmap.
ggsketch 1.1.0
New geoms (Tier 1 coverage — all reuse the existing grobs and base ggplot2 stats; no new hard dependencies):
-
geom_sketch_histogram()andgeom_sketch_freqpoly()— binned distributions (viastat_bin). -
geom_sketch_jitter()—geom_sketch_point()with a jitter position. -
geom_sketch_violin()— mirrored kernel density with hachure fill (viastat_ydensity). - Interval family:
geom_sketch_linerange(),geom_sketch_pointrange(),geom_sketch_errorbar(),geom_sketch_crossbar(). - Reference lines:
geom_sketch_abline(),geom_sketch_hline(),geom_sketch_vline().
ggsketch 1.0.0
First public release. A grammar-native, pure-R implementation of the rough.js hand-drawn aesthetic for ggplot2 — no JavaScript, works on every graphics device.
Geoms
- Lines & points:
geom_sketch_line(),geom_sketch_path(),geom_sketch_point(). - Bars & tiles:
geom_sketch_col(),geom_sketch_bar(),geom_sketch_rect(),geom_sketch_tile(). - Areas & curves:
geom_sketch_polygon()(concave-safe fill),geom_sketch_ribbon(),geom_sketch_area(),geom_sketch_density(),geom_sketch_smooth(). - Circular & composed:
geom_sketch_circle(),geom_sketch_ellipse(),geom_sketch_segment(),geom_sketch_step(),geom_sketch_boxplot(). - Helpers:
annotate_sketch()andtheme_sketch()(light + dark presets).
Fill styles
-
hachure,cross_hatch,zigzag,zigzag_line,dots,dashed,solid, via a full Active-Edge-Table scan-line filler that handles concave polygons.
Design
- Three strict layers: pure geometry (Layer 1), grid grobs with
makeContent()(Layer 2), ggproto geoms (Layer 3). Roughening happens in device-inch space. - Reproducible: every randomized routine draws from a seeded local RNG stream and never mutates the user’s global
.Random.seed. Setoptions(ggsketch.seed=). - Sketchy legend keys for every geom.
Notes
- Algorithms reimplemented in original R from published descriptions; no rough.js source is included (see
inst/NOTICE). Independent of, and not affiliated with, rough.js or ggrough.
Development history (internal milestones)
- 0.0.1 — Layer 1 sketch core (roughen, ellipse, Bézier, hachure, fill styles).
- 0.1.0 — Layer 2 grobs + first geoms (line/path/point) +
theme_sketch(). - 0.2.0 — Filled rectangular geoms (col/bar/rect/tile) + fill-style coverage.
- 0.3.0 — Polygons, areas, ribbons, density, smooth.
- 1.0.0 — Circular & composed geoms, annotations, theming, docs, release.
