Public
Edited
Sep 10, 2024
3 forks
Importers
12 stars
Insert cell
Insert cell
map = {
let div = d3
.create("div")
.style("width", `${w}px`)
.style("height", `${h}px`)
.style("overflow", "hidden");

let svg = div.append("svg").attr("width", w).attr("height", h);
let path = d3.geoPath().projection(proj);

let map = svg.append("g");
map
.append("g")
.selectAll("path")
.data(stategeo.features)
.join("path")
.attr("fill", "#eaeaea")
.attr("d", path);
let MSAs = map
.append("g")
.selectAll("path")
.data(msageo.features)
.join("path")
.attr("data-id", (d) => d.properties.ZillowRegionID)
.attr("fill", function (d) {
let result;
try {
result = get_current_price(
zillow_data.filter(
(o) => o.RegionID == d.properties.ZillowRegionID
)[0]
);
d.properties.got_it = true;
return d3.interpolateReds(result / max_current_house_price);
} catch {
d.properties.got_it = false;
console.log([d, "is bad"]);
}
})
.attr("stroke", "black")
.attr("stroke-linejoin", "round")
.attr("stroke-width", 0.2)
.attr("d", (d) => (d.properties.got_it ? path(d) : null));

map
.append("path")
.datum(stategeo)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-linejoin", "round")
.attr("pointer-events", "none")
.attr("stroke-width", 1)
.attr("d", path);

let tippy_instances = new Map();
MSAs.nodes().forEach(function (city) {
let city_id = city.getAttribute("data-id");
let tippy_instance = tippy(city, {
maxWidth: 500,
theme: "light",
content: city.getAttribute("data-id")
});
tippy_instances.set(city_id, tippy_instance);
});

MSAs.on("pointerenter", function (a, d) {
tippy_instances.get(d.properties.ZillowRegionID.toString()).setContent(
make_time_series_graphs({
highlight: d.properties.ZillowRegionID,
max_size: 0.8 * width < 500 ? 0.8 * width : 500,
opacity: 0.02
})
);
d3.select(this).attr("stroke-width", 2).raise();
}).on("pointerleave mouseleave", function (a, d) {
d3.select(this).attr("stroke-width", 0.2);
// MSAs.attr("stroke-width", 0.2);
});

svg.call(
d3
.zoom()
.extent([
[0, 0],
[w, h]
])
.translateExtent([
[0, 0],
[w, h]
])
.scaleExtent([1, 8])
.duration(500)
.on("zoom", function (evt, a, b) {
map.attr("transform", evt.transform);
})
);

return div.node();
}
Insert cell
Insert cell
Insert cell
Insert cell
Object.keys(zillow_data[0]).slice(-8)
Insert cell
zillow_data[0].time_series
Insert cell
// https://files.zillowstatic.com/research/public_csvs/mlp/Metro_mlp_uc_sfrcondo_sm_month.csv
// https://files.zillowstatic.com/research/public_csvs/zhvi/Metro_zhvi_uc_sfrcondo_tier_0.33_0.67_sm_sa_month.csv

zillow_data = {
let zillow_data = d3.csvParse(
await (
await fetch(
"https://files.zillowstatic.com/research/public_csvs/mlp/Metro_mlp_uc_sfrcondo_sm_month.csv"
)
).text()
);

// return zillow_data;

// // Temporarily doing a static version
// let zillow_data = d3.csvParse(
// await FileAttachment("static_zillow_data.csv").text()
// );

let us_data = zillow_data[0];
us_data = Object.keys(us_data);
let start_date = parseDate(us_data[5]);
// start_date = parseDate("2012-01-01");
let end_date = parseDate(us_data.slice(-1)[0]);

zillow_data = zillow_data.map(function (d) {
let keys = Object.keys(d).slice(5);
let time_series = keys
.map((key) => [parseDate(key), parseInt(d[key])])
.filter((a) => !isNaN(a[1]) && a[0] > start_date);
d.time_series = time_series;
return d;
}); //.slice(0, 200);

zillow_data = d3.sort(zillow_data, get_current_price);
zillow_data.reverse();

zillow_data = zillow_data; //.slice(0, 400);

zillow_data.start_date = start_date; // d3.sort(zillow_data, get_current_price).slice(0, 101);
zillow_data.end_date = end_date;

return zillow_data;
}

// zillow_data = FileAttachment("zillow_data.csv").csv({ typed: true })
Insert cell
Insert cell
max_current_house_price = d3.max(
zillow_data.map((d) => d.time_series.slice(-1)[0][1])
)
Insert cell
get_current_price = function (v) {
return v.time_series.slice(-1)[0][1];
}
Insert cell
parseDate = d3.timeParse("%Y-%m-%d")
Insert cell
function make_time_series_graphs(opts = {}) {
let {
max_size = 1200,
margin = { left: 70, right: 30, bottom: 25, top: 20 },
opacity = 0.2,
highlight = "102001",
interactive = true
} = opts;
let graph_width = width < max_size ? width : max_size;
let graph_height = 0.625 * graph_width;

let svg = d3
.create("svg")
.attr("width", graph_width)
.attr("height", graph_height)
.attr("viewBox", [0, 0, graph_width, graph_height]);

let x_scale = d3
.scaleTime()
.domain([zillow_data.start_date, zillow_data.end_date])
.range([margin.left, graph_width - margin.right]);
let y_scale = d3
.scaleLinear()
.domain([0, max_current_house_price])
.range([graph_height - margin.bottom, margin.top]);
let pts_to_path = d3
.line()
.x((d) => x_scale(d[0]))
.y((d) => y_scale(d[1]));

let axes = svg.append("g");
let x_axis = d3.axisBottom(x_scale);
axes
.append("g")
.attr("transform", `translate(0,${graph_height - margin.bottom})`)
.call(x_axis);
let y_axis = d3.axisLeft(y_scale);
axes.append("g").attr("transform", `translate(${margin.left})`).call(y_axis);

let time_series_plots = svg.append("g");
let text_group = svg
.append("text")
.attr("x", 1.3 * margin.left)
.attr("y", 1.5 * margin.top)
.attr("font-size", 16)
.attr("text-anchor", "start")
.attr("fill", "steelblue");
let text_group2 = svg
.append("text")
.attr("x", 1.3 * margin.left)
.attr("y", 1.5 * margin.top + 20)
.attr("font-size", 16)
.attr("text-anchor", "start")
.attr("fill", "steelblue");

zillow_data.forEach(function (d) {
time_series_plots
.append("path")
.attr("d", pts_to_path(d.time_series))
.attr("class", "time_series")
.attr("id", `city${d.RegionID}`)
.attr("stroke", d.RegionID == highlight ? "steelblue" : "black")
.attr("stroke-opacity", d.RegionID == highlight ? 1 : opacity)
.attr("stroke-width", d.RegionID == highlight ? 4 : 0.7)
.attr("stroke-linejoin", "round")
.attr("fill", "none");
if (d.RegionID == highlight) {
text_group.text(d.RegionName);
text_group2.text(
`Current median price: $${d.time_series.slice(-1)[0][1]}`
);
}
});
if (highlight) {
time_series_plots.select(`#city${highlight}`).raise();
}

if (interactive) {
svg
.on("touchmove", (e) => e.preventDefault()) // prevent scrolling
.on("pointermove", function (evt) {
let p = d3.pointer(evt);
let x = p[0];
let y = p[1];
let day = x_scale.invert(x);
let price = y_scale.invert(y);
let closest = get_closest(day, price);
if (closest) {
time_series_plots
.selectAll("path.time_series")
.attr("stroke-opacity", opacity)
.attr("stroke-width", 0.7)
.attr("stroke", "black");
time_series_plots
.select(`#city${closest.cityID}`)
.attr("stroke-opacity", 1)
.attr("stroke-width", 4)
.attr("stroke", "steelblue")
.raise();
let zd = zillow_data.filter((o) => o.RegionID == closest.cityID)[0];
text_group.text(zd.RegionName);
text_group2.text(
`Current median price: $${zd.time_series.slice(-1)[0][1]}`
);
}
})
.on("pointerleave mouseleave", function () {
text_group.text("");
time_series_plots
.selectAll("path.time_series")
.attr("stroke-opacity", opacity)
.attr("stroke-width", 0.7)
.attr("stroke", "black");
if (highlight) {
time_series_plots
.select(`#city${highlight}`)
.attr("stroke-opacity", 1)
.attr("stroke-width", 4)
.attr("stroke", "steelblue")
.raise();
let zd = zillow_data.filter((o) => o.RegionID == highlight)[0];
text_group.text(zd.RegionName);
text_group2.text(
`Current median price: $${zd.time_series.slice(-1)[0][1]}`
);
}
});
}

return svg.node();
}
Insert cell
function get_closest(time, price) {
let results = [];

zillow_data.forEach(function (city_data) {
let these_dates = city_data.time_series.map((pt) => pt[0]);
try {
let idx = d3.bisect(these_dates, time);
let t1 = these_dates[idx - 1].getTime();
let t2 = these_dates[idx].getTime();
let p1 = city_data.time_series[idx - 1][1];
let p2 = city_data.time_series[idx][1];
let p = p1 + ((p2 - p1) * (time.getTime() - t1)) / (t2 - t1);
results.push({
time: time,
value: p,
err: Math.abs(p - price),
cityID: city_data.RegionID
});
} catch {
("pass");
}
});
return results.sort((o1, o2) => o1.err - o2.err)[0];
}
Insert cell
tippy_style = html`<link rel="stylesheet" href="${await require.resolve(
`tippy.js/themes/light.css`
)}">`
Insert cell
tippy = require("tippy.js@6")
Insert cell
proj = d3.geoIdentity().reflectY(true).fitSize([w, h], stategeo)
Insert cell
stategeo = topojson.feature(topo, topo.objects.tl_2019_us_state)
Insert cell
msageo.features
.map((o) => o.properties)
.filter(
(o) => o.NAMELSAD.slice(-10) == "Metro Area" //&& o.NAME.slice(-2) == "NC"
)
.map((o) => o.NAME)
Insert cell
smaller_zillow_data
Insert cell
// smaller_zillow_data.forEach(function (o) {
// delete o.time_series;
// })
Insert cell
smaller_zillow_data = zillow_data.filter((o) =>
region_ids.find((id) => id == o.RegionID)
)
Insert cell
region_ids = msageo.features
.map((o) => o.properties)
.filter((o) => o.LSAD == "M1")
.map((o) => o.ZillowRegionID)
Insert cell
msageo = topojson.feature(topo, topo.objects.tl_2019_us_cbsa)
Insert cell
topo = FileAttachment("AlberseStatesAndMSAsWithZillowIDs@1.json").json()
Insert cell
// w = width < 1200 ? width : 1200

w = 800
Insert cell
h = 0.625641 * w
Insert cell
d3.zip([1, 2], [3, 4])
Insert cell
m = new Map([["1", 2]])
Insert cell
d3.format(",")(3948739)
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more