All posts by Tophe

Faire cohabiter React et D3 – vidéo talk Best of Web 2017

En juin dernier, j’ai présenté un talk à la conférence Best of Web 2017 sur le sujet “Faire cohabiter React et d3”.

Voici la vidéo du talk, publiée par l’organisation de @bestofwebconf:

Retrouvez les autres talks sur cette playlist “Best of Web 2017”.

Ressources:

PS: Pour ceux qui se poseraient la question, la librairie que j’utilise pour faire mes slides est FormidableLabs/spectacle (les auteurs de victory), en adaptant un peu leur boilerplate, mais surtout, en utilisant l’extension thejameskyle/spectacle-code-slide qui permet une présentation de code très efficace.

Tophe @bestofweb 2017 Tophe @bestofweb 2017

Crédits:

Package a module for npm in CommonJS/ES2015/UMD with babel and rollup

About a year ago, I started the rxjs-experiments project. Aside of rxjs, it is all vanilla JS. I needed a simple frontend router with at least a deferred mounting feature (only mount a route when a promise is resolved). After some research on npm and github, I choose to write it myself.

The purpose of this article is not the router itself, but the whole workflow around it to get it to a published package that will be:

  • maintainable (you should have a linter, unit tests and a CI like in any other of your projects)
  • format unopinionated – as in whatever way you choose to consume the package – be it using:
    • webpack/browserify/rollup (or any other module bundler) in CommonJS or ES2015 module mode
    • directly in the browser (via a umd build)
  • providing some documentation and example

You can apply some of the following concepts to any regular project by the way …

Source code available at topheman/lite-router.

Getting started

git init
npm init -y

I’ll be using yarn in the examples. You can use npm as well of course.

.gitignore

Since you will be publishing your package on the npm registry and example on github pages, your workspace will contain artefacts (files that aren’t from your source code but were generated from a build task) that shouldn’t be versioned in git (to avoid noise and problems in merge conflicts).

Your .gitignore file should look like something like that:

.DS_Store
*.log
node_modules
.idea
dist
lib
es
coverage
build

That way, any generated file won’t be versioned in git – though, they will be part of your published package as we’ll see bellow.

.editorconfig

An .editorconfig is just good practice (especially when you work with a team), to enforce things such as:

  • Indent style: tabs or space
  • Encoding
  • EOL

Example of .editorconfig:

# http://editorconfig.org
root = true

[*]
# change these settings to your own preference
indent_style = space
indent_size = 2

# it's recommend to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

[{package,bower}.json]
indent_style = space
indent_size = 2

Source code

Put your source code in the src folder.

Setup Babel

Run

yarn add babel-cli babel-core babel-preset-env cross-env --dev

The .babelrc file lets you describe how you want babel to behave:

  • The env option will let you use a specific config according to BABEL_ENV (Jest will override it using NODE_ENV)
  • The babel-preset-env is a preset that will determine which babel plugins you need, based on the options you pass
{
  "env": {
    // jest doesn't take account of BABEL_ENV, you need to set NODE_ENV - https://facebook.github.io/jest/docs/getting-started.html#using-babel
    "commonjs": {
      "presets": [
        ["env", {
          "useBuiltIns": false
        }]
      ]
    },
    "es": {
      "presets": [
        ["env", {
          "useBuiltIns": false,
          "modules": false
        }]
      ]
    }
  }
}
  • useBuiltIns means that we don’t want to ship any useless polyfills (leave that choice to the final user).
  • "modules": false means that you don’t want the modules to be transform to CommonJS (will be used when building ES2015 modules)

Setup jest, eslint and pre-commit hook

You can totally skip this section if you don’t bother about unit tests, code quality …

jest

Run

yarn add babel-jest jest --dev

Add the following to your package.json:

"scripts": {
  "jest": "cross-env NODE_ENV=commonjs ./node_modules/.bin/jest",
  "jest:watch": "npm run jest -- --watch"
},
"jest": {
  "testRegex": "(/tests/.*\\.spec.js)$"
}

jest.testRegex describes the pattern of your unit tests filenames. So create a unit test file like tests/index.spec.js:

describe('foo', () => {
  it('bar', () => {
    expect(true).toBe(true)
  })
})

Now, you can run:

  • npm run jest: one shot unit-test
  • npm run jest:watch: runs unit-tests in watch mode

How jest manages BABEL_ENV:

Jest sets the NODE_ENV to “test” if it isn’t provided and otherwise let’s you use a custom override. It doesn’t use BABEL_ENV at all.

eslint

Run

yarn add eslint eslint-plugin-import eslint-config-airbnb-base babel-eslint --dev

Create a .eslintrc file:

{
  "parser": "babel-eslint",
  "rules": {
    "max-len": 0,
    "comma-dangle": 0,
    "brace-style": [2, "stroustrup"],
    "no-console": 0,
    "padded-blocks": 0,
    "indent": [2, 2, {"SwitchCase": 1}],
    "spaced-comment": 1,
    "quotes": ["error", "single", { "allowTemplateLiterals": true }],
    "import/prefer-default-export": "off",
    "arrow-parens": 0,
    "consistent-return": 0,
    "no-useless-escape": 0,
    "no-underscore-dangle": 0
  },
  "extends": "airbnb-base",
  "env": {
    "browser": true,
    "jest": true
  }
}

What did you just install ? What does this .eslintrc file contains ?

  • the package eslint will let you lint your files
  • the package eslint-plugin-import is necessary when you lint ES2015+ (ES6+) based source code
  • the package babel-eslint will be used as a parser by eslint, because eslint itself might not support all babel features
  • the package eslint-config-airbnb-base is an extensible set of rules shared by airbnb (you could use an other preset) – those rules are overridable in the rule section.

Add the following in the scripts section of your package.json:

"lint": "./node_modules/.bin/eslint src",
"lint-fix": "./node_modules/.bin/eslint --fix src --ext .js",
"test": "npm run lint && npm run jest"

Now, running npm test will both lint your source code and run your unit tests.

Pre-commit hook

To make sure you don’t commit broken code, setup a pre-commit hook that will lint your source code and run your tests before each of your commits.

Run

yarn add pre-commit --dev

Add the following section to your package.json:

"pre-commit": [
  "test"
],

Setup build steps

We will build and distribute our package in 3 different formats, that way the end user will be able to use the one that fits the most his use / build tools.

Tools like webpack are able to use any of the 3 formats bellow, though, your end user might want to use ES2015 aware tools (like rollup) that can take advantage of there feature (such as importing only what you use in your bundle or even tree shaking)

And if your end user doesn’t want to use tools like webpack or browserify, but use AMD (or even use the package defined in global namespace), you will provide the UMD build (useful on platforms like codepen).

This is why, as a package author, it is interesting to publish in those different formats for your final users.

To install rollup and rimraf, run

yarn add rimraf rollup rollup-plugin-babel rollup-plugin-commonjs rollup-plugin-node-resolve rollup-plugin-replace rollup-plugin-uglify rollup-watch --dev

Add the following in the scripts section of your package.json:

"clean": "rimraf lib dist es",
"build": "npm run build:commonjs && npm run build:umd && npm run build:umd:min && npm run build:es",
"build:watch": "echo 'build && watch the COMMONJS version of the package - for other version, run specific tasks' && npm run build:commonjs:watch",
"build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib",
"build:commonjs:watch": "npm run build:commonjs -- --watch",
"build:es": "cross-env BABEL_ENV=es babel src --out-dir es",
"build:es:watch": "npm run build:es -- --watch",
"build:umd": "cross-env BABEL_ENV=es NODE_ENV=development node_modules/.bin/rollup src/index.js --config --sourcemap --output dist/lite-router.js",
"build:umd:watch": "npm run build:umd -- --watch",
"build:umd:min": "cross-env BABEL_ENV=es NODE_ENV=production rollup src/index.js --config --output dist/lite-router.min.js",

Each build:* task will build your package in a specific folder.
Each build:*:watch task will build your package in that specific folder in watch mode.

You can npm run the following tasks:

  • build:commonjs: will build the CommonJS version in the lib folder
  • build:es: will build the ES2015+ modules version in the es folder
  • build:umd: will build the UMD version at dist/lite-router.js (with sourcemaps)
  • build:umd:min: will build the minified UMD version at dist/lite-router.min.js

npm run clean will cleanup the directories created by those build tasks.

Setup example

I will go deeper on how to manage github pages (git orphan branch as a deployment channel) in an other post – in the mean time, you can check out the README of the lite-router project (actually, most of my projects hold the gh-pages branch in a build/dist directory).

In your development workflow, you might want to both work on your package and use it (without re-publishing a new version to npm at each change) on an other project. For that, you can use npm link.

Say you work on my-package (this the name attribute in your package.json) and you want to be able to test it directly in my-project:

cd my-package
npm link
cd ../my-project
npm link my-package

From there you will be able to import { myFeature } from 'my-package' in your project (as if you had npm installed your my-package).

Just run the correct build:*:watch task in your my-package so that the build stays up to date with the changes you might apply to its source code.

Upgrade the package.json file

All the following explanations were applied to this package.json file.You can also checkout the npm doc about the package.json.

Information related

In your package.json, make sure you have:

  • a name, a version, a description and an author section
  • a license, a homepage and a keywords section
  • a repository and bugs section

Specify endpoints

So that your build files will be part of your final published package, you will have to declare them. Add the following to your package.json:

"main": "lib/index.js",
"module": "es/index.js",
"jsnext:main": "es/index.js",
"files": [
  "dist",
  "lib",
  "es",
  "src"
],
  • The files section tells npm to package those folders when publishing (otherwise, they would be ignored, since they are listed in the .gitignore file)
  • main defines the endpoint of the CommonJS build
  • jsnext:main and module define the endpoint of the ES2015 build (we define twice the endpoint because jsnext:main was the first to be in use but it’s more likely that module will be standardized)

Add prepare script

Add the following in the scripts section of your package.json:

"prepare": "npm run clean && npm test && npm run build",

This will make sure the build files (which aren’t part of the git repo) are generated when your contributers (NOT users) run npm install after forking and cloning your repo.

More on prepare script.

Travis CI

Don’t bother about this section if you don’t use Travis CI. If you use an other CI tool, well, it’s pretty much the same.

The following .travis.yml file will test your builds, run the linter and the unit tests:

language: node_js
node_js:
  - "6"
script:
  - npm test

Note: Since I added npm test in the prepare script, the tests will run twice (this is also why there is no mention of npm run build, since it’s tested after the install). There are ways to avoid it I wont talk about it here.

Checkout an example of that kind of travis test.

Publish your package

Don’t forget to add a README.md file with:

  • Why you made the package
  • How to install it
  • A short example
  • Describe the API
  • In a subsection, explain how to contribute (your git workflow, how to install, run, test, build …)
  • Add some license

If you have your npm account setup on your computer, you are ready to publish your package. Just run:

npm run build
npm publish

Once published, you can

  • npm install it somewhere else
  • access the different builds through unpkg.com (useful platform made by Michael Jackson, one of the author of React Router) – example.

Resources / Credits

Source code available at topheman/lite-router.

Automate AppCache offline support in your Webpack build

webpack-logo

Why would you use AppCache ? An API that is messy, not as advanced as service-workers and moreover, which is being removed from the Web Standards ?…

With the Progressive Web Apps, we hear a lot about service-workers. They are very powerfull for a lot of things (including offline support). Though, they’re not supported on IE nor Safari … 🙁

So, until the rest of the browser vendors catch up, if you want to provide some offline experience to all your users, you’ll have to use AppCache which is still widely supported.

AppCache in a few words

  • You have to provide a manifest.appcache file, served with the content type text/cache-manifest
  • This file will consist of three different parts:
    • CACHE: files that will be explicitly cached after they’re downloaded for the first time (this is the default section)
    • NETWORK: white-listed resources that require a connection to the server
    • FALLBACK: fallback pages the browser should use if a resource is inaccessible
  • You will reference this manifest.appcache as an attribute on the html tag of the page that will use it
  • If the manifest.appcache file is updated, the browser will download the resources listed in this manifest (if not, or offline, it will use the cached resources)

More infos on MDN

AppCache in a SPA

Note: Skip this part if you don’t bother about providing different index.html file whether your users are online or offline.

If you’re developing a SPA, and you want to provide a different index.html entry point whether you are online or offline, you’ll have to use a little trick. You won’t reference your manifest.appcache directly in your index.html but in an other html file that you’ll include in an iframe to your index.html.

That way, the index.html file won’t be cached by default (as the master entry) and you’ll be able to define a fallback in the manifest.appcache

index.html

...
<iframe src="./iframe-inject-appcache-manifest.html" style="display: none"></iframe>
...

iframe-inject-appcache-manifest.html

<html manifest="manifest.appcache"></html>

dummy manifest.appcache file

CACHE MANIFEST
# v1 (some version id)

assets/foo.png
assets/bundle.js
assets/style.css
# more assets ...

NETWORK:
*

# that way, you'll be able to force the fallback of index.html
# to an other file when you're offline
FALLBACK:
. offline.html

Automate AppCache

If your app relies on a large code base, with a build step, you will need to automate this task. You’ll also have to ensure that AppCache doesn’t mess with your development workflow (meaning disabling it when developing).

I will describe the steps I took to automate AppCache support on topheman/rxjs-experiments, a little project using RxJS (no other frameworks involved). The workflow of this project is based on a seed I made and open-sourced: topheman/webpack-babel-starter.

Checkout the App

Step 1 – Define if we should “activate” AppCache

When you’re running webpack in dev-server mode, that means you’re developing, so you want your sources to be kept up to date (you don’t want AppCache to cache them).

webpack.config.js

const MODE_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') > -1 ? true : false;
// ...
const APPCACHE = process.env.APPCACHE ? JSON.parse(process.env.APPCACHE) : !MODE_DEV_SERVER;// if false, nothing will be cached by AppCache

Continue reading

D3 React Components with Victory – Reusability / Composability

react-logo-1000-transparentlogo_d3-svg

On the topheman/d3-react-experiments project, at the end of last month, I released a few examples of React components exposing charts based either on vanilla d3 code or react-faux-dom.

In this new release, I took a full JSX approach, using some components from the Victory library. I’ve already used some of them in the past (simple pie charts / histograms), but this time, I made a more advanced chart:

  • dual Y axis (two Y axis with independent scale) / multi lines
  • user interractions:
    • mouse:
      • hover legends or lines will highlight relevent lines and axis
      • hover the chart will show a tooltip with some contextual infos
    • touch: toggle touch legends or lines will highlight relevent lines and axis
  • fully responsive
  • data comes from the npm registry API:
    • a simple configuration object will setup all the charts

Test the Demo!

Declarative

When you come back to some d3 code after a while, it can be hard to dive back in (even if it’s yours). This is where the declarative approach of React/JSX makes sense. It is easier to read and understand what’s happening, since you only have to deal with components that you feed with some props.

The code might be easier to reason about, but on the other hand, you’ll have to change your vanilla d3 approach to a component based one (and some times directly deal with svg via JSX).

Reusability / Composability

When I started doing datavisualisation in React, I felt like each chart component library I tested was too weak / not composable enough – that all you could do eventually was a single pie chart, bar chart or line chart. I almost started my own library but didn’t go through when I realised that all I would have done was making exactly the same components as libraries like Victory …

In fact, you can compose those components with each others to make more advanced charts. That way, you benefit from already available reusable components. You can even break those advanced components in multiple reusable ones on your end.

Limits

In that case, there is a limit to reusability / composability. Like any framework which gives you high level abstractions, there will come a point when your needs will become very specific and won’t fit in. If you want to make some hard core visualisations like some of which you can see on bl.ocks.org, you’ll be better coding them in vanilla d3 encapsulated in a React component.

Conclusion

  • You shouldn’t need to re-invent the wheel. React datavisualisation libraries like Victory provide components like VictoryAxis, VictoryBar, VictoryLine … They will cover your basic use cases, out of the box.
  • Those components can be composed as HOC to make advanced charts. We need people to create examples.
  • Those libraries may not be suited to make complex charts. In that case, some good ole vanilla d3, embeded in a React component will be a better solution.

Tophe

Test the Demo!

Resources:

Plain d3 code and React working together

react-logo-1000-transparentlogo_d3-svg

A few months ago, I released the v1 of topheman/d3-react-experiments. At that time, my goal was to experiment some of the existing d3 libraries that you could directly use as React components. I managed to setup a few examples with Victory and d3act.

For the v2, I took the approach of a pure d3 user that may or may not know React but would want to keep full control over the creation of his chart, using the d3 API – mixed with React lifecycle hooks.

Test the Demo!

D3 / StaticMultiLineChart

That way, I created the StaticMultiLineChart component which embeds plain d3 code (I nearly made a full copy/paste of this bl.ocks.org example) and it works just out of the box with React, without needing to know much about React lifecycle hooks.

As you can see, we only need a componentWillUpdate() lifecycle hook, to cleanup the svg node, so that drawLineChart() will work properly on each update (since DOMNodes are not reused and it appends some on each update, we would end up with multiple charts).

This is a very naive approach, but it shows how, with little knowing about React lifcecyle hooks and JSX, you can still do d3 … Transitions may not be possible to implement that way (see TransitionMultiLineChart).

View source code on Github

import React from 'react';

import ColorHash from 'color-hash';

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

const colorHash = new ColorHash();

export default class StaticMultiLineChart extends React.Component {

  static propTypes = {
    margin: React.PropTypes.object,
    width: React.PropTypes.number,
    height: React.PropTypes.number,
    data: React.PropTypes.object.isRequired,
    minX: React.PropTypes.number,
    maxX: React.PropTypes.number,
    minY: React.PropTypes.number,
    maxY: React.PropTypes.number
  }

  static defaultProps = {
    margin: {
      top: 20,
      right: 20,
      bottom: 30,
      left: 50
    },
    width: 700,
    height: 400
  }

  constructor() {
    super();
  }

  /**
   * This example is a reuse of some plain code from an example on https://bl.ocks.org/d3noob/4db972df5d7efc7d611255d1cc6f3c4f
   * Since the render method contains .append() invocations, I remove any child of the root node at each render
   *
   * See the other examples for smarter approaches
   */
  componentWillUpdate() {
    // each update, flush the nodes of the chart - this isn't the best way - see the other example for better practice
    while (this.rootNode.firstChild) {
      this.rootNode.removeChild(this.rootNode.firstChild);
    }
  }

  drawLineChart() {
    // ... the d3 code manipulating the DOM Node this.rootNode goes here
  }

  render() {
    // only start drawing (accessing the DOM) after the first render, once we get hold on the ref of the node
    if (this.rootNode) {
      this.drawLineChart();
    }
    else {
      // setTimeout necessary for the very first draw, to ensure drawing using a DOMNode and prevent the following error:
      // "Uncaught TypeError: Cannot read property 'ownerDocument' of null"
      setTimeout(() => this.drawLineChart(), 0);
    }

    return (
       <svg ref={(node) => this.rootNode = node}></svg>
    );
  }

}

D3 / TransitionMultiLineChart

To apply d3 transitions, you won’t be able to flush all the DOMNodes on each updates, you’ll need to keep a reference to each of them inside the component to let d3 mutate them (this isn’t specific to React).

We will use componentDidMount() hook to call our init() method which will create the nodes for the axis and line groups and store them on the component instance. Thanks to componentDidMount() behavior, it will only be called once and ensure we have access to the DOM created by React.

All our d3 code updating the DOM is in update() – split in updateSize() and updateData(). This update() method is called in componentDidUpdate(), which fires when props or state have changed (such as data, width, height …).

Since componentDidUpdate() is not called at first render, we call some setState() in componentDidMount() to ensure that our chart updates after the very first render.

To avoid some unnecessary DOMNodes attributes changing, we use componentWillReceiveProps() hook to check whether if it’s worth it to call updateSize() inside update() on the next tick by tagging this.shouldUpdateSize.

In fact, the difference between this code and plain d3 is only a little management of the lifecycle hooks of React to ensure you have access to you DOMNode or know when your data changed …

View source code on Github

import React from 'react';

import ColorHash from 'color-hash';

import { scaleLinear } from 'd3-scale';
import { line } from 'd3-shape';
import { select } from 'd3-selection';
import { axisLeft, axisBottom } from 'd3-axis';
import 'd3-transition';

const colorHash = new ColorHash();

export default class TransitionMultiLineChart extends React.Component {

  static propTypes = {
    margin: React.PropTypes.object,
    width: React.PropTypes.number,
    height: React.PropTypes.number,
    data: React.PropTypes.object.isRequired,
    minX: React.PropTypes.number,
    maxX: React.PropTypes.number,
    minY: React.PropTypes.number,
    maxY: React.PropTypes.number
  }

  static defaultProps = {
    margin: {
      top: 20,
      right: 20,
      bottom: 30,
      left: 50
    },
    width: 700,
    height: 400
  }

  constructor() {
    super();
    this.shouldUpdateSize = true;
    // minimal state to manage React lifecycle
    this.state = {
      initialized: false
    };
  }

  /**
   * From React doc https://facebook.github.io/react/docs/component-specs.html#mounting-componentdidmount :
   *
   * "Invoked once, only on the client (not on the server), immediately after the initial rendering occurs.
   * At this point in the lifecycle, you can access any refs to your children (e.g., to access the underlying DOM representation).
   * The componentDidMount() method of child components is invoked before that of parent components."
   *
   * this.init is called here because:
   * - we need the ref to the svg node
   * - it won't we called again
   */
  componentDidMount() {
    console.log('componentDidMount');
    this.init();
    // the code bellow is to trigger componentDidUpdate (which is not called at first render)
    setTimeout(() => {
      this.setState({
        initialized: true
      });
    });
  }

  /**
   * From React doc : https://facebook.github.io/react/docs/component-specs.html#updating-componentwillreceiveprops
   *
   * "Invoked when a component is receiving new props. This method is not called for the initial render.
   * Use this as an opportunity to react to a prop transition before render() is called
   * by updating the state using this.setState().
   * The old props can be accessed via this.props. Calling this.setState() within this function will not trigger an additional render."
   *
   * I use this hook to check whether or not this.updateSize should be called on the next update
   * Doing the same thing about this.updateData would involve deep checking the whole data passed.
   */
  componentWillReceiveProps({ margin, width, height, minX, maxX, maxY }) {
    console.log('componentWillReceiveProps');
    if (margin !== this.props.margin || width !== this.props.width || height !== this.props.height ||
      minX !== this.props.minX || maxX !== this.props.maxX || maxY !== this.props.maxY) {
      console.log('change size');
      this.shouldUpdateSize = true;
    }
  }

  /**
   * From React doc https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate :
   *
   * "Invoked immediately after the component's updates are flushed to the DOM.
   * This method is not called for the initial render.
   * Use this as an opportunity to operate on the DOM when the component has been updated."
   *
   * this.update is called here because:
   * - it's not called for initial render - componentDidMount ensures to have our svg element init
   * - it's called after each update of the component - we get the new props
   */
  componentDidUpdate() {
    console.log('componentDidUpdate');
    this.update();
  }

  extractSize() {
    const { margin, width: widthIncludingMargins, height: heightIncludingMargins } = this.props;
    const width = widthIncludingMargins - margin.left - margin.right;
    const height = heightIncludingMargins - margin.top - margin.bottom;
    return {
      width,
      height,
      margin
    };
  }

  /**
   * Create svg nodes in order to reuse them
   */
  init() {
    console.log('init');
    this.lineGroup = this.rootNode.append('g');
    this.axisLeftGroup = this.lineGroup.append('g');
    this.axisBottomGroup = this.lineGroup.append('g');
  }

  updateSize() {
    console.log('updateSize');
    const { width, height, margin } = this.extractSize();
    const { minX, maxX, maxY } = this.props;

    // resize/re-align root nodes
    this.rootNode
      .attr('width', width + margin.left + margin.right)
      .attr('height', height + margin.top + margin.bottom);
    this.lineGroup
      .attr('transform',
        'translate(' + margin.left + ',' + margin.top + ')');

    // set domain for axis
    const xScale = scaleLinear().range([0, width]);
    const yScale = scaleLinear().range([height, 0]);

    // Scale the range of the data
    xScale.domain([minX, maxX]);
    yScale.domain([0, maxY]);

    // Update the X Axis
    this.axisBottomGroup.transition()
      .attr('transform', 'translate(0,' + height + ')')
      .call(axisBottom(xScale).ticks(width > 500 ? Math.floor(width / 80) : 4)); // prevent from having too much ticks on small screens

    // Update the Y Axis
    this.axisLeftGroup.transition()
      .call(axisLeft(yScale));

    // this.line is not called directy since it's used as a callback and is re-assigned. It is wrapped inside this.lineReference
    this.line = line() // .interpolate("monotone")
      .x(d => xScale(d.x))
      .y(d => yScale(d.y));
  }

  updateData() {
    console.log('updateData');
    const { data } = this.props;

    const drawLine = this.line;

    // prepare data to [ [{x, y, color}, {x, y, color}], [{x, y, color}, {x, y, color}] ... ]
    const processedData = [];
    Object.keys(data).forEach(countryName => {
      processedData.push(data[countryName].map((infos) => ({ color: colorHash.hex(countryName), ...infos})));
    });

    // generate line paths
    const lines = this.lineGroup.selectAll('.line').data(processedData);

    // [Update] transition from previous paths to new paths
    this.lineGroup.selectAll('.line')
      .transition()
      .style('stroke', d => d[0] ? d[0].color : null)
      .attr('d', drawLine);

    // [Enter] any new data
    lines.enter()
      .append('path')
      .attr('class', 'line')
      .style('stroke-width', '2px')
      .style('fill', 'none')
      .style('stroke', d => d[0] ? d[0].color : null)
      .attr('d', drawLine);

    // [Exit]
    lines.exit()
      .remove();
  }

  update() {
    console.log('update');
    // only call this.updateSize() if some props involving size have changed (check is done on componentWillReceiveProps)
    if (this.shouldUpdateSize === true) {
      this.updateSize();
      this.shouldUpdateSize = false;
    }
    this.updateData();
  }

  render() {
    console.log('render');
    return (
       <svg ref={(node) => this.rootNode = select(node)}></svg>
    );
  }

}

Conclusion

I just wanted to expose how I managed to make d3 work with React and how, even if you’re a pure d3 user, you should still be able to take advantage of the whole possibilities of d3.

Though, with its component approach and its JSX declaration syntax, React has a great potential for reusable chart components (that’s where it becomes very interesting).

The next step on topheman/d3-react-experiments will be to try to make reusable charts based on JSX (leave the computation part to d3 and make the rendering in React, without letting d3 mutate the DOM).

This may seem like rebuilding the wheel, there are already libraries that do that kind of thing – I’ve even tested some of them on that project – but I can’t seem to find any clear winner …

Are you making data visualisations on React ? What libraries are you using ? Do you use home-made components ?…

Tophe