Skip to content

Instantly share code, notes, and snippets.

@bojanvidanovic
Last active September 22, 2020 18:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bojanvidanovic/f65742550cdf6d6d2505a50622ede9ec to your computer and use it in GitHub Desktop.
Save bojanvidanovic/f65742550cdf6d6d2505a50622ede9ec to your computer and use it in GitHub Desktop.
D3.js visualisation of Yahoo Weather forecast, full article on: https://bojanvidanovic.com
// SVG dimensions
var margin = {
top: 15,
right: 15,
bottom: 20,
left: 15
};
var width = 1200 - margin.left - margin.right;
var height = 140 - margin.top - margin.bottom;
// Parse the time
var parseDate = d3.timeParse('%H %M %d %m %Y');
var timeFormat = d3.timeFormat('%H:%M');
var svg = d3.select("#chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
// Get the data
d3.json("https://gist.githubusercontent.com/b0jan/4bfcdf766867bd65c6b07882c01abd9f/raw/8ce8482f9e2335d15c5b1a3bdd7573c3cc105318/forecast.json", function(error, data) {
if (error) throw error;
// Hourly forecast data
var data = data.hourly_forecast;
// Format the data
for (var i = 0, temp_counter = 1, icon_counter = 1, len = data.length; i < len; i++) {
// Parse time
var timeBase = data[i].FCTTIME;
var hour = timeBase.hour_padded;
var minutes = timeBase.min;
var day = timeBase.mday_padded;
var month = timeBase.mon_padded;
var year = timeBase.year;
var date = [hour, minutes, day, month, year].join(' ');
data[i].time = parseDate(date);
// POP value to Int
data[i].pop = +data[i].pop;
// Group temperature
if (i < len - 1) {
if (data[i].temp.metric === data[i+1].temp.metric){
temp_counter++;
} else {
var moveHere = i - Math.round(--temp_counter / 2);
data[moveHere].temp_read = data[i].temp.metric;
temp_counter = 1;
}
} else {
if (data[i].temp.metric === data[i-1].temp.metric){
var moveHere = i - Math.round(--temp_counter / 2);
data[moveHere].temp_read = data[i].temp.metric;
temp_counter = 1;
} else {
data[i].temp_read = data[i].temp.metric;
}
}
// Group weather icons
if (i < len - 1) {
if (data[i].icon_url === data[i+1].icon_url){
if(data[i+2]){
icon_counter++;
}
} else {
if(icon_counter === 1){
data[i].graph_icon = data[i].icon_url;
data[i+1].icon_limit = true;
} else {
var moveHere = i - Math.round(icon_counter / 2);
data[moveHere].graph_icon = data[i].icon_url;
data[i].icon_limit = true;
icon_counter = 1;
}
}
} else {
if (data[i].icon_url === data[i-1].icon_url){
var moveHere = i - Math.round(icon_counter / 2) - 1;
data[moveHere].graph_icon = data[i].icon_url;
} else {
data[i].graph_icon = data[i].icon_url;
icon_counter = 1
}
}
};
// Set scales
var xScale = d3.scaleTime()
.domain(d3.extent(data, (d) => d.time))
.range([0, width]);
var yScale = d3.scaleLinear()
.domain([0, d3.max(data, (d) => d.temp.metric)])
.range([60, height - 30]);
// Bottom scale
svg.append("g")
.attr("transform", "translate(0," + height + ")")
.call(d3.axisBottom(xScale).tickFormat(timeFormat));
// Draw temperature line
svg.append("path")
.datum(data)
.style('stroke', '#ffa500')
.style('stroke-width', '3')
.style('fill', 'none')
.attr("d", d3.line()
.curve(d3.curveCardinal)
.x((d) => xScale(d.time))
.y((d) => height - yScale(d.temp.metric))
);
// Fill temperature area
svg.append("path")
.datum(data)
.attr("fill", "#fffbc1")
.attr("stroke-width", "0")
.attr("opacity", ".5")
.attr("d", d3.area()
.curve(d3.curveCardinal)
.x((d) => xScale(d.time))
.y0(height)
.y1((d) => height - yScale(d.temp.metric))
);
// Temperature change point and value
var tempPoint = svg.selectAll(".temp-point")
.data(data)
.enter()
.filter((d) => {
if(d.time) return d.temp_read
});
tempPoint.append("circle")
.attr("fill", "#ffa500")
.attr("stroke", "#ffa500")
.attr("stroke-width", "2")
.attr("cx", (d) => {return xScale(d.time)})
.attr("cy", (d) => {return height - yScale(d.temp_read)})
.attr("r", 3);
tempPoint.append("text")
.attr('x', (d) => xScale(d.time))
.attr('y', (d) => height - yScale(d.temp_read))
.attr('dy', "-10px")
.style("text-anchor", "middle")
.text((d) => d.temp_read + "°");
// Define POP Y scale
var popY = d3.scaleLinear()
.domain([0, d3.max(data, (d) => d.pop)])
.range([0, 20]);
// Draw POP line
svg.append("path")
.datum(data)
.attr("stroke", "#000")
.attr("stroke-width", ".3")
.attr("fill", "none")
.attr("d", d3.line()
.curve(d3.curveStep)
.x((d) => xScale(d.time))
.y((d) => height - popY(d.pop))
);
// Paint POP Area
svg.append("path")
.datum(data)
.attr("class", "pop-area")
.attr("fill", "#3590df")
.attr("stroke-width", "0")
.attr("d", d3.area()
.curve(d3.curveStep)
.x((d) => xScale(d.time))
.y0(height)
.y1((d) => height - popY(d.pop))
);
// POP value
var pop = svg.selectAll('.pop-value')
.data(data)
.enter()
.append("text")
.attr('class', 'pop-value')
.attr('font-size', '8px')
.attr('x', (d) => xScale(d.time))
.attr('y', (d) => height - popY(d.pop))
.attr('dy', "-5px")
.style("text-anchor", "middle")
.text((d) => {
if(d.pop != 0) return d.pop + "%"
});
// Forecast icon
svg.selectAll("image")
.data(data)
.enter()
.filter((d) => {
if(d.graph_icon) return d.time;
})
.append('image')
.attr('xlink:href', (d) => d.graph_icon)
.attr('height', '19')
.attr('width', '20')
.attr("x", (d) => xScale(d.time) + 7)
.attr('y', 55);
// Separate forecast icons
svg.selectAll(".divider-line")
.data(data)
.enter()
.filter((d) => { if(d.icon_limit) return d.time; })
.append("line")
.style("stroke-dasharray","3,3")
.style("stroke", "grey")
.attr('class', 'divider-line')
.attr("x1", (d) => xScale(d.time))
.attr("y1", height)
.attr("x2", (d) => xScale(d.time))
.attr("y2", (d) => height - yScale(d.temp.metric) + 2);
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment