45  Design laws

gog is governed by nine laws. They are not preferences or style guides; they are rules the package never breaks.

Every new feature, every argument, every naming decision is checked against these rules. If a proposed addition breaks one, it is rejected.

Laws 1–5 govern the daily grammar you write. Laws 6–8 take the Hangeul (한글) analysis furthest, and govern the grammar’s structure: what composes, what renders, and what the engine may refuse. Law 9 says what a specification is, and what it must never contain.

45.1 Law 1: Orthogonality

Every compatible atom combines with every other atom. No atom is redundant.

If color works with point, it works with line, bar, area, and every other mark. Where a channel makes sense for a mark, it is there. color is never withheld from one mark and given to its neighbor.

The rule that follows: if you want a mark-specific version of a channel (point_color vs bar_color), that is a violation. The channel must be generalized, not specialized.

45.2 Law 2: No Exceptions

A transform behaves identically on every mark. > No per-mark special cases.

Without this law you would have to learn each transform again for every mark that can read it. bin cuts the same buckets and counts the same rows whichever mark reads it. Change only the mark, and what changes is the geometry:

gapminder_2007: first 5 of 142 rows
country continent year life population gdp
Afghanistan Asia 2007 43.828 31889923 974.5803
Albania Europe 2007 76.423 3600523 5937.0295
Algeria Africa 2007 72.301 33333216 6223.3675
Angola Africa 2007 42.731 12420476 4797.2313
Argentina Americas 2007 75.320 40301927 12779.3796
((data(gapminder_2007) + bar * bin + x(life)) |
   (data(gapminder_2007) + line * bin + x(life))) /
((data(gapminder_2007) + area * bin + x(life)) |
   (data(gapminder_2007) + step * bin + x(life)))
(((data(gapminder_2007) + bar * bin + x(col.life)) |
   (data(gapminder_2007) + line * bin + x(col.life))) /
((data(gapminder_2007) + area * bin + x(col.life)) |
   (data(gapminder_2007) + step * bin + x(col.life))))
((data(gapminder_2007) + bar * bin + x(:life)) |
  (data(gapminder_2007) + line * bin + x(:life))) /
  ((data(gapminder_2007) + area * bin + x(:life)) |
  (data(gapminder_2007) + step * bin + x(:life)))
below(beside(plot(data(gapminder_2007), layer(bar, bin), x(col.life)),
  plot(data(gapminder_2007), layer(line, bin), x(col.life))),
  beside(plot(data(gapminder_2007), layer(area, bin), x(col.life)),
  plot(data(gapminder_2007), layer(step, bin), x(col.life))))
50 60 70 80 0 10 20 30 Count Life 50 60 70 80 10 20 30 Count Life 50 60 70 80 0 10 20 30 Count Life 50 60 70 80 0 10 20 30 Count Life

“Given gapminder 2007: bars derived by bin, x is life, beside a line derived by bin, x is life; above an area derived by bin, x is life, beside a step outline derived by bin, x is life.”

One cut and one set of counts, four times over: rectangles, a line through the bin centers, that same line filled, and the counts as a staircase. Read any bar’s height, then read the same bin in the panel beside it: the number is the same. The transform never asks which mark will draw its result.

This is the hardest law to keep as the package grows. Every user request that begins “but for maps I need…” or “histograms are special because…” is a pressure to break it. What keeps it is that the permitted bindings sit in one table inside the engine, covering every mark × channel pair. A mark cannot leave a channel out, because there is no place to leave it out.

45.3 Law 3: Plain Names

Every taught name is a common English word. No acronyms. No abbreviations. Two words maximum, joined by _. Only x, y, z are literal exceptions.

The name you may know The plain name here
alpha opacity
loess, stat_smooth smooth
coord_polar polar
geom_point, aes point, and channels called by their plain names: x( ), color( )
color_by, size_by | color, size |

If a new user has to look up what a name means, it fails this law.

The last row is the one where a word was removed rather than replaced, and it repays a second reading because the word is not lost. A plotting library could reasonably write color_by(species) and size_by(population). The suffix marks the argument as a column, which says nothing here, because every atom that takes an argument takes a column. Now read color(continent) aloud. It is “color by continent”. The word by is already in how you read the sentence, so writing it into the name says it twice. That is a silent letter by Law 2’s measure, and every channel would have had to carry one.

45.3.1 One spelling of English

gog writes American English everywhere: color, center, gray, and every word of the kernel. There is no British alternative. ggplot2 takes both spellings, so scale_colour_manual() and scale_color_manual() are one function there; here there is one spelling and no second one to learn.

Two spellings for one word is a silent letter, and Law 2 refuses silent letters in the vocabulary as well as in the grammar. The cost is invisible to the person who already knows both forms, and everyone else pays it. A reader of your code must recognize two shapes for one idea, a search for color( finds only half the code, and every document that lists the vocabulary lists it twice.

Typing colour is the likeliest way to meet this law, so the refusal names the word to write:

data(gapminder_2007) + point + x(gdp) + y(life) + style(colour = "tomato")
data(gapminder_2007) + point + x(col.gdp) + y(col.life) + style(colour = "tomato")
data(gapminder_2007) + point + x(:gdp) + y(:life) +
  style(colour = "tomato")
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  style({ colour: "tomato" }))
Error:
! gog: `style(colour = )` is not a setting. gog spells it `color`: American English is the grammar's only spelling, and unlike ggplot2 there is no British alternative.

That is the setting. The channel is refused the same way, and colour exists in the package for no other purpose: it is not a channel, it is a word whose only job is to name the channel you meant.

data(gapminder_2007) + point + x(gdp) + y(life) + colour(continent)
data(gapminder_2007) + point + x(col.gdp) + y(col.life) + colour(col.continent)
data(gapminder_2007) + point + x(:gdp) + y(:life) + colour(:continent)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  colour(col.continent))
Error:
! gog: there is no `colour()` channel. gog spells it `color(<column>)`: American English is the grammar's only spelling, and unlike ggplot2 there is no British alternative.

The same holds for border_colour and centre. Everywhere else in the grammar, the American spelling is simply the only one there has ever been.

45.4 Law 4: Bind Once

A table is named once. Columns are then bare names. The nearest data() wins.

Other plotting libraries often repeat the table in every call, or quote each column as a string. Naming the table once is what lets the rest of the sentence read as plain words.

# Correct: table named once, columns bare
data(gapminder_2007) + point + x(gdp) + y(life)
data(gapminder_2007) + point + x(col.gdp) + y(col.life)
data(gapminder_2007) + point + x(:gdp) + y(:life)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“Given gapminder 2007: points, x is gdp, y is life.”

Quoting a column name is a habit brought from other plotting libraries rather than a typo, so gog says which fix it wants:

data(gapminder_2007) + point + x("gdp") + y("life")
data(gapminder_2007) + point + x("gdp") + y("life")
data(gapminder_2007) + point + x("gdp") + y("life")
plot(data(gapminder_2007), point, x("gdp"), y("life"))
Error:
! gog: `x("gdp")` binds a *value*, and a channel takes a *column*: `x(gdp)` maps the column called `gdp`.

This law makes a plot specification readable as an English sentence. It also lets your session offer column names as you type, and lets a mistake be refused while you are still writing rather than when the plot is drawn.

45.5 Law 5: Explicit Over Implicit

Short is better than long, unless short is ambiguous. Then say it out loud.

Implicit behavior saves typing now and costs clarity later. gog chooses the other way: when brevity would leave a reader guessing what the engine decided, the engine makes you say it.

This law is also what the error messages are for. A refusal must tell you what gog assumed or what gog cannot do, never merely that something went wrong.

45.5.1 Setting is not mapping

The tempting short spelling for a constant color is color("red"), reusing the channel and letting the quotes decide the meaning. gog does not reuse the channel. Quoting a column name is a habit rather than a mistake, so color("continent") would be genuinely ambiguous: a column, or a color named “continent”. Deciding by “is it a column in this data?” would make the same expression mean different things against different tables.

So gog says it out loud. Mapping is a channel; setting is style():

data(gapminder_2007) + point + x(gdp) + y(life) +
  style(color = "seagreen", opacity = 0.5)
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  style(color = "seagreen", opacity = 0.5))
data(gapminder_2007) + point + x(:gdp) + y(:life) +
  style(color = "seagreen", opacity = 0.5)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  style({ color: "seagreen", opacity: 0.5 }))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

“Given gapminder 2007: points, x is gdp, y is life, colored seagreen, with opacity 0.5.”

This also gives the two operations the different rules they deserve. A mapping earns a guide and a setting does not, and line, which cannot map opacity because one stroke has one opacity, can still set it.

ggplot2 puts the same boundary in a different place. There the boundary is a pair of parentheses: color = "red" inside aes() maps, and the same words outside it set. gog moves that boundary from a punctuation mark to a word, so the atom you write says which one you meant. The parentheses have one failure that a word cannot have. Writing aes(color = "red") is legal, so it maps every row to a category with a single level. You get a legend with one key labeled “red”, and points drawn in the palette’s first color rather than in red.

45.5.2 A missing name must not become a default

A palette name that matches nothing could be answered two ways: refuse it, or quietly draw in the default colors. gog refuses, and the message lists every name it would have accepted:

data(gapminder_2007) + point + x(gdp) + y(life) +
  color(continent) + palette("red")
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  color(col.continent) + palette("red"))
data(gapminder_2007) + point + x(:gdp) + y(:life) + color(:continent) +
  palette("red")
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  color(col.continent), palette("red"))
Error:
! gog: `palette("red")` is not a known palette. Named palettes are gog, okabe, soft for categories; blue, viridis, magma, inferno, plasma, cividis, gray for numbers; blue_red, brown_teal for numbers that diverge from a center. `"red"` is a color, not a palette. To paint every mark one color use `style(color = "red")`; to give each category its own color pass a vector: `palette(c("red", ...))`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

A default is the quiet way software breaks this law: the plot still appears, so nothing looks wrong, and the person reading the plot is the one misled.

45.5.3 Illegal is not the same as unsupported

gog separates two refusals, because they ask different things of you:

Refusal Meaning What to do
Illegal The grammar forbids it. It will never work. Rewrite the expression.
Unsupported The grammar allows it; this engine does not draw it. Use another atom that renders.

Calling a valid combination “illegal” would teach you a rule that does not exist. So gog says which it is.

medals: all 5 rows
country gold silver bronze
USA 46 37 38
China 38 31 22
Great Britain 29 17 19
Russia 19 18 9
Germany 17 10 15
# Illegal: size encodes magnitude, and a category has none
data(gapminder_2007) + point + x(gdp) + y(life) + size(continent)
data(gapminder_2007) + point + x(col.gdp) + y(col.life) + size(col.continent)
data(gapminder_2007) + point + x(:gdp) + y(:life) + size(:continent)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  size(col.continent))
Error:
! gog: `size(continent)` maps a categorical (text) column, but `size` on `point` needs a continuous (numeric) column. Use `color`, `shape`, or `pattern` to distinguish categories.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
# Illegal: a bar has no size feature at all; its extent is y
data(medals) + bar + x(country) + y(gold) + size(gold)
data(medals) + bar + x(col.country) + y(col.gold) + size(col.gold)
data(medals) + bar + x(:country) + y(:gold) + size(:gold)
plot(data(medals), bar, x(col.country), y(col.gold), size(col.gold))
Error:
! gog: `size` cannot be bound to `bar` — a bar has no size feature. Remove the `gold` mapping from `size`, or use a mark that has one.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.
# Illegal: `z` stands a `point`, `path` or `bar` up into the cube (see Space),
# but a `line` is read along its x axis, and a cube has no left to right
data(medals) + line + x(country) + y(gold) + z(silver)
data(medals) + line + x(col.country) + y(col.gold) + z(col.silver)
data(medals) + line + x(:country) + y(:gold) + z(:silver)
plot(data(medals), line, x(col.country), y(col.gold), z(col.silver))
Error:
! gog: `line` reads a *domain* left to right — it sorts by `x` and draws one value for each — and a cube has no left to right: `x` is one of three equal positions, and at some viewing angles it runs into the page and becomes depth. A `line` in space would be sorted by an axis the reader cannot see, so this is refused rather than drawn. For a route through three dimensions use `path`, which is `line` with that sort removed: `path + x(<a>) + y(<b>) + z(silver)`.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

That third one is worth reading closely, because it is the kind that is easy to mistake for a missing feature. A 3-D line is not unbuilt. It is ruled out, and the refusal says why: a line sorts by x and draws one value for each. A cube has no left to right, so there is no axis for that sort to run along. The direction it gives is path, which is line with that sort removed, and it does draw in space.

An Unsupported refusal looks different. Here is one:

# Unsupported: `z` is valid grammar for `text`; this engine has not built it
data(medals) + text + x(country) + y(gold) + z(silver) + label(country)
data(medals) + text + x(col.country) + y(col.gold) + z(col.silver) + label(col.country)
data(medals) + text + x(:country) + y(:gold) + z(:silver) +
  label(:country)
plot(data(medals), text, x(col.country), y(col.gold), z(col.silver),
  label(col.country))
Error:
! gog: `z` is valid grammar for `text`, but this engine does not draw it yet — `z(silver)` would have no visual effect. Remove it, or use a channel that renders.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

In every case gog refuses to render. It does not quietly drop the channel and hand you a plot that looks finished. A silently ignored binding is a silent letter in software: written, and with no effect on what comes out.

The four refusals above are live: the engine prints them as this page builds. The last one is a gap the engine can close; the first three are rulings that never close. That is the whole distinction. A check re-runs every refusal in this book and fails if one has started drawing instead.

45.5.4 Drawing it anyway

Every refusal the engine makes ends with the same offer: fix it, or set GOG_STRICT=0 to draw anyway. That switch turns refusals into warnings. You get the same message, in the same words, and you get the plot underneath it.

Set it on a line of its own, before the sentence you want drawn:

Language Set it with
R Sys.setenv(GOG_STRICT = 0)
Python os.environ["GOG_STRICT"] = "0"
Julia ENV["GOG_STRICT"] = "0"
JavaScript process.env.GOG_STRICT = "0"
Shell export GOG_STRICT=0

Only 0 works. GOG_STRICT=false and GOG_STRICT=no still refuse, because a switch this simple is better with one spelling than with a list of words to guess at.

It then stays on for the rest of your session, which is the part worth remembering: every later refusal is a warning too, and readers stop reading warnings. Clear the variable again once you have seen the plot you wanted.

You get the plot with the refused part left out, so bar + size(gold) draws the bars and no size.

The switch cannot reach a refusal your binding makes before the engine is asked, because there is no half-drawn plot to offer. Your binding catches a quoted column name, a British spelling, a sentence that does not begin with data(), and a malformed atom such as box(whiskers = "middle").

45.6 Law 6: Compositional Invariance

A composed sub-expression means the same thing in every context. No enclosing expression may silently reinterpret an inner one.

The Korean syllable 하 (ha) is pronounced identically in 하늘 (haneul, sky), 하루 (haru, a day), and 하지만 (hajiman, but). Its reusability comes from stability, not from meaning. English composition is not stable: “ough” is a different sound in though, through, thought, tough, cough, and bough. A gog expression must be 하, never “ough”: layering, faceting, or animating a plot may add to it, but may not change what the existing part meant.

You can see the law by using one sub-expression in two sentences. bar * bin is one sub-expression: a mark and its transform, composed once.

data(gapminder_2007) + bar * bin + x(life)
data(gapminder_2007) + bar * bin + x(col.life)
data(gapminder_2007) + bar * bin + x(:life)
plot(data(gapminder_2007), layer(bar, bin), x(col.life))
50 60 70 80 0 10 20 30 Count Life

Now put that same block inside a faceted sentence:

data(gapminder_2007) + bar * bin + x(life) | facet(continent)
data(gapminder_2007) + bar * bin + x(col.life) | facet(col.continent)
data(gapminder_2007) + bar * bin + x(:life) | facet(:continent)
plot(data(gapminder_2007), layer(bar, bin), x(col.life),
  across(col.continent))
50 60 70 80 0 5 10 15 50 60 70 80 50 60 70 80 50 60 70 80 50 60 70 80 Asia Europe Africa Americas Oceania Count Life

“Given gapminder 2007: bars derived by bin, x is life, split into panel columns by continent.”

The enclosing expression added panels. It did not reinterpret the sub-expression: each panel bins and counts exactly as the single plot did. That is the whole law, and it is what lets you read a long sentence one piece at a time. A sub-expression can be held in a variable too, piled <- bar * bin, and spoken wherever you like.

45.7 Law 7: Minimum Syllable

A visible plot is a mark plus its required positions. A mark alone is silent; a position alone has no carrier.

In Hangeul the smallest pronounceable unit is a consonant plus a vowel: ㄱ (g) alone is silent, and 가 (ga) is the minimum syllable. A bare vowel cannot stand alone either: ㅏ (a) must be written 아 (a), on a silent carrier.

A mark is the silent consonant: point with no positions draws nothing, because there is nowhere to draw it. A position is the vowel that needs its carrier: x(gdp) without a mark answers no question. It does not say what sits at that x. gog refuses the incomplete syllable, and the refusal names every missing half:

data(gapminder_2007) + point
data(gapminder_2007) + point
data(gapminder_2007) + point
plot(data(gapminder_2007), point)
Error:
! gog: `point` needs `x()` but none is set. Add `x(<column>)` — a point cannot be drawn without it.
gog: `point` needs `y()` but none is set. Add `y(<column>)` — a point cannot be drawn without it.
gog: nothing was rendered. Fix the above, or set GOG_STRICT=0 to draw anyway.

45.8 Law 8: Pronounceable ≠ Usable

The grammar guarantees well-formedness, never good taste. Grammar is law; taste is advice.

꿲 (kkwek) obeys every Hangeul syllable rule, you can pronounce it, yet no Korean word uses it. Plots work the same way. This renders, because it is grammatically sound; whether it says anything is your judgment, not the engine’s:

# Legal, and nearly invisible: the engine's job is to obey, not to veto
data(gapminder_2007) + point + x(gdp) + y(life) +
  style(opacity = 0.05)
(data(gapminder_2007) + point + x(col.gdp) + y(col.life) +
  style(opacity = 0.05))
data(gapminder_2007) + point + x(:gdp) + y(:life) + style(opacity = 0.05)
plot(data(gapminder_2007), point, x(col.gdp), y(col.life),
  style({ opacity: 0.05 }))
0K 10K 20K 30K 40K 50K 40 50 60 70 80 Life Gdp

It draws, and almost nothing is visible. No legal combination is ever refused for being ugly: one that looks pointless today may be a real plot tomorrow, so refusing it now on taste grounds removes something nobody has thought of yet.

The law has a second half, and the two viewing angles of a cube show both at once. space(turn = 390) is a full circle plus 30 degrees, so it is the same view as space(turn = 30), and gog draws it without comment. space(tilt = 95) is refused. The difference is not that one is uglier. A bearing wraps, so 390 names a view you can stand in. A tilt runs out at 90, where you are looking straight down, so 95 names no view at all. Taste is guided softly and never blocked; well-formedness is enforced hard. Space draws both.

The engine will not judge, so here is the advice instead. gog exists so that you can say a visualization in clean, minimal syntax. It will let you combine anything the grammar allows, and that includes combinations that hide what you wanted to show. Be like a good writer, and do not let decoration distract from the main idea.

That advice is about the picture, and it applies to the sentence too. Two sentences can build the identical plot while one is much harder to follow. A second data() applies to a single mark, so a mark written after it returns to the plot’s first table. A reader who has not learned that rule will read such a sentence wrong. gog accepts either arrangement, so the fix is a habit rather than a rule, and Data gives it.

45.9 Law 9: Universal Transcription

A specification describes the visual, never one renderer’s drawing commands.

A specification is declarative: you say what you want to see, never how to draw it. The form is far older than software. “And God said, Let there be light: and there was light” (King James Bible, 1769, Genesis 1:3) states an outcome and nothing else. There is no procedure in it, no order of steps, and no mention of how the light is made. Every declarative sentence leaves that part out, and something else has to supply it. In gog the engine does, and every stroke on these pages answers a sentence that never mentioned strokes.

What you write becomes a small, renderer-neutral specification (which marks, which columns on which channels, which transforms), and that is what renders. One renderer reads it, and it produces the SVGs on these pages. The same specification, unchanged, is what the Python, Julia and JavaScript bindings build. It is also why a specification names no shape and no stroke: it says what to see, and the renderer decides how to draw it.

45.9.1 Why that renderer writes SVG

The law says the specification is neutral. It does not say which format the renderer writes, and SVG is a choice with reasons.

The first reason is the one you can see. An SVG stores shapes and coordinates, not a grid of colored dots. A plot therefore redraws at any size it is given, and stays sharp at every one. Print one at poster size and its curves are still curves.

The second is that text stays text. Every axis label, tick number and title is written as characters in the file, not drawn as a picture of characters. You can select them, a search can find them, and a screen reader can read them aloud. It is also what lets a plot in this book carry Hangeul, or any other writing system, without the engine knowing anything about the letters.

The third reason is the one this project depends on most, and it is easy to miss. An SVG is a text file, so two of them can be compared character by character. That is what makes the promise of four bindings checkable: every sentence in this book is drawn from R, Python, Julia and JavaScript, and the four files must match exactly. Comparing pictures instead would mean comparing colored dots, which changes with the version of whatever drew them. An exact test would become an approximate one, and a small disagreement between two languages could pass as a rounding difference.

The fourth is motion. A play animation is written into the same file as the plot, using SVG’s own timing elements. A time-lapse is one picture that moves, with no video file and no JavaScript beside it.

45.9.2 Why there is one renderer

gog has exactly one renderer, and the reason is what a second one would have to copy.

A renderer decides five things the specification does not name: layout, tick selection, palettes, axis ranges, and label formatting. Write a second renderer beside the first and those five decisions exist twice. Two copies of a decision do not stay equal. One would bin and count, the other would draw the raw rows, and bar * bin would then mean two different pictures from one specification.

That would be Law 9 broken by the renderer rather than by the specification: a renderer deciding things the specification is supposed to own, with nothing in the design to make it visible. So there is exactly one renderer. A second one is possible only once those five decisions live in a single shared layer that both would read.

Raster output comes from converting the SVG rather than from a second renderer. render_svg() hands you the plot as ordinary text, in every binding:

svg <- render_svg(data(gapminder_2007) + point + x(gdp) + y(life))
cat(substr(svg, 1, 51))
<svg xmlns="http://www.w3.org/2000/svg" width="800"

Those are the first 51 characters; svg holds the whole plot, about 14,000 of them. Writing it to a file saves it, and any tool that reads SVG turns it into another format: rsvg-convert -w 1600 plot.svg -o plot.png gives you a raster image, and rsvg-convert -f pdf is what places each plot in this book’s PDF. Converting is the deliberate answer rather than a missing feature. It keeps every decision about what a plot looks like in the one place that is allowed to make them, which is the whole of the law above.

The engine does that conversion itself in one case, and the case is worth seeing because it shows where the boundary sits. A plot that binds play() moves in a browser and nowhere else, so save_gif() writes the same sequence as a picture that a slide or a message will play. That is still conversion in the sense this section means. The frames come out of the one renderer, and the GIF writer only turns them into pixels. It chooses no tick, no color and no layout, so there is nothing for it to drift from.

45.10 The settable rule

A setting (style(…)) is an atom like any other, so the first two laws govern it as well: a setting is available on every mark whose geometry can carry it, behaves the same across them, and is absent exactly where composition already expresses the feature more capably. The first clause is No Exceptions: no mark-specific gap you have to memorize; the second is Orthogonality’s non-redundant half: no setting for what marks already do by combining.

That is why style(border_color =, border_size =), an outline distinct from the fill, is available on all five closed-glyph fills (bar, box, point, zone, surface), not one of them: their perimeter is a shape you cannot trace by composition, so a setting is the only way to draw it. And why style(pattern =), the paint’s texture, is realized once per geometry: on the six path strokes (line, step, interval, path, rule, edge) as a dash, and on the five fills (bar, box, area, ribbon, zone) as a hatch: one setting, one form per geometry, the way color is a stroke on a line and a fill on a bar. A curve fill (area, ribbon) takes the hatch (a region can be textured) but no border: its edge is a data curve, which already is a line (area + line, line * bounds), more capable than any border. The classes, not the individual marks, decide.

The failure this prevents is the quiet surprise: learning a setting on one mark and finding it missing on its sibling for no reason you can see. Where a class is incomplete, gog says so with an Unsupported message rather than a silent nothing, so the gap is visible.

45.11 Why these laws matter

Each law refuses the same enemy, the expert’s shortcut.

The requests for a special case are always reasonable ones:

  • “Can bar have a different color scale than point?”
  • “Can smooth have a span parameter?”
  • “Can I just write alpha? Everyone knows what it means.”

Each request sounds reasonable in isolation. Each one costs more than the one before it: the tenth exception makes the first nine harder to remember.

The laws are the answer: not “no, because we don’t want to,” but “no, because that would break Law 2 / Law 3 / …” The rule, not the maintainer, says no.