Oak

The shape of code: plotting line frequencies in large codebases

← Posts 4 Apr 2022

Last week, @rsms shared a little script to draw a histogram of the distribution of line lengths in a codebase.

I've played around with ways to visualize shapes of code before, because I think we read code so differently than we do prose — we pay less attention to individual tokens and glean more information from the silhouette and visual rhythm of lines and spaces.

There were a few things I liked about this particular take on visualizing the shape of code:

  1. It proved to me that Unicode box drawing characters actually let you draw some pretty nice looking graphics in the terminal.
  2. A fairly simple program can generate a chart like this, if it doesn't have to run at light speed. It's probably a fun toy problem for learning a new programming language (or polishing an existing one, like Oak).
  3. This got me curious about what line length distributions of different languages and codebases would look like, including those belonging to some of my own projects.

I really enjoy building small toy utilities with Oak to distract myself from real work, so I wrote a little CLI called codecols, which does almost exactly what Rasmus's script does, with a few extra tricks:

Armed with this new toy, the first codebase I tried to measure was, of course, Oak's very own. I fed all *.oak files in the Oak codebase as of today to codecols.

$ cat oak/**/*.oak | codecols --max-cols 100
cols count
2    835   ███████████████████████████████████████▍
4    1272  ████████████████████████████████████████████████████████████
6    850   ████████████████████████████████████████
8    432   ████████████████████▍
10   390   ██████████████████▍
12   643   ██████████████████████████████▎
14   672   ███████████████████████████████▋
16   661   ███████████████████████████████▏
18   631   █████████████████████████████▊
20   581   ███████████████████████████▍
22   413   ███████████████████▍
24   417   ███████████████████▋
26   371   █████████████████▌
28   358   ████████████████▉
30   323   ███████████████▏
32   250   ███████████▊
34   286   █████████████▍
36   227   ██████████▋
38   231   ██████████▉
40   176   ████████▎
42   221   ██████████▍
44   213   ██████████
46   190   ████████▉
48   134   ██████▎
50   153   ███████▏
52   152   ███████▏
54   145   ██████▊
56   126   █████▉
58   100   ████▋
60   113   █████▎
62   96    ████▌
64   98    ████▌
66   95    ████▍
68   107   █████
70   101   ████▊
72   95    ████▍
74   94    ████▍
76   124   █████▊
78   126   █████▉
80   71    ███▎
82   19    ▉
84   23    █
86   15    ▋
88   19    ▉
90   17    ▊
92   9     ▍
94   7     ▎
96   11    ▌
98   9     ▍
100  10    ▍
average columns per line: 25.56

Despite representing a codebase written a completely different language, this plot looks a lot like Rasmus's original — it's a bimodal distribution with a long tail. There's one big peak at around 2-4 columns, a gap at around 6-12 columns, and a wider, slightly smaller peak thereafter. In this plot of the Oak codebase, the count drops off quickly after 80-column-wide lines, because I use a text editor that likes to wrap lines at 80 characters.

This bi-modality is interesting, because it doesn't occur in natural language prose. In English prose, sentence length and paragraph length have unimodal distributions, at least in my personal writing, more closely resembling the normal bell-curve shape.

After noticing this bimodal trend, I got curious about just how universal this pattern is among other programming languages, especially languages that don't have the traditional C-like syntax. I spent the next hour excitedly running codecols on a bunch of different large codebases with idiomatic code style representing different languages, and I thought I'd show them to you here, starting with more classic, bimodally-distributed languages. I also speculate towards the end about why some languages might display this behavior, while some do not.

Here's Go's net/http package from its standard library. Go is a very C-like language in many ways, syntax being one of them. Predictably, the line length distribution looks very much like C's.

cols count
2    6998  ████████████████████████████████████████████████████████████
4    4035  ██████████████████████████████████▌
6    894   ███████▋
8    707   ██████
10   1526  █████████████
12   1703  ██████████████▌
14   1880  ████████████████
16   2404  ████████████████████▌
18   2823  ████████████████████████▏
20   2222  ███████████████████
22   1815  ███████████████▌
24   1764  ███████████████
26   1608  █████████████▊
28   1603  █████████████▋
30   1415  ████████████▏
32   1429  ████████████▎
34   1231  ██████████▌
36   1241  ██████████▋
38   1129  █████████▋
40   1112  █████████▌
42   1115  █████████▌
44   1001  ████████▌
46   959   ████████▏
48   986   ████████▍
50   1015  ████████▋
52   910   ███████▊
54   1038  ████████▉
56   871   ███████▍
58   895   ███████▋
60   974   ████████▎
62   1013  ████████▋
64   860   ███████▎
66   781   ██████▋
68   707   ██████
70   880   ███████▌
72   553   ████▋
74   688   █████▉
76   395   ███▍
78   321   ██▊
80   289   ██▍
82   228   █▉
84   209   █▊
86   154   █▎
88   157   █▎
90   118   █
92   93    ▊
94   95    ▊
96   68    ▌
98   55    ▍
100  39    ▎
average columns per line: 30.74

Another C-like language is JavaScript. Here's the distribution for the react package, which is the core of the React library. We continue to see the general pattern of a peak near zero, a trough around 6-12 columns, and then a second peak. I also think it's quite cool to see the drop-off after 80 characters that signals code style with a maximum line length.

cols count
2    269   ██████████████▍
4    386   ████████████████████▊
6    1114  ████████████████████████████████████████████████████████████
8    884   ███████████████████████████████████████████████▌
10   373   ████████████████████
12   325   █████████████████▌
14   326   █████████████████▌
16   658   ███████████████████████████████████▍
18   499   ██████████████████████████▉
20   417   ██████████████████████▍
22   462   ████████████████████████▉
24   428   ███████████████████████
26   343   ██████████████████▍
28   402   █████████████████████▋
30   322   █████████████████▎
32   299   ████████████████
34   379   ████████████████████▍
36   397   █████████████████████▍
38   311   ████████████████▊
40   352   ██████████████████▉
42   295   ███████████████▉
44   275   ██████████████▊
46   404   █████████████████████▊
48   320   █████████████████▏
50   263   ██████████████▏
52   373   ████████████████████
54   193   ██████████▍
56   225   ████████████
58   249   █████████████▍
60   197   ██████████▌
62   185   █████████▉
64   163   ████████▊
66   205   ███████████
68   126   ██████▊
70   152   ████████▏
72   172   █████████▎
74   164   ████████▊
76   170   █████████▏
78   160   ████████▌
80   145   ███████▊
82   50    ██▋
84   38    ██
86   21    █▏
88   17    ▉
90   19    █
92   9     ▍
94   22    █▏
96   10    ▌
98   7     ▍
100  14    ▊
average columns per line: 33.02

Here's the plot for *.rb files (excluding tests) in the Rails codebase. Ruby's syntax is similar to C-style languages in a few ways. It's an object-oriented imperative language with pretty conventional syntax for that style. But it uses do ... end and def ... end rather than { ... } for block delimiters, and in general prefers keywords over characters. I suspect that's why, even though we still see a C-like distribution here, the distribution is shifted up a few columns.

cols count
2    110   ▎
4    6726  ████████████████▋
6    24119 ████████████████████████████████████████████████████████████
8    19542 ████████████████████████████████████████████████▌
10   14453 ███████████████████████████████████▉
12   11422 ████████████████████████████▍
14   8542  █████████████████████▏
16   6409  ███████████████▉
18   6007  ██████████████▉
20   6641  ████████████████▌
22   7448  ██████████████████▌
24   7832  ███████████████████▍
26   7557  ██████████████████▊
28   7994  ███████████████████▉
30   11033 ███████████████████████████▍
32   8261  ████████████████████▌
34   8634  █████████████████████▍
36   8102  ████████████████████▏
38   8153  ████████████████████▎
40   7941  ███████████████████▊
42   8128  ████████████████████▏
44   8282  ████████████████████▌
46   7549  ██████████████████▊
48   7571  ██████████████████▊
50   6974  █████████████████▎
52   6540  ████████████████▎
54   6230  ███████████████▍
56   5836  ██████████████▌
58   5434  █████████████▌
60   5122  ████████████▋
62   4913  ████████████▏
64   4668  ███████████▌
66   4320  ██████████▋
68   4123  ██████████▎
70   4082  ██████████▏
72   3876  █████████▋
74   4018  █████████▉
76   3849  █████████▌
78   4032  ██████████
80   3472  ████████▋
82   2756  ██████▊
84   2297  █████▋
86   2046  █████
88   1778  ████▍
90   1800  ████▍
92   1535  ███▊
94   1340  ███▎
96   1367  ███▍
98   1244  ███
100  1114  ██▊
102  1067  ██▋
104  923   ██▎
106  861   ██▏
108  739   █▊
110  696   █▋
112  666   █▋
114  564   █▍
116  530   █▎
118  561   █▍
120  434   █
average columns per line: 40.56

Lastly, here's Typescript's infamous 45,000-line checker.ts type checker file. It's notable for its almost complete lack of short lines, and a long "tail" of super long lines. If you read the checker.ts file, you'll see why this is the case — this file is very notationally heavy with long variable names and complex function and type signatures that are compressed into single lines rather than spread out for readability.

cols count
2    3426  ████████████████████████████████████████████████████████████
4    0
6    31    ▌
8    0
10   1783  ███████████████████████████████▏
12   388   ██████▊
14   2265  ███████████████████████████████████████▋
16   182   ███▏
18   1859  ████████████████████████████████▌
20   190   ███▎
22   1265  ██████████████████████▏
24   329   █████▊
26   1071  ██████████████████▊
28   508   ████████▉
30   852   ██████████████▉
32   595   ██████████▍
34   775   █████████████▌
36   543   █████████▌
38   672   ███████████▊
40   612   ██████████▋
42   683   ███████████▉
44   754   █████████████▏
46   698   ████████████▏
48   854   ██████████████▉
50   957   ████████████████▊
52   840   ██████████████▋
54   785   █████████████▋
56   806   ██████████████
58   790   █████████████▊
60   730   ████████████▊
62   750   █████████████▏
64   724   ████████████▋
66   681   ███████████▉
68   647   ███████████▎
70   645   ███████████▎
72   614   ██████████▊
74   563   █████████▊
76   610   ██████████▋
78   558   █████████▊
80   563   █████████▊
82   491   ████████▌
84   533   █████████▎
86   475   ████████▎
88   455   ███████▉
90   444   ███████▊
92   471   ████████▏
94   447   ███████▊
96   466   ████████▏
98   424   ███████▍
100  428   ███████▍
102  405   ███████
104  403   ███████
106  418   ███████▎
108  373   ██████▌
110  407   ███████▏
112  356   ██████▏
114  369   ██████▍
116  352   ██████▏
118  362   ██████▎
120  306   █████▎
122  303   █████▎
124  308   █████▍
126  249   ████▎
128  259   ████▌
130  248   ████▎
132  209   ███▋
134  192   ███▎
136  188   ███▎
138  156   ██▋
140  175   ███
142  188   ███▎
144  177   ███
146  150   ██▋
148  137   ██▍
150  130   ██▎
152  115   ██
154  124   ██▏
156  88    █▌
158  90    █▌
160  97    █▋
162  121   ██
164  87    █▌
166  86    █▌
168  74    █▎
170  56    ▉
172  57    ▉
174  66    █▏
176  47    ▊
178  54    ▉
180  44    ▊
182  35    ▌
184  44    ▊
186  30    ▌
188  39    ▋
190  40    ▋
192  26    ▍
194  30    ▌
196  26    ▍
198  24    ▍
200  19    ▎
average columns per line: 61.35

