Mastering SVG Bonus Content: A D3 Line Chart

My latest book, Mastering SVG, has a whole chapter on D3. It had been several years since I’d worked with D3 so it was a lot of fun to dive back in and write some D3 code for the book. I did three long examples. It was a lot of fun to come up with the visualizations.

It was so much fun, in fact, I’ve continued to work with D3, even though the book is out in the world. This article will go over one such example I did for my comics site.

This chart illustrates the progression of the record price paid for an American comic book over the past 50+ years. It’s a line chart and with some libraries, generating a basic line chart would be a few lines of code. This D3 example is more than a few lines of code, but it also illustrates how D3 offers control over every aspect of the visualization. It really shows the value of working with a rich API like the one offered by D3.

The code for this example is available on Github. You can view an interactive demo on github.io.

Let’s look at how this visualization was put together.

D3

In case you’re not familiar with it, D3 (for Data-Driven Documents) is a powerful library for working with SVG and doing data visualizations. It’s also one of the most popular open source projects in the world. It marries the inherent power of SVG with a number of data manipulation tools and a host of SVG-manipulation utilities to create one of the most robust libraries out there for work on the web.

It’s available as a download from d3js.org, via npm or via cdnjs.

If you’ve never worked with D3 then, be warned, this article is not a gentle introduction. I’s a full fledged visualization and not an intro demo. I do promise to go over everything in detail so hopefully you’ll be able to catch up even if you have no D3 experience. And, if you do catch on, you’ll actually have some experience with many core concepts, so you’ll be ready to tackle your own visualizations.

From an API perspective, if you’re familiar with chaining JavaScript method calls and have worked with a fluent interface like jQuery, then major parts of D3 should feel familiar to you. They even share some API signatures.

The Data

The data is as follows. sales is an array of objects representing each sale. The properties are as follows:

  • title The comic’s title
  • issue The comic’s issue number
  • grade The numerical grade of the comic. This can either be a grade on the modern 1-10 scale, a string describing the old scale (e.g. “NM” for Near Mint or VF for “Very Fine”) or another anecdote regarding the grade (“high grade”)
  • buyer If known, the buyer of the comic
  • note An optional note about the sale
  • date The date of the sale
  • seller The seller of the comic
  • price The price paid for the comic.
  • goodDate A Boolean indicating whether or not the date is precise. In putting this data together I’ve run into roadblocks getting precise dates on many of the sales.
{
  "sales": [
    {
      "title": "Action Comics",
      "issue": "1",
      "grade": "9.0",
      "buyer": "Metropolis (for Ayman Hariri)",
      "note":"",
      "date": "2014-08-24",
      "seller": "Pristine Comics on eBay",
      "price": "3207852",
      "goodDate": true
    },
    {
      "title": "Action Comics",
      "issue": "1",
      "grade": "9.0",
      "buyer": "Ayman Hariri",
      "note":"Cage Copy",
      "date": "2011-11-30",
      "seller": "ComicConnect",
      "price": "2161000",
      "goodDate": true
    },
    {
      "title": "Action Comics",
      "issue": "1",
      "grade": "8.5",
      "buyer": "",
      "note":"",
      "date": "2010-03-29",
      "seller": "ComicConnect",
      "price": "1500000",
      "goodDate": true
    },
    {
      "title": "Detective Comics",
      "issue": "27",
      "grade": "8.0",
      "buyer": "",
      "note":"",
      "date": "2010-02-25",
      "seller": "Heritage",
      "price": "1075000",
      "goodDate": true
    },
    {
      "title": "Action Comics",
      "issue": "1",
      "grade": "8.0",
      "buyer":"",
      "note": "Kansas City",
      "date": "2010-02-22",
      "seller": "ComicConnect",
      "price": "1000000",
      "goodDate": true
    },
    {
      "title": "Flash Comics",
      "issue": "1",
      "grade": "9.6",
      "note":"Church copy",
      "buyer": "JP the Mint",
      "date": "2004-01-01",
      "seller": "unknown",
      "price": "350000",
      "goodDate": false
    },
    {
      "title": "Marvel Comics",
      "issue": "1",
      "grade": "9.0",
      "note":"Pay Copy",
      "buyer": "JP the Mint",
      "date": "2003-01-01",
      "seller": "Steve Geppi",
      "price": "350000",
      "goodDate": true
    },
    {
      "title": "Captain America Comics",
      "issue": "1",
      "grade": "9.6",
      "note": "Allentown",
      "buyer": "John Verzyl",
      "date": "2001-01-01",
      "seller": "unknown",
      "price": "260000",
      "goodDate": false
    },
    {
      "title": "Action Comics",
      "issue": "1",
      "grade": "Now: CGC 8.5",
      "note":"current CGC 8.5 copy",
      "buyer": "Daniel Kramer",
      "date": "1995-01-01",
      "seller": "PCE",
      "price": "137500",
      "goodDate": false
    },
    {
      "title": "Detective Comics",
      "issue": "27",
      "grade": "8.5",
      "note": "Church",
      "buyer":"",
      "date": "1994-01-01",
      "seller": "Dave Anderson?",
      "price": "125000",
      "goodDate": false
    },
    {
      "title": "Detective Comics",
      "issue": "27",
      "grade": "high grade",
      "note": "'other high grade' copy",
      "buyer":"",
      "date": "1993-01-01",
      "seller": "unknown",
      "price": "101000",
      "goodDate": false
    },
    {
      "title": "Action Comics",
      "issue": "1",
      "grade": "78",
      "note":"",
      "buyer": "Metropolis (for actor Nic Cage)",
      "seller": "Sotheby's",
      "price": "82500",
      "date": "1992-09-30",
      "goodDate": true
    },
    {
      "title": "Detective Comics",
      "issue": "27",
      "grade": "NM-MT",
      "note": "Allentown",
      "buyer":"Dave Anderson",
      "price": "80000",
      "seller": "Metropolis",
      "date": "1990-01-01",
      "goodDate": false
    },
    {
      "title": "Action Comics",
      "issue": "1",
      "grade": "NM",
      "buyer":"Dave Anderson",
      "note": "Church Copy",
      "seller": "John Snyder",
      "price": "25000",
      "date": "1984-01-01",
      "goodDate": false
    },
    {
      "title": "Marvel Comics",
      "issue": "1",
      "grade": "",
      "note":"",
      "buyer": "Steve Geppi",
      "date": "1979-10-08",
      "seller": "John Snyder",
      "price": "17500",
      "goodDate": true
    },
    {
      "title": "Marvel Comics",
      "issue": "1",
      "grade": "",
      "note":"",
      "buyer": "John Snyder",
      "date": "1979-01-01",
      "seller": "unknown",
      "price": "13000",
      "goodDate": false
    },
    {
      "title": "Marvel Comics",
      "issue": "1",
      "grade": "",
      "note":"",
      "buyer": "",
      "date": "1977-01-01",
      "seller": "Robert Crestohl?",
      "price": "7500",
      "goodDate": false
    },
    {
      "title": "Motion Picture Funnies Weekly",
      "issue": "1",
      "grade": "",
      "buyer": "",
      "note":"",
      "date": "1976-01-01",
      "seller": "unknown",
      "price": "6300",
      "goodDate": false
    },
    {
      "title": "Whiz",
      "issue": "2",
      "grade": "",
      "note":"Reilly Copy",
      "buyer": "Burl Rowe",
      "date": "1974-01-04",
      "seller": "Comics & Comix",
      "price": "2000",
      "goodDate": true
    },
    {
      "title": "Action",
      "issue": "1",
      "grade": "",
      "note":"",
      "buyer": "Bruce Hamilton",
      "date": "1973-04-02",
      "seller": "Gene Henderson",
      "price": "1000",
      "goodDate": true
    },
    {
      "title": "Action",
      "issue": "1",
      "grade": "",
      "note":"",
      "buyer": "Theo Hostein",
      "date": "1973-04-22",
      "seller": "Bruce Hamilton",
      "price": "1500.00",
      "goodDate": true
    },
    {
      "title": "Action",
      "issue": "1",
      "grade": "",
      "note":"",
      "buyer": "Mitch Mehdy",
      "date": "1973-05-01",
      "seller": "Theo Hostein",
      "price": "1801.26",
      "goodDate": true
    },
    {
      "title": "Marvel Comics",
      "issue": "1",
      "grade": "",
      "buyer": "",
      "note":"",
      "date": "1968-01-01",
      "seller": "Howard Rogolfsky",
      "price": "330",
      "goodDate": false
    },
    {
      "title": "Action Comics",
      "issue": "1",
      "grade": "",
      "buyer": "",
      "note":"",
      "date": "1965-01-01",
      "seller": "unknown",
      "price": "250",
      "goodDate": false
    }
  ]
}

The HTML

Next up we have the markup. The head of the document this demo includes the Bootstrap CSS, the Raleway font, and a link to the the style sheet for this particular visualization, main.css.In the body there’s some simple markup that we will populate with JavaScript. The div#target is where the visualization will live. Next, are a couple of checkboxes used to toggle inflation adjusted/nominal results on or off. Finally, there’s a placeholder div#data for the data to be output as a friendly list.

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8">
    <title>A Timeline of World Record Comic Book Sales</title>

    <link href="_assets/css/normalize.css" rel="stylesheet">

    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm"
        crossorigin="anonymous">
    <link href="https://fonts.googleapis.com/css?family=Raleway" rel="stylesheet">
    <link href="_assets/css/main.css" rel="stylesheet">

</head>

<body>
    <div class="container-fluid">
        <div class="row">
            <div class="col-12 nominal" id="target">

            </div>
        </div>
        <div class ="row">
            <div class="col-12">
                  <div class="form-check">
                    <input class="form-check-input" type="checkbox" value="inflation" id="inflationValue">
                    <label class="form-check-label" for="inflationValue">
                      Show Inflation Adjusted Results
                    </label>
                  </div>
                  <div class="form-check">
                    <input class="form-check-input" type="checkbox" value="nominal" id="nominalValue" checked>
                    <label class="form-check-label" for="nominalValue">
                      Show Nominal Results
                    </label>
                  </div>
            </div>
        </div>
        <div class="row mt-5">
            <div class="col-12" id="data">
                <h1>The Data</h1>

            </div>
        </div>
    </div>


    <script src="node_modules/d3/dist/d3.min.js"></script>
    <script src="node_modules/d3-fetch/dist/d3-fetch.min.js"></script>
    <script src="_assets/js/inflation.js"></script>
    <script src="node_modules/moment/min/moment.min.js"></script>
    <script src="_assets/js/comics.js"></script>



</body>

</html>

The Styles

Next we have the styles for the visualization. There are basically three sets of styles:

  • Rules for the basic text styles. We use the Raleway font. I like Raleway.
  • Rules defining both nominal and inflation-adjusted fill and stroke styles for SVG elements.
  • Rules defining our Bootstrap tooltip styles
/* Text Styles */
text {
    font-family: Raleway;
    font-size: .8em;
    text-anchor: middle;
   
}
text.legend{
    font-size: 1.25em;
    fill: #000;
}
.y.axis .tick text {
    text-anchor: end;
    font-size: 12px;
}
/* lines and circles */
.line {
    fill:none;
    stroke: rgb(0, 100, 201);
    stroke-width: 2;
}
.line.inflation {
    stroke: rgba(0, 100, 201, .5);
}
.dot {
    fill: rgba(255, 165, 0, .5);
    stroke: rgb(100, 0, 201);
}
.axis .tick line {
    stroke: #ccc;
}
.dot,
.line {
    visibility: hidden;
}
.nominal .dot.nominal,
.nominal .line.nominal,
.inflation .dot.inflation,
.inflation .line.inflation {
    visibility: visible;
}
.form-check {
    margin-left: 100px;
}
/* tooltip customization */
.tooltip-inner {
    background-color: rgb(0, 100, 201);
}
.tooltip.bs-tooltip-right .arrow:before {
    border-right-color: rgb(0, 100, 201) !important;
}
.tooltip.bs-tooltip-left .arrow:before {
    border-right-color: rgb(0, 100, 201) !important;
}
.tooltip.bs-tooltip-bottom .arrow:before {
    border-right-color: rgb(0, 100, 201) !important;
   }

.tooltip.bs-tooltip-top .arrow:before {
    border-right-color: rgb(0, 100, 201) !important;
}

.inflation .tooltip-inner {
    background-color: rgba(0, 100, 201, .8);
}

.inflation.tooltip.bs-tooltip-right .arrow:before {
    border-right-color: rgba(0, 100, 201, .8) !important;
}

.inflation.tooltip.bs-tooltip-left .arrow:before {
    border-right-color: rgba(0, 100, 201, .8) !important;
}

.inflation.tooltip.bs-tooltip-bottom .arrow:before {
    border-right-color: rgba(0, 100, 201, .8) !important;
}

.inflation.tooltip.bs-tooltip-top .arrow:before {
    border-right-color: rgba(0, 100, 201, .8) !important;
}

The JavaScript

And now we get to the JavaScript. This file is big and I’ll go through the whole thing in a lot of detail, so please grab some popcorn, open up the source code in another window or in a code editor and let’s get started.

The top of the file includes many constants for use in the visualization.

  • margin defines a buffer around the visualization
  • width and height define the height and width of the SVG element. These are used in multiple calculations.
  • yMargin and xMargin arr two properties used to calculated metrics, inclusive of the defined x and y margins.
  • svg is our core D3 SVG element, created using the previously defined width and height. This illustrates the common pattern of working with D3. You have a selection (either a DOM element or a data selection) and you chain methods on it, manipulating the selection in a variety of ways.
  • g is a group element appended to the svg element, translated to the top/left of the visualization (as defined by the margin properties.)
  • radius is an arbitrary value for the radius of circles used in the visualization
  • parseTime is a local reference to the D3.timeParse method, configured with a pre-defined formatting string.
  • tooltip is a small function to append a Bootstrap tooltip into the DOM. This is done using D3’s select, append, attr and style utilities.
  • inflationTooltip is a small function to append a Bootstrap tooltip into the DOM, complete with styling for inflation. Yes, I’m being slightly lazy. I should have one function with an option for inflation. Sorry!
  • list is an unordered list element appended to the DOM, in order to hold the data written out in list form.
  • x is a configured D3 scale function to generate the time-based x axis of the visualization. Scales are the pixel representation of the data. All conversions from data to pixels are done using scales.
  • y is a configured D3 scale function to generate the y axis of the visualization.
  • line and inflationLine are two functions used to draw line graphs, based on the data set. This is where D3 actually draws the line charts for our visualization.

const margin = {
  'top': 100,
  'right': 20,
  'bottom': 30,
  'left': 100
};
const width = 1440;
const height = 768;
const yMargin = margin.top + margin.bottom;
const xMargin = margin.right + margin.left;

const svg = d3.select('#target')
  .append('svg')
  .attr('width', width)
  .attr('height', height);

const g = svg.append('g')
  .attr('transform', `translate(${margin.left},${margin.top})`);
const radius = 5;
const parseTime = d3.timeParse('%Y-%m-%d');

const tooltip = d3.select('body')
  .append('div')
  .attr('class', 'tooltip bs-tooltip-right')
  .style('opacity', 0);
tooltip.html(`
  <div class="arrow"></div>
  <div class="tooltip-inner">
  </div>`);
const inflationTooltip = d3.select('body')
  .append('div')
  .attr('class', 'tooltip bs-tooltip-right inflation')
  .style('opacity', 0);
inflationTooltip.html(`
  <div class="arrow"></div>
  <div class="tooltip-inner">
  </div>`);

const list = d3.select('#data')
  .append('ul').attr('class','list-group');

const x = d3.scaleTime()
  .rangeRound([0, width - xMargin]);

const y = d3.scaleLinear()
  .rangeRound([height - yMargin, 0]);


const line = d3.line()
  .x1);

  sales.forEach2;
    d.inflationAdjustedPrice = inflation({'amount': d.price, 'year': year});
  });

In this next block we take the previously defined x and y scales and populate them with our data.

The x scale’s domain (the universe of the data being visualized) is set using the sales data, with the start of the earliest year as the minimum value (found using d3.min which returns the minimum value in a set) and the current date as the maximum value.

The y scale’s domain is set using 0 as the lower bound and the max value in the set (found using d3.max,) rounded up to the nearest 500,000. The rounding is done by dividing the max value (in this case just over 3.2 million) by 500,000. That returns around 6.4. Calling math.ceil on that, to get the next highest whole number, gives us 7. Multiplying that number by 500,000, gives us our top range of $3,500,000.

  x.domain([
    d3.min(sales, (d) => {
      return moment(d.date).startOf('year').toDate();
    }), moment().toDate()]);
  y.domain([0, d3.max(sales, (d) => {
    return Math.ceil(d.price / 500000) * 500000;
  })]);
  const years = parseInt(moment().format('YYYY')) - parseInt(moment(x.domain()[0]).format('YYYY'));

Now that the domains are set, it’s time to actually draw the axes. First we append an SVG g (group) element to the DOM and store it in a variable called axis. Then we append two more g elements for each individual axis.

Here is the first place in this demo where D3 really shows the complexity of its API. It’s also here where D3 shows its power.

First we create a g element for the y axis. Then two classes are added to it using D3.attr, “y” and “axis.” Then we use the D3 utility method, call. D3.call is a method that allows you to invoke a function on a D3 selection and then return the modified selection. This allows you to do some actions which might normally interrupt the flow of chained methods. In this case we pass in a call to d3.axisLeft with our previously configured y scale as the argument. axisLeft takes that linear scale, with our configured domain. and generates a legend on the left side of the visualization. We then further configure it, setting the tick size and formatting the tick legends.

Using d3.tickSize here we actually create the lines that run horizontally across the visualization. This is done by setting a negative width equal to the width of the SVG element minus the xMargin, which nicely fits these lines within the bounds of the core visualization. The negative length works here because the tick mark is actually drawn from the right edge of the axis. A positive value would draw from the right edge to the left. A negative value draws itself into the visualization itself.

We format the tick legends using tickFormat, which accepts d3.format as an argument. Here we pass a formatting string, “($~s” into d3.format. This formatting string indicates that d3 should use no sign for positive numbers and “(” for negative numbers (which aren’t relevant in this case, it’s just the indicator I would normally use;) should include the locale currency, “$” and with “s” that it should render the number in decimal notation with a locale specific prefix (for example, $3.5M for the top end of the scale.)

Describing what we just did sounds like this: We start with three chained method calls. The third of those three calls has an additional three chained method calls passed in as an argument. The final method call in that inner chain has a formatting function passed in as an argument. It’s complicated, sure, but because you have entry points to the data, the selection and the rendering at each of those points, you have pinpoint control over the visualization. That’s powerful.

Next we generate the x axis. As before we add a new g element, and add two classes, “x” and “axis.” Then we translate the g element to its place at the bottom of the visualization. Finally we use d3.call again, this time passing in d3.axisBottom with our x scale as the argument. Off of that, we chain d3.ticks, passing in years to generate ticks for each year in our range, d3.tickSize (once again using the negative length trick to create the gridlines in the visualization) and d3.tickPadding (which gives us a little padding to make the numbers more legible against the line.

  let axis = g.append('g')
    .attr('class', 'axis');

  axis.append('g')
    .attr('class', 'y axis')
    .call(d3.axisLeft(y)
      .tickSize(-(width - xMargin)).tickFormat(d3.format('($~s')));
  axis.append('g')
    .attr('class', 'x axis')
    .attr('transform', `translate(0, ${height - yMargin})`)
    .call(d3.axisBottom(x).ticks(years).tickSize(-(height - yMargin)).tickPadding(10));

The next section actually appends the line charts into the SVG element. This is one of the more straightforward interactions with D3 that you can have. This is because the generated line graph is simply points on an SVG path.

To start we append a new g element with a class of “paths.” Then we append two path elements to it. One of these is for inflation adjusted results and the other is for nominal results.

In each case we call the d3.data method on the returned path element. This method call loads the selection (in this case the previously created path element) with the current data. Then we add two classes: “line” and “nominal” or “inflation” depending on whether or not the line chart is using nominal or inflation adjusted numbers. Finally we populate the d attribute with the functions line and inflationLine. These instances of the D3 visualization method use the data attached to the current selection to generates a line graph.


  let paths = g.append('g')
    .attr('class', 'paths');

  paths.append('path')
    .data([sales])
    .attr('class', 'line nominal')
    .attr('d', line);

  paths.append('path')
    .data([sales])
    .attr('class', 'inflation line')
    .attr('d', inflationLine);

The next section is, once again, repetitive. Once again, both blocks do basically the same thing except for some differences relating to the data being inflation adjusted or nominal.

It also illustrates some important concepts in D3.

The first call should be familiar at this point. We append a new g element and give it a class of “dots.” That is stored as dots.

Next up, we do something new. We run d3.selectAll(".dot") d3.selectAll, at its most basic, works like jQuery’s core $ function or the core DOM method document.querySelectorAll. It takes in a CSS selector argument and returns a collection of elements that match that CSS selector. Like jQuery (but not like querySelectorAll) any subsequent calls will operate on all members of the collection.

In this case, there are no elements that match that “dot” class. So, what’s the point? The next two chained method calls, dots.selectAll(".dot").data(sales).enter() are the key. The call to data binds our current data set to the (still empty) selection. Subsequently calling enter populates the previously empty selection with a collection of elements corresponding to each data point. Every action after the call to enter operates on every member of the collection (which corresponds to every member of the data set. ) So when we call append, it adds a circle element for every member of the collection.

For each of those circles we then perform several operations. Let’s look at each

  • we add a couple of classes, dot and nominal/inflation
  • We set the cx, cy (center x and center y) using the x and y scales we previously created. Passing in properties of the d (data) object passed as an argument, returns pixel representations of the data points.

    These scales were also used to create the trendlines themselves in line and inflationLine, so this illustrates on the of the ways that D3 makes it easy to compose visualizations in a very powerful way. Whatever you’re doing, you now have the scale available to render elements on the screen using the same, predefined scale. The circle element we’re using here is a standard SVG element and all of the attributes we’re manipulating here are standard SVG attributes. But since we’re working with D3 we can easily integrate regular SVG elements into our visualization with ease.

  • Next we set the r (radius) of the circle using the radius constant.
  • Next we add some events using D3’s event handling system (selection.on(“eventName”, callbackFunction). On mouseover we populate the tooltip with information about the specific sale, including title, issue number grade and price, formatted as dollar denominated currency in the locale format for currency. On mouseout, we simply hide the tooltip.
    
      let dots = g.append('g')
        .attr('class', 'dots');
    
      dots.selectAll('.dot')
        .data(sales)
        .enter()
        .append('circle')
        .attr('class', 'dot nominal')
        .attr('cx',  (d)=> {
          return x(d.date);
        })
        .attr('cy',  (d)=> {
          return y(d.price);
        })
        .attr('r', radius)
        .on('mouseout', (d)=> {
          tooltip.style('opacity', 0);
        })
        .on('mouseover', (data)=> {
          tooltip.style('opacity', 1);
          tooltip.style('left', (d3.event.pageX + radius) + 'px')
            .style('top', (d3.event.pageY) + 'px');
          tooltip.select('.tooltip-inner')
            .text((d)=> {
              return `${data.title} #${data.issue} ${data.grade}  ${data.price.toLocaleString('us-EN', {style: 'currency', currency: 'USD'})}`;
            });
        });
      dots.selectAll('.inflation dot')
        .data(sales)
        .enter().append('circle')
        .attr('class', 'inflation dot')
        .attr('cx',(d)=> {
          return x(d.date);
        })
        .attr('cy',(d)=> {
          return y(d.inflationAdjustedPrice);
        })
        .attr('r', radius)
        .on('mouseout', ()=> {
          inflationTooltip.style('opacity', 0);
        })
        .on('mouseover', (data)=> {
          inflationTooltip.style('opacity', 1);
          inflationTooltip.style('left', (d3.event.pageX + radius) + 'px')
            .style('top', (d3.event.pageY) + 'px');
          inflationTooltip.select('.tooltip-inner')
            .text(()=> {
              return `${data.title} #${data.issue} ${data.grade}  ${data.inflationAdjustedPrice.toLocaleString('us-EN', {style: 'currency', currency: 'USD'})} (inflation adj.)`;
            });
        });
    

    The next block of code populates the section at the bottom of the screen where we list the sales in human language. It might look a little complicated, but it’s all just simple conditional logic designed to populate some strings. The initial set-up is familiar- we make a selection, of every “li” in the list variable we previously created. Then then call data on it to load it up with our data set and call enter on it to populate the list with entries for every member of the data set. We then append an li element for each member of the data set. Finally, we add a class of “list-group-item” to each.

    The next bit is a call to D3.text which sets the text value of a node. The function argument takes the data object for that member of the data set and eventually returns a formatted string representing that sale. Inside the callback we do a few things. We initially create several variables to hold our data. soldBy, soldTo and note are single spaced strings, so that if we fall through they still look okay in the sentence describing the sale. date is created with no initial value.

    Then we populate the strings with the best data we have for each field. If we have a seller, we use the seller name, otherwise we use the phrase ” by an unknown seller .” if a buyer exists, we update the soldTo variable to reference the buyer. If there’s a note, we update the string to reference the note. Finally, if we have a good date, we populate the string with a formatted date string which includes the month and year, in ${moment(d.date).format('MMMM of YYYY')}. If we only know the year, we populate the date string with a string that simple includes the year, `sometime in ${moment(d.date).format('YYYY')}`.

      list.selectAll('li')
        .data(sales)
        .enter()
        .append('li')
        .attr('class','list-group-item')
        .text((d)=>{
          let soldBy = ' ';
          let soldTo = ' ';
          let note = ' ';
          let date;
          if (d.seller !== '') {
            if (d.seller === 'unknown') {
              soldBy = ' by an unknown seller ';
            } else {
              soldBy = ` sold by ${d.seller} `;
            }
          }
          if (d.buyer !== '') {
            soldTo = ` to ${d.buyer} `;
          }
          if (d.note !== '') {
            note = ` (${d.note}) `;
          }
          if (d.goodDate) {
            date = `in ${moment(d.date).format('MMMM of YYYY')}`;
          } else {
            date = `sometime in ${moment(d.date).format('YYYY')}`;
          }
          return `${d.title} #${d.issue}${note}${soldBy} for ${d.price.toLocaleString('us-EN', {style: 'currency', currency: 'USD'})} (${d.inflationAdjustedPrice.toLocaleString('us-EN', {style: 'currency', currency: 'USD'})})${soldTo} ${date}`;
        });
    
    });
    

    The final block of code, simply toggles the classes indicating whether or not the inflation adjusted and/or nominal figures should be displayed. This visibility of the different versions of the chart are entirely run based on classes attached to the root SVG element. If the class is there, that set shows, if it isn’t there, that set doesn’t show.

    That said, I do actually have to walk through this a bit and the reason why has nothing to do with D3. This is an instance where some things you might know from old-school DOM manipulation
    might fail you when working with SVG. If you’ve ever manipulated the HTML DOM directly you’re likely used to working with the Element.className property. On HTML
    elements, the className property is a read/write string that maps to the class attribute on the HTML element. You can manipulate the string and changes are
    reflected in the DOM immediately.

    The DOM interface SVGElement does have a className property, but it isn’t a string you can simply manipulate. Under the hood it’s an SVGAnimatedString property with two string values AnimVal and BaseVal. That means the stuff you are used to doing won’t work here.

    The good news is, there’s a better, more modern way to manipulate classes. You can use the SVGElement.classList property to manipulate
    the CSS classes instead. classList is a structured interface to the CSS classes on an element. Accessed directly, classList is readonly, but there are methods available to query and manipulate the list of classes. That’s what we’re doing here, calling the toggle method to toggle the classes on an off.

    document.getElementById('inflationValue').addEventListener('change', () => {
      document.getElementById('target').classList.toggle('inflation');
    });
    document.getElementById('nominalValue').addEventListener('change', () => {
      document.getElementById('target').classList.toggle('nominal');
    });
    

    And with that, we’ve made it to the end of this rather long demo. I hope you’ve enjoyed it. It was fun to create and fun to write about. Mastering SVG was a challenging project since it covered the breadth of SVG. It was worth it for the ability to really dive into areas of SVG that interest me. D3 is one such area. Check the book out if you’re interested in getting full exposure to the world of SVG. It’s a lot of fun and opens up a number of possibilities on the web.


    1. d)=> { return x(d.date); }) .y((d)=> { return y(d.price); }); const inflationLine = d3.line() .x((d)=> { return x(d.date); }) .y((d)=> { return y(d.inflationAdjustedPrice); });

      Almost the whole of the rest of the visualization happens inside a call to d3.json. d3.json fetches the data (literally, it’s a convenience method built on top of fetch) and then runs a callback function, with data as the single argument.

      
      d3.json('data/books.json').then((data) => { /* everything happens here*/  }
      
      

      Inside that callback function lots of things happen. We’ll go through each section individually.

      Initially we take the data.sales array and sort it using the moment.diff function to compare each of the dates. Once they are sorted we manipulate the data in a forEach loop, adding an inflationAdjustedPrice for every member of the data set. This is done by passing the price and year into a function which returns an inflation adjusted value based on an amount and a timeframe.

      
        const sales = data.sales.sort((d1, d2) => moment.utc(d1.date).diff(moment.utc(d2.date 

    2. d) => { d.date = parseTime(d.date); d.price = parseInt(d.price); const year = parseInt(moment(d.date).format('YYYY' 

Leave a Reply

Your email address will not be published. Required fields are marked *