Animated Bar Chart

In a previous tutorial, we created a simple bar chart. Let’s build on top of that and add an animation to the bars, so their height’s will grow from 0 to their final height like below.

0 5 10 15 20 per 1,000 population1990199520002005201020152020202520302035

Let’s look at the logic behind the animation. What we need to do here is to transition the y and theheight attributes of the rect element simultaneously, starting from where the height is 0 and the y attribute corresponds to the Bar at the lowest position to the positions in the final state.

xyheight: 1width00XY

What we are transitioning here is from this rect element:

<rect x="80" y="199" width="50" height="1" fill="#fcd34d"></rect>

to this one:

<rect x="80" y="50" width="50" height="150" fill="#fcd34d"></rect>

Let’s see now how we can implement this in our Bar chart example.

BarChart.svelte

It is possible to do this animation by using d3 transition and select functions but svelte has built-in function tweened that interpolates between a starting and ending value that we will use here to animate our bar chart.

Let’s create a component for the <rect> elements that it is easier to set up our tweened functions.

<!-- Rect.svelte -->
<script>
  import { tweened } from 'svelte/motion';
  import { cubicOut } from 'svelte/easing';
  import { interpolate } from 'd3-interpolate';

  export let x;
  export let value;
  export let yScale;

  export let width;
  export let height;
  export let i;
  export let fill;

  const tweenParams = {
    delay: 300 + i * 50,
    duration: 250,
    interpolate,
    easing: cubicOut
  };

  const tY = tweened(undefined, tweenParams);
  const tFill = tweened(undefined, { ...tweenParams, delay: 800 + i * 50 });

  $: tY.set(value);
  $: tFill.set(fill);
</script>

<rect
  {x}
  {width}
  y={yScale($tY)}
  height={yScale(0) - yScale($tY)}
  fill={$tFill}
  stroke="none" />

Let’s explaing to code above. We componentized our svg rect element and defined attributes as props so that they can be set from the a parent component.

What does tweened do is that when data is changed in the parent they will transition the old values to the new values e.g y={yScale($tY)} and height={yScale(0) - yScale($tY)} over time by using the parameters defined in the tweenParams. We are also using the index i value of the points array so bars will be shown sequentially one after the other and with a longer delay in tweenParamsFill we make sure that the fill color will animate only after all the bars are visible.

There is not a big change in our Barchart.svelte compoment than the simple bar chart example. We only added a setTimeout function to change the data and replaced the rect elements with the rect component.

<!-- Barchart.svelte -->
<script>
  import { scaleLinear } from 'd3-scale';
  import Rect from './Rect.svelte';

  // 1. Basic Setup: Get the data

  let points = [
    { year: 1990, birthrate: 0 },
    { year: 1995, birthrate: 0 },
    { year: 2000, birthrate: 0 },
    { year: 2005, birthrate: 0 },
    { year: 2010, birthrate: 0 },
    { year: 2015, birthrate: 0 },
    { year: 2020, birthrate: 0 },
    { year: 2025, birthrate: 0 },
    { year: 2030, birthrate: 0 },
    { year: 2035, birthrate: 0 }
  ];

  let fill = '#fffbeb';

  setTimeout(() => {
    fill = '#fcd34d';
    points = [
      { year: 1990, birthrate: 6.7 },
      { year: 1995, birthrate: 4.6 },
      { year: 2000, birthrate: 14.4 },
      { year: 2005, birthrate: 18 },
      { year: 2010, birthrate: 7 },
      { year: 2015, birthrate: 12.4 },
      { year: 2020, birthrate: 17 },
      { year: 2025, birthrate: 10.9 },
      { year: 2030, birthrate: 8 },
      { year: 2035, birthrate: 12.9 }
    ];
  }, 100);

  // 2. Dimensions, Margins & Scales

  // Data for plotting x-y axis
  const yTicks = [0, 5, 10, 15, 20];
  const padding = { top: 20, right: 15, bottom: 20, left: 25 };

  let width = 500;
  let height = 350;

  $: xScale = scaleLinear()
    .domain([0, points.length])
    .range([padding.left, width - padding.right]);

  $: yScale = scaleLinear()
    .domain([0, Math.max.apply(null, yTicks)])
    .range([height - padding.bottom, padding.top]);

  $: innerWidth = width - (padding.left + padding.right);
  $: barWidth = innerWidth / points.length;

  // 3. Functions needed to create the Data elements or Helper functions, e.g d3.line d3.arc when needed

  // Shorten the date axis values for mobile
  function formatMobile(tick) {
    return "'" + tick.toString().slice(-2);
  }

  // 4. Create the main data elements (usually by iteration, using svelte each blocks)
  // We will do this in the html markup
</script>

<div class="chart" bind:clientWidth={width}>
  <svg {width} {height}>
    <!-- 4. Design the bars -->
    <g class="bars">
      {#each points as point, i}
        <!-- New Rect component -->
        <Rect
          {i}
          {yScale}
          value={point.birthrate}
          x={xScale(i) + 2}
          width={barWidth * 0.9}
          {fill} />
        <!-- old version -->
        <!-- <rect
          fill="#fcd34d"
          x={xScale(i) + 2}
          y={yScale($tY) - yScale(0)}
          width={barWidth * 0.9}
          height={yScale(0) - yScale(point.birthrate)} /> -->
      {/each}
    </g>
    <!-- Design y axis -->
    <g class="axis y-axis">
      {#each yTicks as tick}
        <g class="tick tick-{tick}" transform="translate(0, {yScale(tick)})">
          <line x2="100%" />
          <text y="-4"
            >{tick} {tick === 20 ? ' per 1,000 population' : ''}</text>
        </g>
      {/each}
    </g>

    <!-- Design x axis -->
    <g class="axis x-axis">
      {#each points as point, i}
        <g class="tick" transform="translate({xScale(i)}, {height})">
          <text x={barWidth / 2} y="-4">
            {width > 380 ? point.year : formatMobile(point.year)}</text>
        </g>
      {/each}
    </g>
  </svg>
</div>

<style>
  .x-axis .tick text {
    text-anchor: middle;
    color: white;
    margin: 40px;
  }

  .tick {
    font-family: Poppins, sans-serif;
    font-size: 0.725em;
    font-weight: 200;
    color: white;
  }

  .tick text {
    fill: white;
    text-anchor: start;
    color: white;
  }

  .tick line {
    stroke: #fcd34d;
    stroke-dasharray: 2;
    opacity: 1;
  }

  .tick.tick-0 line {
    display: inline-block;
    stroke-dasharray: 0;
  }
</style>

Thanks for reading! In case you have a question or comment please join us on Discord!