Created
March 11, 2012 06:22
-
-
Save bobmonteverde/2015279 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
text { | |
font: 12px sans-serif; | |
} | |
.axis path { | |
fill: none; | |
stroke: #000; | |
stroke-opacity: .75; | |
shape-rendering: crispEdges; | |
} | |
.x.axis path.domain, | |
.y.axis path.domain { | |
stroke-opacity: .75; | |
} | |
.axis line { | |
fill: none; | |
stroke: #000; | |
stroke-opacity: .25; | |
shape-rendering: crispEdges; | |
} | |
.axis line.zero { | |
stroke-opacity: .75; | |
} | |
.lines path { | |
fill: none; | |
stroke-width: 1.5px; | |
stroke-opacity: 1; | |
} | |
.point-paths path { | |
stroke: none; | |
} | |
.point.hover { | |
stroke: #000 !important; | |
stroke-width: 15px; | |
stroke-opacity: .2; | |
} | |
</style> | |
<body> | |
<svg id="test1"></svg> | |
<script src="http://mbostock.github.com/d3/d3.v2.js"></script> | |
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script> | |
<script> | |
function genericLineChart() { | |
var margin = {top: 20, right: 10, bottom: 40, left: 60}, | |
width = 960, | |
height = 500, | |
animate = 500, | |
dotRadius = function() { return 2.5 }, | |
xAxisRender = true, | |
yAxisRender = true, | |
xAxisLabelText = false, | |
yAxisLabelText = false, | |
color = d3.scale.category20().range(); | |
//TODO: consider calculating margins based on if axes, axis labels, legend, etc. exist. | |
var x = d3.scale.linear(), | |
y = d3.scale.linear(), | |
xAxis = d3.svg.axis().scale(x).orient('bottom'), //TODO: Time scale for time series? | |
yAxis = d3.svg.axis().scale(y).orient('left'); | |
function chart(selection) { | |
selection.each(function(data) { | |
//TODO: implement optional accessors to remap data automatically | |
var series = data.map(function(d) { return d.data }); | |
x .domain(d3.extent(d3.merge(series), function(d) { return d.x } )) | |
.range([0, width - margin.left - margin.right]); | |
y .domain(d3.extent(d3.merge(series), function(d) { return d.y } )) | |
.range([height - margin.top - margin.bottom, 0]); | |
xAxis.ticks( width / 100 ).tickSize(-(height - margin.top - margin.bottom), 0); | |
yAxis.ticks( height / 36 ).tickSize(-(width - margin.right - margin.left), 0); | |
var wrap = d3.select(this).selectAll('g.wrap').data([data]); | |
var gEnter = wrap.enter().append('g').attr('class', 'wrap d3line').append('g'); | |
gEnter.append('g').attr('class', 'x axis'); | |
gEnter.append('g').attr('class', 'y axis'); | |
gEnter.append('g').attr('class', 'lines'); | |
gEnter.append('g').attr('class', 'point-clips'); | |
gEnter.append('g').attr('class', 'points'); | |
gEnter.append('g').attr('class', 'point-paths'); | |
var g = wrap.select('g') | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); | |
//TODO: Think of a more elegant solution to store series index and series color on data points | |
var vertices = d3.merge(data.map(function(d,i) { | |
return d.data.map(function(p) { | |
return [x(p.x), y(p.y), i, d.color]; | |
}) | |
})); | |
//TODO: make IDs unique for when there are multiple charts on one page | |
var voronoiClip = gEnter.append('g').attr('class', 'voronoi-clip') | |
.append('clipPath') | |
.attr('id', 'voronoi-clip-path') | |
.append('rect'); | |
wrap.select('.voronoi-clip rect') | |
.attr('x', -10) | |
.attr('y', -10) | |
.attr('width', width - margin.left - margin.right + 20) | |
.attr('height', height - margin.top - margin.bottom + 20); | |
wrap.select('.point-paths') | |
.attr('clip-path', 'url(#voronoi-clip-path)'); | |
var pointClips = wrap.select('.point-clips').selectAll('.clip-path') | |
.data(vertices); | |
pointClips.enter().append('clipPath').attr('class', 'clip-path') | |
.attr('id', function(d, i) { return 'clip-' + i }) | |
.append('circle') | |
.attr('r', 20); | |
pointClips.exit().remove(); | |
pointClips | |
.attr('transform', function(d) { return d[3] !== 'none' ? | |
'translate(' + d[0] + ',' + d[1] + ')' : | |
'translate(-100,-100)' }) | |
var pointPaths = wrap.select('.point-paths').selectAll('path') | |
.data(d3.geom.voronoi(vertices)); | |
pointPaths.enter().append('path') | |
.attr('class', function(d,i) { return 'path-'+i; }) | |
.style('fill', d3.rgb(230, 230, 230,0)) | |
.style('fill-opacity', 0); | |
pointPaths | |
.attr('clip-path', function(d,i) { return 'url(#clip-'+i+')'; }) | |
.attr('d', function(d) { return 'M' + d.join(',') + 'Z'; }) | |
wrap.select('.point-paths').selectAll('path') | |
.on('mouseover', function(d, i) { | |
wrap.select('circle.point-' + i) | |
.classed('hover', true) | |
//TODO: show toolip | |
//log(Math.round(x.invert(wrap.select('circle#point-' + i).attr('cx'))*10000)/10000, | |
//Math.round(y.invert(wrap.select('circle#point-' + i).attr('cy'))*10000)/10000); | |
}) | |
.on('mouseout', function(d, i) { | |
wrap.select('circle.point-' + i) | |
.classed('hover', false) | |
//TODO: hide toolip | |
}); | |
//TODO: Fix the way I do point detection so that the other points implementation | |
// where points are stored with their series can be used | |
var points = wrap.select('.points').selectAll('circle.point') | |
.data(vertices) | |
points.enter().append('circle') | |
.attr('class', function(d,i) { return 'point point-'+i; }) | |
.attr('cx', function(d) { return d[0] }) | |
.attr('cy', function(d) { return y.range()[0] }) | |
.style('fill', function(d, i){ return d[3] || color[d[2] * 2 % 20] }) | |
points.exit().remove(); | |
points | |
.attr('r', dotRadius()) | |
.transition().duration(animate) | |
.attr('cx', function(d,i) { | |
if (typeof d[0] === 'object') console.log("Error on Point#" + i + "d value IS :", d, "-----SHOULD BE: ", vertices[i]) | |
return vertices[i][0] | |
}) | |
.attr('cy', function(d,i) { return vertices[i][1] }) | |
//.attr('cx', function(d) { return d[0] }) | |
//.attr('cy', function(d) { return d[1] }) | |
var lines = wrap.select('.lines').selectAll('.line') | |
.data(function(d) { return d }); | |
lines.enter().append('g').attr('class', 'line'); | |
lines.exit().remove(); | |
lines | |
.style('fill', function(d, i){ return d.color || color[i * 2 % 20] }) | |
.style('stroke', function(d, i){ return d.color || color[i * 2 % 20] }); | |
var paths = lines.selectAll('path') | |
.data(function(d) { return [d.data] }); | |
paths.enter().append('path') | |
.attr('d', d3.svg.line() | |
.x(function(d) { return x(d.x) }) | |
.y(function(d) { return y.range()[0] }) | |
); | |
paths.exit().remove(); | |
paths | |
.transition().duration(animate) | |
.attr('d', d3.svg.line() | |
.x(function(d) { return x(d.x) }) | |
.y(function(d) { return y(d.y) }) | |
); | |
/* | |
//Old way I was plotting points, leaving incase I think of a better way for point detection | |
var points = lines.selectAll('circle.point') | |
.data(function(d) { return d.data }); | |
points.enter().append('circle').attr('class', 'point') | |
.attr('cx', function(d) { return x(d.x) }) | |
.attr('cy', function(d) { return y(y.domain()[0]) }); | |
points.exit().remove(); | |
points | |
.transition().duration(animate) | |
.attr('cx', function(d) { return x(d.x) }) | |
.attr('cy', function(d) { return y(d.y) }) | |
.attr('r', dotRadius()); | |
*/ | |
//TODO: Extract axes component with Labels for reuse | |
//TODO: Add point distribution on axes like scatterize demo, for scatter plots | |
var xAxisLabel = g.select('.x.axis').selectAll('text.axislabel') | |
.data([xAxisLabelText || null]); | |
xAxisLabel.enter().append('text').attr('class', 'axislabel') | |
.attr('text-anchor', 'middle') | |
.attr('x', x.range()[1] / 2) | |
.attr('y', 30); | |
xAxisLabel.exit().remove(); | |
xAxisLabel.text(function(d) { return d }); | |
var yAxisLabel = g.select('.y.axis').selectAll('text.axislabel') | |
.data([yAxisLabelText || null]); | |
yAxisLabel.enter().append('text').attr('class', 'axislabel') | |
.attr('transform', 'rotate(-90)') | |
.attr('text-anchor', 'middle') | |
.attr('y', -30); | |
yAxisLabel.exit().remove(); | |
yAxisLabel | |
.attr('x', -y.range()[0] / 2) | |
.text(function(d) { return d }); | |
g.select('.x.axis') | |
.attr('transform', 'translate(0,' + y.range()[0] + ')') | |
.call(xAxis) | |
.selectAll('line.tick') | |
.filter(function(d) { return !d }) | |
.classed('zero', true); | |
g.select('.y.axis') | |
.call(yAxis) | |
.selectAll('line.tick') | |
.filter(function(d) { return !d }) | |
.classed('zero', true); | |
}); | |
return chart; | |
} | |
chart.margin = function(_) { | |
if (!arguments.length) return margin; | |
margin = _; | |
return chart; | |
}; | |
chart.width = function(_) { | |
if (!arguments.length) return width; | |
width = _; | |
return chart; | |
}; | |
chart.height = function(_) { | |
if (!arguments.length) return height; | |
height = _; | |
return chart; | |
}; | |
chart.dotRadius = function(_) { | |
if (!arguments.length) return dotRadius; | |
dotRadius = d3.functor(_); | |
return chart; | |
}; | |
chart.animate = function(_) { | |
if (!arguments.length) return animate; | |
animate = _; | |
return chart; | |
}; | |
//TODO: fix this so that tickFormat returns instance of chart | |
//TODO: make axes optional | |
//TODO (MAYBE): expose axes directly on chart | |
// Expose the x-axis' tickFormat method. | |
chart.xAxis = {}; | |
d3.rebind(chart.xAxis, xAxis, 'tickFormat'); | |
chart.xAxis.label = function(_) { | |
if (!arguments.length) return xAxisLabelText; | |
xAxisLabelText = _; | |
return chart; | |
} | |
// Expose the y-axis' tickFormat method. | |
chart.yAxis = {}; | |
d3.rebind(chart.yAxis, yAxis, 'tickFormat'); | |
chart.yAxis.label = function(_) { | |
if (!arguments.length) return yAxisLabelText; | |
yAxisLabelText = _; | |
return chart; | |
} | |
return chart; | |
} | |
$(document).ready(function() { | |
var margin = {top: 20, right: 10, bottom: 50, left: 60}, | |
chart = genericLineChart() | |
.xAxis.label('Time (ms)') | |
.width(width(margin)) | |
.height(height(margin)) | |
.yAxis.label('Voltage (v)'); | |
//chart.xaxis.tickFormat(d3.format(".02f")) | |
d3.select('#test1') | |
.datum(sinAndCos()) | |
.attr('width', width(margin)) | |
.attr('height', height(margin)) | |
.call(chart); | |
$(window).resize(function() { | |
var margin = chart.margin(), | |
animate = chart.animate(); | |
chart | |
.animate(0) | |
.width(width(margin)) | |
.height(height(margin)); | |
d3.select('#test1') | |
.attr('width', width(margin)) | |
.attr('height', height(margin)) | |
.call(chart); | |
chart | |
.animate(animate); | |
}); | |
function width(margin) { | |
var w = $(window).width() - 40; | |
return ( (w - margin.left - margin.right - 20) < 0 ) ? margin.left + margin.right + 2 : w; | |
} | |
function height(margin) { | |
var h = $(window).height() - 40; | |
return ( h - margin.top - margin.bottom - 20 < 0 ) ? | |
margin.top + margin.bottom + 2 : h; | |
} | |
//data | |
function sinAndCos() { | |
var sin = [], | |
cos = []; | |
for (var i = 0; i < 100; i++) { | |
sin.push({x: i, y: Math.sin(i/10)}); | |
cos.push({x: i, y: .5 * Math.cos(i/10)}); | |
} | |
return [ | |
{ | |
data: sin, | |
label: "Sine Wave", | |
color: "#ff7f0e" | |
}, | |
{ | |
data: cos, | |
label: "Cosine Wave", | |
color: "#2ca02c" | |
} | |
]; | |
} | |
}); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment