Categories
Visx

Create a React Stacked Bar Chart with the Visx Library

Visx is a library that lets us add graphics to our React app easily.

In this article, we’ll look at how to use it to add stacked bar charts into our React app.

Install Required Packages

We have to install a few modules.

To get started, we run:

npm i @visx/axis @visx/grid @visx/group @visx/legend @visx/mock-data @visx/responsive @visx/scale @visx/shape @visx/tooltip

to install the packages.

Create the Chart

We can create the chart by adding the items provided by the modules.

We use the data from the @visx/mock-data module.

To add the stacked bar chart, we write:

import React from "react";
import { BarStack } from "@visx/shape";
import { Group } from "@visx/group";
import { Grid } from "@visx/grid";
import { AxisBottom } from "@visx/axis";
import cityTemperature from "@visx/mock-data/lib/mocks/cityTemperature";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { timeParse, timeFormat } from "d3-time-format";
import { useTooltip, useTooltipInPortal, defaultStyles } from "@visx/tooltip";
import { LegendOrdinal } from "@visx/legend";

const purple1 = "#6c5efb";
const purple2 = "#c998ff";
export const purple3 = "#a44afe";
export const background = "#eaedff";
const defaultMargin = { top: 40, right: 0, bottom: 0, left: 0 };
const tooltipStyles = {
  ...defaultStyles,
  minWidth: 60,
  backgroundColor: "rgba(0,0,0,0.9)",
  color: "white"
};

const data = cityTemperature.slice(0, 12);
const keys = Object.keys(data[0]).filter((d) => d !== "date");

const temperatureTotals = data.reduce((allTotals, currentDate) => {
  const totalTemperature = keys.reduce((dailyTotal, k) => {
    dailyTotal += Number(currentDate[k]);
    return dailyTotal;
  }, 0);
  if (Array.isArray(allTotals)) {
    allTotals.push(totalTemperature);
    return allTotals;
  }
  return [];
});

const parseDate = timeParse("%Y-%m-%d");
const format = timeFormat("%b %d");
const formatDate = (date) => format(parseDate(date));

const getDate = (d) => d.date;

const dateScale = scaleBand({
  domain: data.map(getDate),
  padding: 0.2
});
const temperatureScale = scaleLinear({
  domain: [0, Math.max(...temperatureTotals)],
  nice: true
});
const colorScale = scaleOrdinal({
  domain: keys,
  range: [purple1, purple2, purple3]
});

let tooltipTimeout;

function Example({ width, height, events = false, margin = defaultMargin }) {
  const {
    tooltipOpen,
    tooltipLeft,
    tooltipTop,
    tooltipData,
    hideTooltip,
    showTooltip
  } = useTooltip();

  const { containerRef, TooltipInPortal } = useTooltipInPortal();

  if (width < 10) return null;
  const xMax = width;
  const yMax = height - margin.top - 100;

  dateScale.rangeRound([0, xMax]);
  temperatureScale.range([yMax, 0]);

  return width < 10 ? null : (
    <div style={{ position: "relative" }}>
      <svg ref={containerRef} width={width} height={height}>
        <rect
          x={0}
          y={0}
          width={width}
          height={height}
          fill={background}
          rx={14}
        />
        <Grid
          top={margin.top}
          left={margin.left}
          xScale={dateScale}
          yScale={temperatureScale}
          width={xMax}
          height={yMax}
          stroke="black"
          strokeOpacity={0.1}
          xOffset={dateScale.bandwidth() / 2}
        />
        <Group top={margin.top}>
          <BarStack
            data={data}
            keys={keys}
            x={getDate}
            xScale={dateScale}
            yScale={temperatureScale}
            color={colorScale}
          >
            {(barStacks) =>
              barStacks.map((barStack) =>
                barStack.bars.map((bar) => (
                  <rect
                    key={`bar-stack-${barStack.index}-${bar.index}`}
                    x={bar.x}
                    y={bar.y}
                    height={bar.height}
                    width={bar.width}
                    fill={bar.color}
                    onClick={() => {
                      if (events) alert(`clicked: ${JSON.stringify(bar)}`);
                    }}
                    onMouseLeave={() => {
                      tooltipTimeout = window.setTimeout(() => {
                        hideTooltip();
                      }, 300);
                    }}
                    onMouseMove={(event) => {
                      if (tooltipTimeout) clearTimeout(tooltipTimeout);
                      const top = event.clientY - margin.top - bar.height;
                      const left = bar.x + bar.width / 2;
                      showTooltip({
                        tooltipData: bar,
                        tooltipTop: top,
                        tooltipLeft: left
                      });
                    }}
                  />
                ))
              )
            }
          </BarStack>
        </Group>
        <AxisBottom
          top={yMax + margin.top}
          scale={dateScale}
          tickFormat={formatDate}
          stroke={purple3}
          tickStroke={purple3}
          tickLabelProps={() => ({
            fill: purple3,
            fontSize: 11,
            textAnchor: "middle"
          })}
        />
      </svg>
      <div
        style={{
          position: "absolute",
          top: margin.top / 2 - 10,
          width: "100%",
          display: "flex",
          justifyContent: "center",
          fontSize: "14px"
        }}
      >
        <LegendOrdinal
          scale={colorScale}
          direction="row"
          labelMargin="0 15px 0 0"
        />
      </div>

      {tooltipOpen && tooltipData && (
        <TooltipInPortal
          key={Math.random()} // update tooltip bounds each render
          top={tooltipTop}
          left={tooltipLeft}
          style={tooltipStyles}
        >
          <div style={{ color: colorScale(tooltipData.key) }}>
            <strong>{tooltipData.key}</strong>
          </div>
          <div>{tooltipData.bar.data[tooltipData.key]}℉</div>
          <div>
            <small>{formatDate(getDate(tooltipData.bar.data))}</small>
          </div>
        </TooltipInPortal>
      )}
    </div>
  );
}

export default function App() {
  return (
    <div className="App">
      <Example width={500} height={300} />
    </div>
  );
}

We create the purple , purple2 , purple3 variables for the colors of the bars.

background is for the chart’s background color.

tooltipStyles have the styles for the tooltips.

The bar data is set as the value of the data variable.

keys have the values for the x-axis ticks.

We computed the temnperatureTotals by adding the temperature values for each day together.

We then create the dateScale for the x-axis scale.

And temperature scale is for the y-axis scale.

We set the max value of temperatureScale to the max value of the temperatureTotals which is the highest value for the y-axis.

colorScale has the color values.

Then we create the Example component to hold the stacked bar chart.

The useTooltip hook returns an object with various methods and states for creating and setting tooltip values.

We then compute the xMax and yMax values to create the x and y-axis scales.

Next, we add the svg element to hold all the chart parts together.

The Grid component has the grid displayed in the background.

The Group component has the BarStack component, which has the stacked bars.

The stacked bars are created by the rect element.

We have the onMouseMove handler to call showTooltip to show the tooltip with the bar values.

The onMouseLeave handler lets us close the tooltip when we navigate away from a bar.

The AxisBottom component renders the x-axis with the styles and the colors.

Conclusion

We can add stacked bar charts into our React app with the modules provided by Visx.

Categories
Visx

Create a React Fill Line Chart with Navigation with the Visx Library

Visx is a library that lets us add graphics to our React app easily.

In this article, we’ll look at how to use it to add filled line charts with navigation into our React app.

Install Required Packages

We have to install a few modules to create the grouped bar chart.

To get started, we run:

npm i @visx/axis @visx/brush @visx/curve @visx/gradient @visx/group @visx/mock-data @visx/pattern @visx/responsive @visx/scale @visx/shape

to install the packages.

Create the Chart

We can create the chart by adding the items provided by the modules.

We use the data from the @visx/mock-data module.

Then to create the filled line chart with a chart for navigation at the bottom, we write:

import React, { useRef, useState, useMemo } from "react";
import { scaleTime, scaleLinear } from "@visx/scale";
import appleStock from "@visx/mock-data/lib/mocks/appleStock";
import { Brush } from "@visx/brush";
import { PatternLines } from "@visx/pattern";
import { LinearGradient } from "@visx/gradient";
import { max, extent } from "d3-array";
import { AxisBottom, AxisLeft } from "@visx/axis";
import { AreaClosed } from "@visx/shape";
import { Group } from "@visx/group";
import { curveMonotoneX } from "@visx/curve";

const stock = appleStock.slice(1000);
const brushMargin = { top: 10, bottom: 15, left: 50, right: 20 };
const chartSeparation = 30;
const PATTERN_ID = "brush_pattern";
const GRADIENT_ID = "brush_gradient";
export const accentColor = "#f6acc8";
export const background = "#584153";
export const background2 = "#af8baf";
const selectedBrushStyle = {
  fill: `url(#${PATTERN_ID})`,
  stroke: "white"
};

const getDate = (d) => new Date(d.date);
const getStockValue = (d) => d.close;
const axisColor = "#fff";
const axisBottomTickLabelProps = {
  textAnchor: "middle",
  fontFamily: "Arial",
  fontSize: 10,
  fill: axisColor
};
const axisLeftTickLabelProps = {
  dx: "-0.25em",
  dy: "0.25em",
  fontFamily: "Arial",
  fontSize: 10,
  textAnchor: "end",
  fill: axisColor
};

function AreaChart({
  data,
  gradientColor,
  width,
  yMax,
  margin,
  xScale,
  yScale,
  hideBottomAxis = false,
  hideLeftAxis = false,
  top,
  left,
  children
}) {
  if (width < 10) return null;
  return (
    <Group left={left || margin.left} top={top || margin.top}>
      <LinearGradient
        id="gradient"
        from={gradientColor}
        fromOpacity={1}
        to={gradientColor}
        toOpacity={0.2}
      />
      <AreaClosed
        data={data}
        x={(d) => xScale(getDate(d)) || 0}
        y={(d) => yScale(getStockValue(d)) || 0}
        yScale={yScale}
        strokeWidth={1}
        stroke="url(#gradient)"
        fill="url(#gradient)"
        curve={curveMonotoneX}
      />
      {!hideBottomAxis && (
        <AxisBottom
          top={yMax}
          scale={xScale}
          numTicks={width > 520 ? 10 : 5}
          stroke={axisColor}
          tickStroke={axisColor}
          tickLabelProps={() => axisBottomTickLabelProps}
        />
      )}
      {!hideLeftAxis && (
        <AxisLeft
          scale={yScale}
          numTicks={5}
          stroke={axisColor}
          tickStroke={axisColor}
          tickLabelProps={() => axisLeftTickLabelProps}
        />
      )}
      {children}
    </Group>
  );
}

function BrushChart({
  compact = false,
  width,
  height,
  margin = {
    top: 20,
    left: 50,
    bottom: 20,
    right: 20
  }
}) {
  const brushRef = useRef(null);
  const [filteredStock, setFilteredStock] = useState(stock);

  const onBrushChange = (domain) => {
    if (!domain) return;
    const { x0, x1, y0, y1 } = domain;
    const stockCopy = stock.filter((s) => {
      const x = getDate(s).getTime();
      const y = getStockValue(s);
      return x > x0 && x < x1 && y > y0 && y < y1;
    });
    setFilteredStock(stockCopy);
  };

  const innerHeight = height - margin.top - margin.bottom;
  const topChartBottomMargin = compact
    ? chartSeparation / 2
    : chartSeparation + 10;
  const topChartHeight = 0.8 * innerHeight - topChartBottomMargin;
  const bottomChartHeight = innerHeight - topChartHeight - chartSeparation;
  const xMax = Math.max(width - margin.left - margin.right, 0);
  const yMax = Math.max(topChartHeight, 0);
  const xBrushMax = Math.max(width - brushMargin.left - brushMargin.right, 0);
  const yBrushMax = Math.max(
    bottomChartHeight - brushMargin.top - brushMargin.bottom,
    0
  );
  const dateScale = useMemo(
    () =>
      scaleTime({
        range: [0, xMax],
        domain: extent(filteredStock, getDate)
      }),
    [xMax, filteredStock]
  );
  const stockScale = useMemo(
    () =>
      scaleLinear({
        range: [yMax, 0],
        domain: [0, max(filteredStock, getStockValue) || 0],
        nice: true
      }),
    [yMax, filteredStock]
  );
  const brushDateScale = useMemo(
    () =>
      scaleTime({
        range: [0, xBrushMax],
        domain: extent(stock, getDate)
      }),
    [xBrushMax]
  );
  const brushStockScale = useMemo(
    () =>
      scaleLinear({
        range: [yBrushMax, 0],
        domain: [0, max(stock, getStockValue) || 0],
        nice: true
      }),
    [yBrushMax]
  );

  const initialBrushPosition = useMemo(
    () => ({
      start: { x: brushDateScale(getDate(stock[50])) },
      end: { x: brushDateScale(getDate(stock[100])) }
    }),
    [brushDateScale]
  );

  const handleClearClick = () => {
    if (brushRef?.current) {
      setFilteredStock(stock);
      brushRef.current.reset();
    }
  };

  const handleResetClick = () => {
    if (brushRef?.current) {
      const updater = (prevBrush) => {
        const newExtent = brushRef.current.getExtent(
          initialBrushPosition.start,
          initialBrushPosition.end
        );

        const newState = {
          ...prevBrush,
          start: { y: newExtent.y0, x: newExtent.x0 },
          end: { y: newExtent.y1, x: newExtent.x1 },
          extent: newExtent
        };

        return newState;
      };
      brushRef.current.updateBrush(updater);
    }
  };

  return (
    <div>
      <svg width={width} height={height}>
        <LinearGradient
          id={GRADIENT_ID}
          from={background}
          to={background2}
          rotate={45}
        />
        <rect
          x={0}
          y={0}
          width={width}
          height={height}
          fill={`url(#${GRADIENT_ID})`}
          rx={14}
        />
        <AreaChart
          hideBottomAxis={compact}
          data={filteredStock}
          width={width}
          margin={{ ...margin, bottom: topChartBottomMargin }}
          yMax={yMax}
          xScale={dateScale}
          yScale={stockScale}
          gradientColor={background2}
        />
        <AreaChart
          hideBottomAxis
          hideLeftAxis
          data={stock}
          width={width}
          yMax={yBrushMax}
          xScale={brushDateScale}
          yScale={brushStockScale}
          margin={brushMargin}
          top={topChartHeight + topChartBottomMargin + margin.top}
          gradientColor={background2}
        >
          <PatternLines
            id={PATTERN_ID}
            height={8}
            width={8}
            stroke={accentColor}
            strokeWidth={1}
            orientation={["diagonal"]}
          />
          <Brush
            xScale={brushDateScale}
            yScale={brushStockScale}
            width={xBrushMax}
            height={yBrushMax}
            margin={brushMargin}
            handleSize={8}
            innerRef={brushRef}
            resizeTriggerAreas={["left", "right"]}
            brushDirection="horizontal"
            initialBrushPosition={initialBrushPosition}
            onChange={onBrushChange}
            onClick={() => setFilteredStock(stock)}
            selectedBoxStyle={selectedBrushStyle}
          />
        </AreaChart>
      </svg>
      <button onClick={handleClearClick}>Clear</button>&nbsp;
      <button onClick={handleResetClick}>Reset</button>
    </div>
  );
}

export default function App() {
  return (
    <div className="App">
      <BrushChart width="500" height="300" />
    </div>
  );
}

We use the appleStock mock data to create our chart.

We just get the first 1000 entries from the appleStock array and render them.

brushMargin is the margins for the chart.

We declare other variables for the colors like accentColor for the fill color.

background and background2 for the gradient colors.

getDate lets us get the date from the data for the x-axis.

getStockValue returns the value for the y-axis.

Then we create the AreaChart component with the filled line chart.

We use the axisBottomTickLabelProps object to style the x-axis.

And we do the same with the y-axis with the axisLeftTickLabelProps .

In the AreaChart component, we add the LinearGradient component with the gradient for the fill of the line chart.

AreaClosed has the fill between the line and the x-axis.

AxisBottom has the x-axis. scale has the x-axis scale. top sets the x-axis location.

AxisLeft has the y-axis.

Next, we create the BrushChart component to create a chart that lets us navigate the filled line chart by dragging it.

We have th onBrushChange function to change the location of the filled line chart.

Then we set the innerHeight , topChartBottomMargin , etc. to set the margins, heights, and the max values for the x and y axes.

We set the scales with the dateScale , stockScale , brushDateScale , and brushStockScale to set the scales for the brush chart.

And we set the initialBrushPosition to a value to set the initial position for the brush chart.

We also have the handleClearClick to clear the brush when we click the Clear button.

handleResetClick resets the brush position.

Finally, we put the filled line chart above the brush chart by putting the 2 AreaCharts together.

The first is for the main line chart and the 2nd is for navigation.

The Brush component lets us drag the 2nd line chart to navigate the first line chart.

Conclusion

We can create a filled line chart with navigation by adding multiple line charts with a brush in our React app.

Categories
Visx

Create a React Horizontal Grouped Bar Chart with the Visx Library

Visx is a library that lets us add graphics to our React app easily.

In this article, we’ll look at how to use it to add horizontal grouped bar charts into our React app.

Install Required Packages

We have to install a few modules to create the grouped bar chart.

To get started, we run:

npm i @visx/axis @visx/group @visx/mock-data @visx/responsive @visx/scale @visx/shape

to install the packages.

Create the Chart

We can create the chart by adding the items provided by the modules.

We use the data from the @visx/mock-data module.

To create the chart, we write:

import React from "react";
import { BarGroupHorizontal, Bar } from "@visx/shape";
import { Group } from "@visx/group";
import { AxisLeft } from "@visx/axis";
import cityTemperature from "@visx/mock-data/lib/mocks/cityTemperature";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { timeParse, timeFormat } from "d3-time-format";

const blue = "#aeeef8";
const green = "#e5fd3d";
const purple = "#9caff6";
const background = "#612efb";
const defaultMargin = { top: 20, right: 20, bottom: 20, left: 50 };

const parseDate = timeParse("%Y-%m-%d");
const format = timeFormat("%b %d");
const formatDate = (date) => format(parseDate(date));
function max(arr, fn) {
  return Math.max(...arr.map(fn));
}

const data = cityTemperature.slice(0, 4);
const keys = Object.keys(data[0]).filter((d) => d !== "date");

const getDate = (d) => d.date;

const dateScale = scaleBand({
  domain: data.map(getDate),
  padding: 0.2
});
const cityScale = scaleBand({
  domain: keys,
  padding: 0.1
});
const tempScale = scaleLinear({
  domain: [0, max(data, (d) => max(keys, (key) => Number(d[key])))]
});
const colorScale = scaleOrdinal({
  domain: keys,
  range: [blue, green, purple]
});

function Example({ width, height, margin = defaultMargin, events = false }) {
  const xMax = width - margin.left - margin.right;
  const yMax = height - margin.top - margin.bottom;

dateScale.rangeRound([0, yMax]);
  cityScale.rangeRound([0, dateScale.bandwidth()]);
  tempScale.rangeRound([0, xMax]);

return width < 10 ? null : (
    <svg width={width} height={height}>
      <rect
        x={0}
        y={0}
        width={width}
        height={height}
        fill={background}
        rx={14}
      />
      <Group top={margin.top} left={margin.left}>
        <BarGroupHorizontal
          data={data}
          keys={keys}
          width={xMax}
          y0={getDate}
          y0Scale={dateScale}
          y1Scale={cityScale}
          xScale={tempScale}
          color={colorScale}
        >
          {(barGroups) =>
            barGroups.map((barGroup) => (
              <Group
                key={`bar-group-horizontal-${barGroup.index}-${barGroup.y0}`}
                top={barGroup.y0}
              >
                {barGroup.bars.map((bar) => (
                  <Bar
                    key={`${barGroup.index}-${bar.index}-${bar.key}`}
                    x={bar.x}
                    y={bar.y}
                    width={bar.width}
                    height={bar.height}
                    fill={bar.color}
                    rx={4}
                    onClick={() => {
                      if (events)
                        alert(
                          `${bar.key} (${bar.value}) - ${JSON.stringify(bar)}`
                        );
                    }}
                  />
                ))}
              </Group>
            ))
          }
        </BarGroupHorizontal>
        <AxisLeft
          scale={dateScale}
          stroke={green}
          tickStroke={green}
          tickFormat={formatDate}
          hideAxisLine
          tickLabelProps={() => ({
            fill: green,
            fontSize: 11,
            textAnchor: "end",
            dy: "0.33em"
          })}
        />
      </Group>
    </svg>
  );
}

export default function App() {
  return (
    <div className="App">
      <Example width="500" height="300" />
    </div>
  );
}

We set the colors of the bars with the blue , green and purple variables.

The background variable has the background color.

defaultMargin have the default margins.

parseDate and format have the date parsing and formatting functions.

We parse the date from the mock data so we can format them to display in the chart.

data has the data for the chart.

keys have the data for the x-axis.

dateScale have the date scale.

cityScale have the city data.

tempScale have the temperature values for the bars.

colorScale have the colors for the bars.

We computed the xMax and yMax values to get the max values for the x and y axes.

Then we call rangeRound to set the max values for the x and y-axis ranges.

Next, we return the svg element and add the chart parts inside to add the bar.

The Group component is the container for the bar parts.

BarGroupHorizontal lets us display the bar groups horizontally.

We set the color , width , and the scales for the bars with the y0 , y0Scale and y1Scale props.

xScale sets the x-axis scale which is the bar scale.

Then we map the barGroups to return the Bar s in the map callback.

We set the bar lengths with the width prop.

Finally, we add the AxisLeft component to render the y-axis.

Conclusion

We can use the modules provided by Visx to create a horizontal grouped bar chart in our React app.

Categories
Visx

Create a React Grouped Bar Chart with the Visx Library

Visx is a library that lets us add graphics to our React app easily.

In this article, we’ll look at how to use it to add grouped bar charts into our React app.

Install Required Packages

We have to install a few modules to create the grouped bar chart.

To get started, we run:

npm i @visx/axis @visx/group @visx/mock-data @visx/responsive @visx/scale @visx/shape

to install the packages.

Create the Chart

We can create the chart by adding the items provided by the modules.

We use the data from the @visx/mock-data module.

To create the chart, we write:

import React from "react";
import { Group } from "@visx/group";
import { BarGroup } from "@visx/shape";
import { AxisBottom } from "@visx/axis";
import cityTemperature from "@visx/mock-data/lib/mocks/cityTemperature";
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
import { timeParse, timeFormat } from "d3-time-format";

const blue = "#aeeef8";
export const green = "#e5fd3d";
const purple = "#9caff6";
export const background = "#612efb";

const data = cityTemperature.slice(0, 8);
const keys = Object.keys(data[0]).filter((d) => d !== "date");
const defaultMargin = { top: 40, right: 0, bottom: 40, left: 0 };

const parseDate = timeParse("%Y-%m-%d");
const format = timeFormat("%b %d");
const formatDate = (date) => format(parseDate(date));

const getDate = (d) => d.date;

const dateScale = scaleBand({
  domain: data.map(getDate),
  padding: 0.2
});
const cityScale = scaleBand({
  domain: keys,
  padding: 0.1
});
const tempScale = scaleLinear({
  domain: [
    0,
    Math.max(...data.map((d) => Math.max(...keys.map((key) => Number(d[key])))))
  ]
});
const colorScale = scaleOrdinal({
  domain: keys,
  range: [blue, green, purple]
});

function Example({ width, height, events = false, margin = defaultMargin }) {
  const xMax = width - margin.left - margin.right;
  const yMax = height - margin.top - margin.bottom;
  dateScale.rangeRound([0, xMax]);
  cityScale.rangeRound([0, dateScale.bandwidth()]);
  tempScale.range([yMax, 0]);

  return width < 10 ? null : (
    <svg width={width} height={height}>
      <rect
        x={0}
        y={0}
        width={width}
        height={height}
        fill={background}
        rx={14}
      />
      <Group top={margin.top} left={margin.left}>
        <BarGroup
          data={data}
          keys={keys}
          height={yMax}
          x0={getDate}
          x0Scale={dateScale}
          x1Scale={cityScale}
          yScale={tempScale}
          color={colorScale}
        >
          {(barGroups) =>
            barGroups.map((barGroup) => (
              <Group
                key={`bar-group-${barGroup.index}-${barGroup.x0}`}
                left={barGroup.x0}
              >
                {barGroup.bars.map((bar) => (
                  <rect
                    key={`bar-group-bar-${barGroup.index}-${bar.index}-${bar.value}-${bar.key}`}
                    x={bar.x}
                    y={bar.y}
                    width={bar.width}
                    height={bar.height}
                    fill={bar.color}
                    rx={4}
                    onClick={() => {
                      if (!events) return;
                      const { key, value } = bar;
                      alert(JSON.stringify({ key, value }));
                    }}
                  />
                ))}
              </Group>
            ))
          }
        </BarGroup>
      </Group>
      <AxisBottom
        top={yMax + margin.top}
        tickFormat={formatDate}
        scale={dateScale}
        stroke={green}
        tickStroke={green}
        hideAxisLine
        tickLabelProps={() => ({
          fill: green,
          fontSize: 11,
          textAnchor: "middle"
        })}
      />
    </svg>
  );
}

export default function App() {
  return (
    <div className="App">
      <Example width="500" height="300" />
    </div>
  );
}

We set the color of the bars with the blue , green , and purple variables.

background is the background color.

The data variable has the mock data values we want to get and display in the bars.

keys have the x-axis values.

defaultMargin has the margin styles.

We create the parseDate and format functions from D3’s time parsing and formatting functions.

Then we create the day and city scales with the dateScale and cityScale variables.

The cityScale values are the bars.

tempScale are the bar heights.

colorScale have the color of the bars.

The Example component has the bar chart. We put everything together in there.

The xMax and yMax values are the max x and y-axis values respectively.

We use it to set the max values of the dateScale and tempScale.

In the return statement, we put everything in the svg element.

Group has the chart components.

We map the barGroups into rect elements to display the bars.

We set the width and height prop to set their width and height.

The x-axis is rendered by the AxisBottom component.

We set the top prop to set the location of the axis.

tickFormat has the ticks.

tickLabelProps have the label styles.

Conclusion

We can add grouped bar charts easily into our React app with the Visx library.

Categories
JavaScript Best Practices

Why Should We Stop Using Objects As Maps in JavaScript?

Before ES6, to make a dictionary or a map, we often used objects to store the keys and values. This has some problems that can be avoided with maps.

An object lets us map strings to values. However, with the pitfalls of JavaScript objects and the existence of the Map constructor, we can finally stop using objects as maps or dictionaries.

Inheritance and Reading Properties

Normally, objects in JavaScript inherit from the Object object if no prototype is explicitly set. This means that we have methods that are in the prototype of the object.

To check if the property is in the object or its prototype, we have to use the hasOwnProperty of the object. This is a pain and we can easily forget about this.

This means that we can accidentally get and set properties that aren’t actually in the object that we defined. For example, if we define an empty object:

let obj = {}

Then, when we write:

'toLocaleString' in obj;

We get the value of true returned. This is because the in operator designates properties in the object’s prototype as being part of the object, which we don’t really want for dictionaries or maps.

To create a pure object with no prototype, we have to write:

let obj = Object.create(null);

The create method takes a prototype object of the object it creates as an argument, so we’ll get an object that doesn’t inherit from any prototype. Built-in methods like toString or toLocaleString, they aren’t enumerable, so they won’t be included in the for...in loop.

However, if we create an object with enumerable properties as we do in the following code:

let obj = Object.create({
  a: 1
});

for (const prop in obj) {
  console.log(prop);
}

Then, we do get the a property logged in the for...in above, which loops through all the owned and inherited properties of an object.

To ignore the inherited properties, we can use the hasOwnProperty method of an object. For example, we can write:

let obj = Object.create({
  a: 1
});

for (const prop in obj) {
  if (obj.hasOwnProperty(prop)) {
    console.log(prop);
  }
}

Then, we don’t get anything logged.

As we can see, accessing values with property keys can be tricky with regular JavaScript objects.

Overriding Values of Properties

With plain objects, we can easily override and delete existing properties. Any writable properties can have their value overridden or deleted.

We can assign values to any property that’s in an object. For example, we can write:

let obj = {};
obj.toString = null;

Then, when we run:

obj.toString();

We get the error Uncaught TypeError: obj.toString is not a function.

This is a big problem since we can easily change the value of any original or inherited property of an object. As we can see, we overwrote the built-in toString method with null with just one assignment operation.

This means that using objects as dictionaries or Maps is risky since we can easily do this accidentally. There’s no way to prevent this other than checking the values that may be names of built-in methods.

The Object’s Prototype

The prototype of an object is accessible by its __proto__ property. It’s a property that we can both get and set. For example, we can write:

let obj = Object.create({
  a: 1
});

obj.__proto__ = {
  b: 1
};

Then, our object’s prototype is { b: 1 }. This means that we changed the prototype of obj, which was { a: 1 } originally, to { b: 1 }, just by setting the __proto__ property of obj.

When we loop through the obj object with the for...in loop like the following code:

for (const prop in obj) {
  console.log(prop);
}

We get b logged.

This means that we have to avoid accessing the __proto__ property when we try to access our object that we use for a dictionary or map. What we have is another trap that might get us when using objects as maps or dictionaries.

Getting Own Enumerable Properties to Avoid Traps

To avoid traps of getting properties that are inherited from other objects, we can use the Object.keys to get the object’s own property names. It returns an array with the keys of the object that we defined and omits any inherited property names.

For example, if we have:

let obj = Object.create({
  a: 1
});
console.log(Object.keys(obj));

Then we get an empty array logged.

Similarly, Object.entries accepts an object as an argument and returns an array with arrays that have the key as the first element and the value of the key as the second element.

For example, if we write:

let obj = Object.create({
  a: 1
});
console.log(Object.entries(obj));

Then we also get an empty array logged.

ES6 Maps

Better yet, we should be using ES6 Map objects, which are an actual implementation of a map or dictionary.

Map objects have the set method that lets us add keys and values, which are the first and second arguments of what the method accepts respectively.

We can define Maps as we do in the following code:

let objMap = new Map();
objMap.set('foo', 'bar');
objMap.set('a', 1);
objMap.set('b', 2);

Instead of using the set method to add our keys and values, we can also pass a nested array where each entry of the array has the key as the first element and the value as the second element.

One good thing about Map objects is that we can use non-string keys. For example, we can write:

let objMap = new Map();
objMap.set(1, 'a');

We can also use nested arrays to define a Map. For example, instead of using the set method multiple times, we can write the following:

const arrayOfKeysAndValues = [
  ['foo', 'bar'],
  ['a', 1],
  ['b', 2]
]
let objMap = new Map(arrayOfKeysAndValues);
console.log(objMap)

There are also specialized methods to get entries by key, get all entries, loop through each entry, and remove entries. We can use the get method to get an entry by its key:

objMap.get('foo'); // 'bar'

We can also get a value from a non-string key, unlike objects. For instance, if we have:

let objMap = new Map();
objMap.set(true, 'a');

Then console.log(objMap.get(true)); will get us 'a'.

And we can clear all entries of the Map object with the clear method. For example, we can write:

objMap.clear();

We can get all entries with the objMap.entries method and we can use the for...of loop to loop through the items as well since it has an iterator.

Conclusion

We should stop using objects as dictionaries now. There are too many pitfalls since objects inherit from the Object object by default and other objects as we set them.

It also lets us override the value of methods like toString which isn’t a result we want most of the time.

To avoid these issues, we should use the Map object which was introduced in ES6. It has special methods to get and set entries and we can loop through them with the for...of loop or convert the object to an array.