Create Accessible Data Visualization for your Gatsby Blog with Charts.css
Ahoy M’Hartys! ⚓ in this post i’m going to demonstrate how you can create Accessible Data visualization using Charts.css for your Gatsby blog
There’s two parts to this post. The first will introduce a concept I like to call “Data Components” and the second will be how to implement Charts.css using data made available via a Data Component.
Links
What are Data Components?
This is a little trick i’ve used a few times and it featured heavily in my Gatsby Theme : gatsby-theme-terminal but, as I discovered recently its not overly clear what I mean by Data Components. TLDR; They utilize React’s Render Props or more specifically Render Functions and they work a bit like this 👇
Component
// my-component.js
import React, { Fragment } from 'react';
const MyComponent = ({ children }) => {
const data = ['foo', 'bar', 'baz'];
return children(data); // => children can also be functions with arguments ☝️
};
export default MyComponent;
Usage
// index.js
import React from 'react';
import MyComponent from './components/my-component';
const IndexPage = () => {
return (
<main>
<MyComponent>
{(data) => {
return (
<ul>
{data.map((item, index) => {
return <li key={index}>{item}</li>;
})}
</ul>
);
}}
</MyComponent>
</main>
);
};
export default IndexPage;
Which would result in a <ul>
being returned with an <li>
for each string in the data
array. E.g
- foo
- bar
- baz
If you were feeling so inclined you could also return an <ol>
under the <ul>
like this 👇
// index.js
import React from 'react'
import MyComponent from './components/my-component'
const IndexPage = () => {
return (
<main>
<MyComponent>
{(data) => {
return (
<ul>
{data.map((item, index) => {
return <li key={index}>{item}</li>
})}
</ul>
)
}}
</MyComponent>
+ <MyComponent>
+ {(data) => {
+ return (
+ <ol>
+ {data.map((item, index) => {
+ return <li key={index}>{item}</li>
+ })}
+ </ol>
+ )
+ }}
+ </MyComponent>
</main>
)
}
export default IndexPage
…which would return something like this 👇
- foo
- bar
- baz
- foo
- bar
- baz
I appreciate that neither of the above examples are that useful but what i’m trying to demonstrate is that
<MyComponent />
returns it’s children as a function and passes along with it the data
What you choose to do with this data
is entirely up to you. This approach to composition can be incredibly powerful
because it decouples the data from the UI elements. This in turn (in my experience) results in fewer “components” being
created to handle one off parts of the UI.
To take this one step further and to get back to demonstrating how to use Charts.css i’m going
to create a Data Component that will query the tags used in frontmatter
from the example MDX posts in the demo repo.
This is the data I’ll be using to drive Charts.css
Create the Data Component
This data component is going to be in charge of querying allMdx.frontmatter.tags
and then transforming the result into
something I can use to drive Charts.css
// components/tags-data.js
import { useStaticQuery, graphql } from 'gatsby';
import React, { Fragment } from 'react';
const TagsData = ({ children }) => {
// 1. Query the allMdx.frontmatter.tags
const tags = useStaticQuery(graphql`
query {
allMdx(filter: { frontmatter: { tags: { ne: null } } }) {
nodes {
frontmatter {
tags
}
}
}
}
`)
// 2. Extract tags
.allMdx.nodes.reduce((items, item) => {
const { tags } = item.frontmatter;
tags.map((tag) => items.push(tag));
return items;
}, [])
// 3. Reduce tags and count them
.reduce((items, item) => {
const existingItem = items.find((index) => index.tag === item);
if (existingItem) {
existingItem.count += 1;
} else {
items.push({
tag: item,
count: 1,
});
}
return items;
}, [])
.sort((a, b) => b.count - a.count);
// .slice(0, 5) // optional, if you only wanted the top 5 results
return (
<Fragment>
{typeof children === 'function'
? // 4. Pass tags data back to children
children(tags.length ? tags : null)
: children}
</Fragment>
);
};
export default TagsData;
It looks a bit wild eh… lemme walk you though it.
- Query
allMdx
and return thetags
fromfrontmatter
- Extract the tags and push them to an array
- Count the occurrences of each tag and sum them up
- Return children with an array of tag data
The above will return an array containing the tag names and a count value for each, like this 👇
[
{
tag: 'Gatsby',
count: 5,
},
{
tag: 'JavaScript',
count: 4,
},
{
tag: 'React',
count: 3,
},
{
tag: 'TypeScript',
count: 2,
},
{
tag: 'CSS',
count: 1,
},
];
Now that the data is in the correct shape and passed via children as a Render Function it cam be accessed from wherever
I chose to use the <TagsData />
component.
To use the list example again, I could create a <ul>
with an <li>
for each tag and display the name and count
// index.js
import TagsData from './components/tags-data'
...
<h2>Tags List</h2>
<p>Just a standard HTML list</p>
<TagsData>
{(tags) => {
return (
<ul>
{tags.map((item, index) => {
const { tag, count } = item;
return <li key={index}>{`${tag} | ${count} `}</li>;
})}
</ul>
);
}}
</TagsData>
...
But what we really want to do is return a Chart right?
To use Charts.css first install it
npm install charts.css
or
yarn add charts.css
Then add the CSS to gatsby-browser.js
// gatsby-browser-js
import 'charts.css';
Now I can re-use the <TagsData />
component again and return all the bits required to render a Charts.css chart
// index.js
...
<h2>Tags Chart</h2>
<p>Bar</p>
<TagsData>
{(tags) => {
return (
<table className="charts-css bar show-labels">
<caption>Tags Chart Bar</caption>
<tbody>
{tags.map((item, index) => {
const { tag, count } = item;
return (
<tr key={index}>
<th>{tag}</th>
<td
style={{
'--size': `calc(${count / 10})`
}}
/>
</tr>
);
})}
</tbody>
</table>
);
}}
</TagsData>
...
You’ll see in the demo repo i’ve returned a few examples of the Charts and they are all children of the same
<TagsData />
component. I like this approach but naturally it might not work for you!
See you around 🕺