TypeScript - D3 powertools

There are many upsides to using TypeScript, which I'll get into in just a moment. Before that, be forewarned that there's bit more effort involved with getting TypeScript set up because it's a transpiler in its own right, which means we won't really use Babel with it anymore.

The good news, however, is that TypeScript compiles modern JavaScript very similarly to how Babel does, and we just need to tweak a few things before we can begin using it. In practice, you probably won't notice the difference between the two. Further, we will actually use both simultaneously, importing TypeScript modules into Babel and vice versa.

First, let's install some more stuff. Modern JavaScript development is such as 50% coding, 20% being confused by what libraries to use, and 30% installing those libraries. So, let's get to it:

$ npm install typescript ts-loader --save-dev

This will install both the TypeScript transpiler and the Webpack loader. Next, run the following:

$ npm install @types/d3 d3-sankey aendrew/d3-sankey --save

This installs the D3 type definitions provided by the community. Previously, you used to need a separate utility to install definitions, but with TypeScript 2.0, they're all now available via the @types npm organization. We save them as a normal dependency because your definitions are part of your main code with TypeScript. It also installs d3-sankey, which isn't part of the main D3 monolib package, and the aendrew/d3-sankey TypeScript definition.

Tom Wanzek is the superhero who spearheaded creating the excellent TypeScript definitions for D3 v4. I cannot express how grateful I am that he did, because that would have been a crazy amount of work.

The one exception to this is aendrew/d3-sankey, which is a TypeScript definition for d3-sankey I wrote in preparation for this chapter. Writing TypeScript definitions is a bit beyond the scope of this chapter, but it doesn't take an enormous degree of familiarity with TypeScript to start contributing to community projects, such as DefinitelyTyped, which is where the @types npm organization gets all of its definitions from.

Next, let's update our Webpack config. Under rules, add the following:

        { 
test: /.ts$/,
loader: 'ts-loader'
},

This will use TypeScript to load all the files ending in .ts. You can go back to all your old files and rename them to .ts, or you can just do like we did here and use both Babel and TypeScript. Once you get used to TypeScript, you'll probably want to start with it from the beginning, but this time around we will mash up both ES2015+ and TypeScript for great awesomeness.

Not done yet! Create a file called .tsconfig in the root of your project, and fill it with the following:

{ 
"compilerOptions": {
"target": "ESNext",
"module": "ES6",
"moduleResolution": "node",
"isolatedModules": false,
"jsx": "react",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declaration": false,
"noImplicitAny": false,
"noImplicitUseStrict": false,
"removeComments": true,
"noLib": false,
"preserveConstEnums": true,
"suppressImplicitAnyIndexErrors": true,
"allowJs": true,
"lib": [
"es2015",
"es2015.iterable",
"es2015.symbol",
"es2015.promise",
"dom"
]
},
"exclude": [
"node_modules"
],
"compileOnSave": false,
"buildOnSave": false,
"atom": {
"rewriteTsconfig": false
}
}

To start, we'll comment out everything in lib/main.js and add the following:

import sankeyChart from './chapter9/index.ts'; 
sankeyChart();

Next, create a folder called lib/chapter9 and a new file in that, called index.ts:

import { 
sankey,
SankeyLink,
SankeyNode,
SankeyData } from 'd3-sankey';
import * as d3 from 'd3';
import chartFactory from '../common/index';

This includes both your TypeScript definitions and imports D3 from node_modules. From here, we can start using TypeScript like we would normally.

If you didn't get the hint from all the stuff we just installed, we will create a Sankey diagram. Much like the force directed diagram we made earlier, a Sankey diagram depicts relations in a graph format; however, its main use is to depict the magnitude of those links. The Sankey diagram we make will depict where seats went in the last UK general election. I suppose the 2016 US presidential election would be slightly more topical, but Sankey diagrams are more fun when we have more nodes than just two to work with.

Let's start using our old friend, chartFactory, to scaffold out our chart:

const chart = chartFactory({ 
margin: { left: 40, right: 40, top: 40, bottom: 40 },
padding: { left: 10, right: 10, top: 10, bottom: 10 },
});

Next, we create an object containing the various colors representing each major party, grouping a bunch of the smaller parties into the "Other" category.

