---
title: 'Accessible Charts'
source: 'https://academia.sh/en/courses/visualization-reporting-and-analysis-at-scale/accessible-charts'
course: 'Visualization, Reporting and Analysis at Scale'
language: en
updated: '2026-10-07T04:31:14+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Encodes color as two numbers made of hue family and lightness step and counts the number of series that can be distinguished with two separate decoders'
    - 'Shows, by comparing two shape distributions, that the axis on which redundant encoding is added determines the result'
    - 'Counts the number of transitions required by the legend box, ordered legend and direct labeling layouts'
    - 'Computes the largest number of series that can be carried in a single display from the number of distinguishable steps and shapes'
---

# Accessible Charts

When eight series are encoded with a default palette made of two hue families and four lightness steps, all eight are distinguished by the full decoder, while a decoder that does not read the hue distinction is left with only four. When the shape channel is distributed in sequence, the number stays at four, because the shapes start over after four series and the color collision also occurs four series apart; when the shape is distributed by collision, it rises to eight. A legend box asks for 16 transitions, an ordered legend 6, direct labeling 0. Of nine lightness steps, five are distinguished at an interval of two, and with four shapes the ceiling of a single display comes out as 20 series.

Over seven lessons what was measured was the number the channel carries, and each time it was assumed
that the reader **can read** the channel. This lesson measures that assumption. If there is more than
one series in a display, the first question is not whether the values of the series are read
correctly; the first question is which series is which. If a series cannot be distinguished from its
neighbor, its value is also counted as not reported.

The contrast ratio calculation, color systems and accessibility criteria were established in the
Fundamentals of Interface Design course and in the accessibility courses; none of them is repeated
here and no color system name is written. The question here is specific to the data and is measured
by a single number: **the number of distinguishable series**. When color is the only encoding channel,
what is this number, and to what does redundant encoding raise it.

- **CD43. Color is not a name but two numbers.** In the model, color is made of a hue family and a
  lightness step. The encoder assigns a pair to a series; no color name or system is mentioned.
- **CD44. The reading resolution is two steps.** Two lightness steps cannot be distinguished unless
  they differ by at least two steps, because in the display the series are thin lines and stand far
  from one another; this is not the distinction of two wide surfaces placed side by side.
- **CD45. Two decoders run.** The full decoder reads both the hue family and the lightness step. The
  reduced decoder does not read the hue distinction and sees only the lightness step; both are
  applied to the same encoding.
- **CD46. The number of distinguishable series is the number of clusters.** Series that cannot be told
  apart from one another are gathered into a single cluster; the number of series read back is the
  number of clusters and is smaller than the total of the series.
- **CD47. The place of redundant encoding determines the result.** Adding a second channel is not
  enough by itself; the channel has to change on the axis on which the color collides.
- **CD48. A single display has a ceiling.** The ceiling is the product of the number of
  distinguishable steps and the number of shapes; above it, the number of series cannot be read back
  without splitting the display.

## When Color Is the Only Channel