I also decided to look at a few languages that don't have some of the characteristic C-style syntax that give JS, Go, and C their shape. For example, here's the Python code behind the Black code formatter for Python, formatted with Black itself.

We no longer see the double-peak shape here, and instead see something much closer to a unimodal distribution with a peak around 22-24 columns. We also see the distribution max out at exactly 88 characters, which is the strict line-length limit set by the Black formatter.

This plot began to convince me that the bimodal distribution was an artifact of C-like syntax, specifically the pattern of ending parentheses or double braces that end up in their own line, as } or });. Python has fewer such constructions (for example, in argument lists), and uses indentation instead for block nesting, so we don't see such short lines emphasized in the plot.

cols count
2    49    █████████▊
4    18    ███▌
6    96    ███████████████████▏
8    178   ███████████████████████████████████▍
10   234   ██████████████████████████████████████████████▋
12   194   ██████████████████████████████████████▋
14   281   ████████████████████████████████████████████████████████
16   217   ███████████████████████████████████████████▎
18   274   ██████████████████████████████████████████████████████▌
20   249   █████████████████████████████████████████████████▋
22   212   ██████████████████████████████████████████▎
24   262   ████████████████████████████████████████████████████▏
26   255   ██████████████████████████████████████████████████▊
28   301   ████████████████████████████████████████████████████████████
30   232   ██████████████████████████████████████████████▏
32   291   ██████████████████████████████████████████████████████████
34   208   █████████████████████████████████████████▍
36   255   ██████████████████████████████████████████████████▊
38   223   ████████████████████████████████████████████▍
40   227   █████████████████████████████████████████████▏
42   184   ████████████████████████████████████▋
44   199   ███████████████████████████████████████▋
46   166   █████████████████████████████████
48   129   █████████████████████████▋
50   152   ██████████████████████████████▎
52   156   ███████████████████████████████
54   144   ████████████████████████████▋
56   131   ██████████████████████████
58   126   █████████████████████████
60   129   █████████████████████████▋
62   101   ████████████████████▏
64   115   ██████████████████████▉
66   113   ██████████████████████▌
68   123   ████████████████████████▌
70   119   ███████████████████████▋
72   104   ████████████████████▋
74   130   █████████████████████████▉
76   162   ████████████████████████████████▎
78   186   █████████████████████████████████████
80   144   ████████████████████████████▋
82   107   █████████████████████▎
84   107   █████████████████████▎
86   102   ████████████████████▎
88   64    ████████████▊
90   0
92   0
94   0
96   0
98   0
100  0
average columns per line: 40.53

Another more exotic language is assembly. This plot shows assembly files (*.s) from Go's crypto standard library packages. Once again, we don't see any bimodal peaks here. In fact, there's a very strong preference in these files for lines of around 12-22 columns long, which makes sense considering how "rectangular" most assembly code looks. I have a feeling that lines longer than 30 columns are comments, rather than code.

cols count
2    111   ███▏
4    170   ████▉
6    83    ██▍
8    232   ██████▊
10   690   ████████████████████▏
12   1111  ████████████████████████████████▍
14   2057  ████████████████████████████████████████████████████████████
16   1313  ██████████████████████████████████████▎
18   1624  ███████████████████████████████████████████████▎
20   1288  █████████████████████████████████████▌
22   1291  █████████████████████████████████████▋
24   934   ███████████████████████████▏
26   829   ████████████████████████▏
28   764   ██████████████████████▎
30   593   █████████████████▎
32   497   ██████████████▍
34   166   ████▊
36   388   ███████████▎
38   638   ██████████████████▌
40   418   ████████████▏
42   176   █████▏
44   245   ███████▏
46   558   ████████████████▎
48   153   ████▍
50   249   ███████▎
52   122   ███▌
54   241   ███████
56   153   ████▍
58   92    ██▋
60   140   ████
62   106   ███
64   96    ██▊
66   122   ███▌
68   136   ███▉
70   101   ██▉
72   92    ██▋
74   43    █▎
76   186   █████▍
78   23    ▋
80   27    ▊
82   29    ▊
84   27    ▊
86   33    ▉
88   14    ▍
90   9     ▎
92   11    ▎
94   22    ▋
96   10    ▎
98   0
100  0
average columns per line: 27.35

I'll conclude this exploration by lingering a bit on Lisp programs and their shape. This plot is for all Clojure source files (*.clj) in the Clojure source tree. Once again, because Lisps don't tend to leave parentheses or braces hanging on their own lines at the end of blocks, we see a more unimodal distribution.

cols count
2    68    ██▎
4    109   ███▊
6    198   ██████▊
8    286   █████████▊
10   447   ███████████████▍
12   542   ██████████████████▋
14   591   ████████████████████▎
16   1741  ████████████████████████████████████████████████████████████
18   736   █████████████████████████▎
20   644   ██████████████████████▏
22   646   ██████████████████████▎
24   728   █████████████████████████
26   585   ████████████████████▏
28   579   ███████████████████▉
30   616   █████████████████████▏
32   571   ███████████████████▋
34   539   ██████████████████▌
36   474   ████████████████▎
38   457   ███████████████▋
40   489   ████████████████▊
42   430   ██████████████▊
44   456   ███████████████▋
46   432   ██████████████▉
48   411   ██████████████▏
50   351   ████████████
52   399   █████████████▊
54   357   ████████████▎
56   337   ███████████▌
58   382   █████████████▏
60   303   ██████████▍
62   361   ████████████▍
64   382   █████████████▏
66   584   ████████████████████▏
68   568   ███████████████████▌
70   614   █████████████████████▏
72   302   ██████████▍
74   286   █████████▊
76   206   ███████
78   147   █████
80   175   ██████
82   180   ██████▏
84   111   ███▊
86   93    ███▏
88   68    ██▎
90   79    ██▋
92   56    █▉
94   66    ██▎
96   43    █▍
98   37    █▎
100  26    ▉
average columns per line: 40

I did something similar with Lisp code from Klisp, my own flavor of Lisp I wrote for hobby interpreter projects. Unsurprisingly, I saw a very similar pattern of a single large peak.

cols count
2    0
4    0
6    50    █████████████████████████
8    72    ████████████████████████████████████
10   81    ████████████████████████████████████████▌
12   69    ██████████████████████████████████▌
14   89    ████████████████████████████████████████████▌
16   94    ███████████████████████████████████████████████
18   120   ████████████████████████████████████████████████████████████
20   108   ██████████████████████████████████████████████████████
22   82    █████████████████████████████████████████
24   82    █████████████████████████████████████████
26   76    ██████████████████████████████████████
28   93    ██████████████████████████████████████████████▌
30   89    ████████████████████████████████████████████▌
32   77    ██████████████████████████████████████▌
34   52    ██████████████████████████
36   34    █████████████████
38   53    ██████████████████████████▌
40   32    ████████████████
42   25    ████████████▌
44   22    ███████████
46   31    ███████████████▌
48   29    ██████████████▌
50   9     ████▌
52   11    █████▌
54   23    ███████████▌
56   8     ████
58   7     ███▌
60   12    ██████
62   9     ████▌
64   1     ▌
66   8     ████
68   4     ██
70   1     ▌
72   7     ███▌
74   6     ███
76   9     ████▌
78   7     ███▌
80   8     ████
82   0
84   0
86   0
88   1     ▌
90   0
92   1     ▌
average columns per line: 26.71

I'm not really sure if there's a lesson in any of this, except that (1) charts are cool! and (2) some syntactic properties of programming languages show up in meaningful and interesting ways in these visualizations. If you want to try running this tool on some of your code, you can find my Oak script on Github Gist or try a Rust implementation if that's more your thing.

I'll leave you with one last set of charts I found on Twitter, comparing the impact of different code formatters on the distribution of lines of Python code: