Skip to content

Instantly share code, notes, and snippets.

@tophtucker
Last active November 4, 2022 18:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tophtucker/f3256f7d912aa9bd7365bb148b7cd56f to your computer and use it in GitHub Desktop.
Save tophtucker/f3256f7d912aa9bd7365bb148b7cd56f to your computer and use it in GitHub Desktop.
Zoomable icicle line chart decomposition
license: gpl-3.0
{
"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
}
}
}

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.

<!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>
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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment