Notes:
Sources:
Data: data.tsv
Code:
map-distorsions.ejs
<% demo = {category: "d3js", key: "map-distorsions", files: files}; %>
<%- partial("../mixins/_demo_title", demo) %>
<p><div class="slider"></div></p>
<div id="chart" class="<%= demo.key %>-chart" style="margin-top: 30px;"></div>
<script type="text/javascript" src="/vendors/bower/d3/d3.min.js"></script>
<script type="text/javascript" src="/js/d3js-utils.js"></script>
<script type="text/javascript" src="/js/d3js/<%= demo.key %>.js"></script>
<%- partial("../mixins/_notes", demo) %>
<%- partial("../mixins/_sources_data", demo) %>
<%- partial("../mixins/_code", demo) %>
map-distorsions.coffee
ready = ->
margin = {top: 90, right: 40, bottom: 20, left: 200}
width = $('#chart').innerWidth() - margin.left - margin.right
height = 750 - margin.top - margin.bottom
colors = ['#7C7CC9','#429742', '#63BD28', '#D14141']
dimensions = [{
name: 'name', scale: d3.scale.ordinal().rangePoints([0, height]), type: String
},
{
name: 'Acc. 40º 150%', scale: d3.scale.linear().range([0, height]), type: Number
},
{
name: 'Scale', scale: d3.scale.linear().range([height, 0]), type: Number
},
{
name: 'Areal', scale: d3.scale.sqrt().range([height, 0]), type: Number
},
{
name: 'Angular', scale: d3.scale.linear().range([height, 0]), type: Number
}
]
svg = d3utils.svg('#chart', width, height, margin)
title = 'Comparison of 41 map projections by four different types of distortion. Lower is better.'
d3utils.middleTitle(svg, width, title, -60)
x = d3.scale.ordinal().domain(dimensions.map((d)-> d.name )).rangePoints([0, width])
line = d3.svg.line().defined((d)-> !isNaN(d[1]))
yAxis = d3.svg.axis().orient('left')
dimension = svg.selectAll('.dimension').data(dimensions).enter().append('g')
.attr('class', 'dimension').attr('transform', (d)-> 'translate(' + x(d.name) + ')')
d3.tsv('/data/d3js/map-distorsions/data.tsv', (data)->
d3utils.filterColor('lines', svg, 2, .4)
data = _.sortBy(data, 'name')
colorFn = d3utils.colorsScale(colors, [0, data.length - 1])
dimensions.forEach((dimension)->
dimension.scale.domain(
if dimension.type is Number
d3.extent(data, (d)-> +d[dimension.name])
else
data.map((d)-> d[dimension.name]).sort()
)
)
draw = ((d)->
line(dimensions.map((dimension)->
[x(dimension.name), dimension.scale(d[dimension.name])]
))
)
tooltipText = ((d)->
vals = _.map(['Acc. 40º 150%', 'Scale', 'Areal', 'Angular'], (item)->
String(Number(d[item]).toFixed(2))
)
d.name + ': ' + vals.join(' - ')
)
svg.append('g').attr('class', 'background').selectAll('path').data(data)
.enter().append('path').attr({d: draw, 'data-title': tooltipText})
svg.append('g').attr('class', 'foreground').selectAll('path').data(data)
.enter().append('path').attr({d: draw, 'data-title': tooltipText})
dimension.append('g').attr('class', 'axis')
.each((d)-> d3.select(this).call(yAxis.scale(d.scale))).append('text')
.attr('class', 'title').attr('text-anchor', 'middle').attr('y', -9)
.text((d)-> d.name )
svg.select('.axis').selectAll('text:not(.title)').attr('class', 'label')
.data(data, (d)-> d.name or d).style({fill: ((d, i)-> colorFn(i))})
moveToFront = ( ->
@parentNode.appendChild(this)
)
mouseover = ((d)->
svg.selectAll('.foreground path').style({filter: 'none'})
svg.classed('active', true)
projection.classed('inactive', (p)-> p isnt d)
projection.filter((p)-> p is d).each(moveToFront)
)
mouseout = ((d)->
svg.selectAll('.foreground path').style({filter: 'url(#drop-shadow-lines)'})
svg.classed('active', false)
projection.classed('inactive', false)
)
svg.selectAll('.foreground path')
.style({filter: 'url(#drop-shadow-lines)', stroke: ((d,i)-> colorFn(i))})
projection = svg.selectAll('.axis text,.background path,.foreground path')
.on('mouseover', mouseover).on('mouseout', mouseout)
d3utils.tooltip('.background path, .foreground path', \
{followMouse: true, leftOffst: 100, topOffst: 50})
)
$(document).ready(ready)
_map-distorsions.styl
.map-distorsions-chart
svg
font-family: sans-serif;
text-shadow 1px 1px 1px #ddd;
.background path {
fill: none;
stroke: none;
stroke-width: 20px;
pointer-events: stroke;
}
.foreground path {
fill: none;
stroke-width: 1.5px;
}
.axis
font-size 12px
.axis .title {
font-size: 11px;
font-family: Times New Roman;
font-weight: bold;
text-transform: uppercase;
}
.axis line,
.axis path {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.label {
-webkit-transition: fill 125ms linear;
&.inactive {
fill: #ccc !important;
}
}
.active .label:not(.inactive) {
font-weight: bold;
}
.foreground path.inactive {
stroke: #ccc !important;
stroke-opacity: .5;
stroke-width: 1px;
}
d3js-utils.coffee
d3utils = {
middleTitle: ((svg, width, text, top = -15)->
element = svg.append('text').attr({class: 'chart-title', 'text-anchor': 'middle', \
transform: 'translate(' + String(width / 2) + ',' + top + ')'})
.text(text).style({'font-weight': 'bold'})
)
svg: ((selector, width, height, margin)->
d3.select(selector).text('').append('svg')
.attr({width: width + margin.left + margin.right, \
height: height + margin.top + margin.bottom})
.append('g').attr({transform: 'translate(' + margin.left + ',' + margin.top + ')'})
)
filterBlackOpacity: ((id, svg, deviation, slope)->
defs = svg.append('defs')
filter = defs.append('filter').attr({id: 'drop-shadow-' + id, width: '500%', height: '500%', \
x: '-200%', y: '-200%'})
filter.append('feGaussianBlur').attr({in: 'SourceAlpha', stdDeviation: deviation})
filter.append('feOffset').attr({dx: 1, dy: 1})
filter.append('feComponentTransfer').append('feFuncA').attr({type: 'linear', slope: slope})
feMerge = filter.append('feMerge')
feMerge.append('feMergeNode')
feMerge.append('feMergeNode').attr('in', 'SourceGraphic')
)
filterColor: ((id, svg, deviation, slope, extra = false)->
defs = svg.append('defs')
filter = defs.append('filter').attr({id:'drop-shadow-' + id})
filter.attr({width: '500%', height: '500%', x: '-200%', y: '-200%'}) if extra
filter.append('feOffset').attr({result: 'offOut', in: 'SourceGraphic', dx: .5, dy: .5})
filter.append('feGaussianBlur')
.attr({result: 'blurOut', in: 'offOut', stdDeviation: deviation})
filter.append('feBlend').attr({in: 'SourceGraphic', in2: 'blurOut', mode: 'normal'})
filter.append('feComponentTransfer').append('feFuncA').attr({type: 'linear', slope: slope})
)
tooltip: ((selector, customOpts = {})->
defaultOpts = {
followMouse: false, followElement: false, elementSelector: ''
leftOffst: 60, topOffst: 40
tOpts: {container: 'body', viewport: {selector: '#chart svg'}}
}
opts = _.merge(defaultOpts, customOpts)
# Bootstrap tooltip.
$(selector).tooltip(opts.tOpts)
# For SVG forms, it is better to position the tooltip manually.
if opts.followMouse
$(selector).hover((e)->
$('.tooltip')
.css({top: String(e.pageY - opts.topOffst) + 'px', \
left: String(e.pageX - opts.leftOffst) + 'px'})
)
else if opts.followElement
$(selector).hover((e)->
$('.tooltip')
.css({top: String($(opts.elementSelector).position().top - opts.topOffst) + 'px', \
left: String($(opts.elementSelector).position().left - opts.leftOffst) + 'px'})
)
)
colorsScale: ((colors, extent)->
c = d3.scale.linear().domain(extent).range([0,1])
colorScale = d3.scale.linear()
.domain(d3.range(0, 1, 1.0 / (colors.length))).range(colors)
return ((p)-> colorScale(c(p)))
)
}
window.d3utils = d3utils