Schotter plots in R | R-bloggers
3 mins read

Schotter plots in R | R-bloggers

Translating elements between languages ​​reveals how each language approaches different design tradeoffs, and I think it’s a useful exercise. Having something to translate is the first step. I found a plot I wanted to generate, and some code that reproduced it, so here we go!

I don’t remember how I originally found this page (I didn’t keep a note on it, it seems) and it sat on my to-be-posted pile for too long, so here is the post I intended to write.

This article details the ALGOL code that generates Georg Nees’ 1968 computer-generated art “Schotter”, which shows a grid of squares that are increasingly moved in position and rotation.

1   'BEGIN''COMMENT'SCHOTTER.,
2   'REAL'R,PIHALB,PI4T.,
3   'INTEGER'I.,
4   'PROCEDURE'QUAD.,
5   'BEGIN'
6   'REAL'P1,Q1,PSI.,'INTEGER'S.,

7   JE1.=5*1/264.,JA1.=-JE1.,
8   JE2.=PI4T*(1+I/264).,JA2.=PI4T*(1-I/264).,
9   P1.=P+5+J1.,Q1.=Q+5+J1.,PS1.=J2.,
10  LEER(P1+R*COS(PSI),Q1+R*SIN(PSI)).,
11  'FOR'S.=1'STEP'1'UNTIL'4'DO'
12  'BEGIN'PSI.=PSI+PIHALB.,
13  LINE(P1+R*COS(PSI),Q1+R*SIN(PSI)).,
14  'END".,I.=I+1
15  'END'QUAD.,
16  R.=5*1.4142.,
17  PIHALB.=3.14159*.5.,P14T.=PIHALB*.5.,
18  I.=0.,
19  SERIE(10.0,10.0,22,12,QUAD)
20  'END' SCHOTTER.,

1   'REAL'P,Q,P1,Q1,XM,YM,HOR,VER,JLI,JRE,JUN,JOB.,
5   'INTEGER'I,M,M,T.,
7   'PROCEDURE'SERIE(QUER,HOCH,XMAL,YMAL,FIGUR).,
8   'VALUE'QUER,HOCH,XMAL,YMAL.,
9   'REAL'QUER,HOCH.,
10  'INTEGER'XMAL,YMAL.,
11  'PROCEDURE'FIGUR.,
12  'BEGIN'
13  'REAL'YANF.,
14  'INTEGER'COUNTX,COUNTY.,
15  P.=-QUER*XMAL*.5.,
16  Q.=YANF.=-HOCH*YMAL*.5.,
17  'FOR'COUNTX.=1'STEP'1'UNTIL'XMAL'DO'
18  'BEGIN'Q.=YANF.,
19  'FOR'COUNTY.=1'STEP'1'UNTIL'YMAL'DO'
20  'BEGIN'FIGUR.,Q.=Q+HOCH
21  'END'.,P.=P+QUER
22  'END'.,
23  LEER(-148.0,-105.0).,CLOSE.,
24  SONK(11).,
25  OPBEN(X,Y)
26  'END'SERIE.,
gravel

gravel

What is missing in this ALGOL code are the seeds necessary for the reproduction of the plot. The author went down a rabbit hole to investigate and calculate different values, but managed to determine them to be “(1922110153) for the x and y shift seed, and (1769133315) for the rotation seed”. They also provided a Python translation

import math
import drawsvg as draw

class Random:
    def __init__(self, seed):
        self.JI = seed

    def next(self, JA, JE):
        self.JI = (self.JI * 5) % 2147483648
        return self.JI / 2147483648 * (JE-JA) + JA

def draw_square(g, x, y, i, r1, r2):
    r = 5 * 1.4142
    pi = 3.14159
    move_limit = 5 * i / 264
    twist_limit = pi/4 * i / 264

    y_center = y + 5 + r1.next(-move_limit, move_limit)
    x_center = x + 5 + r1.next(-move_limit, move_limit)
    angle = r2.next(pi/4 - twist_limit, pi/4 + twist_limit)

    p = draw.Path()
    p.M(x_center + r * math.sin(angle), y_center + r * math.cos(angle))
    for step in range(4):
        angle += pi / 2
        p.L(x_center + r * math.sin(angle), y_center + r * math.cos(angle))
    g.append(p)

def draw_plot(x_size, y_size, x_count, y_count, s1, s2):
    r1 = Random(s1)
    r2 = Random(s2)
    d = draw.Drawing(180, 280, origin='center', style="background-color:#eae6e2")
    g = draw.Group(stroke='#41403a', stroke_width='0.4', fill='none',
                   stroke_linecap="round", stroke_linejoin="round")

    y = -y_size * y_count * 0.5
    x0 = -x_size * x_count * 0.5
    i = 0

    for _ in range(y_count):
        x = x0
        for _ in range(x_count):
            draw_square(g, x, y, i, r1, r2)
            x += x_size
            i += 1
        y += y_size
    d.append(g)
    return d
  
d = draw_plot(10.0, 10.0, 12, 22, 1922110153, 1769133315).set_render_size(w=500) 
print(d.as_svg())


I wanted to see if I could also translate this into R – base plot I can draw line segments very well, and I was curious to color the squares in my own way.

Most of this code translates simply, except that the “randomness” is actually a sequence of values, starting with a specific seed. I recently talked about an old article of mine that (abusively) uses the set.seed() function to generate specific “random” words

printStr <- function(str) paste(str, collapse="")

set.seed(2505587); x <- sample(LETTERS, 5, replace=TRUE)
set.seed(11135560);y <- sample(LETTERS, 5, replace=TRUE)

paste(printStr(x), printStr(y))
## [1] "HELLO WORLD"

which I wanted to revisit based on an article by Andrew Heiss.

THE Random The class in this Python translation produces an iterator that returns a “next” value each time it is called with a specific “seed” and two values

class Random:
    def __init__(self, seed):
        self.JI = seed

    def next(self, JA, JE):
        self.JI = (self.JI * 5) % 2147483648
        return self.JI / 2147483648 * (JE-JA) + JA
      
r = Random(1)
r.next(2, 3)
## 2.0000000023283064
r.next(2, 3)
## 2.000000011641532
r.next(2, 3)
## 2.000000058207661

with the added complexity that subsequent calls update the seed itself.

When I first saw this, my mind went back to reading the “original” R paper “R: A Language for Data Analysis and Graphics” by Ross Ihaka and Robert Gentleman, in which I remember seeing the cool example of an OO system maintaining (non-global) state via <<-

Maintain the status of the total balance internal to the functionMaintain the status of the total balance internal to the function

Maintain the status of the total balance internal to the function

With this same trick, we can write an equivalent of Random class that also updates the seed internally

random <- function(seed) 
  list(
    nextval = function(a, b)  
      seed <<- (seed * 5) %% 2147483648
      seed / 2147483648 * (b-a) + a
    
  )


r <- random(1)
print(r$nextval(2, 3), digits = 16)
## [1] 2.000000002328306
print(r$nextval(2, 3), digits = 16)
## [1] 2.000000011641532
print(r$nextval(2, 3), digits = 16)
## [1] 2.000000058207661

Cool!

The rest of the translation mostly aligns with the base plot syntax.

This is what I ended up with

draw_square <- function(x, y, i, r1, r2, col) 
  r = 5 * 1.4142
  move_limit = 5 * i / 264
  twist_limit = pi/4 * i / 264
  
  y_center = y + 5 + r1$nextval(-move_limit, move_limit)
  x_center = x + 5 + r1$nextval(-move_limit, move_limit)
  angle = r2$nextval(pi/4 - twist_limit, pi/4 + twist_limit)
  
  x0 <- x_center + r * sin(angle)
  y0 <- y_center + r * cos(angle)
  
  for (step in 1:4) 
    angle <- angle + pi / 2
    x1 <- x_center + r * sin(angle)
    y1 <- y_center + r * cos(angle)
    segments(x0, y0, x1, y1, lwd = 1.75, col = col)
    x0 <- x1
    y0 <- y1
  


draw_plot <- function(x_size, y_size, x_count, y_count, s1, s2) 
  r1 = random(s1)
  r2 = random(s2)
  
  plot(NULL, NULL, xlim = c(-60, 60), ylim = c(120, -120), axes = FALSE, ann = FALSE)
  
  y = -y_size * y_count * 0.5
  x0 = -x_size * x_count * 0.5
  i = 0
  
  for (z in 1:y_count) 
    x = x0
    for (zz in 1:x_count) 
      draw_square(x, y, i, r1, r2, "black")
      x <- x + x_size
      i <- i + 1
    
    y <- y + y_size
  


draw_plot(10.0, 10.0, 12, 22, 1922110153, 1769133315)

'gravel' in R'gravel' in R

Figure 1: “Gravel” in R

Which uses the special seeds discovered in this original post. Checking the rotations, this does indeed appear to match the original art.

But why stop there? Now that I can trace it, I can change things…what if I used a different set of seeds, for example swapping them?

draw_plot(10.0, 10.0, 12, 22, 1769133315, 1922110153)

'Schotter' in R with swapped seeds'Schotter' in R with swapped seeds

Figure 2: ‘Schotter’ in R with swapped seeds

or completely different values?

draw_plot(10.0, 10.0, 12, 22, 12345, 67890)

'Schotter' in R with new seeds'Schotter' in R with new seeds

Figure 3: ‘Schotter’ in R with new seeds

What if we changed the colors? I could plot the color based on the progress on the grid, which seems pretty cool to me.

draw_plot <- function(x_size, y_size, x_count, y_count, s1, s2) 
  r1 = random(s1)
  r2 = random(s2)
  
  plot(NULL, NULL, xlim = c(-60, 60), ylim = c(120, -120), axes = FALSE, ann = FALSE)
  
  y = -y_size * y_count * 0.5
  x0 = -x_size * x_count * 0.5
  i = 0
  
  for (z in 1:y_count) 
    x = x0
    rcol <- scales::viridis_pal(option = "viridis")(y_count)[z]
    for (zz in 1:x_count) 
      draw_square(x, y, i, r1, r2, rcol)
      x <- x + x_size
      i <- i + 1
    
    y <- y + y_size
  


draw_plot(10.0, 10.0, 12, 22, 1922110153, 1769133315)

'Schotter' in R with viridis colors'Schotter' in R with viridis colors

Figure 4: ‘Schotter’ in R with viridis colors

Since writing this article, I have seen other examples of similar work. This tool demonstrated a simplified version

suppressPackageStartupMessages(library(tidyverse))
crossing(x=0:10, y=x) |>  
  mutate(dx = rnorm(n(), 0, (y/20)^1.5),  
         dy = rnorm(n(), 0, (y/20)^1.5)) |>  
  ggplot() +  
  geom_tile(aes(x=x+dx, y=y+dy, fill=y), colour='black',  
            lwd=2, width=1, height=1, alpha=0.8, show.legend=FALSE) +  
  scale_fill_gradient(high='#9f025e', low='#f9c929') +  
  scale_y_reverse() + theme_void()

while this one showed a book ‘Crisis Engineering’ with a similar idea

Cover of “Crisis Engineering”Cover of “Crisis Engineering”

Cover of “Crisis Engineering”

I’m sure I’ve seen others too.

It was a fun exploration of artistically inspired code translation, and I got to stretch my “maintaining internal state” muscles a bit. I have no doubt that someone more artistic than me could do much more.

As always, I can be found on Mastodon and in the comments section below.

devtools::session_info()
## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.5.3 (2026-03-11)
##  os       macOS Tahoe 26.3.1
##  system   aarch64, darwin20
##  ui       X11
##  language (EN)
##  collate  en_US.UTF-8
##  ctype    en_US.UTF-8
##  tz       Australia/Adelaide
##  date     2026-04-17
##  pandoc   3.6.3 @ /Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/aarch64/ (via rmarkdown)
##  quarto   1.7.31 @ /usr/local/bin/quarto
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package      * version date (UTC) lib source
##  blogdown       1.23    2026-01-18 [1] CRAN (R 4.5.2)
##  bookdown       0.46    2025-12-05 [1] CRAN (R 4.5.2)
##  bslib          0.10.0  2026-01-26 [1] CRAN (R 4.5.2)
##  cachem         1.1.0   2024-05-16 [1] CRAN (R 4.5.0)
##  cli            3.6.5   2025-04-23 [1] CRAN (R 4.5.0)
##  devtools       2.4.6   2025-10-03 [1] CRAN (R 4.5.0)
##  digest         0.6.39  2025-11-19 [1] CRAN (R 4.5.2)
##  dplyr        * 1.2.0   2026-02-03 [1] CRAN (R 4.5.2)
##  ellipsis       0.3.2   2021-04-29 [1] CRAN (R 4.5.0)
##  evaluate       1.0.5   2025-08-27 [1] CRAN (R 4.5.0)
##  farver         2.1.2   2024-05-13 [1] CRAN (R 4.5.0)
##  fastmap        1.2.0   2024-05-15 [1] CRAN (R 4.5.0)
##  forcats      * 1.0.1   2025-09-25 [1] CRAN (R 4.5.0)
##  fs             1.6.7   2026-03-06 [1] CRAN (R 4.5.2)
##  generics       0.1.4   2025-05-09 [1] CRAN (R 4.5.0)
##  ggplot2      * 4.0.2   2026-02-03 [1] CRAN (R 4.5.2)
##  glue           1.8.0   2024-09-30 [1] CRAN (R 4.5.0)
##  gtable         0.3.6   2024-10-25 [1] CRAN (R 4.5.0)
##  hms            1.1.4   2025-10-17 [1] CRAN (R 4.5.0)
##  htmltools      0.5.9   2025-12-04 [1] CRAN (R 4.5.2)
##  jquerylib      0.1.4   2021-04-26 [1] CRAN (R 4.5.0)
##  jsonlite       2.0.0   2025-03-27 [1] CRAN (R 4.5.0)
##  knitr          1.51    2025-12-20 [1] CRAN (R 4.5.2)
##  labeling       0.4.3   2023-08-29 [1] CRAN (R 4.5.0)
##  lattice        0.22-9  2026-02-09 [1] CRAN (R 4.5.3)
##  lifecycle      1.0.5   2026-01-08 [1] CRAN (R 4.5.2)
##  lubridate    * 1.9.5   2026-02-04 [1] CRAN (R 4.5.2)
##  magrittr       2.0.4   2025-09-12 [1] CRAN (R 4.5.0)
##  Matrix         1.7-4   2025-08-28 [1] CRAN (R 4.5.3)
##  memoise        2.0.1   2021-11-26 [1] CRAN (R 4.5.0)
##  otel           0.2.0   2025-08-29 [1] CRAN (R 4.5.0)
##  pillar         1.11.1  2025-09-17 [1] CRAN (R 4.5.0)
##  pkgbuild       1.4.8   2025-05-26 [1] CRAN (R 4.5.0)
##  pkgconfig      2.0.3   2019-09-22 [1] CRAN (R 4.5.0)
##  pkgload        1.5.0   2026-02-03 [1] CRAN (R 4.5.2)
##  png            0.1-9   2026-03-15 [1] CRAN (R 4.5.2)
##  purrr        * 1.2.1   2026-01-09 [1] CRAN (R 4.5.2)
##  R6             2.6.1   2025-02-15 [1] CRAN (R 4.5.0)
##  RColorBrewer   1.1-3   2022-04-03 [1] CRAN (R 4.5.0)
##  Rcpp           1.1.1   2026-01-10 [1] CRAN (R 4.5.2)
##  readr        * 2.2.0   2026-02-19 [1] CRAN (R 4.5.2)
##  remotes        2.5.0   2024-03-17 [1] CRAN (R 4.5.0)
##  reticulate     1.45.0  2026-02-13 [1] CRAN (R 4.5.2)
##  rlang          1.1.7   2026-01-09 [1] CRAN (R 4.5.2)
##  rmarkdown      2.30    2025-09-28 [1] CRAN (R 4.5.0)
##  rstudioapi     0.18.0  2026-01-16 [1] CRAN (R 4.5.2)
##  S7             0.2.1   2025-11-14 [1] CRAN (R 4.5.2)
##  sass           0.4.10  2025-04-11 [1] CRAN (R 4.5.0)
##  scales         1.4.0   2025-04-24 [1] CRAN (R 4.5.0)
##  sessioninfo    1.2.3   2025-02-05 [1] CRAN (R 4.5.0)
##  stringi        1.8.7   2025-03-27 [1] CRAN (R 4.5.0)
##  stringr      * 1.6.0   2025-11-04 [1] CRAN (R 4.5.0)
##  tibble       * 3.3.1   2026-01-11 [1] CRAN (R 4.5.2)
##  tidyr        * 1.3.2   2025-12-19 [1] CRAN (R 4.5.2)
##  tidyselect     1.2.1   2024-03-11 [1] CRAN (R 4.5.0)
##  tidyverse    * 2.0.0   2023-02-22 [1] CRAN (R 4.5.0)
##  timechange     0.4.0   2026-01-29 [1] CRAN (R 4.5.2)
##  tzdb           0.5.0   2025-03-15 [1] CRAN (R 4.5.0)
##  usethis        3.2.1   2025-09-06 [1] CRAN (R 4.5.0)
##  vctrs          0.7.1   2026-01-23 [1] CRAN (R 4.5.2)
##  viridisLite    0.4.3   2026-02-04 [1] CRAN (R 4.5.2)
##  withr          3.0.2   2024-10-28 [1] CRAN (R 4.5.0)
##  xfun           0.56    2026-01-18 [1] CRAN (R 4.5.2)
##  yaml           2.3.12  2025-12-10 [1] CRAN (R 4.5.2)
## 
##  [1] /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
##  * ── Packages attached to the search path.
## 
## ─ Python configuration ───────────────────────────────────────────────────────
##  python:         /Users/jono/.cache/uv/archive-v0/3n3euDImmjsw3EYTJjfeY/bin/python
##  libpython:      /Users/jono/.local/share/uv/python/cpython-3.12.12-macos-aarch64-none/lib/libpython3.12.dylib
##  pythonhome:     /Users/jono/.cache/uv/archive-v0/3n3euDImmjsw3EYTJjfeY:/Users/jono/.cache/uv/archive-v0/3n3euDImmjsw3EYTJjfeY
##  virtualenv:     /Users/jono/.cache/uv/archive-v0/3n3euDImmjsw3EYTJjfeY/bin/activate_this.py
##  version:        3.12.12 (main, Oct 28 2025, 11:52:25) [Clang 20.1.4 ]
##  numpy:          /Users/jono/.cache/uv/archive-v0/3n3euDImmjsw3EYTJjfeY/lib/python3.12/site-packages/numpy
##  numpy_version:  2.4.4
##  
##  NOTE: Python version was forced by VIRTUAL_ENV
## 
## ──────────────────────────────────────────────────────────────────────────────


PakarPBN

A Private Blog Network (PBN) is a collection of websites that are controlled by a single individual or organization and used primarily to build backlinks to a “money site” in order to influence its ranking in search engines such as Google. The core idea behind a PBN is based on the importance of backlinks in Google’s ranking algorithm. Since Google views backlinks as signals of authority and trust, some website owners attempt to artificially create these signals through a controlled network of sites.

In a typical PBN setup, the owner acquires expired or aged domains that already have existing authority, backlinks, and history. These domains are rebuilt with new content and hosted separately, often using different IP addresses, hosting providers, themes, and ownership details to make them appear unrelated. Within the content published on these sites, links are strategically placed that point to the main website the owner wants to rank higher. By doing this, the owner attempts to pass link equity (also known as “link juice”) from the PBN sites to the target website.

The purpose of a PBN is to give the impression that the target website is naturally earning links from multiple independent sources. If done effectively, this can temporarily improve keyword rankings, increase organic visibility, and drive more traffic from search results.

Jasa Backlink

Download Anime Batch