[smartics : d3]

Where to start? Besides work and being sick, I got some d3 visualizations done over the last weekend and follow-up nights. What’s my take? I thought it will be more complicated in general, but less complicated for myself. So all in all it was quite some effort to get some specifics with axes, scaling and refreshing done as I wanted. After a quick screenshot that already reveals some nice smart charging work – more on that later, I’ll show a quick intro into d3 and how I can show energy consumption and production of a day.

Smartics daily overview (energy consumption in red, production yellow).

Wait! d3? Ah, let me explain: Data-Driven Documents (hence the 3) is a rather big Javascript library with a lot of functionality to modify data used for data visualizations and to create and modify elements in the DOM. Wait DOM? Maybe a bit of basic research won’t hurt (Document Object Model, or at Wikipedia if that’s your thing).
Nice side effects when using d3 to render visualizations: using the browsers Javascript engine (keeping server load low), getting SVGs which scale quite well (I will need that when I’ll write about responsiveness) and don’t need all that image file data. Also, there are quite a lot of frameworks using d3 already. It might be easier using one of those if less specific functionality is needed. I chose it directly to stay flexible and because I wanted to have a look at it anyway.


I don’t want to go into too much detail on the InverterApp.jsx. That is to be found on github. What I rather show is just how to include d3 into the react application. So the base is the last post on how to include react into the maven project. This means I can simply add the d3 packages I need to the package.json. It is important that I still like to keep a small footprint also when loading smartics, which is why I only import the d3 packages needed and not just d3. Currently, I need axis for – surprise – x and y axes, scale for linear and band scaling functions, selection to select the relevant DOM elements I am working with and shape for the line or path functionality for the energy consumption.

  "dependencies": {
    "d3-axis": "^1.0.12",
    "d3-scale": "^3.2.1",
    "d3-selection": "^1.4.1",
    "d3-shape": "^1.3.7",
    "react": "^16.12.0",
    "react-dom": "^16.12.0",
    "rest": "^1.3.1"
  },

That’s basically everything for the setup, easily done when using npm with ‘npm i d3-shape‘ for example. The next step is already including the actual chart component in the InverterApp.jsx. Line 18 shows the API call to get the metering data summary in five minute steps. This is used as state inverter data. In line 32 the chart is included with the converted summary data as well as some size specifications and a boolean to show a line for the current time. Line 42 shows the conversion being simply multiplying the minutes to get a Wh number for the whole hour which makes the graph as current output more logical. Other helper functions used (prettyTime, getCurrentDate) and the rest of the returned <div> can be found on github.

import React, { Component } from 'react';
import EnergyChart from "./EnergyChart.jsx";

class InverterApp extends Component {
   constructor(props) {
      super(props);
      this.state = {
         inverter: [],
         selectDate: new Date(),
      };
   }

   componentDidMount() {
      this.onNavigate(this.state.selectDate);
   }

   onNavigate(selectDate) {
      fetch('api/meterdatasummary/' + this.getCurrentDate(selectDate))
         .then(response => response.json())
         .then((data) => {
            this.setState({
               inverter: data,
               selectDate: selectDate,
            })
         })
         .catch(console.log)
   }

   render() {
      return (
         <div>
             <EnergyChart data={this.getEnergyForMins(this.state.inverter.meteringDataMinDtos)} size={[550, 300]}
               showline={this.getCurrentDate(new Date()) == this.getCurrentDate(this.state.selectDate)} />
         </div >
      );
   }

   getEnergyForMins(dto) {
      if (dto == undefined) {
         return [];
      }
      return dto.map(item => [this.prettyTime(item.startTime), item.powerConsumed * 12, item.powerProduced * 12]);
   }
}

export default InverterApp

This leaves the most important component: EnergyChart.jsx using d3 to throw a bunch of objects into the DOM. It is important to create an own function for the chart (line 21) bind and call it when you want to recreate it as well as to remove the relevant objects beforehand (line 36 – 39). What else is important? Define the right margins to be able to add axes (line 92 – 117). In line 120 the showline property is used to determine if its data about the current day and a line should be shown at the current time.

import React, { Component } from 'react';
import { scaleLinear, scaleBand } from 'd3-scale';
import { select } from 'd3-selection';
import { axisBottom, axisLeft } from 'd3-axis';
import { line } from 'd3-shape';

class EnergyChart extends Component {
   constructor(props) {
      super(props);
      this.createBarChart = this.createBarChart.bind(this);
   }

   componentDidMount() {
      this.createBarChart();
   }

   componentDidUpdate() {
      this.createBarChart();
   }

   createBarChart() {
      const node = this.node;
      const maxY = 5500;
      const maxX = 12 * 24;
      let margin = { top: 10, right: 10, bottom: 30, left: 30 },
         width = this.props.size[0] - margin.left - margin.right,
         height = this.props.size[1] - margin.top - margin.bottom;
      const xScale = scaleLinear()
         .domain([0, maxX])
         .range([margin.left, margin.left + width]);
      const yScale = scaleLinear()
         .domain([0, maxY])
         .range([height, 0]);

      // cleanup elements inside svg
      select(node).selectAll('g').remove();
      select(node).selectAll('rect').remove();
      select(node).selectAll('circle').remove();
      select(node).selectAll('path').remove();

      select(node)
         .selectAll('rect')
         .data(this.props.data)
         .enter()
         .append('rect');

      select(node)
         .selectAll('circle')
         .data(this.props.data)
         .enter()
         .append('circle');

      select(node)
         .selectAll('path')
         .data(this.props.data)
         .enter()
         .append('path');

      // show production
      select(node)
         .selectAll('rect')
         .data(this.props.data)
         .style('fill', '#fe9922')
         .attr('x', d => xScale(d[0]))
         .attr('y', d => yScale(d[2]) + margin.top)
         .attr('height', d => height - yScale(d[2]))
         .attr('width', 2);

      // show consumption
      select(node)
         .selectAll('circle')
         .data(this.props.data)
         .style('fill', '#bb4444')
         .attr('cx', d => xScale(d[0]))
         .attr('cy', d => yScale(d[1]) + margin.top)
         .attr('r', 2)
         .style("opacity", .7);

      const lineFunction = line()
         .x(d => xScale(d.time))
         .y(d => yScale(d.consumed) + margin.top);

      select(node)
         .select('path')
         .attr('fill', "none")
         .attr('stroke', '#bb4444')
         .attr('stroke-width', 1.5)
         .attr('d', lineFunction(this.getArrayObjectForEnergy(this.props.data)))
         .style('opacity', .5);

      // prepare axis
      let yAxis = axisLeft()
         .scale(yScale)
         .ticks(10, "s")
         .tickSize(-width, 0, 0);

      const scaleTitle = scaleBand()
         .domain(this.get24hrsArray())
         .range([margin.left, margin.left + width])
         .paddingInner(10);
      let xAxis = axisBottom(scaleTitle)
         .tickSizeOuter(0);

      // show axis
      select(node)
         .append('g')
         .attr('transform', `translate(0,${margin.top + height})`)
         .call(xAxis)
         .selectAll("text")
         .attr("transform", "translate(-5,5)rotate(-45)")
         .style("text-anchor", "end");

      select(node)
         .append('g')
         .attr('transform', `translate(${margin.left},${margin.top})`)
         .call(yAxis)
         .attr("class", "grid");

      // show current time pointer
      if (this.props.showline) {
         select(node)
            .select('rect')
            .style('fill', '#4444aa')
            .attr('x', margin.left + this.getCurrentPos(width))
            .attr('y', margin.top - 5)
            .attr('height', height + 15)
            .attr('width', 1.5);
      }
   }

   render() {
      return (
         <svg ref={node => this.node = node} width={this.props.size[0]} height={this.props.size[1]}>
         </svg>
      )
   }

   getArrayObjectForEnergy(data) {
      return data.map(el => { return { time: el[0], consumed: el[1] }; });
   }

   getCurrentPos(width) {
      if (width == undefined) {
         return 0;
      }
      const oneMin = width / (60 * 24);
      const date = new Date();
      return (date.getHours() * 60 + date.getMinutes()) * oneMin;
   }

   get24hrsArray() {
      let times = [];
      for (let i = 0; i <= 24; i++) {
         times[i] = i + "h";
      }
      return times;
   }
}

export default EnergyChart

Apart from that I just use a small style adaptation in the CSS to get the grid lines a bit into the background. But basically that’s it. Note: as soon as there is an partial reloading of the page – which I already use, see github or one of the next posts – the remove part of the chart component becomes very important. Without it you can inspect your page and see your DOM grow.

More on smart charging, or a different visualization of the energy consumption/production relation next time. Warning: I might also write a bit about code formatting or testing.
— Raphael


1 thought on “[smartics : d3]”

Leave a comment