Skip to content

Instantly share code, notes, and snippets.

@catherinekerr
Last active December 5, 2023 09:28
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save catherinekerr/e345a906f8e2bae8d07dbc79f8f04036 to your computer and use it in GitHub Desktop.
Save catherinekerr/e345a906f8e2bae8d07dbc79f8f04036 to your computer and use it in GitHub Desktop.
D3 with embedded SVG map

Embedding SVG Map

This example uses a map that was created in Adobe Illustrator and exported as an SVG file. Although it might be preferable to use GeoJSON files for geographical maps, they are not always available in the detail that is required. In any case, we could be using any irregular shaped SVG graphic created by an application like Illustrator - this example shows how to embed the SVG into D3 and bind data to the paths.

The SVG file is quite simple. Each county is identified by a path and an id. Some paths are grouped together to denote Northern Ireland and the Republic. The path titles are dynamically created in the program.

The data in this case comes from the Irish Central Statistics Office and gives the average rental prices for houses/apartments in each county in the Republic of Ireland in 2015. The map is a chloropleth using a quantize scale. Another useful feature of this example is the zoom and center feature which centers the county's bounding box in the map container.

Hover over a county to display the title with county name and value. Click on a county in the map or in the table to zoom in to that county. Click on a legend box to highlight all counties in that range.

<!DOCTYPE html>
<meta charset="utf-8">
<style>
@import url(style.css);
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script src="//d3js.org/queue.v1.min.js"></script>
<body>
<!-- Table to hold the data table, map and legend -->
<table border="0" cellpadding="10" style="overflow-y: scroll;">
<tr>
<td><div id="table_container" class="csvTable"></div></td>
<td><div id="map_container"></div></td>
<td><div id="legend_container"></div></td>
</tr>
</table>
<script>
var mw = 500; // map container width
var mh = 600; // map container height
var main_chart_svg = d3.select("#map_container")
.append("svg")
.attr({
"width":mw,
"height":mh,
});
var legend_svg = d3.select("#legend_container")
.append("svg")
.attr({
"width":200,
"height":600,
});
var hue = "g"; /* b=blue, g=green, r=red colours - from ColorBrewer */
/* break the data values into 9 ranges of €100 each */
/* max and min values already known so 400-1300 works */
var quantize = d3.scale.quantize()
.domain([400, 1300])
.range(d3.range(9).map(function(i) { return hue + i + "-9"; }));
/* declare locale so we can format values with euro symbol */
var ie = d3.locale({
"decimal": ".",
"thousands": ",",
"grouping": [3],
"currency": ["€", ""],
"dateTime": "%a %b %e %X %Y",
"date": "%d/%m/%Y",
"time": "%H:%M:%S",
"periods": ["AM", "PM"],
"days": ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
"shortDays": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"],
"months": ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
"shortMonths": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
});
var rateById = d3.map();
var lastActive = "";
var ireland;
var data;
var defaultScale = 0.6; /* default scale of map - fits nicely on standard screen */
var scale = 3; /* maximum size to zoom county */
var format = ie.numberFormat("$, #,##0d");
/* Thanks to http://stackoverflow.com/users/3128209/ameliabr for tips on creating a quantized legend */
var legend = legend_svg.selectAll('g.legendEntry')
.data(quantize.range())
.enter()
.append('g').attr('class', 'legendEntry');
legend
.append('rect')
.attr("x", 20)
.attr("y", function(d, i) {
return i * 25 + 20;
})
.attr("width", 15)
.attr("height", 15)
.attr("class", function(d){ return d;})
.style("stroke", "black")
.style("stroke-width", 1)
.on("click", function(d)
{
if (lastActive == "") {
resetAll();
d3.select(ireland).selectAll("." + d).attr("class", "highlight"); /* Highlight all counties in range selected */
}
});
legend
.append('text')
.attr("x", 40) //leave 5 pixel space after the <rect>
.attr("y", function(d, i) {
return i * 25 + 20;
})
.attr("dy", "0.8em") //place text one line *below* the x,y point
.text(function(d,i) {
var extent = quantize.invertExtent(d);
//extent will be a two-element array, format it however you want:
return format(extent[0]) + " - " + format(+extent[1])
})
.style("font-family", "sans-serif")
.style("font-size", "12px");
/* Data has key "county" and value "rental" - i.e. average rental price per county */
queue()
.defer(d3.csv, "rentals-2015-bycounty.csv", data)
.await(ready);
function ready(error, data) {
if (error) throw error;
d3.map(data, function(d) {rateById.set(d.county, +d.rental)}); /* create the data map */
d3.xml("ireland.svg", "image/svg+xml", function(error, xml) { /* embed the SVG map */
if (error) throw error;
var countyTable = tabulate(data, ["county", "rental"]); /* render the data table */
var svgMap = xml.getElementsByTagName("g")[0]; /* set svgMap to root g */
ireland = main_chart_svg.node().appendChild(svgMap); /* island of Ireland map */
d3.select(ireland).selectAll("#NI") /* Group Northern Ireland together */
.attr("class", "region NI");
d3.select(ireland).selectAll("#republic") /* Group Republic of Ireland together */
.attr("class", "region republic");
d3.select(ireland).selectAll("#republic").selectAll("path") /* Map Republic counties to rental data */
.attr("class", function(d) {
return quantize(rateById.get(this.id));
})
.append("title").text(function(d) { /* add title = name of each county and average rental */
return this.parentNode.id + ", " + format(rateById.get(this.parentNode.id))
});
d3.select(ireland).selectAll("#republic").selectAll("path")
.on("mouseover", function(d)
{
if (d3.select(this).classed("active")) return; /* no need to change class when county is already selected */
d3.select(this).attr("class", "hover");
})
.on("mouseout", function(d)
{
if (d3.select(this).classed("active")) return;
d3.select(this).attr("class", function(d) { /* reset county color to quantize range */
return quantize(rateById.get(this.id))
});
})
.on("click", function (d) { zoomed(d3.select(this)); });
/* Let's add an id to each group that wraps a path */
d3.select(ireland).selectAll("#republic").selectAll("path")
.each(function(d) {
d3.select(this.parentNode).attr("id", this.id);
});
/* Now add a text box to the group with content equal to the id of the group */
d3.select(ireland).selectAll("#republic").selectAll("g")
.append("svg:text")
.text(function(d){
return this.parentNode.id;
})
.attr("x", function(d){
console.log(d3.select(this.parentNode).select("path").attr("d"));
//return 600;
//d3.select(ireland).select("path")
return getBoundingBox(d3.select(this.parentNode).select("path"))[4];
})
.attr("y", function(d){
return getBoundingBox(d3.select(this.parentNode).select("path"))[5];
})
// .attr("text-anchor","middle")
// .attr("font-family", "sans-serif")
// .attr("stroke-width", 0.5)
.classed("text", true)
// .attr("fill", "#333")
// .attr('font-size','10pt')
;
});
}
/* Thanks to http://bl.ocks.org/phil-pedruco/7557092 for the table code */
/* and style - and what a coincidence he also used a map of Ireland! */
function tabulate(data, columns) {
var table = d3.select("#table_container").append("table")
thead = table.append("thead"),
tbody = table.append("tbody");
// append the header row
thead.append("tr")
.selectAll("th")
.data(columns)
.enter()
.append("th")
.text(function(column) { return column; });
// create a row for each object in the data
var rows = tbody.selectAll("tr")
.data(data)
.enter()
.append("tr")
.on("click", function (d) { tableRowClicked(d)});
// create a cell in each row for each column
var cells = rows.selectAll("td")
.data(function(row) {
return columns.map(function(column) {
return {column: column, value: row[column]};
});
})
.enter()
.append("td")
// .attr("style", "font-family: Courier") // sets the font style
.html(function(d) {
if (d.column == "rental") return format(d.value); else return d.value;
});
return table;
}
function zoomed(d) {
/* Thanks to http://complextosimple.blogspot.ie/2012/10/zoom-and-center-with-d3.html */
/* for a simple explanation of transform scale and translation */
/* This function centers the county's bounding box in the map container */
/* The scale is set to the minimum value that enables the county to fit in the */
/* container, horizontally or vertically, up to a maximum value of 3. */
/* If the full width of container is not required, the county is horizontally centred */
/* Likewise, if the full height of the container is not required, the county is */
/* vertically centred. */
var xy = getBoundingBox(d); /* get top left co-ordinates and width and height */
if (d.classed("active")) { /* if county is active reset map scale and county colour */
d.attr("class", function(d) {
return quantize(rateById.get(this.id))
});
main_chart_svg.selectAll("#viewport")
.transition().duration(750).attr("transform", "scale(" + defaultScale + ")");
lastActive = "";
} else { /* zoom into new county */
resetAll(); /* reset county colors */
/* scale is the max number of times bounding box will fit into container, capped at 3 times */
scale = Math.min(mw/xy[1], mh/xy[3], 3);
/* tx and ty are the translations of the x and y co-ordinates */
/* the translation centers the bounding box in the container */
var tx = -xy[0] + (mw - xy[1]*scale)/(2*scale);
var ty = -xy[2] + (mh - xy[3]*scale)/(2*scale);
main_chart_svg.selectAll("#viewport")
.transition().duration(750).attr("transform", "scale(" + scale + ")translate("+ tx +"," + ty + ")");
d.attr("class", "active");
lastActive = d.attr("id");
}
}
function reset(selection) {
/* resets the color of a single county */
if (selection != "")
d3.select(ireland).select("#" + selection).attr("class", function(d) {
return quantize(rateById.get(this.id))
});
}
function resetAll() {
/* resets the color of all counties */
d3.select(ireland).selectAll("#republic").selectAll("path")
.attr("class", function(d) {
return quantize(rateById.get(this.id))
});
}
function tableRowClicked(x) {
/* resets colors and zooms into new county */
resetAll();
lastActive = x.county;
zoomed(d3.select(ireland).selectAll("#" + x.county).select("path"));
}
function getBoundingBox(selection) {
/* get x,y co-ordinates of top-left of bounding box and width and height */
var element = selection.node(),
bbox = element.getBBox();
cx = bbox.x + bbox.width/2;
cy = bbox.y + bbox.height/2;
return [bbox.x, bbox.width, bbox.y, bbox.height, cx, cy];
}
d3.select(self.frameElement).style("height", "650px");
</script>
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.
type bedrooms address rental county
All property types All bedrooms Carlow 633.84 Carlow
All property types All bedrooms Cavan 453.01 Cavan
All property types All bedrooms Clare 524.19 Clare
All property types All bedrooms Cork 806.69 Cork
All property types All bedrooms Donegal 452.97 Donegal
All property types All bedrooms Dublin 1207.37 Dublin
All property types All bedrooms Galway 829.52 Galway
All property types All bedrooms Kerry 553.56 Kerry
All property types All bedrooms Kildare 887.66 Kildare
All property types All bedrooms Kilkenny 644.59 Kilkenny
All property types All bedrooms Laois 567.85 Laois
All property types All bedrooms Leitrim 414.23 Leitrim
All property types All bedrooms Limerick 643.55 Limerick
All property types All bedrooms Longford 422.36 Longford
All property types All bedrooms Louth 672.48 Louth
All property types All bedrooms Mayo 514.76 Mayo
All property types All bedrooms Meath 765.04 Meath
All property types All bedrooms Monaghan 499.16 Monaghan
All property types All bedrooms Offaly 545.5 Offaly
All property types All bedrooms Roscommon 462.79 Roscommon
All property types All bedrooms Sligo 600.72 Sligo
All property types All bedrooms Tipperary 539.24 Tipperary
All property types All bedrooms Waterford 554.23 Waterford
All property types All bedrooms Westmeath 568.02 Westmeath
All property types All bedrooms Wexford 560.88 Wexford
All property types All bedrooms Wicklow 911.88 Wicklow
/* style.css */
.region.republic {
fill: #fee391;
stroke: #333;
stroke-width: 1px;
}
.active
{fill: #f29929;
}
.hover
{fill: #fee391;
}
.highlight
{fill: #fec44f;
}
.region.NI {
fill: #aaa;
stroke: #aaa;
}
rect {
fill: none;
pointer-events: all;
}
/* Colors taken from colorbrewer2.org - blue */
.b0-9 { fill:rgb(247,251,255); }
.b1-9 { fill:rgb(222,235,247); }
.b2-9 { fill:rgb(198,219,239); }
.b3-9 { fill:rgb(158,202,225); }
.b4-9 { fill:rgb(107,174,214); }
.b5-9 { fill:rgb(66,146,198); }
.b6-9 { fill:rgb(33,113,181); }
.b7-9 { fill:rgb(8,81,156); }
.b8-9 { fill:rgb(8,48,107); }
/* Colors taken from colorbrewer2.org - red */
.r0-9 { fill:rgb(255,245,240); }
.r1-9 { fill:rgb(254,224,210); }
.r2-9 { fill:rgb(252,187,161); }
.r3-9 { fill:rgb(252,146,114); }
.r4-9 { fill:rgb(251,106,74); }
.r5-9 { fill:rgb(239,59,44); }
.r6-9 { fill:rgb(203,24,29); }
.r7-9 { fill:rgb(165,15,21); }
.r8-9 { fill:rgb(103,0,13); }
/* Colors taken from colorbrewer2.org - green */
.g0-9 { fill:rgb(247,252,245); }
.g1-9 { fill:rgb(229,245,224); }
.g2-9 { fill:rgb(199,233,192); }
.g3-9 { fill:rgb(161,217,155); }
.g4-9 { fill:rgb(116,196,118); }
.g5-9 { fill:rgb(65,171,93); }
.g6-9 { fill:rgb(35,139,69); }
.g7-9 { fill:rgb(0,109,44); }
.g8-9 { fill:rgb(0,68,27); }
.text { stroke:#333;
text-anchor: middle;
font-family: "sans-serif";
stroke-width: 0.5;
fill: #333;
font-size: 10pt;
}
.csvTable table {
border-collapse: collapse;
text-align: left;
width: 100%;
}
.csvTable {
font: normal 12px/120% Arial, Helvetica, sans-serif;
background: #fff;
overflow: hidden;
border: 1px solid #063;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.csvTable table td, .csvTable table th {
padding: 3px 10px;
}
.csvTable table thead th {
background: 0;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#006699',endColorstr='#00557F');
background-color: #006D2C;
color: #FFF;
font-size: 15px;
font-weight: 700;
border-left: 1px solid #0070A8;
}
.csvTable table thead th:first-child {
border: none;
}
.csvTable table tbody td {
color: #006633; /* #00496B */
border-left: 1px solid #E1EEF4;
font-size: 12px;
border-bottom: 1px solid #E1EEF4;
font-weight: 400;
}
.csvTable table tbody td:first-child {
border-left: none;
}
.csvTable table tbody tr:last-child td {
border-bottom: none;
}
.csvTable tr:hover td {
background-color: #063;
color: white;
}
@briannaess
Copy link

Great map! I'm attempting something similar, but with a click to zoom to a country in a map of the world. The trick, though, is that I have the SVG auto-resizing based on browser size. How might someone modify your code to get it to find the centroid and zoom into the SVG in the right spot when the SVG is inside a div? Thanks!

@catherinekerr
Copy link
Author

Hi @briannaess,
Apologies for not replying earlier but I was on holidays. Also, I haven't looked at this code in ages and had to refresh my memory. I am gobsmacked that someone sent me a comment after so long :)

Have you tried adjusting the mw and mh values based on browser size?

var mw = 500;	// map container width
var mh = 600;	// map container height
var main_chart_svg = d3.select("#map_container")
        .append("svg")
        .attr({
            "width":mw,
            "height":mh,
        });

Alternatively, you can create the map with appropriate width and height proportions and then scale according to browser size?

You should also take a look at my other gist - https://bl.ocks.org/catherinekerr/b3227f16cebc8dd8beee461a945fb323 - as this explains a bit better how to center SVGs, with zoom etc. This was an early attempt at using D3 with SVGs. I am no expert though as I haven't worked with D3 in a few years.

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