Úvod do dostupných vizualizací dat pomocí D3.js

Původně zveřejněno na fossheim.io

Vizualizace dat může být skvělá pro snadnou komunikaci složitých dat. Bohužel, pokud jde o dostupnost, je toho hodně, co se může pokazit. Před několika týdny jsem se rozhodl procházet jeden z nejvýše uvedených panelů COVID-19 pomocí VoiceOveru a sotva jsem dokázal překonat první graf, než jsem frustrovaně zavřel prohlížeč.

Ale jsou v tom sotva sami – a ani jim to vlastně nemůžu mít za zlé. Zaručil jsem, že jsem v minulosti udělal podobné chyby, protože většina výukových programů D3.js nezmiňuje přístupnost a mnoho knihoven vizualizací postavených na D3.js je ve výchozím nastavení nepřístupných.

Data jsou všude a měla by být dostupná pro všechny. Tak jsem se rozhodl o tom začít psát vlastní sérii!

Tento první tutoriál bude poměrně široký, ale podrobněji se budeme věnovat v nadcházejících příspěvcích. Budete potřebovat základní znalosti D3.js, abyste je mohli sledovat; ale nebojte se, připravuje se také úvod do série D3.js.

Výchozí bod

V tomto tutoriálu začneme jednoduchým sloupcovým grafem, který vizualizuje množství unikátních návštěvníků, které web měl za poslední týden. Dny, kdy je počet návštěvníků 100 nebo nižší, budou muset být označeny jako špatné.

Tento graf má několik problémů:

  1. Barvy pruhů a textu nemají dostatečný kontrast s pozadím
  2. Používané barvy jsou pro barvoslepé hůře rozlišitelné
  3. Význam barev není vysvětlen
  4. Neznáme měřítko osy y ani to, co je zde zobrazeno
  5. Nejsou uvedeny žádné hodnoty
    • Toto nikomu nesděluje přesný počet návštěvníků, existuje pouze vizuální indikace toho, které dny mají více návštěvníků než jiné
    • Asistenční technologie (čtečky obrazovky) nebudou mít žádnou hodnotu pro komunikaci s uživatelem, takže nevidomí a slabozrací z toho nebudou mít žádné informace

Tyto problémy projdeme krok za krokem a převedeme je do grafu, který je již nyní mnohem dostupnější. Všimněte si, že se jedná o poměrně základní graf s malým množstvím dat a bez interakcí. Čím více funkcí a složitosti přidáme, tím více budeme muset vymyslet.

Barvy

Začněme výběrem barev, které splňují požadavky na kontrast (poměr AA nebo AAA) a přesto vypadají dostatečně odlišně pro různé typy barvosleposti. Osobně k tomu nejraději používám Figmu, protože ji používám již ve fázi návrhu. Obvykle barvy zkopíruji a vložím do samostatného rámečku a spustím na něm plugin Able a Color Blind.

Pokud nepoužíváte žádný program, který to podporuje, nebo jen preferujete práci z prohlížeče, Colorblinding a WCAG Color Contrast Checker jsou rozšíření pro Chrome se stejnou funkčností.

Pro jednoduchost jsem zvolil standardní tmavší modro/červené řešení, které je bezpečné jak z hlediska barvosleposti, tak i kontrastu. Můžete použít nástroje jako Khroma, Coolors nebo Colorsafe, které vám pomohou vytvořit dostupné palety.

Pokud chcete být extra v bezpečí nebo se nemůžete vyhnout použití barev, které splňují pokyny, pokud jde o barvoslepost, můžete do svých grafů přidat vzory. Nepřehánějte to a jděte na klidné vzory, jinak by graf mohl být příliš zaneprázdněný i pro oči.

Vzory můžeme přidat jako pozadí vytvořením <pattern> prvek uvnitř SVG. Budeme muset vzoru přiřadit ID, šířku a výšku. Uvnitř <pattern> můžeme nakreslit jakýkoli objekt SVG, který chceme. Potom v objektu, ke kterému chceme přidat vzor pozadí, můžeme nastavit výplň na url(#idOfOurPattern)

<pattern id="dots" x="0" y="0" width="3" height="3" patternUnits="userSpaceOnUse">
  <rect fill="#5D92F6" x="0" y="0" width="3" height="3"></rect>
  <circle fill="#11419B" cx="1" cy="1" r="1"></circle>
</pattern>
.bar {
  fill: url(#dots)
}

Vysvětlení barev přidáním legendy

V grafu používáme různé barvy, což může být složité, pokud jde o přístupnost. Ale je to také obecný problém UX, který je třeba řešit.

Ne každý uvidí barvy stejně (například kvůli barvosleposti) a pro různé lidi a kultury mají barvy různé významy. Všem uživatelům tedy nebude zřejmé, že v našem příkladu červený pruh znamená, že náš web ten den navštívilo méně než 100 lidí. Zde vstupují do hry legendy.

Začněme přidáním skupiny (<g> ) a přiřaďte jej k legend konstantní.

const legend = chart.append("g");

Budeme také muset přidat buď aria-label atribut nebo <title> doprovázeno aria-labelledby atribut, takže asistenční technologie může uživateli poskytnout další informace o tom, co se čte.

const legend = chart.append("g").attr("aria-label", "Legend");

Případně můžeme zobrazit vizuální název:

const legend = chart.append("g");
legend.append("text")
    .text("Legend")
    .attr("x", margin.left / 2)
    .attr("y", margin.top)
    .attr("class", "legendTitle");

Jakmile vytvoříme skupinu legend, můžeme do ní přidat obdélníky a textová pole.

// First color: blue with dots
legend.append("rect")
  .attr("fill", "url(#dots)")
  .attr("width", 13)
  .attr("height", 13)
  .attr("rx", 2)
  .attr("x", margin.left / 2)
  .attr("y", margin.top);

// First color: explanation
legend.append("text")
  .text("Over 100 daily visitors")
  .attr("x", margin.left / 2 + 20)
  .attr("y", margin.top + 10);

// Second color: red with lines
legend.append("rect")
  .attr("fill", "url(#lines)")
  .attr("width", 13)
  .attr("height", 13)
  .attr("rx", 2)
  .attr("x", margin.left / 2)
  .attr("y", margin.top + 30);

// Second color: explanation
legend.append("text")
  .text("Under 100 daily visitors")
  .attr("x", margin.left / 2 + 20)
  .attr("y", margin.top + 40);

Čtečky obrazovky čtou prvky DOM v pořadí, v jakém se objevují ve vašem kódu. Takže v mém příkladu jsem přidal kód pro legendu nahoře před kód pro osu x ze dvou důvodů:

  1. Tam je umístěn i vizuálně, takže je nejlogičtější pro lidi, kteří vizuály poslouchají i prohlížejí.
  2. Než se ponoříte do čísel, je dobré znát základní informace o grafu.

Označení dat

Stále nemáme ponětí, na jaké hodnoty se vlastně díváme. Vidíme, že pondělí mělo zhruba poloviční počet návštěvníků než neděle, ale nevíme přesné počty.

Budeme muset přidat hodnoty do horní části sloupců a označit osu y, která označí jednotku našich dat (v našem případě je jednotkou počet unikátních návštěvníků).

Pro každý řádek v našich datech se vytiskne počet návštěvníků:

chart.selectAll(".label")
  .data(data)
  .enter().append("text")
  .text(row => row.visitors);

Tyto štítky by měly být umístěny uprostřed nad každým pruhem. Abychom toho dosáhli, nejprve nastavíme text-anchor atribut middle , takže střed textového prvku se použije k výpočtu jeho souřadnic.

chart.selectAll(".label")
  .data(data)
  .enter().append("text")
  .text(row => row.visitors)
    .attr("text-anchor", "middle");

Dále nastavíme x souřadnic ke stejnému jako pruh. Protože pruh v našem příkladu je 10px široký a chceme, aby byl text vystředěn, musíme text posunout o dalších (10/2)px doprava. y souřadnice by měla být o několik pixelů menší než y pruhu také koordinovat.

chart.selectAll(".label")
  .data(data)
  .enter().append("text")
  .text(row => row.visitors)
    .attr("text-anchor", "middle")
    .attr("x", (row, index) => x(index + 1) + 5)
  .attr("y", row => y(row.visitors) + margin.top / 2 - 5)
    .attr("class", "label");

To by mělo pro hodnoty fungovat. Nakonec můžeme přidat popisek na osu y takto:

chart.append("text")
  .text("Amount of unique visitors")
  .attr("class", "yAxis")
  .attr("transform", "rotate(-90)")
  .attr("text-anchor", "middle")
  .attr("x", -height / 2 - margin.top)
  .attr("y", margin.left / 2 + 5);

Označená data a čtečky obrazovky

Už tam skoro jsme. Vizuálně řečeno, toto je již mnohem dostupnější. VoiceOver však stále nekomunikuje graf optimálně. Nejprve načte všechny dny na ose x, poté přejde na čtení všech hodnot nad pruhy.

Získáváme přístup ke všem informacím, a protože se zabýváme pouze 7 body dat, není nemožné sledovat, která hodnota mapuje ke kterému dni. Ale čím větší je naše datová sada, tím těžší je ji sledovat.

Existuje mnoho různých způsobů, jak to můžeme vyřešit, a během příštích tutoriálů se do toho určitě ponoříme hlouběji. Ale nyní se podívejme na dvě různá řešení:

Řešení A:Přidejte štítky a značky ke stejnému prvku

Jednou z možností by mohla být restrukturalizace kódu a seskupení dnů a hodnot do jednoho prvku. Způsob, jakým je náš kód D3 právě strukturován, bude výstupem v HTML:

<svg>
    <g class="legend"></g>

    <!-- x-axis -->
    <text>Mon</text>
    <text>Tue</text>
    <text>Wed</text>
    ...

    <!-- y-axis -->
    <text>Amount of unique visitors</text>

    <!-- bars -->
    <rect></rect>
    ...

    <!-- labels -->
    <text>100</text>
    <text>172</text>
    <text>92</text>
    ...
</svg>

Lepší zážitek by mohl být, kdyby VoiceOver četl náš graf takto:"Počet unikátních návštěvníků v pondělí:100, úterý:172, středa:92, ...". To propojí každý den na ose x s hodnotou každého grafu najednou, což usnadňuje sledování.

Místo toho, abychom nejprve procházeli našimi daty, abychom nakreslili hodnoty na ose x a později procházeli daty podruhé, abychom nakreslili popisky nad grafy, projdeme naše data pouze jednou a připojíme k nim skupinu.

const ticks = chart.selectAll(".tick")
  .data(data)
  .enter().append("g")
  .attr("class", "tick");

Výsledkem bude <g></g> pro každý bod v datové sadě. Potom můžeme zavolat ticks.append() dvakrát, jednou pro přidání popisků osy x a jednou pro přidání hodnot.

ticks.append("text")
  .text((data) => data.day)
  .attr("x", function(row, index) { return x(index + 1) + 5; })
  .attr("y", height + margin.top)
  .attr("width", 30)
  .attr("text-anchor", "middle");

ticks.append("text")
  .text(row => row.visitors)
    .attr("text-anchor", "middle")
    .attr("x", (row, index) => x(index + 1) + 5)
  .attr("y", row => y(row.visitors) + margin.top / 2 - 5)
  .attr("class", "label");

Výsledkem bude následující HTML:


    <g>
        <text>Mon</text>
        <text>100</text>
    </g>
    <g>
        <text>Tue</text>
        <text>172</text>
    </g>
    <g>
        <text>Wed</text>
        <text>92</text>
    </g>
    ...

Pokud také posuneme označení osy y, která má být nakreslena, před značky, tato datová sada se již bude číst mnohem přirozeněji.

Řešení B:Přidání dalšího kontextu do štítků

Výše uvedené řešení se čte zcela přirozeně, ale také přichází s omezením pro velké datové sady, kde ne každý pruh bude mít odpovídající štítek na ose x. Někdy nechceme označit každý bod na ose x, zvláště když se zabýváme většími datovými sadami.

Pojďme tedy prozkoumat i další možnost. V tomto řešení bude čtečka obrazovky číst osu x jako původně ("pondělí, úterý, středa, čtvrtek, pátek, sobota, neděle"). Poté přečte označení osy y. A když se dostane na štítky nad pruhy, zopakuje hodnotu x každého z nich.

V našem příkladu by to znělo jako "osa X:dny v týdnu. Pondělí, úterý , ... . Osa Y:Počet unikátních návštěvníků. Pondělí:100. Úterý:172. Středa:92. ..." .

Tentokrát se nemusíme dotýkat kódu pro osu x, ale místo toho upravíme kód pro čárové štítky. Začněme jejich přidáním do jednoho textového prvku s názvem barLabels .

const barLabels = chart.selectAll(".label")
  .data(data)
  .enter().append("text");

Dále znovu přidáme náš štítek, který čte hodnotu z osy y. Použijeme tspan a připojte jej k barLabels .

barLabels.append("tspan")
  .text(row => row.visitors)
    .attr("text-anchor", "middle")
    .attr("x", (row, index) => x(index + 1) + 5)
  .attr("y", row => y(row.visitors) + margin.top / 2 - 5);

Než však přečte tuto hodnotu, chceme, aby také přečetl odpovídající hodnotu na ose x. Můžeme zkopírovat a vložit kód shora, ale změňte row => row.visitors na row => row.day .

/* Shows the corresponding value from the x-axis (day of the week). */
barLabels.append("tspan")
  .text(row => row.day)
    .attr("text-anchor", "middle")
    .attr("x", (row, index) => x(index + 1) + 5)
  .attr("y", row => y(row.visitors) + margin.top / 2 - 5)
    .attr("class", "xLabel");

/* Shows the corresponding value from the y-axis (# visitors). */
barLabels.append("tspan")
  .text(row => row.visitors)
    .attr("text-anchor", "middle")
    .attr("x", (row, index) => x(index + 1) + 5)
  .attr("y", row => y(row.visitors) + margin.top / 2 - 5)
    .attr("class", "yLabel");

Toto zní dobrý, ale teď máme jeden vizuální štítek příliš mnoho. Opakování štítku pro čtečky obrazovky má smysl, aby lidé mohli sledovat data. Ale ukazovat to dvakrát není nutné a v tomto případě to přidává do vizualizace další nepořádek.

Nemůžeme přidat nic jako display: none; nebo visibility: hidden na naše xLabel , protože tyto vlastnosti také skrývají prvek před čtečkami obrazovky.

Možným řešením je změna x a y polohování tak, aby bylo možné jej vysunout z rámu.

/* Shows the corresponding value from the x-axis (day of the week). */
barLabels.append("tspan")
  .text(row => row.day)
    .attr("text-anchor", "middle")
    .attr("x", -width)
  .attr("y", -height)
    .attr("class", "xLabel");

/* Shows the corresponding value from the y-axis (# visitors). */
barLabels.append("tspan")
  .text(row => row.visitors)
    .attr("text-anchor", "middle")
    .attr("x", (row, index) => x(index + 1) + 5)
  .attr("y", row => y(row.visitors) + margin.top / 2 - 5)
    .attr("class", "yLabel");

Možná další vylepšení

Dalším dobrým postupem je přidat ke grafům název a popis. To je něco, co lze provést v čistém HTML, například takto:

Můžeme také přidat popisek na osu x, podobný tomu vedle osy y. Zvláště pokud jsou hodnoty na ose x čísla, doporučuje se přidat osu x, která uvádí jednotku.

Je také dobrým zvykem přidat vedle štítků nad pruhy zatržítka na ose y.

Je také vhodné přidat stejná data do (dostupné!) tabulky i jinde na vaší stránce nebo poskytnout odkaz na jinou stránku, která uvádí data v tabulce.

Výsledek

Začali jsme s grafem, který vypadal dobře, ale měl spoustu problémů s přístupností. Poté, co jsme prošli všemi kroky v tomto tutoriálu, jsme skončili s grafem, který stále vypadá dobře, ale je mnohem dostupnější. A trvalo přibližně stejnou dobu, jakou by nám trvalo vytvoření nepřístupné verze grafu!

Toto bude pokračující série. Nadcházející tutoriály se zaměří na různé typy grafů, velké datové sady, komplexní vizualizace a vlastní funkce.

Pokud chcete, abych se zmínil o konkrétním tématu, typu vizualizace nebo otázce, můžete mi dát vědět zasláním zprávy na Twitter (@liatrisbian). Pokud se vám tento druh obsahu líbí, zvažte, zda mi nekoupíte kávu nebo se stanete patronem.

Více zdrojů

  • Dostupnost s Lindsey:Přístupné sloupcové grafy
  • Dostupnost s Lindsey:Přístupné prstencové grafy
  • Dostupné prvky SVG na tricích CSS
  • Dostupné vizualizace dat
  • Složité obrázky
  • Navrhování vizualizací přístupných dat
  • Použití VoiceOveru k vyhodnocení dostupnosti webu
  • Jak tato data zní? Vizualizace dat a VoiceOver