By Paul Scanlon

Create Accessible Data Visualization for your Gatsby Blog with Charts.css

  • React
  • Gatsby
  • JavaScript
  • MDX
  • 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.

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
  1. foo
  2. bar
  3. 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.

  1. Query allMdx and return the tags from frontmatter
  2. Extract the tags and push them to an array
  3. Count the occurrences of each tag and sum them up
  4. 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 🕺

Hey!

Leave a reaction and let me know how I'm doing.

  • 0
  • 0
  • 0
  • 0
  • 0
Powered byNeon