Adding Interactive Charts to Astro thumbnail

Adding Interactive Charts to Astro

October 30, 2024 David Teather

Table of Contents

If there’s one thing I consistently do, it’s start writing a blog post, want a new feature for it and then spend the rest of the time adding that feature and then writing a blog post about it. This one is about adding data graph visualizations to Astro using apex charts but the same basic principals likely would apply to other graphing tools.

Since I still haven’t used mermaid diagrams from my last post I feel compelled to do that.

Goals

This is what I wanted and why I chose apex charts

  1. Interactive charts
  2. Define the chart in markdown
  3. Support darkmode

Example Chart (from example)

Example Chart

Rendered Out With

```chart
{
  "chart": {
    "type": "line"
  },
  "series": [{
    "name": "sales",
    "data": [30,40,35,50,49,60,70,91,125]
  }],
  "xaxis": {
    "categories": [1991,1992,1993,1994,1995,1996,1997, 1998,1999]
  }
}
```

Initial Setup

Integration With Rehype

Our first step is to tell our markdown, rehype in this case to support these kinds of codeblocks.

```chart
{
    // our Apex Charts config
}
```

To get this done, we’ll use a custom Rehype plugin. Here’s what mine likes, although my conditions might be different, since I’m accumulating a lot of these Rehype plugins.

import { visit } from 'unist-util-visit';
import crypto from 'crypto';
 
export function rehypeChart() {
    return (tree) => {
        visit(tree, 'element', (node) => {
            // The structure my markdown gets parsed as is <pre><code>...</code></pre>
            // and because of styling I want to replace the <pre><code> with just one <div>
            if (
                node.tagName === 'pre' &&
                node.children.length === 1 &&
                node.children[0].tagName === 'code'
            ) {
                const childNode = node.children[0];
 
                // check if the child node is a code block with the language set to 'chart'
                if (
                    childNode.tagName === 'code' &&
                    childNode.properties.className &&
                    childNode.properties.className.length > 0 &&
                    childNode.properties.className[0] === 'language-chart'
                ) {
                    // Check if the code block contains only text
                    if (childNode.children.length !== 1 || childNode.children[0].type !== 'text') {
                        throw new Error('Invalid chart code block');
                    }
 
                    // Parse the chart's data from the code block
                    var chartData = JSON.parse(childNode.children[0].value);
                    // Generate a unique ID for the chart, this will let us reference the chart later for dark mode
                    const uuid = crypto.randomBytes(16).toString('hex');
                    chartData.chart.id = uuid
 
                    // Replace the node with a div element with the chart data attached
                    node.type = 'element';
                    node.tagName = 'div';
                    node.properties = {
                        id: uuid,
                        className: ['chart-container'],
                        'data-chart': JSON.stringify(chartData)
                    };
                    node.children = [];
                }
            }
        });
    };
}

Then we can use this function in our astro config which should look like something like this

astro.config.mjs
export default defineConfig({
    markdown: {
        rehypePlugins: [rehypeChart] // add to your existing plugins array
    }
})

Adding The Javascript

From that, we’ll just get a div without any interactive components to it (or a cool visualization). Next, we’ll add javascript so that the chart actually renders out.

First install Apex Charts to your project with

npm i apexcharts

Then, in your page where you render out the markdown in my case ArticleLayout.astro, add the following

---
<script>
  import ApexCharts from "apexcharts";
 
  // when the page loads
  document.addEventListener("DOMContentLoaded", () => {
    // look for each of our charts
    document.querySelectorAll(".chart-container").forEach((chartDiv) => {
      // extract our Apex Charts config and render it out
      const chartDataAttr = chartDiv.getAttribute("data-chart");
      if (chartDataAttr) {
        const chartData = JSON.parse(chartDataAttr);
        const chart = new ApexCharts(
          chartDiv,
          chartData
        );
        chart.render();
      }
    });
  });
</script>

With this you should be able to get your charts working and maybe try out some of the examples or just use the simple one from earlier with

```chart
{
  "chart": {
    "type": "line"
  },
  "series": [{
    "name": "sales",
    "data": [30,40,35,50,49,60,70,91,125]
  }],
  "xaxis": {
    "categories": [1991,1992,1993,1994,1995,1996,1997, 1998,1999]
  }
}
```

However the styling isn’t that great and I wanted to support dark-mode.

Supporting Darkmode

Luckily the charts have a dark theme. I found this nice function on a GitHub issues page that allows us to take our config and conditionally apply the dark mode or not.

function flipChartThemeMode(chartOptions: any, darkMode: boolean) {
  const theme = chartOptions.theme;
  const tooltip = chartOptions.tooltip;
  const chart = chartOptions.chart;
 
  chartOptions = {
    ...chartOptions,
    chart: {
      ...chart,
      background: "rgba(0, 0, 0, 0)", // also make the background transparent
      fontFamily: "inherit", // inherit the font family
    },
    theme: {
      ...theme,
      mode: darkMode ? "dark" : "light",
    },
    tooltip: {
      ...tooltip,
      theme: darkMode ? "dark" : "light",
    },
  };
 
  return chartOptions;
}

Then I updated our function from earlier to this for the initialization of the page.

<script>
  import ApexCharts from "apexcharts";
  import { flipChartThemeMode } from "../utils/theme-helpers"
 
  document.addEventListener("DOMContentLoaded", () => {
    // this is how I track my theme, with DaisyUI
    const dataTheme = document.documentElement.getAttribute("data-theme"); 
 
    document.querySelectorAll(".chart-container").forEach((chartDiv) => {
      const chartDataAttr = chartDiv.getAttribute("data-chart");
      if (chartDataAttr) {
        const chartData = JSON.parse(chartDataAttr);
 
        const chart = new ApexCharts(
          chartDiv,
          flipChartThemeMode(chartData, dataTheme === "dark")
        );
        chart.render();
      }
    });
  });
</script>

Then I also wanted to add to my toggle switch, and if you notice above that just handles on page load. So I added this segment to my function that is triggered when the theme should switch

// inside of an existing function called toggleTheme()
// this will depend if you also have a toggle for your theme
document.querySelectorAll(".chart-container").forEach((chartDiv) => {
    const chartDataAttr = chartDiv.getAttribute("data-chart");
    if (chartDataAttr) {
        const chartData = JSON.parse(chartDataAttr);
        const chartID = chartDiv.firstElementChild?.id; // uses our ID from earlier
        if (!chartID) return;
        const chart = ApexCharts.getChartByID(
            chartID.replace("apexcharts", "")
        );
        if (chart) {
            chart.updateOptions(
                flipChartThemeMode(chartData, newTheme === "dark")
            );
        } else {
            console.error("chart not found");
        }
    }
});

Finally I added the following global css. I’m using tailwind prose to format my markdown and it has some builtin colors/styles that I wanted to keep the charts the style in. So feel free to change to fit your design

.apexcharts-text {
    fill: var(--tw-prose-headings) !important;
    color: var(--tw-prose-headings) !important;
}
 
.apexcharts-tooltip {
    color: var(--tw-prose-headings) !important;
}
 
.apexcharts-xaxistooltip {
    color: var(--tw-prose-headings) !important;
    background-color: transparent !important;
    display: none; /* I wanted to hide the x-axis tooltip */
}
 
.apexcharts-legend-text {
    color: var(--tw-prose-headings) !important;
}

I’m not totally happy with the tooltip background colors at the moment but it fits well enough for now. If you have any better css for tailwind prose please shoot them my way and I’ll update the blog post!

Anyways, this is a pretty short blog post but hope you did enjoy and hopefully now you can use charts in Astro :D

If you found this helpful please let me know, or if you’d prefer more tutorial content that is built on top of the base astro sites please let me know. Since I know these posts sometimes are just me spewing and copy pasting my solutions into a markdown file haha, but still hopefully they help.

Back to blog