Added
- **JavaScript: `html_block()` returns a plot the reader can turn.** The same
SVG `render_svg()` writes, wrapped for a page, with the controls beside it
and the engine that turns a cube or a globe. Write it into a page, a notebook
cell, or a dashboard panel. It was in the package and reachable from nothing:
a JavaScript reader could get a turnable plot in a browser window through
`show()`, and a still picture everywhere else, while R, Python and Julia each
handed a notebook the turnable one automatically. Those three answer a
question their host asks an object about how to display itself; JavaScript
has no such question, so it gets a function.
- **`network()` is a coordinate space, `layout` places a graph in it, and
`edge` is the fourteenth mark: the network diagram.** `layout(from, to)`
reads the two endpoint columns of an edge table — one row is one relation —
and the engine computes a position for every distinct name, the same way in
every language, so one sentence is one picture to the byte. Three marks read
the placement: `edge * layout(from, to)` draws the connections, `point *
layout(...)` the nodes, `text * layout(...) + label(name)` their names, and
the layout publishes each node's `name` and `degree` so `size(degree)` reads
its busyness. The space draws no axes, no ticks and no grid, because a
layout's positions mean nothing as quantities; for the same reason a bound
position, a brush, or an axis label is refused with its reason rather than
drawn. Stating a viewing angle states the cube — `network(turn = 35, tilt =
20)` computes the same layout in three dimensions and draws the glass box
without numbers, and in the web edition it turns with a drag. A row from a
node to itself is refused with its count; a row missing an endpoint is left
out and counted. Edges map `color` by any of their columns and `opacity`
continuously, the stroke that fades by weight.
- **`flow` lays a magnitude through its stages: the flow diagram.** The stages
are categorical columns named in the atom, in reading order, and one row of
the table is one path through all of them — `ribbon * flow(class, sex,
survived) + y(n)` draws the Titanic's people as bands, each as thick as its
path's count at both ends. Three marks read the one layout: `ribbon` the
bands, `zone` each stage's stacked slots, and `text * flow(...) +
label(name)` their names, so the layers always agree about where everything
sits. `color(<stage>)` on the band layer colors every band by the category
its path holds at that stage; a column outside the stages is refused, since
only a stage holds a value a whole path carries. Bind nothing to `y` and
every row weighs 1, the same tally `partition` falls back on. The stacks are
contiguous, so the measure axis keeps its ticks and reads true cumulative
magnitude, and rows missing a stage value are counted out loud rather than
dropped in silence. Bending a flow into `polar()` — the chord diagram — is
valid grammar the engine does not draw yet, and says so.
- **`globe()` is a coordinate space: the earth itself, viewed.** `x` is
longitude and `y` is latitude, exactly as on `map()`, and the same five marks
stand on the sphere's facing half — a `point` at a place, a `path` bending
along great circles, a `text` naming a place, a `rule` holding a whole
meridian or parallel, and a `zone` with `group()` filling each region of a
boundary, where a country the horizon cuts is closed again along the edge of
the disk. `globe(turn =, tilt = )` names the place the view faces, a bearing
that wraps and a latitude that stops at the poles. Rows on the far half are
hidden behind the sphere and the plot says how many, never dropping them in
silence. The graticule is the panel grid, so `theme(grid = )` reaches it, and
a globe draws no axes at all. In the web edition, dragging turns the globe
the way it turns the cube. A binned field on the sphere is designed and not
drawn yet: its correct tiling is hexagonal, and that equal-area grid is not
built, so `zone` without a boundary refuses with the reason named.
- **A `bar` on the globe is a spike: the measure standing on the radius.** The
flattened map has no axis to spare, and the sphere has exactly one — the
radius, pointing away from every place — so `bar + x(<lon>) + y(<lat>) +
z(<column>) + globe()` stands a spike at each place, measuring outward from
the surface against the fitted top. The sphere itself is the clip: a spike
just behind the horizon still peeks over the limb when it is tall enough. A
value below zero has nowhere to point and is counted out loud, a `bar`
without `z` is asked for its measure, and `z` with no `bar` to read it is
refused rather than ignored. Shaping the measure is the host's line of code:
`z` takes no `scale` here, and the refusal says why and what to write.
- **`cluster` joins the closest leaves one pair at a time: the cluster tree, and
the clustered heatmap.** The leaves are the levels of the bound categorical
position, each described by its value at every level of a profile column —
`path * cluster(amount, over = nutrient) + x(food)` draws the tree, with
the merge distance on the unbound axis, which names itself; `y(food)` lays
the same tree on its side. The statistic is fixed and stated: Euclidean
distance on the values as given, branches joined at their average distance,
leaves ordered so the closest neighbors sit adjacent. `zone * cluster(over
= nutrient)` is the second reading: the tile plot unchanged, its slots
reordered to the tree's leaf order. Composed with `|` and `/`, a clustered
panel decides the order of any categorical axis it shares, so trees above
and beside a plain tile plot are the whole clustered heatmap — three plots
and two operators, no new figure type. A profile with a missing or doubled
cell is refused with the pair named, `order()` against the tree's own axis
is refused as two orders for one axis, and two composed panels deriving
different orders refuse rather than letting one win in silence. Coloring
subtrees, the circular tree, and clustering both axes in one sentence are
valid grammar the engine does not draw yet, and each says so.
Changed
- **A repelled label now rests on whichever side of its point has room.**
`text * repel` pulled every label back toward its starting place above its
dot, and moved colliding labels apart only straight up, down, left or right
— so nearly every name ended up stacked on top of its point, 24 of 26 on the
fixture that measured it, where ggrepel spreads them wherever there is room.
The placement now pulls each label toward its own point, pushes it off every
word and every dot along the line between their centers, and lets the two
motions settle just clear of the dot on the free side. A lone label still
rests above its point, `style(nudge = )` still names the side a label
prefers, the count of labels still touching is still reported, and the
placement is still deterministic. Reported, measured against ggrepel, and
prototyped by lh (2).
- **`repel` now composes with every transform, `layout` and `flow` included.**
It was classified as a position modifier and so collided with any transform
that places its own marks, but repel moves *ink*, at draw time, after every
computation has run — it decides where a word rests, never where a mark
sits. `text * layout(from, to) * repel + label(name)` is the network whose
names step off their dots, and the same composition now works wherever
`text` reads a computed placement.
- **The boundary refusal on `zone` names both geographic spaces.** Writing
`zone + group(<column>)` outside them is refused as before, and the
direction now says to add `map()` or `globe()` if the column names regions
on a boundary — it named only `map()` while `map` was the one space that
could read one.
- **A written `space(tilt = )` outside -90 to 90 is now refused.** `tilt` is how
high your eye is, and height runs out: at 90 you look straight down and at -90
straight up, so past either end the scene hangs upside down with all three axis
names piled into one corner. Dragging already stopped at both ends, and writing
now does too. `space(turn = )` is unaffected and stays silent at any value,
because a bearing genuinely wraps: 390 is 30, and refusing both angles alike
would teach a cap the grammar does not have.
- **`smooth` now refuses a group with fewer than three rows.** Two points are a
straight line and one is a point, so there is no curve to fit. Below three rows
the curve could not be computed and the rows themselves were drawn instead,
which put a two-point segment beside a hundred-point curve with nothing to say
that one was a fit and the other the data. The count is per group and per panel,
not per table, since a statistic runs inside each group and faceting splits
before it — so a table of 284 rows split into 142 pairs is refused, and the
message names the split to give up.
Fixed
- **Turning a cube past a full circle no longer loses axis labels.**
`space(turn = -360)` is the same view as `space(turn = 0)` and drew the
same marks and the same box, but two of eighteen tick numbers went missing: an
axis silently lost part of its own scale, with every mark in place and nothing
reported. Equal bearings now draw the same picture to the byte, however many
laps the number carries.
Removed
- **Three warnings about a missing `x()` or `y()` are gone.** Each said
"Rendering empty chart", and each named a plot that was already refused with
direction, so the message was never the only voice and never the deciding one.
It also described neither outcome: nothing is rendered when the plot is refused,
and a chart *is* rendered under `GOG_STRICT=0`. The refusals themselves are
unchanged.
- **`gog_table()` refuses a table name the book does not have.** It says which
name it could not find, and when one table is within two letters it names that
table. JavaScript was the worst of the four: a name that does not exist gets a
404 page from the site, and that page was parsed as a table, so the caller
received 88 rows in a column named `<!DOCTYPE html>`. The other three stopped,
but each with its host language's words for a failed request, which
`except GogError` and `catch (e instanceof GogError)` do not catch. All four
now refuse the same way the rest of the grammar refuses, and a site that cannot
be reached is told apart from a name that does not exist.
- **R's `save_gif()` suggests a path in the directory you asked for.** Given
`out/sub/wave.png` it answered `save_gif(p, "wave.gif")`, dropping everything
but the file's name, so following the advice wrote the file into the working
directory. Python, Julia and JavaScript already kept the directory.