|
<!DOCTYPE html> |
|
<html> |
|
<meta charset="utf-8"> |
|
<head> |
|
<style> |
|
|
|
body { |
|
font: 10px arial; |
|
margin: auto; |
|
position: relative; |
|
width: 960px; |
|
} |
|
|
|
text { |
|
font: 10px sans-serif; |
|
} |
|
|
|
.axis15 path, |
|
.axis15 line { |
|
fill: none; |
|
stroke: #000; |
|
shape-rendering: crispEdges; |
|
} |
|
.y15.axis15 .tick:not(.tick--one) line { |
|
stroke-opacity: .2; |
|
} |
|
.y15.axis15 path { |
|
display: none; |
|
} |
|
.y15.axis15 line { |
|
} |
|
.line15 { |
|
fill: none; |
|
} |
|
|
|
form { |
|
position: relative; |
|
left: 33px; |
|
top: 10px; |
|
} |
|
.overlay { |
|
fill: none; |
|
pointer-events: all; |
|
} |
|
.focus circle { |
|
fill: white; |
|
stroke: steelblue; |
|
stroke-width:2px; |
|
} |
|
.area { |
|
fill-opacity: 0.6; |
|
} |
|
|
|
.area--below { |
|
fill: darkorange; |
|
} |
|
|
|
.area--above { |
|
fill: steelblue; |
|
} |
|
.label3 { |
|
font-size:12px; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
|
|
<form> |
|
<label><input type="radio" name="city" value="New York" checked> New York</label> |
|
<label><input type="radio" name="city" value="San Francisco"> San Francisco</label> |
|
</form> |
|
<div id="chart"> |
|
</div> |
|
<script src="//d3js.org/d3.v3.min.js"></script> |
|
<script> |
|
|
|
var margin = {top: 20, right: 80, bottom: 30, left: 50}, |
|
width = 960 - margin.left - margin.right, |
|
height = 450 - margin.top - margin.bottom; |
|
|
|
var city = "New York", |
|
parseDate = d3.time.format("%Y%m%d").parse, |
|
parseDate2 = d3.time.format("%d-%b-%y"); |
|
|
|
var formatPercent = d3.format("+.0%"), |
|
bisectDate = d3.bisector(function(d) { return d.date; }).left, |
|
formatChange = function(x) { return formatPercent(x - 1); }; |
|
|
|
var x = d3.time.scale() |
|
.range([0, width]); |
|
|
|
var y = d3.scale.linear()//log |
|
.range([height, 0]); |
|
|
|
var xAxis = d3.svg.axis() |
|
.scale(x) |
|
.orient("bottom"); |
|
|
|
var yAxis = d3.svg.axis() |
|
.scale(y) |
|
.tickSize(-width, 0) |
|
.orient("left") |
|
.tickFormat(formatChange); |
|
|
|
var line = d3.svg.line() |
|
.interpolate("cardinal") |
|
.x(function(d) { return x(d.date); }) |
|
.y(function(d) { return y(d[city]); }); |
|
|
|
var area = d3.svg.area() |
|
.interpolate("cardinal") |
|
.x(function(d) { return x(d.date); }) |
|
.y(function(d) { return y(d[city]); }); |
|
|
|
var svg = d3.select("#chart").append("svg") |
|
.attr("width", width + margin.left + margin.right) |
|
.attr("height", height + margin.top + margin.bottom) |
|
.append("g") |
|
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
|
|
var gY = svg.append("g") |
|
.attr("class", "axis15 axis15--y15"); |
|
|
|
d3.csv("data.csv", function(error, data) { |
|
if (error) throw error; |
|
var baseValue = +data[0][city]; |
|
data.forEach(function(d) { |
|
d.date = parseDate(d.date); |
|
d["New York"] = +d["New York"] / baseValue; |
|
d["San Francisco"] = +d["San Francisco"] / baseValue; |
|
}); |
|
|
|
var maximum = d3.max(data, function(d) { return d[city]; }); |
|
var maximumObj = data.filter(e=>e[city]===maximum)[0]; |
|
|
|
var minimum = d3.min(data, function(d) { return d[city]; }); |
|
|
|
x.domain([data[0].date, data[data.length - 1].date]); |
|
y.domain([0.99*d3.min(data, function(d) { return d[city]; }), |
|
1.05*d3.max(data, function(d) { return d[city]; })]); |
|
|
|
area.y0(y(1)); |
|
|
|
yAxis.tickValues(d3.scale.linear() |
|
.domain(y.domain()) |
|
.ticks(12)); |
|
|
|
var defs = svg.append("defs"); |
|
|
|
defs.append("clipPath") |
|
.attr("id", "clip-above") |
|
.append("rect") |
|
.attr("width", width) |
|
.attr("height", y(1)); |
|
|
|
defs.append("clipPath") |
|
.attr("id", "clip-below") |
|
.append("rect") |
|
.attr("y", y(1)) |
|
.attr("width", width) |
|
.attr("height", height - y(1)); |
|
|
|
svg.append("path") |
|
.datum(data) |
|
.attr("clip-path", "url(#clip-above)") |
|
.attr("class", "area area--above") |
|
.attr("d", area); |
|
|
|
svg.append("path") |
|
.datum(data) |
|
.attr("clip-path", "url(#clip-below)") |
|
.attr("class", "area area--below") |
|
.attr("d", area); |
|
|
|
svg.append("line") |
|
.attr('stroke',"#000") |
|
.attr('stroke-width',"1px") |
|
.attr('shape-rendering',"crispEdges") |
|
.attr("x1",0) |
|
.attr("x2",0) |
|
.attr("y1",0) |
|
.attr("y2",height); |
|
|
|
svg.append("g") |
|
.attr("class", "x15 axis15") |
|
.attr("transform", "translate(0," + height + ")") |
|
.call(xAxis); |
|
|
|
svg.append("g") |
|
.append("text") |
|
.attr("transform", "rotate(0)") |
|
.attr("y", 10) |
|
.attr("dx", "6em") |
|
.style("text-anchor", "end") |
|
.text("Gain / Loss"); |
|
|
|
gY.call(yAxis).attr("class", "y15 axis15") |
|
.selectAll(".tick") |
|
.classed("tick--one", function(d) { return Math.abs(d - 1) < 1e-6; }); |
|
|
|
svg.append("path") |
|
.datum(data) |
|
.attr("class", "line15") |
|
.attr("stroke","#777") |
|
.attr("stroke-width","1.5px") |
|
.attr("d", line); |
|
|
|
svg.append("text") |
|
.datum(data[data.length - 1]) |
|
.attr("class", "label") |
|
.attr("transform", transform) |
|
.attr("x", 3) |
|
.attr("dy", ".35em") |
|
.text(city); |
|
|
|
//Display current rate |
|
svg.append("text") |
|
.datum(data[data.length - 1]) |
|
.attr("class", "label2") |
|
.attr("transform", transform) |
|
.attr("x", 3) |
|
.attr("dy", "1.35em") |
|
.text(function(d){return formatChange(d[city]);}); |
|
|
|
// Display max value |
|
svg.append("text") |
|
.attr("class","maxValue") |
|
.attr("x",width/2) |
|
.attr("y",y(maximum)) |
|
.text('Peak: ' + formatChange(maximum)) |
|
|
|
// Display min value |
|
svg.append("text") |
|
.attr("class","minValue") |
|
.attr("x",width/2) |
|
.attr("y",y(minimum)) |
|
.text('Lowest: ' + formatChange(minimum)); |
|
|
|
var maxCircle = svg.append("circle") |
|
.attr("class", "maxCircle") |
|
.attr("cx", x(maximumObj.date)) |
|
.attr("cy", y(maximumObj[city])) |
|
.attr("r", 10) |
|
.attr("fill", "none") |
|
.attr("stroke", "red") |
|
.attr("stroke-width", "2px"); |
|
|
|
(function repeat() { |
|
maxCircle.transition() |
|
.duration(2000) |
|
.attr("r", 2) |
|
.transition() |
|
.duration(1000) |
|
.attr("r", 16) |
|
.ease('sine') |
|
.each("end", repeat); |
|
})(); |
|
|
|
var focus = svg.append("g") |
|
.attr("class", "focus") |
|
.style("display", "none"); |
|
|
|
focus.append("circle") |
|
.attr("class","circle") |
|
.attr("r", 3); |
|
|
|
focus.append("line").attr("class", "x--line") |
|
.style("stroke", "#777") |
|
.style("shape-rendering", "crispEdges") |
|
.style("stroke-dasharray", "1,1") |
|
.style("opacity", 0.8) |
|
.attr("y1",-height) |
|
.attr("y2",0); |
|
|
|
focus.append("text").attr("class", "y1--text") |
|
.style("stroke", "white") |
|
.style("stroke-width", "3px") |
|
.style("opacity", 0.8) |
|
.attr("dx", 8) |
|
.attr("dy", "0em"); |
|
|
|
focus.append("text").attr("class", "y2--text") |
|
.attr("fill","#000") |
|
.attr("dx", 8) |
|
.attr("dy", "0em"); |
|
|
|
focus.append("text").attr("class", "y3--text") |
|
.style("stroke", "white") |
|
.style("stroke-width", "3px") |
|
.style("opacity", 0.8) |
|
.attr("dx", 8) |
|
.attr("dy", "1em"); |
|
|
|
focus.append("text").attr("class", "y4--text") |
|
.attr("fill","#000") |
|
.attr("dx", 8) |
|
.attr("dy", "1em"); |
|
|
|
svg.append("rect").attr("class", "overlay") |
|
.attr("width", width) |
|
.attr("height", height) |
|
.on("mouseover", function() { focus.style("display", null); }) |
|
.on("mouseout", function() { focus.style("display", "none"); }) |
|
.on("mousemove", mousemove); |
|
|
|
function mousemove() { |
|
var x0 = x.invert(d3.mouse(this)[0]), |
|
i = bisectDate(data, x0, 1), |
|
d0 = data[i - 1], |
|
d1 = data[i], |
|
d = x0 - d0.date > d1.date - x0 ? d1 : d0; |
|
focus.select("text.y3--text").attr("transform", "translate(" + x(d.date) + "," + (height/2 - 6) + ")") |
|
.text( parseDate2(d.date)); |
|
focus.select("text.y4--text").attr("transform", "translate(" + x(d.date) + "," + (height/2 - 6) + ")") |
|
.text( parseDate2(d.date)); |
|
focus.select("text.y1--text").attr("transform", "translate(" + x(d.date) + "," + (height/2 - 16) + ")") |
|
.text(formatChange(d[city])); |
|
focus.select("text.y2--text").attr("transform", "translate(" + x(d.date) + "," + (height/2 - 16) + ")") |
|
.text(formatChange(d[city])); |
|
focus.select(".circle").attr("transform", "translate(" + x(d.date) + "," + y(d[city]) + ")"); |
|
focus.select(".x--line").attr("transform", "translate(" + x(d.date) + "," + (height) + ")"); |
|
} |
|
|
|
d3.selectAll("input").on("change", change); |
|
|
|
function change() { |
|
|
|
city = this.value; |
|
var maximum = d3.max(data, function(d) { return d[city]; }); |
|
var minimum = d3.min(data, function(d) { return d[city]; }); |
|
|
|
|
|
|
|
// Then transition the y-axis. |
|
y.domain([0.99*d3.min(data, function(d) { return d[city]; }), |
|
1.05*d3.max(data, function(d) { return d[city]; })]); |
|
|
|
var maximumObj = data.filter(e=>e[city]===maximum)[0]; |
|
|
|
maxCircle.attr("cx", x(maximumObj.date)) |
|
.attr("cy", y(maximumObj[city])); |
|
|
|
svg.select('#clip-below>rect').transition().duration(600) |
|
.attr("y", y(1)) |
|
.attr("height", height - y(1)); |
|
svg.select('#clip-above>rect').transition().duration(600) |
|
.attr("height", y(1)); |
|
area.y0(y(1)); |
|
|
|
var t1 = svg.transition().duration(600); |
|
t1.selectAll(".line15").attr("d", line); |
|
t1.selectAll(".area").attr("d", area); |
|
t1.selectAll(".maxValue").attr("y",y(maximum)).text('Peak: ' + formatChange(maximum)) |
|
t1.selectAll(".minValue").attr("y",y(minimum)).text('Lowest: ' + formatChange(minimum)) |
|
t1.selectAll(".label").attr("transform", transform).text(city); |
|
t1.selectAll(".label2").attr("transform", transform).text(function(d){return formatChange(d[city]);}); |
|
t1.selectAll(".y15.axis15").call(yAxis); |
|
} |
|
|
|
function transform(d) { |
|
return "translate(" + x(d.date) + "," + y(d[city]) + ")"; |
|
} |
|
}); |
|
|
|
</script> |
|
</body> |
|
</html> |