block by zanarmstrong d863e83b41e94ad6aa6dc36077d3cff4

pictogram table | d3v4

Full Screen

an es2015 d3v4 iteration on the block Pictogram Table from @armollica

a comment in a recent talk from @elijah_meeks exposed me to the ISOTYPE design system developed by Otto Neurath and colleagues in the 1930s. learning about the origins of the use of pictograms in information graphics sparked my interest in working with d3 examples like this. for more, read on to this nice set of blog posts on ISOTYPE from @eagereyes

special thanks to @yonester who helped me spot just what I needed to get column sorting working again. hooray d3js community!

solution

yonester [10:45 AM] @micahstubbs you just forgot to merge tdEnter and tdUpdate before you update the HTML. I.e., you need tdEnter.merge(tdUpdate).html(d => d.html);


Using pictograms within a table. Click headers to sort.

Notes: Uses technique from this blog post to implement HTML tables with D3 in a nice way. Growth rates are from Q2-2012 to Q2-2015. Data from BLS. State icons from ProPublica’s StateFace project. Other icons from Font Awesome.

forked from micahstubbs‘s block: pictogram table | d3v4

forked from anonymous‘s block: pictogram table | d3v4

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8'>
    <title></title>
    <link rel='stylesheet' href='https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css'>
    <link rel='stylesheet' href='style.css'>
    <link rel="icon" href="data:;base64,iVBORw0KGgo=">
  </head>
  <body>
    <div class='table-container'></div>
    <script src='//d3js.org/d3.v4.min.js' charset='utf-8'></script>
    <script src='https://cdnjs.cloudflare.com/ajax/libs/queue-async/1.0.7/queue.min.js'></script>
    <script src='vis.js'></script>
  </body>
</html>

lebab.sh

# safe
lebab --replace vis.js --transform arrow
lebab --replace vis.js --transform for-of
lebab --replace vis.js --transform for-each
lebab --replace vis.js --transform arg-rest
lebab --replace vis.js --transform arg-spread
lebab --replace vis.js --transform obj-method
lebab --replace vis.js --transform obj-shorthand
lebab --replace vis.js --transform multi-var
# unsafe
lebab --replace vis.js --transform let
lebab --replace vis.js --transform template

qcew.json

[  
  {  
    "fips":"01",
    "state":"Alabama",
    "state_abbrev":"AL",
    "emp":1894247,
    "emp_pc":0.0312,
    "wage":819,
    "wage_pc":0.046,
    "estabs":118496,
    "estabs_pc":0.0268
  },
  {  
    "fips":"02",
    "state":"Alaska",
    "state_abbrev":"AK",
    "emp":338078,
    "emp_pc":0.0192,
    "wage":1028,
    "wage_pc":0.0764,
    "estabs":22338,
    "estabs_pc":0.0315
  },
  {  
    "fips":"04",
    "state":"Arizona",
    "state_abbrev":"AZ",
    "emp":2585850.6667,
    "emp_pc":0.0687,
    "wage":904,
    "wage_pc":0.0487,
    "estabs":151145,
    "estabs_pc":0.0317
  },
  {  
    "fips":"05",
    "state":"Arkansas",
    "state_abbrev":"AR",
    "emp":1183673,
    "emp_pc":0.021,
    "wage":762,
    "wage_pc":0.0628,
    "estabs":88603,
    "estabs_pc":0.0414
  },
  {  
    "fips":"06",
    "state":"California",
    "state_abbrev":"CA",
    "emp":16272726,
    "emp_pc":0.0863,
    "wage":1131,
    "wage_pc":0.1013,
    "estabs":1420008,
    "estabs_pc":0.0027
  },
  {  
    "fips":"08",
    "state":"Colorado",
    "state_abbrev":"CO",
    "emp":2489827.6667,
    "emp_pc":0.1003,
    "wage":989,
    "wage_pc":0.0773,
    "estabs":185399,
    "estabs_pc":0.0817
  },
  {  
    "fips":"09",
    "state":"Connecticut",
    "state_abbrev":"CT",
    "emp":1678711.6667,
    "emp_pc":0.0255,
    "wage":1177,
    "wage_pc":0.0594,
    "estabs":115445,
    "estabs_pc":0.039
  },
  {  
    "fips":"10",
    "state":"Delaware",
    "state_abbrev":"DE",
    "emp":433342,
    "emp_pc":0.0654,
    "wage":991,
    "wage_pc":0.0465,
    "estabs":30521,
    "estabs_pc":0.0961
  },
  {  
    "fips":"11",
    "state":"District Of Columbia",
    "state_abbrev":"DC",
    "emp":743073,
    "emp_pc":0.0399,
    "wage":1599,
    "wage_pc":0.0363,
    "estabs":37199,
    "estabs_pc":0.0468
  },
  {  
    "fips":"12",
    "state":"Florida",
    "state_abbrev":"FL",
    "emp":8003431.3333,
    "emp_pc":0.0909,
    "wage":861,
    "wage_pc":0.0682,
    "estabs":658263,
    "estabs_pc":0.0896
  },
  {  
    "fips":"13",
    "state":"Georgia",
    "state_abbrev":"GA",
    "emp":4154506.3333,
    "emp_pc":0.0776,
    "wage":903,
    "wage_pc":0.0649,
    "estabs":289226,
    "estabs_pc":0.079
  },
  {  
    "fips":"15",
    "state":"Hawaii",
    "state_abbrev":"HI",
    "emp":635094.3333,
    "emp_pc":0.0499,
    "wage":876,
    "wage_pc":0.0815,
    "estabs":39508,
    "estabs_pc":0.0476
  },
  {  
    "fips":"16",
    "state":"Idaho",
    "state_abbrev":"ID",
    "emp":668081,
    "emp_pc":0.0854,
    "wage":713,
    "wage_pc":0.0642,
    "estabs":55412,
    "estabs_pc":0.0466
  },
  {  
    "fips":"17",
    "state":"Illinois",
    "state_abbrev":"IL",
    "emp":5876319.6667,
    "emp_pc":0.0378,
    "wage":1015,
    "wage_pc":0.0651,
    "estabs":428328,
    "estabs_pc":0.0944
  },
  {  
    "fips":"18",
    "state":"Indiana",
    "state_abbrev":"IN",
    "emp":2952937,
    "emp_pc":0.0453,
    "wage":811,
    "wage_pc":0.0629,
    "estabs":159694,
    "estabs_pc":-0.0046
  },
  {  
    "fips":"19",
    "state":"Iowa",
    "state_abbrev":"IA",
    "emp":1545515.3333,
    "emp_pc":0.038,
    "wage":802,
    "wage_pc":0.0809,
    "estabs":100605,
    "estabs_pc":0.0575
  },
  {  
    "fips":"20",
    "state":"Kansas",
    "state_abbrev":"KS",
    "emp":1376710,
    "emp_pc":0.035,
    "wage":819,
    "wage_pc":0.0734,
    "estabs":86812,
    "estabs_pc":0.0367
  },
  {  
    "fips":"21",
    "state":"Kentucky",
    "state_abbrev":"KY",
    "emp":1843882.6667,
    "emp_pc":0.0394,
    "wage":822,
    "wage_pc":0.0648,
    "estabs":121697,
    "estabs_pc":0.1017
  },
  {  
    "fips":"22",
    "state":"Louisiana",
    "state_abbrev":"LA",
    "emp":1935351.6667,
    "emp_pc":0.0283,
    "wage":850,
    "wage_pc":0.0559,
    "estabs":126513,
    "estabs_pc":0.0031
  },
  {  
    "fips":"23",
    "state":"Maine",
    "state_abbrev":"ME",
    "emp":598530,
    "emp_pc":0.0205,
    "wage":768,
    "wage_pc":0.0682,
    "estabs":50631,
    "estabs_pc":0.0229
  },
  {  
    "fips":"24",
    "state":"Maryland",
    "state_abbrev":"MD",
    "emp":2609762,
    "emp_pc":0.0328,
    "wage":1046,
    "wage_pc":0.0555,
    "estabs":167250,
    "estabs_pc":0.0048
  },
  {  
    "fips":"25",
    "state":"Massachusetts",
    "state_abbrev":"MA",
    "emp":3441869,
    "emp_pc":0.0555,
    "wage":1211,
    "wage_pc":0.092,
    "estabs":239474,
    "estabs_pc":0.0826
  },
  {  
    "fips":"26",
    "state":"Michigan",
    "state_abbrev":"MI",
    "emp":4182290.6667,
    "emp_pc":0.0572,
    "wage":916,
    "wage_pc":0.0676,
    "estabs":237670,
    "estabs_pc":-0.0093
  },
  {  
    "fips":"27",
    "state":"Minnesota",
    "state_abbrev":"MN",
    "emp":2798098.6667,
    "emp_pc":0.0505,
    "wage":977,
    "wage_pc":0.0772,
    "estabs":164087,
    "estabs_pc":-0.0114
  },
  {  
    "fips":"28",
    "state":"Mississippi",
    "state_abbrev":"MS",
    "emp":1116829.3333,
    "emp_pc":0.025,
    "wage":709,
    "wage_pc":0.0411,
    "estabs":71906,
    "estabs_pc":0.0532
  },
  {  
    "fips":"29",
    "state":"Missouri",
    "state_abbrev":"MO",
    "emp":2739022,
    "emp_pc":0.0415,
    "wage":842,
    "wage_pc":0.0658,
    "estabs":191050,
    "estabs_pc":0.0821
  },
  {  
    "fips":"30",
    "state":"Montana",
    "state_abbrev":"MT",
    "emp":454126,
    "emp_pc":0.0452,
    "wage":754,
    "wage_pc":0.0771,
    "estabs":45380,
    "estabs_pc":0.0737
  },
  {  
    "fips":"31",
    "state":"Nebraska",
    "state_abbrev":"NE",
    "emp":962499.3333,
    "emp_pc":0.0401,
    "wage":787,
    "wage_pc":0.0961,
    "estabs":71548,
    "estabs_pc":0.0723
  },
  {  
    "fips":"32",
    "state":"Nevada",
    "state_abbrev":"NV",
    "emp":1243926.6667,
    "emp_pc":0.0961,
    "wage":855,
    "wage_pc":0.0491,
    "estabs":78447,
    "estabs_pc":0.0809
  },
  {  
    "fips":"33",
    "state":"New Hampshire",
    "state_abbrev":"NH",
    "emp":638569.3333,
    "emp_pc":0.0392,
    "wage":967,
    "wage_pc":0.0865,
    "estabs":50742,
    "estabs_pc":0.0436
  },
  {  
    "fips":"34",
    "state":"New Jersey",
    "state_abbrev":"NJ",
    "emp":3941282.6667,
    "emp_pc":0.0333,
    "wage":1126,
    "wage_pc":0.0653,
    "estabs":266867,
    "estabs_pc":0.0404
  },
  {  
    "fips":"35",
    "state":"New Mexico",
    "state_abbrev":"NM",
    "emp":808459,
    "emp_pc":0.027,
    "wage":805,
    "wage_pc":0.0281,
    "estabs":56059,
    "estabs_pc":0.0148
  },
  {  
    "fips":"36",
    "state":"New York",
    "state_abbrev":"NY",
    "emp":9062160.3333,
    "emp_pc":0.0508,
    "wage":1180,
    "wage_pc":0.0766,
    "estabs":636614,
    "estabs_pc":0.0601
  },
  {  
    "fips":"37",
    "state":"North Carolina",
    "state_abbrev":"NC",
    "emp":4171099.3333,
    "emp_pc":0.0643,
    "wage":850,
    "wage_pc":0.0787,
    "estabs":266026,
    "estabs_pc":0.0275
  },
  {  
    "fips":"38",
    "state":"North Dakota",
    "state_abbrev":"ND",
    "emp":442512.6667,
    "emp_pc":0.0693,
    "wage":939,
    "wage_pc":0.0982,
    "estabs":32132,
    "estabs_pc":0.1009
  },
  {  
    "fips":"39",
    "state":"Ohio",
    "state_abbrev":"OH",
    "emp":5282683,
    "emp_pc":0.0409,
    "wage":865,
    "wage_pc":0.06,
    "estabs":290156,
    "estabs_pc":0.0073
  },
  {  
    "fips":"40",
    "state":"Oklahoma",
    "state_abbrev":"OK",
    "emp":1598213.6667,
    "emp_pc":0.0334,
    "wage":818,
    "wage_pc":0.0665,
    "estabs":108844,
    "estabs_pc":0.0423
  },
  {  
    "fips":"41",
    "state":"Oregon",
    "state_abbrev":"OR",
    "emp":1786037.6667,
    "emp_pc":0.0847,
    "wage":899,
    "wage_pc":0.0741,
    "estabs":143057,
    "estabs_pc":0.0961
  },
  {  
    "fips":"42",
    "state":"Pennsylvania",
    "state_abbrev":"PA",
    "emp":5726758,
    "emp_pc":0.0192,
    "wage":958,
    "wage_pc":0.0728,
    "estabs":354050,
    "estabs_pc":0.0152
  },
  {  
    "fips":"44",
    "state":"Rhode Island",
    "state_abbrev":"RI",
    "emp":474281.6667,
    "emp_pc":0.0427,
    "wage":925,
    "wage_pc":0.0756,
    "estabs":36360,
    "estabs_pc":0.0335
  },
  {  
    "fips":"45",
    "state":"South Carolina",
    "state_abbrev":"SC",
    "emp":1954333.6667,
    "emp_pc":0.0719,
    "wage":782,
    "wage_pc":0.0625,
    "estabs":121168,
    "estabs_pc":0.0857
  },
  {  
    "fips":"46",
    "state":"South Dakota",
    "state_abbrev":"SD",
    "emp":421137.3333,
    "emp_pc":0.0388,
    "wage":740,
    "wage_pc":0.0931,
    "estabs":32363,
    "estabs_pc":0.0334
  },
  {  
    "fips":"47",
    "state":"Tennessee",
    "state_abbrev":"TN",
    "emp":2820220.6667,
    "emp_pc":0.059,
    "wage":863,
    "wage_pc":0.0576,
    "estabs":149702,
    "estabs_pc":0.0659
  },
  {  
    "fips":"48",
    "state":"Texas",
    "state_abbrev":"TX",
    "emp":11651792.6667,
    "emp_pc":0.0861,
    "wage":988,
    "wage_pc":0.0716,
    "estabs":634983,
    "estabs_pc":0.0679
  },
  {  
    "fips":"49",
    "state":"Utah",
    "state_abbrev":"UT",
    "emp":1338293,
    "emp_pc":0.0994,
    "wage":821,
    "wage_pc":0.0718,
    "estabs":92945,
    "estabs_pc":0.0994
  },
  {  
    "fips":"50",
    "state":"Vermont",
    "state_abbrev":"VT",
    "emp":306212,
    "emp_pc":0.0271,
    "wage":831,
    "wage_pc":0.0559,
    "estabs":24657,
    "estabs_pc":0.0125
  },
  {  
    "fips":"51",
    "state":"Virginia",
    "state_abbrev":"VA",
    "emp":3739379.6667,
    "emp_pc":0.0295,
    "wage":1000,
    "wage_pc":0.0504,
    "estabs":247583,
    "estabs_pc":0.0442
  },
  {  
    "fips":"53",
    "state":"Washington",
    "state_abbrev":"WA",
    "emp":3137354.3333,
    "emp_pc":0.0838,
    "wage":1026,
    "wage_pc":0.0846,
    "estabs":235511,
    "estabs_pc":0.0027
  },
  {  
    "fips":"54",
    "state":"West Virginia",
    "state_abbrev":"WV",
    "emp":704558.6667,
    "emp_pc":-0.0126,
    "wage":803,
    "wage_pc":0.0348,
    "estabs":50066,
    "estabs_pc":0.0144
  },
  {  
    "fips":"55",
    "state":"Wisconsin",
    "state_abbrev":"WI",
    "emp":2806129.3333,
    "emp_pc":0.0331,
    "wage":836,
    "wage_pc":0.0746,
    "estabs":166660,
    "estabs_pc":0.045
  },
  {  
    "fips":"56",
    "state":"Wyoming",
    "state_abbrev":"WY",
    "emp":284545.3333,
    "emp_pc":0.0119,
    "wage":869,
    "wage_pc":0.0333,
    "estabs":26050,
    "estabs_pc":0.0238
  }
]

stateface.json

{
  "AL": "B",
  "AK": "A",
  "AZ": "D",
  "AR": "C",
  "CA": "E",
  "CO": "F",
  "CT": "G",
  "DE": "H",
  "DC": "y",
  "FL": "I",
  "GA": "J",
  "HI": "K",
  "ID": "M",
  "IL": "N",
  "IN": "O",
  "IA": "L",
  "KS": "P",
  "KY": "Q",
  "LA": "R",
  "ME": "U",
  "MD": "T",
  "MA": "S",
  "MI": "V",
  "MN": "W",
  "MS": "Y",
  "MO": "X",
  "MT": "Z",
  "NE": "c",
  "NV": "g",
  "NH": "d",
  "NJ": "e",
  "NM": "f",
  "NY": "h",
  "NC": "a",
  "ND": "b",
  "OH": "i",
  "OK": "j",
  "OR": "k",
  "PA": "l",
  "RI": "m",
  "SC": "n",
  "SD": "o",
  "TN": "p",
  "TX": "q",
  "UT": "r",
  "VT": "t",
  "VA": "s",
  "WA": "u",
  "WV": "w",
  "WI": "v",
  "WY": "x",
  "US": "z"
}

style.css

@font-face {
   font-family: 'StateFaceRegular';
   src: url('stateface-regular-webfont.eot');
   src: url('stateface-regular-webfont.eot?#iefix') format('embedded-opentype'),
        url('stateface-regular-webfont.woff') format('woff'),
        url('stateface-regular-webfont.ttf') format('truetype'),
        url('stateface-regular-webfont.svg#StateFaceRegular') format('svg');
   font-weight: normal;
   font-style: normal;
}

.stateface {
  font-family: "StateFaceRegular";
}

body {
  font: 12px monospace;
}

table-container {
  width: 960px;
  overflow: auto;
}

table {
  border-collapse: collapse;
  position: relative;
  left: 160px;
}

th {
  border-bottom: 1px solid black;
  cursor: pointer;
}

td, th {
  padding-left: 10px;
  padding-right: 10px;
}

span.stateface,
span.fa {
  display: inline-block;
}

span.stateface {
  width: 25px;
  line-height: 14px;
  text-align: center;
}

td > span {
  float: left;
}

span.text {
  width: 40px;
  text-align: right;
  padding-right: 10px;
}

th.emp, td.emp
th.wage, td.wage {
  width: 100px;
}

th.emp_pc, td.emp_pc
th.wage_pc, td.wage_pc {
  width: 90px;
}

.emp span.fa {
  width: 8px;
}

.fa-arrow-up {
  color: darkgreen;
}

.fa-arrow-right {
  color: slategrey;
}

.fa-arrow-down {
  color: red;
}

vis.js

/* global d3  queue */

const table = d3.select('.table-container').append('table');
table.append('thead');
table.append('tbody');

queue()
  .defer(d3.json, 'qcew.json')
  .defer(d3.json, 'stateface.json')
  .await(ready);

function ready(error, qcew, stateface) {
  if (error) throw error;

  const columns = [
    {
      head: 'State',
      cl: 'state',
      html(row) {
        const sfLetter = stateface[row.state_abbrev];
        const icon = `<span class='stateface'>${sfLetter}</span>`;
        const text = `<span class='title'>${row.state}</span>`;
        return icon + text;
      },
    },
    {
      head: 'Employment (millions)',
      cl: 'emp',
      html(row) {
        const scale = d3.scaleThreshold()
          .domain([1, 2, 4, 6])
          .range([1, 2, 3, 4, 5]);

        const icon = '<span class="fa fa-male"></span>';
        const value = d3.format(',.1f')(row.emp / 1000000);
        const nIcons = scale(value);
        const text = `<span class='text'>${value}</span>`;
        return text + d3.range(nIcons)
          .map(() => icon).join('');
      },
    },
    {
      head: 'Change in Employment',
      cl: 'emp_pc',
      html(row) {
        const scale = d3.scaleThreshold()
          .domain([0, 0.045])
          .range(['down', 'right', 'up']);
        const icon = `<span class='fa fa-arrow-${scale(row.emp_pc)}'></span>`;
        const value = d3.format(',.0%')(row.emp_pc);
        const text = `<span class='text'>${value}</span>`;
        return text + icon;
      },
    },
    {
      head: 'Wage (weekly)',
      cl: 'wage',
      html(row) {
        const scale = d3.scaleThreshold()
          .domain([850, 1000])
          .range([1, 2, 3]);

        const icon = '<span class="fa fa-money fa-rotate-90"></span>';
        const nIcons = scale(row.wage);
        const value = d3.format('$,')(row.wage);
        const text = `<span class='text'>${value}</span>`;
        return text + d3.range(nIcons)
          .map(() => icon).join('');
      },
    },
    {
      head: 'Change in Wage',
      cl: 'wage_pc',
      html(row) {
        const scale = d3.scaleThreshold()
          .domain([0, 0.07])
          .range(['down', 'right', 'up']);

        const icon = `<span class='fa fa-arrow-${scale(row.wage_pc)}'></span>`;
        const value = d3.format(',.0%')(row.wage_pc);
        const text = `<span class='text'>${value}</span>`;
        return text + icon;
      },
    },
  ];

  table.call(renderTable);

  function renderTable(table) {
    const tableUpdate = table.select('thead')
      .selectAll('th')
        .data(columns);

    const tableEnter = tableUpdate
      .enter().append('th')
        .attr('class', d => d.cl)
        .text(d => d.head)
        .on('click', (d) => {
          let ascending;
          if (d.ascending) {
            ascending = false;
          } else {
            ascending = true;
          }
          d.ascending = ascending;
          qcew.sort((a, b) => {
            if (ascending) {
              return d3.ascending(a[d.cl], b[d.cl]);
            }
            return d3.descending(a[d.cl], b[d.cl]);
          });
          table.call(renderTable);
        });

    const trUpdate = table.select('tbody').selectAll('tr')
      .data(qcew);

    const trEnter = trUpdate.enter().append('tr');

    const trMerge = trUpdate.merge(trEnter)
      .on('mouseenter', mouseenter)
      .on('mouseleave', mouseleave);

    const tdUpdate = trMerge.selectAll('td')
      .data((row, i) => columns.map((c) => {
        const cell = {};
        d3.keys(c).forEach((k) => {
          cell[k] = typeof c[k] === 'function' ? c[k](row, i) : c[k];
        });
        return cell;
      }));

    const tdEnter = tdUpdate.enter().append('td');

    tdEnter
      .attr('class', d => d.cl)
      .style('background-color', '#fff')
      .style('border-bottom', '.5px solid white');

    tdEnter.merge(tdUpdate).html(d => d.html);
  }
}

function mouseenter() {
  d3.select(this).selectAll('td')
    .style('background-color', '#f0f0f0')
    .style('border-bottom', '.5px solid slategrey');
}

function mouseleave() {
  d3.select(this).selectAll('td')
    .style('background-color', '#fff')
    .style('border-bottom', '.5px solid white');
}