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.
This is what I wanted and why I chose apex charts
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]
}
}
```
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
export default defineConfig({
markdown: {
rehypePlugins: [rehypeChart] // add to your existing plugins array
}
})
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.
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