```python
# series.py — MODEL: the measurement network of K03-K05 is generated
# with the same seed. The eight series are subsets of the same 3,199 readings:
# five regions and three tariff tiers. Color is not a name but two numbers:
# hue family and lightness step.
import math

SEED, RAW, M32 = 20260218, 1400, 0xFFFFFFFF
PERIODS = ["2026-01", "2026-02", "2026-03"]
TARIFF = [10, 25, 40]
REGION = [("north", 0.28, 21), ("south", 0.22, 17), ("east", 0.18, 26),
          ("west", 0.14, 14), ("central", 0.18, 23)]


def rng(t):
    x = ((t ^ (t >> 16)) * 2246822507) & M32
    x = ((x ^ (x >> 13)) * 3266489909) & M32
    s = [(x ^ (x >> 16)) & M32]

    def next_value():
        s[0] = (s[0] * 1664525 + 1013904223) & M32
        return s[0] / 4294967296
    return next_value


def pick(u, w):
    t = 0.0
    for i, x in enumerate(w):
        t += x
        if u < t:
            return i
    return len(w) - 1


_kept, _dropped = [], []
for i in range(RAW):
    r = rng(SEED + i)
    b = REGION[pick(r(), [x[1] for x in REGION])]
    pick(r(), [0.06, 0.24, 0.30, 0.24, 0.11, 0.05])
    pick(r(), [0.08, 0.14, 0.27, 0.34, 0.17])
    (_dropped if r() < 0.046 else _kept).append(
        {"subscriber_id": 10001 + i, "region": b[0], "base": b[2]})
VALID = {a["subscriber_id"] for a in _kept}

READINGS = []
for a in _kept + _dropped:
    r = rng(SEED + 7000 + a["subscriber_id"])
    for d in range(pick(r(), [0.05, 0.12, 0.21, 0.62])):
        t = 0.0 if r() < 0.038 else math.floor(
            a["base"] * math.exp((r() + r() + r() - 1.5) * 0.62)
            * (1 - d * 0.05) * 100 + 0.5) / 100
        if a["subscriber_id"] not in VALID:
            r()
            continue
        READINGS.append({"subscriber_id": a["subscriber_id"],
                         "region": a["region"], "period": PERIODS[d],
                         "consumption_m3": t})
        r()


def tier(v):
    return 1 + sum(1 for t in TARIFF if v >= t)


def average(f):
    v = [o["consumption_m3"] for o in READINGS if f(o)]
    return sum(v) / len(v), len(v)


SERIES = []
for name, _, _ in REGION:
    m, n = average(lambda o, x=name: o["region"] == x)
    SERIES.append((name, m, n))
for tr in (2, 3, 4):
    m, n = average(lambda o, x=tr: tier(o["consumption_m3"]) == x)
    SERIES.append((f"tier{tr}", m, n))

# Color channel: hue family (0/1) and lightness step (0-8). The palette picks
# four steps from each of two families; this is a natural default distribution
# for eight series.
LEVELS, SEPARATION, SHAPES = 9, 2, 4
PALETTE = [(i // 4, [1, 3, 5, 7][i % 4]) for i in range(len(SERIES))]


def full_decoder(x, y):
    """Are two series distinguishable: the hue differs, or the step difference is SEPARATION."""
    return x[0] != y[0] or abs(x[1] - y[1]) >= SEPARATION


def reduced_decoder(x, y):
    """The decoder that does not read the hue distinction sees only the lightness step."""
    return abs(x[1] - y[1]) >= SEPARATION


def distinguishable(code, decoder):
    """Series that cannot be told apart form one cluster; the number of clusters is the series read back."""
    root = list(range(len(code)))

    def find(i):
        while root[i] != i:
            root[i] = root[root[i]]
            i = root[i]
        return i
    for i in range(len(code)):
        for j in range(i + 1, len(code)):
            if not decoder(code[i], code[j]):
                root[find(i)] = find(j)
    return len({find(i) for i in range(len(code))})


print(f"{len(SERIES)} series, all subsets of the same {len(READINGS)} readings")
print(f"{'series':11s} {'count':>6s} {'mean':>9s} {'hue':>4s} {'light':>8s}")
for (name, m, n), (t, light) in zip(SERIES, PALETTE):
    print(f"{name:11s} {n:6d} {m:9.3f} {t:4d} {light:8d}")
print(f"reading resolution: two lightness steps cannot be distinguished unless "
      f"they differ by at least {SEPARATION} steps")
print(f"distinguishable series with the full decoder: "
      f"{distinguishable(PALETTE, full_decoder)}/{len(SERIES)}")
print(f"with the decoder that does not read the hue distinction: "
      f"{distinguishable(PALETTE, reduced_decoder)}/{len(SERIES)}")
```

```
8 series, all subsets of the same 3199 readings
series       count      mean  hue    light
north          921    20.210    0        1
south          645    16.602    0        3
east           557    24.674    0        5
west           476    13.778    0        7
central        600    22.396    1        1
tier2         2209    17.506    1        3
tier3          692    30.009    1        5
tier4           58    44.171    1        7
reading resolution: two lightness steps cannot be distinguished unless they differ by at least 2 steps
distinguishable series with the full decoder: 8/8
with the decoder that does not read the hue distinction: 4/8
```

The palette looks flawless. Each of the eight series has received a separate color pair, no pair has
been repeated, and the full decoder distinguishes all eight. This is the result seen by the person who
chose the palette.

The last line says what happens to the same palette under the second decoder: **4 of 8 series** are
left. North and central carry the same lightness step, south and the second tariff tier are the same,
east and the third are the same, west and the fourth are the same. Four pairs of series fall to a
single series each, and which member of each pair is being read remains unclear. The report thinks it
shows eight subsets of the 3,199 readings, while it shows four.

This is not a palette flaw, it is a flaw of **structure**. The palette crossed two hue families with
four lightness steps and filled eight cells; when the hue family cannot be read, the cross table is
left with columns and no rows. If color is the only encoding channel, this result is unavoidable, and
increasing the number of colors in the palette does not help.

## Where Redundant Encoding Is Added

```python
# redundant.py — the shape channel is added. Where it is added determines the result.
SEQUENTIAL = [(p[0], p[1], i % SHAPES) for i, p in enumerate(PALETTE)]
BY_COLLISION = [(p[0], p[1], i // SHAPES) for i, p in enumerate(PALETTE)]


def shaped(x, y):
    return reduced_decoder(x, y) or x[2] != y[2]


print(f"{'layer':29s}{'full decoder':>13s}{'reduced decoder':>17s}")
for name, code, dec in (("color only", PALETTE, reduced_decoder),
                        ("color + shape (sequential)", SEQUENTIAL, shaped),
                        ("color + shape (by collision)", BY_COLLISION, shaped)):
    full = distinguishable([k[:2] for k in code], full_decoder)
    print(f"{name:28s} {full:12d} {distinguishable(code, dec):16d}")
print(f"the shapes distributed sequentially number {SHAPES} and start over at the "
      f"{SHAPES}th series; since the color collision is also {SHAPES} series apart, "
      f"two collisions overlap")
```

```
layer                         full decoder  reduced decoder
color only                              8                4
color + shape (sequential)              8                4
color + shape (by collision)            8                8
the shapes distributed sequentially number 4 and start over at the 4th series; since the color collision is also 4 series apart, two collisions overlap
```

The middle line is the most expensive finding of this lesson. The shape channel was added, four
separate shapes were distributed to the series in sequence, and the number read by the narrowed
decoder **stayed at four**. Redundant encoding was added, but nothing improved.

The reason is in the last line. There are four shapes and they start over at the fifth series; the
color collision of the palette is also exactly four series apart. When the two cycles overlap, the
same colliding pairs this time share both the same step and the same shape. The second channel
collides together with the first, in the same place.

The third line gives the correction: when the shape is distributed not by order but **by collision** —
that is, when the first four series get one shape and the next four series get another — the narrowed
decoder also distinguishes all eight. The number of shapes used **fell** from four to two, and the
result improved. The measure of redundant encoding is not the number of channels but where the
channel changes.

## Legend Box and Transition Count

```python
# transitions.py — the legend box sends the reader back and forth between the
# display and the box. The right-edge position is a third channel and carries the order.
ROWS = 12
lowest, highest = min(m for _, m, _ in SERIES), max(m for _, m, _ in SERIES)
position = [round((m - lowest) / (highest - lowest) * (ROWS - 1)) for _, m, _ in SERIES]
overlapping = sum(1 for i in range(len(SERIES)) for j in range(i + 1, len(SERIES))
                  if position[i] == position[j])
unique = [i for i in range(len(SERIES))
          if all(position[i] != position[j] for j in range(len(SERIES)) if j != i)]
print(f"right-edge positions ({ROWS} rows): {position}")
print(f"of {len(SERIES)} series, {len(unique)} have a unique position, "
      f"{overlapping} pair falls on the same row")
box = 2 * len(SERIES)
ordered = 2 + 2 * (len(SERIES) - len(unique))
print(f"{'layout':27s}{'transitions':>14s}{'distinguishable series':>23s}")
for name, g, s in (("legend box", box, distinguishable(BY_COLLISION, shaped)),
                   ("ordered legend box", ordered,
                    distinguishable(BY_COLLISION, shaped)),
                   ("direct labeling", 0, len(SERIES))):
    print(f"{name:26s} {g:13d} {s:22d}")
```

```
right-edge positions (12 rows): [2, 1, 4, 0, 3, 1, 6, 11]
of 8 series, 6 have a unique position, 1 pair falls on the same row
layout                        transitions distinguishable series
legend box                            16                      8
ordered legend box                     6                      8
direct labeling                        0                      8
```

Distinguishability is a necessary condition, not a sufficient one. The eight series may be told apart
from one another and the reader still does not know which one is which region; this they learn only
by looking at the legend box. The measure here is **the number of transitions**: how many times the
gaze goes back and forth between the display and the legend box.

An ordinary legend box asks for one trip there and one trip back for each series; eight series come
to **16 transitions**. When the order in the legend box is made the same as the order of the series on
the right edge, the reader solves the whole list with a single mapping and the transitions fall to
**6**. The remaining four come from the single pair that falls on the same row on the right edge: at
the resolution of 12 rows, south and the second tariff tier cannot be separated, and those two series
have to be matched one by one.

Direct labeling brings the number of transitions down to **zero**, because the name is written where
the series ends and there is no matching job left. Ordering and direct labeling are also layers of
redundant encoding; their difference is that they carry not distinguishability but **recognition**.
All three layouts distinguish the same eight series, but the work they ask of the reader is 16, 6 and
0.

## The Ceiling of a Single Display

```python
# ceiling.py — how many series can be carried in a single display.
DISTINCT_LEVELS = len(range(0, LEVELS, SEPARATION))
CEILING = DISTINCT_LEVELS * SHAPES
print(f"of {LEVELS} lightness steps, {DISTINCT_LEVELS} are distinguishable at an "
      f"interval of {SEPARATION}; with {SHAPES} shapes the ceiling is {CEILING} series")
print(f"{'series':>6s}{'distinguishable':>18s}{'parts':>7s}")
for n in (8, 12, 16, 20, 24, 30, 40):
    code = [(0, (i % DISTINCT_LEVELS) * SEPARATION,
             (i // DISTINCT_LEVELS) % SHAPES) for i in range(n)]
    e = distinguishable(code, shaped)
    print(f"{n:6d} {e:17d} {max(1, -(-n // CEILING)):6d}")
print(f"at most {CEILING} series can be distinguished in a single display; above that "
      f"the number of series cannot be read back without splitting the display")
```

```
of 9 lightness steps, 5 are distinguishable at an interval of 2; with 4 shapes the ceiling is 20 series
series   distinguishable  parts
     8                 8      1
    12                12      1
    16                16      1
    20                20      1
    24                20      2
    30                20      2
    40                20      2
at most 20 series can be distinguished in a single display; above that the number of series cannot be read back without splitting the display
```

The ceiling comes out of two numbers. Of nine lightness steps, five can be distinguished at a
resolution of two steps; its product with four shapes is **20**. Up to twenty every series is
distinguished, at 24 series the number stays at 20 and four become unreadable; at 40 series it is
still 20.

This is the hardest limit of the display and it cannot be exceeded by design. If more than twenty
series are to be carried, the display is split: the series are divided into groups that do not
exceed the ceiling and each group is drawn in its own display. The price is that comparison between
groups can no longer be read from the same display. The rule of the topic comes from here — **a
display carries as many series as the number of distinguishable series; the rest counts as drawn but
not reported**.

## Summary

- Color is not a name but two numbers, and in the model it is made of a hue family and a lightness
  step; the reading resolution is two steps.
- The default palette that crosses two hue families with four lightness steps has eight series
  distinguished by the full decoder and four by the decoder that does not read the hue distinction.
- When the shape channel is distributed in sequence, the number stays at four, because the shape
  starts over after four series and overlaps with the color collision; when distributed by collision
  it rises to eight with two shapes.
- Distinguishability does not give recognition: the same eight series ask for 16 transitions with a
  legend box, 6 with an ordered legend, and 0 with direct labeling.
- The ceiling of a single display is the product of the number of distinguishable steps and the
  number of shapes; with five steps and four shapes it is 20 series, and above that the display has
  to be split.

## Next Step

Over eight lessons a single display was built correctly. The question type was matched to the
channel, the shape of the distribution came down to a number, the slice without a baseline was
measured, the point where the relationship becomes unreadable was found, the slope and the season
were separated on the time axis, the weight of the area channel was counted, the four distortions
were printed one by one, and finally how many series are distinguished was measured. The measure was
the same in all of them: a display is as correct as the number the reader can read back.

A report, however, is not made of a single display. The reader meets more than one number at a
glance, and they are encoded with different channels and stand at different resolutions. The next
topic carries the measure here: how many criteria can be read back at a glance, how the same
criterion can be moved without moving how it measures, and whether a report gives the same number
when it is produced a second time. The reader is no longer a person who reads a number back, but a
person who makes a decision with that number.
