block by nitaku 8242365

"Political-physical" fractal treemap

Full Screen

Full source, including python scripts, here.

A combination of a fractal treemap and a fractal tree depth map into a sort-of physical-political map.

index.js

(function() {

  window.main = function() {
    /* "globals"
    */
    var categories, colorify, container, dx, dy, height, legend, max, min, path_generator, radius, svg, vis, width, _i, _results;
    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) {
        return this.stream.point(x * dx / 2, -(y - (2 - (y & 1)) / 3) * dy / 2);
      }
    }));
    /* depth scale
    */
    /* color ramp from Gretchen N. Peterson's 'Cartographer's Toolkit' (pages 38-40)
    */
    colorify = d3.scale.quantize().domain([1, 8]).range(['#59A80F', '#9ED54C', '#C4ED68', '#E2FF9E', '#F0F2DD', '#F8CA8C', '#E9A189', '#D47384']);
    /* legend
    */
    legend = svg.append('g').attr('id', 'legend').attr('transform', 'translate(30,20)');
    min = colorify.domain()[0];
    max = colorify.domain()[1];
    categories = legend.selectAll('.category').data((function() {
      _results = [];
      for (var _i = min; min <= max ? _i <= max : _i >= max; min <= max ? _i++ : _i--){ _results.push(_i); }
      return _results;
    }).apply(this)).enter().append('g').attr('class', 'category').attr('transform', function(d, i) {
      return "translate(0," + (i * 22) + ")";
    });
    categories.append('rect').attr('x', 0).attr('y', 0).attr('width', 22).attr('height', 22).attr('fill', function(d) {
      return colorify(d);
    });
    categories.append('text').attr('y', 11).attr('dx', -4).attr('dy', '0.35em').text(function(d) {
      return d;
    });
    /* load topoJSON data
    */
    return queue().defer(d3.json, 'regions_48207.topo.json').defer(d3.json, 'depth_regions.topo.json').await(function(error, political, physical) {
      /* compute the maximum depth to set the color scale domain
      */
      /* define the level zero region (the land)
      */
      var defs, i;
      defs = svg.append('defs');
      defs.append('path').datum(topojson.feature(physical, physical.objects.depth_regions).features.filter(function(d) {
        return d.properties.depth === 1;
      })[0]).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');
      /* draw the contour lines
      */
      vis.selectAll('.contour').data(topojson.feature(physical, physical.objects.depth_regions).features).enter().append('path').attr('class', 'contour').attr('d', path_generator).attr('fill', function(d) {
        return colorify(d.properties.depth);
      });
      /* draw the land border
      */
      /* draw the political boundaries
      */
      for (i = 1; i < 3; i++) {
        vis.append('path').datum(topojson.mesh(political, political.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', "" + (1.0 / i) + "px");
      }
      /* draw the labels
      */
      return vis.selectAll('.label').data(topojson.feature(political, political.objects[2]).features.concat(topojson.feature(political, political.objects[1]).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);

index.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Fractal treemap (political+physical)</title>
        <link type="text/css" href="index.css" rel="stylesheet"/>
        <script src="//d3js.org/d3.v3.min.js"></script>
        <script src="//d3js.org/topojson.v1.min.js"></script>
        <script src="//d3js.org/queue.v1.min.js"></script>
        <script src="index.js"></script>
    </head>
    <body onload="main()"></body>
</html>

gosper_depth_contour_lines.py

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_filename):
    leaves_done = 0
    contour_regions = {}
    
    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]))
                hex = shapely.wkt.loads(hexes_row[0])
                
                depth = len(prefix)+1
                
                # add the hex to all the regions with d <= depth
                for d in xrange(1,depth+1):
                    if d not in contour_regions:
                        contour_regions[d] = hex
                    else:
                        contour_regions[d] = contour_regions[d].union(hex)
                        
                # logging
                leaves_done += 1
                print('%d leaves done' % leaves_done, end='\r')
                
    schema = {'geometry': 'Polygon', 'properties': {'depth': 'int'}}
    
    with collection(output_filename, 'w', 'GeoJSON', schema) as output:
        for depth, region in contour_regions.items():
            output.write({
                'properties': {
                    'depth': depth
                },
                'geometry': mapping(region)
            })
        

gosper_regions.py

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)
                })
            

index.coffee

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) ->
                this.stream.point(x * dx / 2, -(y - (2 - (y & 1)) / 3) * dy / 2)
        })
        
    ### depth scale ###
    ### color ramp from Gretchen N. Peterson's 'Cartographer's Toolkit' (pages 38-40) ###
    colorify = d3.scale.quantize()
        .domain([1,8]) # WARNING domain clipped to actual data domain by hardcoding
        # .range(['#396353','#0DB14B','#6DC067','#ABD69B','#DAEAC1','#DFCCE4','#C7B2D6','#9474B4'])#,'#754098','#504971'])
        .range(['#59A80F','#9ED54C','#C4ED68','#E2FF9E','#F0F2DD','#F8CA8C','#E9A189','#D47384'])#,'#AC437B','#8C286E'])
        # .range(['#FFFCF6','#FFF7DB','#FFF4C2','#FEECAE','#F8CA8C','#F0A848','#C07860','#A86060','#784860','#604860'])
        
    ### legend ###
    legend = svg.append('g')
        .attr('id','legend')
        .attr('transform', 'translate(30,20)')
        
    min = colorify.domain()[0]
    max = colorify.domain()[1]
    
    categories = legend.selectAll('.category')
        .data([min..max])
      .enter().append('g')
        .attr('class', 'category')
        .attr('transform', (d,i) -> "translate(0,#{i*22})")
        
    categories.append('rect')
        .attr('x', 0).attr('y', 0)
        .attr('width', 22).attr('height', 22)
        .attr('fill', (d) -> colorify(d))
        
    categories.append('text')
        .attr('y', 11)
        .attr('dx', -4)
        .attr('dy', '0.35em')
        .text((d) -> d)
        
    ### load topoJSON data ###
    queue()
      .defer(d3.json, 'regions_48207.topo.json')
      .defer(d3.json, 'depth_regions.topo.json')
      .await (error, political, physical) ->
        ### compute the maximum depth to set the color scale domain ###
        # max_depth =  d3.max(topojson.feature(physical, physical.objects.depth_regions).features, (d) -> d.properties.depth)
        
        # colorify
            # .domain([1, max_depth])
        
        ### define the level zero region (the land) ###
        defs = svg.append('defs')
        
        defs.append('path')
            .datum(topojson.feature(physical, physical.objects.depth_regions).features.filter((d) -> d.properties.depth is 1)[0])
            .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')
            
        ### draw the contour lines ###
        vis.selectAll('.contour')
            .data(topojson.feature(physical, physical.objects.depth_regions).features)
          .enter().append('path')
            .attr('class', 'contour')
            .attr('d', path_generator)
            .attr('fill', (d) -> colorify(d.properties.depth))
            
        ### draw the land border ###
        # vis.append('use')
            # .attr('class', 'land-fill')
            # .attr('xlink:href', '#land')
            
        ### draw the political boundaries ###
        for i in [1...3]
            vis.append('path')
                .datum(topojson.mesh(political, political.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', "#{1.0/i}px")
                
        ### draw the labels ###
        vis.selectAll('.label')
            .data(topojson.feature(political, political.objects[2]).features.concat(
                  topojson.feature(political, political.objects[1]).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)
            

index.css

#land {
  fill: none;
}

.land-glow-outer {
  stroke: #eeeeee;
  stroke-width: 12px;
}

.land-glow-inner {
  stroke: #dddddd;
  stroke-width: 6px;
}

.land-fill {
  stroke: #777777;
  stroke-width: 0.7px;
}

.contour {
  stroke: none;
}

.category text {
  text-anchor: end;
  fill: #555555;
  font-family: sans-serif;
  font-weight: bold;
  font-size: 14px;
  text-shadow: -1px 0 1px white, 0 1px white, 1px 0 white, 0 -1px white;
}

.boundary {
  stroke: #555555;
  fill: none;
}

.label {
  text-anchor: middle;
  font-size: 2.5px;
  fill: #555555;
  pointer-events: none;
  text-shadow: -1px 0 1px white, 0 1px white, 1px 0 white, 0 -1px white;
}

.overlay {
  fill: transparent;
}

index.sass

#land
    fill: none
    
.land-glow-outer
    stroke: #EEE
    stroke-width: 12px
    
.land-glow-inner
    stroke: #DDD
    stroke-width: 6px
    
.land-fill
    stroke: #777
    stroke-width: 0.7px
    
.contour
    stroke: none
    
.category text
    text-anchor: end
    fill: #555
    font-family: sans-serif
    font-weight: bold
    font-size: 14px
    text-shadow: -1px 0 1px white, 0 1px white, 1px 0 white, 0 -1px white
    
.boundary
    stroke: #555
    fill: none
    
.label
    text-anchor: middle
    font-size: 2.5px
    fill: #555
    pointer-events: none
    text-shadow: -1px 0 1px white, 0 1px white, 1px 0 white, 0 -1px white
    
// zoom and pan
.overlay
    fill: transparent