block by tophtucker f3256f7d912aa9bd7365bb148b7cd56f

Zoomable icicle line chart decomposition

Full Screen

Click anywhere to zoom in, or click on the top bar to zoom out.

Converting Bostock’s classic to d3 v4. You may find the diff helpful in understanding the d3-hierarchy api changes! Note that I didn’t touch the json.

index.html

<!DOCTYPE html>
<meta charset="utf-8">
<style>

* {
  box-sizing: border-box;
}

html, body {
  margin: 0;
  padding: 0;
  overflow: hidden;
  font-family: helvetica, sans-serif;
  font-size: 10px;
}

svg {
  display: inline-block;
}

rect {
  stroke: #fff;
}

path.line {
  stroke-width: 2;
  fill: none;
}

path.line.highlighted {
  stroke-width: 4;
}

.container {
  display: inline-block;
  position: relative;
}

.container div {
  position: absolute;
  padding: 1px;
  border: 1px solid white;
  opacity: .2;
  color: rgba(255,255,255,0);
}

.container div.discovered {
  opacity: 1;
  color: black;
  cursor: pointer;
}

.container div.discovered:hover {
  font-weight: bold;
}

</style>
<body>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>

var sectors = [
  {name: "Energy", parent: "Universe"},
  {name: "Basic Materials", parent: "Universe"},
  {name: "Industrials", parent: "Universe"},
  {name: "Cyclical Consumer Goods & Services", parent: "Universe"},
  {name: "Non-Cyclical Consumer Goods & Services", parent: "Universe"},
  {name: "Financials", parent: "Universe"},
  {name: "Healthcare", parent: "Universe"},
  {name: "Technology", parent: "Universe"},
  {name: "Telecommunications Services", parent: "Universe"},
  {name: "Utilities", parent: "Universe"}
]

var subsectors = d3.range(80).map(function(d) {
  var parent = sectors[Math.floor(Math.random() * sectors.length)].name
  return {
    name: parent.substr(0,6) + "... " + d,
    parent: parent
  }
})

var data = d3.range(1000).map(getRandomSeries)
  .sort((a,b) => b.data[b.data.length-1][1] - a.data[a.data.length-1][1])
  .concat(sectors, subsectors, [{name: "Universe", parent: ""}])

var root = d3.stratify()
    .id(function(d) { return d.name; })
    .parentId(function(d) { return d.parent; })
    (data)
    .sum(function(d) {
      return d.value ? d.value : 0;
    })
    .eachAfter(function(d) {
      if(d.data.return === undefined) {
        if(d.children) {
          var kids = d.children.filter(d => d.data.data)
          d.data.return = d3.mean(kids.map(d => d.data.return));
          d.data.data = kids[0].data.data.map((dd,i) => [
            dd[0],
            d3.mean(kids.map(ddd => ddd.data.data ? ddd.data.data[i][1] : undefined))
          ])
        }
      }
    })
    .sort(function(a, b) { return b.height - a.height || b.data.return - a.data.return; })

var depth = 0;

//

var width = innerWidth/2,
    height = innerHeight,
    marginLeft = 40,
    format = d3.format("$.2f");

var x = d3.scaleLinear()
    .range([0, height]);

var y = d3.scaleLinear()
    .range([0, width]);

var color = d3.scaleLinear()
  .domain([
    d3.min(root.children.map(d => d.data.return)),
    0,
    d3.max(root.children.map(d => d.data.return))
  ])
  .range(["rgb(255, 44, 44)", "rgb(200,200,200)", "rgb(99, 255, 99)"])
  .clamp(true);

var x2 = d3.scaleTime()
  .domain(d3.extent(root.children.filter(d => d.data.data)[0].data.data.map(d => d[0])))
  .range([marginLeft, width])

var y2 = d3.scaleLinear()
  .domain(padExtent(d3.extent(d3.merge(root.children.filter(d => d.data.data).map(d => d.data.data.map(d => d[1]))))))
  .range([height, 0])

var line = d3.line()
  .x(d => x2(d[0]))
  .y(d => y2(d[1]));

var partition = d3.partition()
    .size([height, width])
    .padding(0)
    .round(true);

partition(root);

var container = d3.select("body").append("div")
  .classed("container", true)
    .style("width", width + 'px')
    .style("height", height + 'px');

var svg2 = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height);

var rect = container.selectAll("div");

rect = rect
    .data(root.descendants())
  .enter().append("div")
    .style("left", function(d) { return d.y0 + 'px'; })
    .style("top", function(d) { return d.x0 + 'px'; })
    .style("width", function(d) { return d.y1 - d.y0 + 'px'; })
    .style("height", function(d) { return d.x1 - d.x0 + 'px'; })
    .style("background", function(d) { return color(d.data.return); })
    // .style("opacity", function(d) { return d.depth <= depth + 1 ? 1 : .2 })
    // .text(d => d.depth <= depth + 1 ? d.id : '')
    .text(d => d.id)
    .classed("discovered", d => d.depth <= depth + 1)
    .on("mouseenter", mouseenter)
    .on("mouseleave", mouseleave)
    .on("click", clicked)

//

var yAxis = d3.axisLeft(y2).tickFormat(format)
var yAxisG = svg2.append("g")
  .attr("class", "axis axis--y")
  .attr("transform", "translate(" + marginLeft + ",0)")
  .call(yAxis)

var path = svg2.selectAll("path.line");

path = path
    .data(root.children, d => d.id)
  .enter().append("path")
    .classed("line", true)
    .attr("d", d => d.data.data ? line(d.data.data) : '')
    .attr("stroke", function(d) { return color(d.data.return); })

//

function mouseenter(d) {
  svg2.selectAll("path.line")
    .filter((dd,ii) => dd.id == d.id)
    .classed("highlighted", true)
}

function mouseleave(d) {
  svg2.selectAll("path.line")
    .filter((dd,ii) => dd.id == d.id)
    .classed("highlighted", false)
}

function clicked(d) {

  var newData = d.children ? d.children : [d]
  var zooming = d.depth > depth;
  depth = d.depth;

  x.domain([d.x0, d.x1]);
  y.domain([d.y0, height]).range([d.depth ? 20 : 0, height]);

  var path = svg2.selectAll("path.line")
    .data(newData, d => d.id)

  var pathExit = path.exit()

  if(zooming) {
    pathExit
      .remove()
  }

  var pathEnter = path.enter()
    .append("path")
      .classed("line", true)
      .attr("d", dd => line(zooming ? dd.parent.data.data : dd.data.data))
      .attr("stroke", function(dd) { return color(dd.data.return); })
      .style("opacity", zooming ? 1 : 0)

  color.domain([
    Math.min(0, d3.min(newData.map(d => d.data.return))),
    0,
    Math.max(0, d3.max(newData.map(d => d.data.return)))
  ])

  x2.domain(d3.extent(newData.filter(d => d.data.data)[0].data.data.map(d => d[0])))
  y2.domain(padExtent(d3.extent(d3.merge(newData.filter(d => d.data.data).map(d => d.data.data.map(d => d[1]))))))

  var t = d3.transition()
    .duration(750)

  yAxisG.transition(t)
    .call(yAxis)

  rect
      .classed("discovered", d => d.depth <= depth + 1)
    .transition(t)
      .style("left", function(d) { return y(d.y0) + 'px'; })
      .style("top", function(d) { return x(d.x0) + 'px'; })
      .style("width", function(d) { return y(d.y1) - y(d.y0) + 'px'; })
      .style("height", function(d) { return x(d.x1) - x(d.x0) + 'px'; })
      .style("background", function(d) { return color(d.data.return); })
      // .style("opacity", function(d) { return d.depth <= depth + 1 ? 1 : .2 })

  if(!zooming) {
    pathExit
      .transition(t)
      .attr("d", dd => line(dd.parent.data.data))
      .remove()
  }

  path.merge(pathEnter)
    .transition(t)
    .attr("d", dd => line(dd.data.data))
    .attr("stroke", function(dd) { return color(dd.data.return); })
    .style("opacity", 1)

}

//

function getRandomSeries() {
  var ts = getRandomTimeSeries(100)
  return {
    name: getRandomTicker(),
    parent: subsectors[Math.floor(Math.random() * subsectors.length)].name,
    value: Math.round(d3.randomLogNormal(10,1)()),
    return: ts[ts.length-1][1] - 1,
    data: ts
  }
}

function getRandomTicker() {
  var length = Math.ceil(Math.random()*4);
  var chars = 'abcdefghijklmnopqrstuvwxyz';
  return d3.range(length).map(() => chars[Math.floor(Math.random()*chars.length)].toUpperCase()).join('');
}

function getRandomTimeSeries(numPoints) {
  var data = d3.range(numPoints).map(d => [
    d3.interpolateDate(new Date("2000/01/01"), new Date("2016/10/01"))(d/numPoints),
    undefined
  ])
  data.forEach(function(d,i,arr) {
    if(i==0) {
      d[1] = 1
    } else {
      d[1] = arr[i-1][1] * d3.randomNormal(1, .02)()
    }
  })
  return data
}

function padExtent(extent) {
  var d = extent[1] - extent[0]
  return [
    extent[0] - d * .1,
    extent[1] + d * .1
  ]
}

</script>

readme.json

{
  "flare": {
    "analytics": {
      "cluster": {
        "AgglomerativeCluster": 3938,
        "CommunityStructure": 3812,
        "HierarchicalCluster": 6714,
        "MergeEdge": 743
      },
      "graph": {
        "BetweennessCentrality": 3534,
        "LinkDistance": 5731,
        "MaxFlowMinCut": 7840,
        "ShortestPaths": 5914,
        "SpanningTree": 3416
      },
      "optimization": {
        "AspectRatioBanker": 7074
      }
    },
    "animate": {
      "Easing": 17010,
      "FunctionSequence": 5842,
      "interpolate": {
        "ArrayInterpolator": 1983,
        "ColorInterpolator": 2047,
        "DateInterpolator": 1375,
        "Interpolator": 8746,
        "MatrixInterpolator": 2202,
        "NumberInterpolator": 1382,
        "ObjectInterpolator": 1629,
        "PointInterpolator": 1675,
        "RectangleInterpolator": 2042
      },
      "ISchedulable": 1041,
      "Parallel": 5176,
      "Pause": 449,
      "Scheduler": 5593,
      "Sequence": 5534,
      "Transition": 9201,
      "Transitioner": 19975,
      "TransitionEvent": 1116,
      "Tween": 6006
    },
    "data": {
      "converters": {
        "Converters": 721,
        "DelimitedTextConverter": 4294,
        "GraphMLConverter": 9800,
        "IDataConverter": 1314,
        "JSONConverter": 2220
      },
      "DataField": 1759,
      "DataSchema": 2165,
      "DataSet": 586,
      "DataSource": 3331,
      "DataTable": 772,
      "DataUtil": 3322
    },
    "display": {
      "DirtySprite": 8833,
      "LineSprite": 1732,
      "RectSprite": 3623,
      "TextSprite": 10066
    },
    "flex": {
      "FlareVis": 4116
    },
    "physics": {
      "DragForce": 1082,
      "GravityForce": 1336,
      "IForce": 319,
      "NBodyForce": 10498,
      "Particle": 2822,
      "Simulation": 9983,
      "Spring": 2213,
      "SpringForce": 1681
    },
    "query": {
      "AggregateExpression": 1616,
      "And": 1027,
      "Arithmetic": 3891,
      "Average": 891,
      "BinaryExpression": 2893,
      "Comparison": 5103,
      "CompositeExpression": 3677,
      "Count": 781,
      "DateUtil": 4141,
      "Distinct": 933,
      "Expression": 5130,
      "ExpressionIterator": 3617,
      "Fn": 3240,
      "If": 2732,
      "IsA": 2039,
      "Literal": 1214,
      "Match": 3748,
      "Maximum": 843,
      "methods": {
        "add": 593,
        "and": 330,
        "average": 287,
        "count": 277,
        "distinct": 292,
        "div": 595,
        "eq": 594,
        "fn": 460,
        "gt": 603,
        "gte": 625,
        "iff": 748,
        "isa": 461,
        "lt": 597,
        "lte": 619,
        "max": 283,
        "min": 283,
        "mod": 591,
        "mul": 603,
        "neq": 599,
        "not": 386,
        "or": 323,
        "orderby": 307,
        "range": 772,
        "select": 296,
        "stddev": 363,
        "sub": 600,
        "sum": 280,
        "update": 307,
        "variance": 335,
        "where": 299,
        "xor": 354,
        "_": 264
      },
      "Minimum": 843,
      "Not": 1554,
      "Or": 970,
      "Query": 13896,
      "Range": 1594,
      "StringUtil": 4130,
      "Sum": 791,
      "Variable": 1124,
      "Variance": 1876,
      "Xor": 1101
    },
    "scale": {
      "IScaleMap": 2105,
      "LinearScale": 1316,
      "LogScale": 3151,
      "OrdinalScale": 3770,
      "QuantileScale": 2435,
      "QuantitativeScale": 4839,
      "RootScale": 1756,
      "Scale": 4268,
      "ScaleType": 1821,
      "TimeScale": 5833
    },
    "util": {
      "Arrays": 8258,
      "Colors": 10001,
      "Dates": 8217,
      "Displays": 12555,
      "Filter": 2324,
      "Geometry": 10993,
      "heap": {
        "FibonacciHeap": 9354,
        "HeapNode": 1233
      },
      "IEvaluable": 335,
      "IPredicate": 383,
      "IValueProxy": 874,
      "math": {
        "DenseMatrix": 3165,
        "IMatrix": 2815,
        "SparseMatrix": 3366
      },
      "Maths": 17705,
      "Orientation": 1486,
      "palette": {
        "ColorPalette": 6367,
        "Palette": 1229,
        "ShapePalette": 2059,
        "SizePalette": 2291
      },
      "Property": 5559,
      "Shapes": 19118,
      "Sort": 6887,
      "Stats": 6557,
      "Strings": 22026
    },
    "vis": {
      "axis": {
        "Axes": 1302,
        "Axis": 24593,
        "AxisGridLine": 652,
        "AxisLabel": 636,
        "CartesianAxes": 6703
      },
      "controls": {
        "AnchorControl": 2138,
        "ClickControl": 3824,
        "Control": 1353,
        "ControlList": 4665,
        "DragControl": 2649,
        "ExpandControl": 2832,
        "HoverControl": 4896,
        "IControl": 763,
        "PanZoomControl": 5222,
        "SelectionControl": 7862,
        "TooltipControl": 8435
      },
      "data": {
        "Data": 20544,
        "DataList": 19788,
        "DataSprite": 10349,
        "EdgeSprite": 3301,
        "NodeSprite": 19382,
        "render": {
          "ArrowType": 698,
          "EdgeRenderer": 5569,
          "IRenderer": 353,
          "ShapeRenderer": 2247
        },
        "ScaleBinding": 11275,
        "Tree": 7147,
        "TreeBuilder": 9930
      },
      "events": {
        "DataEvent": 2313,
        "SelectionEvent": 1880,
        "TooltipEvent": 1701,
        "VisualizationEvent": 1117
      },
      "legend": {
        "Legend": 20859,
        "LegendItem": 4614,
        "LegendRange": 10530
      },
      "operator": {
        "distortion": {
          "BifocalDistortion": 4461,
          "Distortion": 6314,
          "FisheyeDistortion": 3444
        },
        "encoder": {
          "ColorEncoder": 3179,
          "Encoder": 4060,
          "PropertyEncoder": 4138,
          "ShapeEncoder": 1690,
          "SizeEncoder": 1830
        },
        "filter": {
          "FisheyeTreeFilter": 5219,
          "GraphDistanceFilter": 3165,
          "VisibilityFilter": 3509
        },
        "IOperator": 1286,
        "label": {
          "Labeler": 9956,
          "RadialLabeler": 3899,
          "StackedAreaLabeler": 3202
        },
        "layout": {
          "AxisLayout": 6725,
          "BundledEdgeRouter": 3727,
          "CircleLayout": 9317,
          "CirclePackingLayout": 12003,
          "DendrogramLayout": 4853,
          "ForceDirectedLayout": 8411,
          "IcicleTreeLayout": 4864,
          "IndentedTreeLayout": 3174,
          "Layout": 7881,
          "NodeLinkTreeLayout": 12870,
          "PieLayout": 2728,
          "RadialTreeLayout": 12348,
          "RandomLayout": 870,
          "StackedAreaLayout": 9121,
          "TreeMapLayout": 9191
        },
        "Operator": 2490,
        "OperatorList": 5248,
        "OperatorSequence": 4190,
        "OperatorSwitch": 2581,
        "SortOperator": 2023
      },
      "Visualization": 16540
    }
  }
}

scratch.js

var sectors = [
  {name: "Energy"},
  {name: "Basic Materials"},
  {name: "Industrials"},
  {name: "Cyclical Consumer Goods & Services"},
  {name: "Non-Cyclical Consumer Goods & Services"},
  {name: "Financials"},
  {name: "Healthcare"},
  {name: "Technology"},
  {name: "Telecommunications Services"},
  {name: "Utilities"}
]

var subsectors = d3.range(40).map(function(d) {
  var parent = sectors[Math.floor(Math.random() * sectors.length)].name
  return {
    name: parent.substr(0,6) + "... " + d,
    parent: parent
  }
})

var data = d3.range(100).map(getRandomSeries)
  .sort((a,b) => b.data[b.data.length-1][1] - a.data[a.data.length-1][1])
  .concat(sectors, subsectors)

//

function getRandomSeries() {
  return {
    name: getRandomTicker(),
    parent: subsectors[Math.floor(Math.random() * subsectors.length)].name,
    data: getRandomTimeSeries(100)
  }
}

function getRandomTicker() {
  var length = Math.ceil(Math.random()*4);
  var chars = 'abcdefghijklmnopqrstuvwxyz';
  return d3.range(length).map(() => chars[Math.floor(Math.random()*chars.length)].toUpperCase()).join('');
}

function getRandomTimeSeries(numPoints) {
  var data = d3.range(numPoints).map(d => [
    d3.interpolateDate(new Date("2000/01/01"), new Date("2016/10/01"))(d/numPoints),
    undefined
  ])
  data.forEach(function(d,i,arr) {
    if(i==0) {
      d[1] = d3.randomNormal(75, 30)()
    } else {
      d[1] = arr[i-1][1] * d3.randomNormal(1, .02)()
    }
  })
  return data
}