const partyColors = { 
CON: '#0087DC',
LAB: '#DC241F',
SNP: '#FFFF00',
LIB: '#FDBB30',
UKIP: '#70147A',
Green: '#6AB023',
Other: '#CCCCCC',
};

I've done a bunch of data parsing and reformatting to make this example nice and straightforward; it's in the book repo data/ directory as uk-election-sankey.json. Let's create a new async function and load in our data using fetch:

async function typescriptSankey() { 
const sankeyData: SankeyData = await (
await fetch('data/uk-election-sankey.json')).json();
const width = chart.width;
const height = chart.height;

const svg = chart.container;
}

If you want to see how the data was generated, the script I used is getSankeyData.ts in data/scripts/ in the book repository. It's in TypeScript too!

We will not annotate many of our variables in this chapter because our type definition does a lot of the hard work for us. We do, however, need to annotate our output from fetch because otherwise TypeScript will just see it as an untyped object. To do this, we use the SankeyData interface we imported earlier, which is part of the d3-sankey TypeScript definition. An interface is essentially just a grouping of type declarations, sort of like an object of types. In its entirety, our SankeyData interface looks as follows:

export interface SankeyData { 
nodes: Array<SankeyNode>;
links: Array<SankeyLink>;
}

Despite the weird syntax, it's pretty straightforward--we're essentially creating an object with two properties: nodes and links--that consist of arrays containing SankeyNodes and SankeyLinks, respectively. Although it's not hard to take a look at what these two additional interfaces look like (if you've installed the excellent atom-typescript plugin for working in this section, right-click on the import and select Go to declaration from the context menu), suffice to say they're pretty straightforward and similar to data structures you've already used many times throughout this book (specifically, SankeyNode has a name and a value property, and the SankeyLink interface has source, target, and value properties). These, in turn, extend another object that contains all the properties d3-sankey decorates our data with, but again, a lot of this is behind the scenes and you don't really interact with it, except when you get something wrong and the TypeScript compiler complains that you're missing a property or whatever. In many ways, this replaces having a linter--TypeScript presently isn't lintable by ESLint and instead has its own linter--tslint--but that's mainly to ensure code formatting and less to flag up problems with your code (the TypeScript compiler itself lets you know when something is amiss).

We haven't done anything with SVG gradients yet, so let's play with those in our last real example. To create a gradient with SVG, we create a gradient definition object and store it in a hidden tag called defs. We then reference that by ID later on. Bear with me here, it's a bit confusing but it'll all make sense in a second.

Still inside typescriptSankey(), add the following:

  const defs = svg.append('defs'); 
Object.keys(partyColors).forEach((d, i, a) => {
a.forEach(v => {
defs.append('linearGradient')
.attr('id', &grave;${d}-${v}&grave;)
.call((gradient) => {
gradient.append('stop')
.attr('offset', '0%')
.attr('style',
&grave;stop-color:${partyColors[d]}; stop-opacity:0.8&grave;);
gradient.append('stop')
.attr('offset', '100%')
.attr('style',
&grave;stop-color:${partyColors[v]}; stop-opacity:0.8&grave;);
});
});
});

This will create a structure resembling the following:

<defs> 
<linearGradient id="CON-LAB" >
<stop
offset="0%"
style="stop-color: #0087DC; stop-opacity: 0.8"
/>
<stop
offset="100%"
style="stop-color: #0087DC; stop-opacity: 0.8"
/>
</linearGradient>
</defs>

A linear gradient can have an arbitrary number of color stops set along a continuum. We transition from the originating party color to the target party color and give it an ID reflective of this relationship.

Oh snap waddup--it's finally time to rock out with our layout generator! Again, add the following inside typescriptSankey:

  const sankeyGenerator = sankey() 
.size([width - 100, height - 100])
.nodeWidth(15)
.nodePadding(10)
.nodes(sankeyData.nodes)
.links(sankeyData.links)
.layout(1);

const path = sankeyGenerator.link();

This creates a Sankey generator set to the size of our browser window, sets the width of each node to 15, pads each node by 10, and assigns all of our links and nodes to the layout generator. We then create a path generator using Sankey.link(), which will draw all of our beautiful swoopy paths between nodes.

With all of that set up, finally we're at the let's actually render stuff on the page part of our example. Draw our links, like so:

  const link = svg.selectAll('.link') 
.data(sankeyData.links)
.enter()
.append('g')
.classed('link', true);

link.append('path')
.attr('d', path)
.attr('fill', 'none')
.attr('stroke', d => {
const source = d.source.name.replace(/(2010|2015)/, '');
const target = d.target.name.replace(/(2010|2015)/, '');
return &grave;url(#${source}-${target})&grave;;
})
.style('stroke-width', d => Math.max(1, d.dy));

The only things that are somewhat confusing here are the stroke-width and stroke attributes--for our stroke color, we generate a string reflective of our source and target party names, and use that to reference the gradients we made earlier. To go back to the earlier example, our gradient transitioning from blue to red (indicating support changing from the Conservative Party to Labour) will be #CON-LAB. To make it reference the earlier linearGradient definition, we wrap this ID in url().

Lastly, we set the stroke-width to the relative y value of each link.

Now that we have all of our links set up, let's render our nodes and labels!

First, we set up a group to contain our rectangles and labels:

  const node = svg.selectAll('.node') 
.data(sankeyData.nodes)
.enter()
.append('g')
.classed('node', true)
.attr('transform', d => &grave;translate(${d.x},${d.y})&grave;);

It's pretty straightforward. Let's add our rectangles:

  node.append('rect') 
.attr('height', d => d.dy)
.attr('width', sankeyGenerator.nodeWidth())
.style('fill', d =>
partyColors[d.name.replace(/(2010|2015)/, '')])
.append('title')
.text(d => &grave;${d.name}nSeats: ${d.value}&grave;);

We get the width of each rectangle using the sankey.nodeWidth() method, which is the same method we used earlier but without an argument, so it returns our earlier width. We set the fill using our partyColor object from earlier, but we remove the year from each name using String.prototype.replace(), so it matches our object.

Lastly, we'll quickly add some labels so that we don't have to identify our parties based on color alone:

  node.append('text') 
.attr('x', -6)
.attr('y', d => d.dy / 2)
.attr('dy', '.35em')
.attr('text-anchor', 'end')
.attr('transform', null)
.text(d => d.name)
.filter(d => d.x < width / 2)
.attr('x', 6 + sankeyGenerator.nodeWidth())
.attr('text-anchor', 'start');

Nothing too weird here; we add 6 to the width of our nodes to offset them a little bit, I've just chosen 6 because it seems to work well. Yay for magic numbers!

Click on Save and check your browser; you should have a Sankey diagram, as illustrated:

Notice the weird "UKIP 2010" label hanging out in the bottom right-hand corner? Remember that, it'll be important later on in this chapter when we do unit testing.

Pretty nice, huh? From this, we can see, at a glance, that both the Liberal Democrat and Labour parties lost a lot of support, with many of the Liberal Democrat voters going to the Labour and Conservative parties, and a lot of Labour voters going to the other category (although it's not obvious, given that we've grouped national and alternative parties into a single other group, the vast bulk of Labour losses in 2015 were in Scotland to the Scottish Nationalist Party, which promised to vote similarly to Labour but with a preference for Scottish interests during the election). And even though the Conservatives were immensely concerned about the effect UKIP would have, only one of their seats went to them. I'm sure former Prime Minister David Cameron wishes he had known that result before he promised that whole Brexit referendum prior to the election!

Okay, enough jabbering on about British politics. Hopefully, what I've demonstrated with this example is that TypeScript looks very similar to normal ES2017 code, with a few minor exceptions here and there. If you're using popular libraries with high-quality definitions, the added development effort to incorporate TypeScript might be worth the instant feedback and internal documentation it gives you.

What if you're using a library that doesn't have a TypeScript definition? You have two options: either declare it using the any type, or turn noImplictAny to false in tsconfig.json. This is probably easier than writing your own definition for a library, but it also loses the benefits that come with TypeScript. It's worth considering what libraries you'll use before you incorporate TypeScript, as dealing with an untyped library can add a lot of headache to a project, as it did for me when I was initially creating this example!

This was a ludicrously shallow overview of TypeScript that barely scratches the surface, but hopefully you've given it a go using a good editor and plugin combo and are already sensing how powerful it can be. To be able to use TypeScript effectively, you should know at least how to typecast each variable and create things such as interfaces. If TypeScript interests you as a technology, I highly recommend reading the handbook at http://www.typescriptlang.org/Handbook.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset