Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active December 31, 2015 18:39
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 nitaku/8028264 to your computer and use it in GitHub Desktop.
Save nitaku/8028264 to your computer and use it in GitHub Desktop.
Fractal treemap (random, Gosper)

Full source, including python code, here.

A more complete example of a fractal treemap (also known as jigsaw treemap) made of Gosper regions, this time representing a hierarchy. Zoom in by using the mouse wheel.

A random tree is generated with this script, obtaining a tree with 48,207 leaves (tree_48207.csv). The file contains the prefixes of all the leaf nodes, sorted in lexicographic order.

Usage (ipython):

run gosper_regions.py
gosperify('tree_48207.csv', 'hexes.wkt.csv', 'regions') # input tree, input tiling, output directory

By running the python script, the sequence of leaves is paired with the sequence of hexagons (generated by this script) that follows the Gosper space-filling curve. Because of the lexicographic order, sibling nodes are placed near to each other, similarly to classes (colors) in this other example. Different tilings can be used in theory, but that was not tested.

In order to have a representation for internal nodes, leaf nodes are recursively merged into regions representing subtrees, until the root, yielding the entire tree, is reached. This is not what the code does, actually. The same result is obtained by using each hexagon prefix (leaf prefix) to merge it to one or more regions, each one representing an internal node at a certain depth. This creates one layer for each tree level, each one having a region representing an internal node. For an in-depth explanation of this layout, see also this example, Auber et al. 2011 and Wattenberg 2005.

The layers are saved into the requested folder, each one as a separate GeoJSON file. The example tree generates eight layers. All the layers are combined into a single TopoJSON file (as in this example):

topojson --cartesian --no-quantization -p prefix -o regions.topo.json regions/0.json regions/1.json regions/2.json regions/3.json regions/4.json regions/5.json regions/6.json regions/7.json

The obtained file is finally rendered with d3.js, using a style similar to the one found in this U.S. map (land glow has been replaced with a faux shadow due to performance reasons). The labels show each region's prefix. A zoom behavior (see this other example) is introduced to help navigating the map. To avoid cluttering the map, only the first three levels are shown. This is an issue that has to be addressed, perhaps using the zoom behavior to load fine-grained regions on demand. Also, labels are placed using the region's centroid, giving a bad label placement in many cases.

The python script uses the shapely library for merging regions, and the Fiona library for storing the results as GeoJSON, so you have to install them if you want to run this script. The Fiona package requires GDAL 1.8+, and it was a bit tricky to install that on my Ubuntu 10:

gdal-config --version # this can be used to see if you really need to update gdal

sudo apt-add-repository ppa:ubuntugis/ubuntugis-unstable
sudo apt-get update
sudo apt-get install python-gdal
sudo apt-get install libgdal1-dev
sudo apt-get install gdal-bin
pip install Fiona

This guide covers shapely & Fiona usage.

Reading a tuple serialization was also an unexpectedly difficult task, because apparently there's no way to do it by using the tuple constructor. The solution was, as always, using regular expressions!

from __future__ import print_function
from itertools import izip
import csv
from shapely.geometry.polygon import Polygon
import shapely.wkt
from fiona import collection
from shapely.geometry import mapping
import re
import os
def gosperify(tree_path, hexes_path, output_dir_path):
leaves_done = 0
layers = {}
with open(tree_path, 'rb') as tree_file:
tree_reader = csv.reader(tree_file, delimiter=';', quotechar='#')
with open(hexes_path, 'rb') as hexes_file:
hexes_reader = csv.reader(hexes_file, delimiter=';', quotechar='#')
for tree_row, hexes_row in izip(tree_reader, hexes_reader):
prefix = tuple(int(v) for v in re.findall('[0-9]+', tree_row[0]))
for i in xrange(len(prefix)+1):
subprefix = prefix[:i]
depth = len(subprefix)
hex = shapely.wkt.loads(hexes_row[0])
if depth not in layers:
layers[depth] = {}
if subprefix not in layers[depth]:
layers[depth][subprefix] = hex
else:
layers[depth][subprefix] = layers[depth][subprefix].union(hex)
# logging
leaves_done += 1
print('%d leaves done' % leaves_done, end='\r')
schema = {'geometry': 'Polygon', 'properties': {'prefix': 'str'}}
if not os.path.exists(output_dir_path):
os.makedirs(output_dir_path)
for depth, regions in layers.items():
with collection(output_dir_path+'/'+str(depth)+'.json', 'w', 'GeoJSON', schema) as output:
for prefix, region in regions.items():
output.write({
'properties': {
'prefix': '.'.join(map(lambda x: str(x), prefix))
},
'geometry': mapping(region)
})
window.main = () ->
### "globals" ###
vis = null
width = 960
height = 500
svg = d3.select('body').append('svg')
.attr('width', width)
.attr('height', height)
### ZOOM and PAN ###
### create container elements ###
container = svg.append('g')
.attr('transform','translate(640, 34)')
container.call(d3.behavior.zoom().scaleExtent([1, 49]).on('zoom', (() -> vis.attr('transform', "translate(#{d3.event.translate})scale(#{d3.event.scale})"))))
vis = container.append('g')
### create a rectangular overlay to catch events ###
### WARNING rect size is huge but not infinite. this is a dirty hack ###
vis.append('rect')
.attr('class', 'overlay')
.attr('x', -500000)
.attr('y', -500000)
.attr('width', 1000000)
.attr('height', 1000000)
### END ZOOM and PAN ###
### custom projection to make hexagons appear regular (y axis is also flipped) ###
radius = 1
dx = radius * 2 * Math.sin(Math.PI / 3)
dy = radius * 1.5
path_generator = d3.geo.path()
.projection d3.geo.transform({
point: (x,y,z) ->
### draw only nodes that are "important" enough ###
# if z >= 2
this.stream.point(x * dx / 2, -(y - (2 - (y & 1)) / 3) * dy / 2)
})
### load topoJSON data ###
d3.json 'regions_48207.topo.json', (error, data) ->
### presimplify the topology (compute the effective area (z) of each point) ###
# topojson.presimplify(data)
### data.objects contain all the map layers ###
### each layer represents a different level of the tree ###
### the number of layers is equal to the depth of the tree ###
depth = Object.keys(data.objects).length
### draw the level zero region (the land) ###
defs = svg.append('defs')
defs.selectAll('#land')
.data(topojson.feature(data, data.objects[0]).features)
.enter().append('path')
.attr('id', 'land')
.attr('d', path_generator)
### faux land glow (using filters takes too much resources) ###
vis.append('use')
.attr('class', 'land-glow-outer')
.attr('xlink:href', '#land')
vis.append('use')
.attr('class', 'land-glow-inner')
.attr('xlink:href', '#land')
vis.append('use')
.attr('class', 'land-fill')
.attr('xlink:href', '#land')
### draw the boundaries ###
for i in [1...4]
vis.append('path')
.datum(topojson.mesh(data, data.objects[i], ((a,b) ->
a.properties.prefix[0...-1] is b.properties.prefix[0...-1]
)))
.attr('d', path_generator)
.attr('class', 'boundary')
.style('stroke-width', "#{0.7/(i*i)}px")
### draw the labels ###
vis.selectAll('.label')
.data(topojson.feature(data, data.objects[1]).features.concat(
topojson.feature(data, data.objects[2]).features,
topojson.feature(data, data.objects[3]).features
))
.enter().append('text')
.attr('class', 'label')
.attr('dy','0.35em')
.attr('transform', ((d) ->
centroid = path_generator.centroid(d)
return "translate(#{centroid[0]},#{centroid[1]}) scale(#{20/(Math.pow(d.properties.prefix.length,1.7))})"
))
.text((d) -> d.properties.prefix)
.land-glow-outer {
stroke: #eeeeee;
stroke-width: 12px;
}
.land-glow-inner {
stroke: #dddddd;
stroke-width: 6px;
}
.land-fill {
stroke: none;
fill: white;
}
.region:hover {
fill: orange;
stroke: black;
}
.boundary {
stroke: #777777;
fill: none;
}
.label {
text-anchor: middle;
font-size: 2.5px;
fill: black;
opacity: 0.5;
pointer-events: none;
}
.overlay {
fill: transparent;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Fractal treemap</title>
<link type="text/css" href="index.css" rel="stylesheet"/>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="index.js"></script>
</head>
<body onload="main()"></body>
</html>
(function() {
window.main = function() {
/* "globals"
*/
var container, dx, dy, height, path_generator, radius, svg, vis, width;
vis = null;
width = 960;
height = 500;
svg = d3.select('body').append('svg').attr('width', width).attr('height', height);
/* ZOOM and PAN
*/
/* create container elements
*/
container = svg.append('g').attr('transform', 'translate(640, 34)');
container.call(d3.behavior.zoom().scaleExtent([1, 49]).on('zoom', (function() {
return vis.attr('transform', "translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
})));
vis = container.append('g');
/* create a rectangular overlay to catch events
*/
/* WARNING rect size is huge but not infinite. this is a dirty hack
*/
vis.append('rect').attr('class', 'overlay').attr('x', -500000).attr('y', -500000).attr('width', 1000000).attr('height', 1000000);
/* END ZOOM and PAN
*/
/* custom projection to make hexagons appear regular (y axis is also flipped)
*/
radius = 1;
dx = radius * 2 * Math.sin(Math.PI / 3);
dy = radius * 1.5;
path_generator = d3.geo.path().projection(d3.geo.transform({
point: function(x, y, z) {
/* draw only nodes that are "important" enough
*/ return this.stream.point(x * dx / 2, -(y - (2 - (y & 1)) / 3) * dy / 2);
}
}));
/* load topoJSON data
*/
return d3.json('regions_48207.topo.json', function(error, data) {
/* presimplify the topology (compute the effective area (z) of each point)
*/
/* data.objects contain all the map layers
*/
/* each layer represents a different level of the tree
*/
/* the number of layers is equal to the depth of the tree
*/
var defs, depth, i;
depth = Object.keys(data.objects).length;
/* draw the level zero region (the land)
*/
defs = svg.append('defs');
defs.selectAll('#land').data(topojson.feature(data, data.objects[0]).features).enter().append('path').attr('id', 'land').attr('d', path_generator);
/* faux land glow (using filters takes too much resources)
*/
vis.append('use').attr('class', 'land-glow-outer').attr('xlink:href', '#land');
vis.append('use').attr('class', 'land-glow-inner').attr('xlink:href', '#land');
vis.append('use').attr('class', 'land-fill').attr('xlink:href', '#land');
/* draw the boundaries
*/
for (i = 1; i < 4; i++) {
vis.append('path').datum(topojson.mesh(data, data.objects[i], (function(a, b) {
return a.properties.prefix.slice(0, -1) === b.properties.prefix.slice(0, -1);
}))).attr('d', path_generator).attr('class', 'boundary').style('stroke-width', "" + (0.7 / (i * i)) + "px");
}
/* draw the labels
*/
return vis.selectAll('.label').data(topojson.feature(data, data.objects[1]).features.concat(topojson.feature(data, data.objects[2]).features, topojson.feature(data, data.objects[3]).features)).enter().append('text').attr('class', 'label').attr('dy', '0.35em').attr('transform', (function(d) {
var centroid;
centroid = path_generator.centroid(d);
return "translate(" + centroid[0] + "," + centroid[1] + ") scale(" + (20 / (Math.pow(d.properties.prefix.length, 1.7))) + ")";
})).text(function(d) {
return d.properties.prefix;
});
});
};
}).call(this);
.land-glow-outer
stroke: #EEE
stroke-width: 12px
.land-glow-inner
stroke: #DDD
stroke-width: 6px
.land-fill
stroke: none
fill: white
.region:hover
fill: orange
stroke: black
.boundary
stroke: #777
fill: none
.label
text-anchor: middle
font-size: 2.5px
fill: black
opacity: 0.5
pointer-events: none
// zoom and pan
.overlay
fill: transparent
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View raw

(Sorry about that, but we can’t show files that are this big right now.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment