Skip to content

Big O Visualizer

Polishing the charts

Change2 min read

This week I've been working on the polish of this project. No, I'm not translating the website to the Polish language (sorry Poland!), but:

pol·ish (verb) make the surface of (something) smooth and shiny by rubbing it.

A.k.a. just raising the bar on things. Whether I'm working on a multi-million user international product or my own little pet project, I like to use my own products every day. I wake up with them and I go to bed with them. And even though I've looked at the same screen every day for the last thirty days... sometimes I can just wake up, open that same screen I've been staring at for ages and think:

No.
This doesn't look right.
Has this always been like this?
This won't do.
I have to change this immediately.

I had a moment like this a couple of days ago when I opened the Big O Visualizer on my smartphone, looked at the charts and thought:

Ugh...

My biggest peeve was that the whole thing just looked constrained and awkward on a smaller viewport. I wondered if it was feasible to cram this much information in such a small space. I mean, whenever I see a big news website embed charts in their articles they don't work that well on my smartphone (iPhone XS for those wondering). Can this be improved? Or are responsive charts like responsive tables: doomed to fail.

And thus I scurried to my desk in my bathrobe and started taking the thing apart.

Highcharts Responsive versus React Responsive

Initially, I tried to use Highchart's own Responsive functionality, which was recently introduced in their 5.0 release. Unfortunately, this didn't play well with react-jsx-highcharts, the React plugin I'm using to integrate Highcharts into this project.

However, the React ecosystem is home to the excellent react-responsive plugin by Eric Schoffstall. This plugin introduces the useMediaQuery Hook, which allows me to do delicious things like:

src/components/complexity-chart.tsx
1const ComplexityChart = ({ title, children }: ComplexityChartProps) => {
2 const { theme } = useThemeUI()
3 const isDesktop = useMediaQuery({ minDeviceWidth: theme.breakpoints?.[0] as string })
4 const yAxisLabels = isDesktop ? { rotation: 0, padding: 5, x: -8 } : { rotation: -90, padding: 0, x: -3 }
5 const titleStyle = isDesktop ? { fontSize: theme.fontSizes?.[2] } : { fontSize: theme.fontSizes?.[1] }
6 const chartMarginRight = isDesktop ? 70 : 0
7 const chartSpacing = isDesktop ? [10, 10, 15, 10] : [10, 5, 15, 5]
8 const [colorMode] = useColorMode()
9 const isDark = colorMode === `dark`
10
11 return (
12 <HighchartsChart plotOptions={plotOptions} callback={setTheme} key={colorMode} sx={{ backgroundColor: "chart" }}>
13 <Chart marginRight={chartMarginRight} spacing={chartSpacing} zoomType="xy" backgroundColor="transparent" />
14 <Title style={titleStyle}>{title}</Title>
15 <Loading>Running analysis...</Loading>
16 <Legend />
17 <Tooltip />
18 <XAxis type="logarithmic" min={10} max={10000}>
19 <XAxis.Title>Elements (n)</XAxis.Title>
20 </XAxis>
21 <YAxis type="logarithmic" min={10} max={100000000} labels={yAxisLabels}>
22 {isDesktop && <YAxis.Title>Operations (O)</YAxis.Title>}
23 {children}
24 </YAxis>
25 </HighchartsChart>
26 )
27}

The lines of interest are highlighted. Below an explanation per line:

  • Line 3. Create a fresh useMediaQuery Hook using the desktop breakpoint from the current theme.
  • Line 4. Render the Y-axis labels normally on desktop and 90 degrees rotated on mobile. This reduces the width of the Y-axis on mobile, so there's more space left for the chart.
  • Line 5. Match the font-size of the title with the rest of the site. This is just to keep things consistent with the theme, which uses a larger font size on desktop than on mobile.
  • Line 6. Reserve some whitespace to the right of the chart on desktop to render the labels. On desktop, we will keep the complexity labels out of the chart area, but on mobile we're going to pull those in.
  • Line 7. Give the chart some more space on desktop.
  • Line 22. Render the title of the YAxis only on desktop. This powerful feature of the useMediaQuery Hook allows you to conditionally render any React component given a media query. Cool stuff.

The ComplexitySeries component is changed so that the complexity labels are pulled inside the chart area on mobile. Again, we use useMediaQuery to achieve this behavior:

src/components/complexity-series.tsx
1const ComplexitySeries = () => {
2 const { theme } = useThemeUI()
3 const isDesktop = useMediaQuery({ minDeviceWidth: theme.breakpoints?.[0] as string })
4 const plotOptions = {
5 lineWidth: 0,
6 marker: {
7 enabled: false,
8 },
9 states: {
10 hover: {
11 lineWidth: 0,
12 },
13 },
14 enableMouseTracking: false,
15 showInLegend: false,
16 dataLabels: {
17 enabled: false,
18 crop: false,
19 allowOverlap: true,
20 overflow: isDesktop ? "allow" : "justify",
21 align: isDesktop ? "left" : "center",
22 verticalAlign: "middle",
23 format: `{series.userOptions.notation}`,
24 x: isDesktop ? 0 : 5,
25 },
26 }
27 const xPoints = Array.from({ length: 42 }, (v, i) => Math.min(10000, 2 ** i / 3))
28 const complexitySeries = Complexities.common.map((r) => (
29 <AreaSeries
30 key={r.name}
31 name={r.name}
32 color={getColorForComplexity(theme, r)}
33 notation={r.notation}
34 data={xPoints.map((x) => ({ x, y: r.calculate(x) }))}
35 {...plotOptions}
36 />
37 ))
38 complexitySeries.forEach((x) =>
39 Object.assign(x.props.data[x.props.data.length - 1], { dataLabels: { enabled: true } })
40 )
41 return complexitySeries
42}

That's it! Just four lines of code were changed to introduce this behavior.

The result

So what does this get us? See the below before and after:

chart unresponsive
chart responsive

Not bad. 👍