This example shows a choropleth map of Connecticut, showing the town subdivisions. The example resizes the map to use the space available, while still keeping the correct aspect ratio.
This visualization is part of a series focused on Run 169. I'm trying to run a road race in each of Connecticut's 169 towns. Hover over a town to see its name, driving time from my town, and races in the next two weeks.
To experience the resize behavior, run this example full-screen and resize the browser.
Notes:
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<title>Resizable choropleth map</title>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v2.min.js"></script>
<script src="d3-tip.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.min.js"></script>
<style>
@import url("https://fonts.googleapis.com/css?family=Roboto");
body {
font-family: Roboto, sans-serif;
font-size: 10pt;
}
/* Make the chart container fill the page using CSS. */
#chart {
position: fixed;
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
}
.area {
stroke: #fff;
stroke-width: 1.5px;
opacity: 0.9;
fill: #d1d1d1;
}
.area:hover {
opacity: 0.4;
}
.alreadyRun {
fill: #16a; /*darkgreen;*/
}
.hasRaceSoon {
fill: #feb24c; /* #428cd1; */
}
.hasRaceVerySoon {
fill: #f03b20; /*#d14242;*/
}
.color-legend text {
font-size: 12pt;
color: #666;
}
.color-legend rect {
opacity: 0.9;
}
.d3-tip {
font-size: 12pt;
line-height: 1.2;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 7px;
}
/* Creates a small triangle extender for the tooltip */
.d3-tip:after {
box-sizing: border-box;
display: inline;
font-size: 10px;
width: 100%;
line-height: 1;
color: rgba(0, 0, 0, 0.8);
content: "\25BC";
position: absolute;
text-align: center;
}
/* style northward tooltips differently */
.d3-tip.n:after {
margin: -1px 0 0 0;
top: 100%;
left: 0;
}
/* more tooltip formats */
.townname {
font-weight: bold;
}
.racedate {
color: skyblue;
font-weight: bold;
}
.racedistance {
color: orange;
font-weight: bold;
}
.racename {
color: #fff;
}
</style>
</head>
<body>
<div id="chart"></div>
<script>
const tip = d3.tip()
.attr("class", "d3-tip")
.offset([-10, 0])
.html(d => "<span class='townname'>" + d.properties.NAME10 + ":</span> <span>"
+ drivingTimesMap[d.properties.NAME10].timeString
+ " driving</span>"
+ "<span>"
+ (d.properties.NAME10 in racesSoonByTown ?
racesSoonByTown[d.properties.NAME10]
: "")
+ "</span>"
);
const chartDiv = document.getElementById("chart");
const svg = d3.select(chartDiv).append("svg");
const colorScale = d3.scaleOrdinal()
.domain(["Race within 1 week", "Race within 2 weeks", "Town already run"])
.range(["#f03b20", "#feb24c", "#16a"]);
const colorLegend = d3.legendColor()
.scale(colorScale)
.shapeWidth(40)
.shapeHeight(20);
const colorLegendG = svg.append("g")
.attr("transform",`translate(10,10)`);
colorLegendG.call(colorLegend)
.attr("class", "color-legend");
function getMapScale(width, height) {
// known size of CT image for given scale
const baseScale = 12000;
const baseWidth = 453;
const baseHeight = 379;
const scale1 = baseScale*width/baseWidth;
const scale2 = baseScale*height/baseHeight;
return d3.min([scale1, scale2]);
}
const drivingTimesMap = {};
build_driving_map = row => {
drivingTimesMap[row.Town] = {};
drivingTimesMap[row.Town].time = +row.DrivingTime;
const hours = Math.floor(+row.DrivingTime/60);
const mins = +row.DrivingTime - 60*hours;
if(hours > 0) {
drivingTimesMap[row.Town].timeString = hours + "h " + mins + " min";
} else {
drivingTimesMap[row.Town].timeString = mins + " min";
}
if(!(row.Town in raceHorizonByTown)) {
raceHorizonByTown[row.Town] = { 'daysToRace': 400, 'raceType': ""};
}
return row;
};
const racesRunMap = {};
build_races_run_map = row => {
racesRunMap[row.Town] = {};
racesRunMap[row.Town].distance = row.Distance;
return row;
};
const today = d3.timeDay(new Date());
const racesSoonByTown = {};
const raceHorizonByTown = {};
fmt = d3.format("02");
parseRaces = row => {
row.Month = +row.Month;
row.Day = +row.Day;
row.Weekday = +row.Weekday;
row.DateString = fmt(row.Month) + "/" + fmt(row.Day);
row.raceDay = d3.timeDay(new Date(2017, row.Month-1, row.Day));
const daysToRace = d3.timeDay.count(today, row.raceDay);
if(daysToRace >= 0 && daysToRace <= 14) {
const raceString = "<tr><td><span class='racedate'>" +
row["Date/Time"] +
"</span></td><td><span class='racedistance'>" +
row.Distance + "</span></td><td><span class='racename'>" +
row.Name + "</span></td></tr>";
if(row.Town in racesSoonByTown) {
racesSoonByTown[row.Town] += raceString;
} else {
racesSoonByTown[row.Town] = "<table>" + raceString;
}
const raceType = daysToRace <= 7 ? "hasRaceVerySoon" : "hasRaceSoon";
if(row.Town in raceHorizonByTown) {
if(daysToRace < raceHorizonByTown[row.Town].daysToRace) {
raceHorizonByTown[row.Town] = {
'daysToRace': daysToRace,
'raceType': raceType
};
}
} else {
raceHorizonByTown[row.Town] = {
'daysToRace': daysToRace,
'raceType': raceType
};
}
}
return row;
};
function completeTooltipTables() {
Object.keys(racesSoonByTown).forEach(
key => { racesSoonByTown[key] += "</table>"; }
);
}
svg.call(tip);
function dataLoaded(error, mapData, drivingTimes, racesRun, races) {
completeTooltipTables();
function redraw(){
// Extract the width and height that was computed by CSS.
const width = chartDiv.clientWidth;
const height = chartDiv.clientHeight;
const centerX = width/2;
const centerY = height/2;
// Use the extracted size to set the size of an SVG element.
svg
.attr("width", width)
.attr("height", height);
// Start work on the choropleth map
// idea from https://www.youtube.com/watch?v=lJgEx_yb4u0&t=23s
const mapScale = getMapScale(width, height);
const CT_coords = [-72.7,41.6032];
const projection = d3.geoMercator()
.center(CT_coords)
.scale(mapScale)
.translate([centerX, centerY]);
const path = d3.geoPath().projection(projection);
const group = svg.selectAll(".path")
.data(topojson.feature(mapData, mapData.objects.townct_37800_0000_2010_s100_census_1_shp_wgs84).features);
const areas = group
.enter()
.append("g").attr("class", "path").append("path")
.attr("d", path)
.attr("class", d =>
d.properties.NAME10 in racesRunMap ?
"area alreadyRun" :
"area " + raceHorizonByTown[d.properties.NAME10].raceType
)
.on("mouseover", tip.show)
.on("mouseout", tip.hide);
areas.merge(group).selectAll("path")
.attr("d", path);
}
// Draw for the first time to initialize.
redraw();
// Redraw based on the new size whenever the browser window is resized.
window.addEventListener("resize", redraw);
}
d3.queue()
.defer(d3.json, "ct_towns_simplified.topojson")
.defer(d3.csv, "driving_times_from_avon.csv", build_driving_map)
.defer(d3.csv, "towns_run.csv", build_races_run_map)
.defer(d3.csv, "races2017.csv", parseRaces)
.await(dataLoaded);
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js
https://d3js.org/topojson.v2.min.js
https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.24.0/d3-legend.min.js