A modification of Susie Lu's radial weather plot. This one shows periods of cloudiness, precipitation and freezing temperatures via the bars around the border. It also includes a circular brush that lets you select a band on the radial chart. The selected band is then displayed in a more traditional linear way on the bottom left.
The original readme explains what's going on with the radial chart:
In the example we're looking at historical weather data for New York provided by intellicast.com and wunderground.com. Inspired by weather-radicals.com.
This example uses scales to roll your own radial projection by mapping out the x, y, and r positions. If you are creating a line or an area you can use d3's convenience functions d3.svg.line.radial and d3.svg.area.radial but this is a method you can use if you want to use different graphical elements in a circular layout.
forked from emeeks's block: Brushable Radial Chart
xxxxxxxxxx
<html lang="en">
<head>
<meta charset="utf-8">
<link href='https://fonts.googleapis.com/css?family=Lato:300' rel='stylesheet' type='text/css'>
<style>
body {
background-color: whitesmoke;
}
.noselect {
-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.extent {
fill-opacity: .1;
fill: rgb(205,130,42);
cursor: move;
}
.e {
fill: rgb(111,111,111);
cursor: move;
}
.w {
fill: rgb(169,169,169);
cursor: move;
}
.freeze {
fill: #A8DFE4;
}
.rain {
fill: #209CD3;
}
.clear {
fill: white
}
.scattered {
fill: lightgray;
}
.cloudy {
fill: #a1a1a1;
}
.overcast {
fill: #616161;
}
svg {
background-color: white;
font-family: 'Lato';
}
.axis {
stroke: white;
opacity: .8;
}
text {
pointer-events: none;
}
text.title {
font-size: 26px;
}
text.months, text.temp {
text-anchor: middle;
font-size: 12px;
fill: #39837B;
}
circle.axis {
stroke: white;
stroke-width: 1px;
fill: none;
}
circle.axis.record {
stroke: #bae0d6;
stroke-width: 1.2px;
opacity: 1;
}
line.record, line.avg, line.yearLow, line.yearHigh{
stroke-width: 2px;
}
line.record {
stroke: #bae0d6;
}
line.avg {
stroke: #3FA39E;
opacity: .5;
}
line.year {
stroke: #006358;
}
line.yearLow, line.yearHigh{
stroke: #F97F5A;
}
.avg {
stroke: #3FA39E;
fill: #3FA39E;
}
.record {
stroke: #bae0d6;
fill: #bae0d6;
.opacity: .5;
}
.year {
stroke: #F97F5A;
fill: #F97F5A;
}
.beyond {
stroke: #445E5B;
fill: #445E5B;
}
</style>
</head>
<body>
<svg width=960 height=500 class=noselect></svg>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.3.0/d3-legend.min.js"></script>
<script src="d3.svg.circularbrush.js" charset="utf-8" type="text/javascript"></script>
<script>
var width = 960,
margin = 20,
height = 500,
svg = d3.select('svg'),
origin = svg.append('g')
.attr('transform', 'translate(' + width*3/5 + ',' + height/2 + ')'),
rScale = d3.scale.linear()
.domain([-10, 110])
.range([0, height/2 - margin]),
yScale = function (day, temp) {return -Math.cos(angleScale(day)*Math.PI/180)*rScale(parseInt(temp))},
xScale = function (day, temp) {return Math.sin(angleScale(day)*Math.PI/180)*rScale(parseInt(temp))},
angleScale = d3.scale.linear()
.range([0, 360]);
var drawRadial = function(chart, cl, data, low, high){
chart.selectAll('line.' + cl)
.data(data)
.enter().append('line')
.attr('x1', function (d) {return xScale(d.index, d[low])})
.attr('x2', function (d) {return xScale(d.index, d[high])})
.attr('y1', function (d) {return yScale(d.index, d[low])})
.attr('y2', function (d) {return yScale(d.index, d[high])})
.attr('class', cl);
};
d3.json('ny.json', function(err, json){
rawData = json;
angleScale.domain([0, json.values.length - 1]);
var min = d3.min(json.values, function (d) {return parseInt(d.recLow)}),
max = d3.max(json.values, function (d) {return parseInt(d.recHigh)});
var months = [];
//find index for months based on data
json.values.forEach(function (d, i) {
var month = d.date.split('-')[1],
prevDaysMonth = ( i === 0 ) ? undefined : json.values[i - 1].date.split('-')[1];
if (i === 0 || month != prevDaysMonth){
months.push({
month: month,
index: i
});
}
})
//circle axis
origin.selectAll('circle.axis-green')
.data([40, 60, 80, 100])
.enter().append('circle')
.attr('r', function (d) {return rScale(d)})
.attr('class', 'axis record')
//record low and high
drawRadial(origin, 'record', json.values, 'recLow', 'recHigh')
//avg low and high
drawRadial(origin, 'avg', json.values, 'avgLow', 'avgHigh')
//this year's temperature
var thisYear = json.values.filter(function (d) {return d.min });
drawRadial(origin, 'year', thisYear, 'min', 'max')
var lowLower = json.values.filter(function (d) {return d.min && parseInt(d.min) < parseInt(d.avgLow)});
drawRadial(origin, 'yearLow', lowLower, 'min', 'avgLow')
var highHigher = json.values.filter(function (d) {return d.min && parseInt(d.max) > parseInt(d.avgHigh)});
drawRadial(origin, 'yearHigh', highHigher, 'max', 'avgHigh')
var circleAxis = [0, 32, 60, 80, 100]
circleAxis = circleAxis.map( function (d) {return {temp: d, index: 320}})
//temperature axis
origin.selectAll('circle.axis-white')
.data(circleAxis)
.enter().append('circle')
.attr('r', function (d) {return rScale(d.temp)})
.attr('class', 'axis')
//temperature axis labels
origin.selectAll('text.temp')
.data(circleAxis)
.enter().append('text')
.attr('x', function (d) {
return xScale(d.index, d.temp)})
.attr('y', function (d) {return yScale(d.index, d.temp)})
.text(function (d) {return d.temp + '°'})
.attr('class', 'temp');
//axis lines
var axis = origin.append('g');
axis.selectAll('line.axis')
.data(months)
.enter().append('line')
.attr('x2', function (d) {
return xScale(d.index, 120)})
.attr('y2', function (d) {return -yScale(d.index, 120)})
.attr('class', 'axis');
var monthLabels = months.filter( function (d,i) {return i%3 === 0})
//month labels
axis.selectAll('text.months')
.data(monthLabels)
.enter().append('text')
.attr('x', function (d) {
return xScale(d.index, 110)})
.attr('y', function (d) {return yScale(d.index, 110)})
.text(function (d) {return d.month})
.attr('class', 'months');
//center for reference
axis.append('circle')
.attr('r', 3)
.attr('class', 'avg')
//title
svg.append('text')
.attr('x', 30)
.attr('y', 60)
.text(json.name)
.attr('class', 'title')
//subtitle
svg.append('text')
.attr('x', 30)
.attr('y', 100)
.text('Historical Weather Data')
//create legend
var legendScale = d3.scale.ordinal()
.domain(['Record', 'Average', 'This Year - within avg', 'This Year - beyond avg', 'Freezing', 'Precipitation', 'Scattered Clouds', "Cloudy", "Overcast"])
.range(['record', 'avg', 'beyond', 'year', 'freeze', 'rain', 'scattered', 'cloudy', 'overcast'])
//d3-legend
var legend = d3.legend.color()
.shapePadding(5)
.useClass(true)
.scale(legendScale);
svg.append('g')
.attr('transform', 'translate(30,120)')
.call(legend);
d3.json("cloud_rain_freeze.json", loadBars);
});
function loadBars(data) {
var freezeBars = [];
var rainBars = [];
var cloudBars = [];
var freeze = {};
var cloud = {start: data[0].date, category: data[0].cloud};
var rain = {};
data.forEach(function (d, i) {
if (d.cloud !== cloud.category) {
cloud.end = d.date;
cloudBars.push(cloud);
cloud = {start: d.date, category: d.cloud};
}
if (freeze.start && !d.freeze) {
freeze.end = d.date;
freezeBars.push(freeze);
freeze = {};
}
else if (d.freeze && !freeze.start) {
freeze.start = d.date;
}
if (rain.start && !d.rain) {
rain.end = d.date;
rainBars.push(rain);
rain = {};
}
else if (d.rain && !rain.start) {
rain.start = d.date;
}
});
drawBars(rainBars, "rain", 205);
drawBars(freezeBars, "freeze", 200);
drawBars(cloudBars, "freeze", 210);
}
function loadBars(data) {
freezeBars = [];
rainBars = [];
cloudBars = [];
var freeze = {};
var cloud = {start: data[0].date, category: data[0].cloud};
var rain = {};
var dateScale = d3.time.scale().domain([new Date("01/01/2015"), new Date("12/31/2015")]).range([1,366]);
data.forEach(function (d, i) {
if (d.cloud !== cloud.category) {
cloud.end = d.date;
cloud.endInt = dateScale(new Date(d.date));
cloudBars.push(cloud);
cloud = {start: d.date, startInt: dateScale(new Date(d.date)), category: d.cloud};
}
if (freeze.start && !d.freeze) {
freeze.end = d.date;
freeze.endInt = dateScale(new Date(d.date));
freezeBars.push(freeze);
freeze = {};
}
else if (d.freeze && !freeze.start) {
freeze.start = d.date;
freeze.startInt = dateScale(new Date(d.date));
}
if (rain.start && !d.rain) {
rain.end = d.date;
rain.endInt = dateScale(new Date(d.date));
rainBars.push(rain);
rain = {};
}
else if (d.rain && !rain.start) {
rain.start = d.date;
rain.startInt = dateScale(new Date(d.date));
}
});
drawBars(rainBars, "rain", 205);
drawBars(freezeBars, "freeze", 200);
drawBars(cloudBars, "cloud", 210);
drawBrush();
}
function drawBrush() {
brush = d3.svg.circularbrush()
.range([1,366])
.innerRadius(10)
.outerRadius(218)
.handleSize(0.1)
.on("brush", brush);
d3.select("svg")
.append("g")
.attr("class", "brush")
.attr("transform", "translate(576,250)")
.call(brush);
d3.select("svg").append("g")
.attr("class", "linear")
.attr("transform", "translate(40,350)")
function brush() {
extent = brush.extent();
var yScale = d3.scale.linear().domain([-10,110]).range([100,0]).clamp(true);
var xScale = d3.scale.linear().domain([1,366]).range([0,250]);
var start = extent[0];
var end = extent[1];
var barOffset = 0;
if (start < end) {
filteredData = rawData.values.filter(function (d) {
return d.index >= start && d.index <= end;
});
filteredRainbars = rainBars.filter(function (d) {
var sDate = d.startInt;
var eDate = d.endInt;
return (eDate <= end && eDate >= start) || (sDate <= end && sDate >= start)
})
filteredFreezebars = freezeBars.filter(function (d) {
var sDate = d.startInt;
var eDate = d.endInt;
return (eDate <= end && eDate >= start) || (sDate <= end && sDate >= start)
})
filteredCloudbars = cloudBars.filter(function (d) {
var sDate = d.startInt;
var eDate = d.endInt;
return (eDate <= end && eDate >= start) || (sDate <= end && sDate >= start)
})
}
else {
var janone = 1;
var decthirtyone = 366
var filteredDataEarly = rawData.values.filter(function (d) {
return (d.index >= start && d.index <= decthirtyone);
});
var filteredDataAfter = rawData.values.filter(function (d) {
return (d.index <= end && d.index >= janone);
});
barOffset = filteredDataEarly.length;
earlyMin = d3.min(filteredDataEarly, function (d) {return d.index})
filteredFreezebarsEarly = beforeBars(freezeBars)
filteredFreezebarsAfter = afterBars(freezeBars)
filteredRainbarsEarly = beforeBars(rainBars)
filteredRainbarsAfter = afterBars(rainBars)
filteredCloudbarsEarly = beforeBars(cloudBars);
filteredCloudbarsAfter = afterBars(cloudBars);
filteredData = filteredDataEarly.concat(filteredDataAfter);
filteredFreezebars = filteredFreezebarsEarly.concat(filteredFreezebarsAfter);
filteredRainbars = filteredRainbarsEarly.concat(filteredRainbarsAfter);
filteredCloudbars = filteredCloudbarsEarly.concat(filteredCloudbarsAfter);
function beforeBars(bars) {
return bars.filter(function (d) {
var sDate = d.startInt;
var eDate = d.endInt;
(d.index >= start && d.index <= decthirtyone)
return (eDate >= start && eDate <= decthirtyone) || (sDate >= start && sDate <= decthirtyone)
}).map(function (d) {
return {startInt: d.startInt - earlyMin, endInt: d.endInt - earlyMin, category: d.category}
})
}
function afterBars(bars) {
return bars.filter(function (d) {
var sDate = d.startInt;
var eDate = d.endInt;
(d.index >= start && d.index <= decthirtyone)
return (eDate <= end && eDate >= janone) || (sDate <= end && sDate >= janone)
}).map(function (d) {
return {startInt: d.startInt + barOffset, endInt: d.endInt + barOffset, category: d.category}
})
}
}
var lineWidth = 250 / filteredData.length;
minDate = d3.min(filteredData, function (d) {return d.index})
maxDate = d3.max(filteredData, function (d) {return d.index})
xScale.domain([0, filteredData.length]);
d3.select("g.linear")
.selectAll("g.linearBars")
.remove();
d3.select("g.linear")
.selectAll("rect")
.remove();
d3.select("g.linear")
.selectAll("text")
.remove();
d3.select("g.linear")
.selectAll("rect.rainbars")
.data(filteredRainbars)
.enter()
.append("rect")
.attr("class", "rain")
.attr("y", -10)
.attr("x", function (d) {return xScale((d.startInt - minDate)) })
.attr("width", function (d) {return Math.min(250 - xScale((d.startInt - minDate)), (d.endInt - d.startInt) * lineWidth) })
.attr("height", "5px")
d3.select("g.linear")
.selectAll("rect.freezebars")
.data(filteredFreezebars)
.enter()
.append("rect")
.attr("class", "freeze")
.attr("y", -0)
.attr("x", function (d) {return xScale((d.startInt - minDate)) })
.attr("width", function (d) {return Math.min(250 - xScale((d.startInt - minDate)), (d.endInt - d.startInt) * lineWidth) })
.attr("height", "5px")
d3.select("g.linear")
.selectAll("rect.cloudbars")
.data(filteredCloudbars)
.enter()
.append("rect")
.attr("class", function (d) {return d.category })
.attr("y", -20)
.attr("x", function (d) {return xScale((d.startInt - minDate)) })
.attr("width", function (d) {return Math.min(250 - xScale((d.startInt - minDate)), (d.endInt - d.startInt) * lineWidth) })
.attr("height", "5px")
d3.select("g.linear")
.selectAll("g.linearBars")
.data(filteredData, function (d) {return d.date})
.enter()
.insert("g", "rect")
.attr("class", "linearBars")
.each(function (d, i) {
if (i === 0 || i === filteredData.length - 1) {
d3.select(this).append("text")
.text(d.date)
.attr("y", -30)
.style("text-anchor", "middle");
}
d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "highlightline")
d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "record")
d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "avg")
d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "yearLow")
d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "yearHigh")
d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "year")
d3.select(this).append("line").style("stroke-width", lineWidth).attr("class", "hoverline")
});
d3.selectAll("g.linearBars")
.attr("transform", function (d,i) {return "translate(" + xScale(i) +",0)" });
d3.selectAll("g.linearBars")
.each(function (d) {
var thisG = this;
d3.select(this).select("line.highlightline")
.attr("y1", -30)
.attr("y2", 100)
.style("stroke-width", 1)
.style("stroke", "black")
.style("opacity", 0);
d3.select(this).select("line.hoverline")
.attr("y1", -30)
.attr("y2", 100)
.style("stroke-width", lineWidth)
.style("opacity", 0.0)
.style("stroke", "black")
.on("mouseover", function () {
d3.select(thisG).select("line.highlightline").style("opacity", 1);
})
.on("mouseout", function () {
d3.selectAll("line.highlightline").style("opacity", 0);
})
d3.select(this).select("line.record")
.attr("y1", yScale(parseInt(d.recHigh)))
.attr("y2", yScale(parseInt(d.recLow)));
d3.select(this).select("line.avg")
.attr("y1", yScale(parseInt(d.avgHigh)))
.attr("y2", yScale(parseInt(d.avgLow)));
if (d.max != null) {
if (d.min < parseInt(d.avgLow)) {
d3.select(this).select("line.yearLow")
.attr("y1", yScale(parseInt(d.min)))
.attr("y2", yScale(parseInt(d.avgLow)));
}
if (d.max > parseInt(d.avgHigh)) {
d3.select(this).select("line.yearHigh")
.attr("y1", yScale(parseInt(d.max)))
.attr("y2", yScale(parseInt(d.avgHigh)));
}
if (!(d.min > parseInt(d.avgHigh) || d.max < parseInt(d.avgLow))) {
d3.select(this).select("line.year")
.attr("y1", yScale(Math.max(d.min,parseInt(d.avgLow))))
.attr("y2", yScale(Math.min(d.max,parseInt(d.avgHigh))));
}
}
})
}
}
function drawBars(data, type, offset) {
dateScale = d3.scale.linear()
.domain([1,366])
.range([0,(2 * Math.PI)]);
var arc = d3.svg.arc().innerRadius(offset).outerRadius(offset + 5);
d3.select("svg").append("g")
.attr("class", type + "bars")
.attr("transform", "translate(576,250)")
.selectAll("path")
.data(data)
.enter()
.append("path")
.attr("d", drawArc)
.attr("class", function (d) {return type + " " + d.category})
function drawArc(d) {
projected = {startAngle: dateScale(d.startInt), endAngle: dateScale(d.endInt) };
return arc(projected);
}
}
</script>
</body>
</html>
https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.js
https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.3.0/d3-legend.min.js