Decatur Townhouses prices gathered from the Dekalb County Tax Commissioner website. I've excluded gifts or inherited "sales" and some other outliers. There are obviously sales from before 1982, but at least online they don't give the actual values for those. Their records only go through 2018 right now. 2019 sales will be updated in 2020.
xxxxxxxxxx
<style>
body {
font: 10px sans-serif;
}
.title-text{
font: 20px sans-serif;
font-weight: bold;}
.d3-tip {
line-height: 1;
font-weight: bold;
padding: 12px;
background: rgba(0, 0, 0, 0.8);
color: #fff;
border-radius: 2px;
}
/* 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;
}
ul,
li {
list-style: none;
margin: 0;
padding: 0;
}
.tg-stuff {
position: absolute;
}
.tgl {
display: none;
}
.tgl, .tgl:after, .tgl:before, .tgl *, .tgl *:after, .tgl *:before, .tgl + .tgl-btn {
box-sizing: border-box;
}
.tgl::-moz-selection, .tgl:after::-moz-selection, .tgl:before::-moz-selection, .tgl *::-moz-selection, .tgl *:after::-moz-selection, .tgl *:before::-moz-selection, .tgl + .tgl-btn::-moz-selection {
background: none;
}
.tgl::selection, .tgl:after::selection, .tgl:before::selection, .tgl *::selection, .tgl *:after::selection, .tgl *:before::selection, .tgl + .tgl-btn::selection {
background: none;
}
.tgl + .tgl-btn {
outline: 0;
display: block;
width: 11em;
height: 2em;
position: relative;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.tgl + .tgl-btn:after, .tgl + .tgl-btn:before {
position: relative;
display: block;
content: "";
width: 50%;
height: 100%;
}
.tgl + .tgl-btn:after {
left: 0;
}
.tgl + .tgl-btn:before {
display: none;
}
.tgl:checked + .tgl-btn:after {
left: 50%;
}
.tgl-flip + .tgl-btn {
padding: 2px;
transition: all .2s ease;
font-family: sans-serif;
-webkit-perspective: 100px;
perspective: 100px;
}
.tgl-flip + .tgl-btn:after, .tgl-flip + .tgl-btn:before {
display: inline-block;
transition: all .4s ease;
width: 100%;
text-align: center;
position: absolute;
line-height: 2em;
font-weight: bold;
color: #fff;
position: absolute;
top: 0;
left: 0;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
border-radius: 4px;
}
.tgl-flip + .tgl-btn:after {
content: attr(data-tg-on);
background: #02C66F;
-webkit-transform: rotateY(-180deg);
transform: rotateY(-180deg);
}
.tgl-flip + .tgl-btn:before {
background: #FF3A19;
content: attr(data-tg-off);
}
.tgl-flip + .tgl-btn:active:before {
-webkit-transform: rotateY(-20deg);
transform: rotateY(-20deg);
}
.tgl-flip:checked + .tgl-btn:before {
-webkit-transform: rotateY(180deg);
transform: rotateY(180deg);
}
.tgl-flip:checked + .tgl-btn:after {
-webkit-transform: rotateY(0);
transform: rotateY(0);
left: 0;
background: #7FC6A6;
}
.tgl-flip:checked + .tgl-btn:active:after {
-webkit-transform: rotateY(20deg);
transform: rotateY(20deg);
}
</style>
<svg width="960" height="500"></svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.25.6/d3-legend.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.min.js"></script>
<script>
const numberWithCommas = (x) => {
var parts = x.toString().split(".");
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",");
return parts.join(".");
}
function median(values) {
values.sort( function(a,b) {return a - b;} );
var half = Math.floor(values.length/2);
if(values.length % 2)
return values[half];
else
return (values[half-1] + values[half]) / 2.0;
}
function getMedians(d) {
var medians = [];
var years = {};
for (i=0; i<d.length; i++) {
sale = d[i];
year = sale.date.getFullYear();
years[year] ? years[year].push(sale.close) : years[year] = [sale.close];
}
for (var year in years) {
medians.push({date: new Date(year,6,1), close:median(years[year])});
}
return medians;
};
var tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d){
return "<strong>Price: </strong>$" + numberWithCommas(d.close) + "</span><br/>Date: " + d.date.toDateString().substring(4) + "<br/>Unit: " + d.unit_letter + d.unit_number + "<br/>Deed Type: " + d.deed_type + "<br/>Sale Condition: " + d.sale_condition;
});
var median_tip = d3.tip()
.attr('class', 'd3-tip')
.offset([-10, 0])
.html(function(d){
return "<strong>Price: </strong>$" + numberWithCommas(d.close) + "</span><br/>Year: " + d.date.getFullYear();
});
var svg = d3.select("svg"),
margin = {top: 70, right: 20, bottom: 30, left: 80},
width = +svg.attr("width") - margin.left - margin.right,
height = +svg.attr("height") - margin.top - margin.bottom,
g = svg.append("g").attr("transform", "translate(" + margin.left + "," + margin.top + ")");
g.call(tip);
g.call(median_tip);
var parseTime = d3.timeParse("%m/%d/%Y");
var dotSize = 2.5;
var selectedDotSize = 3.5;
var x = d3.scaleTime()
.rangeRound([0, width]);
var y = d3.scaleLinear()
.rangeRound([height, 0]);
var line = d3.line()
.x(function(d) { return x(d.date); })
.y(function(d) { return y(d.close); });
var ordinal = d3.scaleOrdinal()
.domain(["Three Bedroom Units", "Two Bedroom Units"])
.range([ "red", "steelblue"]);
var median_scale = d3.scaleOrdinal()
.domain(["Three Bedroom Units", "Two Bedroom Units"])
.range([ "orange", "green"]);
var legendOrdinal = d3.legendColor()
.shape("path", d3.symbol().type(d3.symbolCircle).size(150)())
.shapePadding(10)
.scale(ordinal);
d3.tsv("data.tsv", function(d) {
d.date = parseTime(d.date);
d.close = +d.close;
return d;
}, function(error, data) {
if (error) throw error;
data = data.sort(function(a, b) {return new Date(a.date)-new Date(b.date);});
two_bedroom_data = data.filter(
function(d) {
if (+d.bedrooms == 2) {
return d;
}
}
);
three_bedroom_data = data.filter(
function(d) {
if (+d.bedrooms == 3) {
return d;
}
}
);
//x.domain(d3.extent(data, function(d) { return d.date; }));
x.domain([new Date(1982,0,1), new Date(2018,11,31)]);
y.domain([0, 400000]);
g.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(x))
.select(".domain")
.remove();
g.append("g")
.attr("class", "legendOrdinal")
.attr("transform", "translate(50,50)");
g.select(".legendOrdinal")
.call(legendOrdinal);
individual_sales_title = "Individual Home Sales (1982-2018)";
median_sales_title = "Annual Median Home Sales (1982-2018)";
g.append("text")
.attr("class", "title-text")
.attr("x", width/2)
.style("text-anchor", "middle")
.text(individual_sales_title);
d3.select("body").append("div").attr("class", "tg-stuff").html(
'<input class="tgl tgl-flip" id="cb5" type="checkbox" onclick="mediansOnly()"/><label class="tgl-btn" data-tg-off="View Annual Medians" data-tg-on="View Individual Sales" for="cb5"></label>'
);
titleHeight = d3.select(".title-text").node().getBBox().height;
d3.select(".tg-stuff").style("left", width/2 + 'px').style("top", (margin.top + titleHeight) + 'px');
g.append("g")
.call(d3.axisLeft(y))
.append("text")
.attr("fill", "#000")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", "0.71em")
.attr("text-anchor", "end")
.text("Price ($)");
g.append("path")
.datum(two_bedroom_data)
.attr("class", "indiv-line")
.attr("fill", "none")
.attr("data-legend", function(d){return 'Two Bedroom Units'})
.attr("stroke", "steelblue")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
g.selectAll(".dot").data(two_bedroom_data).enter().append("circle")
.attr("class", "indiv-dots")
.attr("r", dotSize)
.style("fill", "steelblue")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y(d.close);})
.on('mouseover', function(d){
d3.select(this).style("r", selectedDotSize).style("fill", "black");
tip.show(d,this);
})
.on("mouseout", function(d){
d3.select(this).style("r", dotSize).style("fill", "steelblue");
tip.hide(d, this);
});
g.append("path")
.datum(three_bedroom_data)
.attr("class", "indiv-line")
.attr("fill", "none")
.attr("stroke", "red")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
g.selectAll(".dot").data(three_bedroom_data).enter().append("circle")
.attr("class", "indiv-dots")
.attr("r", dotSize).style("fill", "red")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y(d.close);})
.on('mouseover', function(d){
d3.select(this).style("r", selectedDotSize).style("fill", "black");
tip.show(d,this);
})
.on("mouseout", function(d){
d3.select(this).style("r", dotSize).style("fill", "red");
tip.hide(d, this);
});
two_bedroom_medians = getMedians(two_bedroom_data);
three_bedroom_medians = getMedians(three_bedroom_data);
g.append("path")
.datum(two_bedroom_medians)
.attr("class", "median-line")
.style("opacity", 0)
.attr("fill", "none")
.attr("stroke", "green")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
g.append("path")
.datum(three_bedroom_medians)
.attr("class", "median-line")
.style("opacity", 0)
.attr("fill", "none")
.attr("stroke", "orange")
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
g.selectAll(".dot").data(three_bedroom_medians).enter().append("circle")
.attr("r", dotSize).style("fill", "orange")
.attr("class", "median-dots")
.style("opacity", 0)
.style("visibility", "hidden")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y(d.close);})
.on('mouseover', function(d){
d3.select(this).style("r", selectedDotSize).style("fill", "black");
median_tip.show(d,this);
})
.on("mouseout", function(d){
d3.select(this).style("r", dotSize).style("fill", "orange");
median_tip.hide(d, this);
});
g.selectAll(".dot").data(two_bedroom_medians).enter().append("circle")
.attr("r", dotSize).style("fill", "green")
.attr("class", "median-dots")
.style("opacity", 0)
.style("visibility", "hidden")
.attr("cx", function(d) {return x(d.date);})
.attr("cy", function(d) {return y(d.close);})
.on('mouseover', function(d){
d3.select(this).style("r", selectedDotSize).style("fill", "black");
median_tip.show(d,this);
})
.on("mouseout", function(d){
d3.select(this).style("r", dotSize).style("fill", "green");
median_tip.hide(d, this);
});
});
var showingMedians = false;
function mediansOnly(){
var svg = d3.select("body").transition().duration(500);
if (!showingMedians) {
svg.select('.title-text').text(median_sales_title)
svg.selectAll('.indiv-line').style("opacity", 0);
svg.selectAll('.indiv-dots')
.style("opacity", 0).transition()
.style("visibility", "hidden");
svg.selectAll('.median-line').style("opacity", 1);
svg.selectAll('.median-dots')
.style("opacity", 1)
.style("visibility", "visible");
legendOrdinal.scale(median_scale);
g.select(".legendOrdinal").call(legendOrdinal);
showingMedians = true;
} else {
svg.select('.title-text').text(individual_sales_title)
svg.selectAll('.indiv-line').style("opacity", 1);
svg.selectAll('.indiv-dots')
.style("opacity", 1)
.style("visibility", "visible");
svg.selectAll('.median-line').style("opacity", 0);
svg.selectAll('.median-dots')
.style("opacity", 0).transition()
.style("visibility", "hidden");
legendOrdinal.scale(ordinal);
g.select(".legendOrdinal").call(legendOrdinal);
showingMedians = false;
}
}
</script>
https://d3js.org/d3.v4.min.js
https://cdnjs.cloudflare.com/ajax/libs/d3-legend/2.25.6/d3-legend.min.js
https://cdnjs.cloudflare.com/ajax/libs/d3-tip/0.9.1/d3-tip.min.js