Created
March 12, 2012 03:48
-
-
Save bobmonteverde/2019586 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; | |
} | |
.legend .series { | |
cursor: pointer; | |
} | |
.legend .disabled circle { | |
fill-opacity: 0; | |
} | |
</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> | |
var nv = {models: {}}; | |
nv.models.legend = function() { | |
var margin = {top: 5, right: 0, bottom: 5, left: 10}, | |
width = 400, | |
height = 20, | |
color = d3.scale.category20().range(), | |
dispatch = d3.dispatch('toggle'); | |
//TODO: rethink communication between charts: | |
// **Maybe everything should be through dispatch instead of linking using the same data | |
// **Maybe not | |
function chart(selection) { | |
selection.each(function(data) { | |
var wrap = d3.select(this).selectAll('g.legend').data([data]); | |
var gEnter = wrap.enter().append('g').attr('class', 'legend').append('g'); | |
var g = wrap.select('g') | |
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); | |
var series = g.selectAll('.series') | |
.data(function(d) { return d }); | |
series.exit().remove(); | |
var seriesEnter = series.enter().append('g').attr('class', 'series') | |
.on('click', function(d, i) { | |
d.disabled = !d.disabled; | |
d3.select(this).classed('disabled', d.disabled); | |
if (!data.filter(function(d) { return !d.disabled }).length) { | |
data.map(function(d) { | |
d.disabled = false; | |
wrap.selectAll('.series').classed('disabled', false); | |
return d; | |
}); | |
} | |
dispatch.toggle(d, i); | |
}); | |
seriesEnter.append('circle') | |
.style('fill', function(d, i){ return d.color || color[i * 2 % 20] }) | |
.style('stroke', function(d, i){ return d.color || color[i * 2 % 20] }) | |
.style('stroke-width', 2) | |
.attr('r', 5); | |
seriesEnter.append('text') | |
.text(function(d) { return d.label }) | |
.attr('text-anchor', 'start') | |
.attr('dy', '.32em') | |
.attr('dx', '8'); | |
var ypos = 5, | |
newxpos = 5, | |
maxwidth = 0, | |
xpos; | |
series | |
.attr('transform', function(d, i) { | |
var length = d3.select(this).select('text').node().getComputedTextLength() + 28; | |
xpos = newxpos; | |
//TODO: 1) Make sure dot + text of every series fits horizontally, or clip text to fix | |
// 2) Consider making columns in line so dots line up | |
// --all labels same width? or just all in the same column? | |
// --optional, or forced always? | |
if (width < margin.left + margin.right + xpos + length) { | |
newxpos = xpos = 5; | |
ypos += 20; | |
} | |
newxpos += length; | |
if (newxpos > maxwidth) maxwidth = newxpos; | |
return 'translate(' + xpos + ',' + ypos + ')'; | |
}); | |
//position legend as far right as possible within the total width | |
g.attr('transform', 'translate(' + (width - margin.right - maxwidth) + ',' + margin.top + ')'); | |
//update height value if calculated larger than current | |
if (height < margin.top + margin.bottom + ypos + 15) | |
height = margin.top + margin.bottom + ypos + 15; | |
//TODO: Fix dimenstion calculations... now if height grows automatically, it doesn't shrink back | |
// 1) Can have calc height vs set height and use largest | |
// 2) Consider ONLY allowing a single dimension, height or width, and the other one always elastic | |
}); | |
return chart; | |
} | |
chart.dispatch = dispatch; | |
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; | |
}; | |
return chart; | |
} | |
nv.models.line = function() { | |
var margin = {top: 0, right: 0, bottom: 0, left: 0}, | |
width = 960, | |
height = 500, | |
animate = 500, | |
dotRadius = function() { return 2.5 }, | |
color = d3.scale.category10().range(), | |
id = Math.floor(Math.random() * 10000), //Create semi-unique ID incase user doesn't select one | |
x = d3.scale.linear(), | |
y = d3.scale.linear(); | |
function chart(selection) { | |
selection.each(function(data) { | |
var series = data.map(function(d) { return d.data }); | |
x .domain(d3.extent(d3.merge(series), function(d) { return d[0] } )) | |
.range([0, width - margin.left - margin.right]); | |
y .domain(d3.extent(d3.merge(series), function(d) { return d[1] } )) | |
.range([height - margin.top - margin.bottom, 0]); | |
var wrap = d3.select(this).selectAll('g.d3line').data([data]); | |
var gEnter = wrap.enter().append('g').attr('class', 'd3line').append('g'); | |
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 + ')'); | |
var vertices = d3.merge(data.map(function(line, index) { | |
return line.data.map(function(point) { | |
var pointKey = line.label + '-' + point[0]; | |
return [x(point[0]), y(point[1]), index, pointKey]; //adding series index to point because data is being flattened | |
}) | |
}) | |
); | |
//TODO: now that user can change ID, probably need to set ID, and anything that uses them, on update not just enter selection | |
var voronoiClip = gEnter.append('g').attr('class', 'voronoi-clip') | |
.append('clipPath') | |
.attr('id', 'voronoi-clip-path-' + id) | |
.append('rect'); | |
wrap.select('.voronoi-clip rect') | |
.attr('x', -10) // TODO: reconsider this static -10, while it makes sense, maybe should calculate from margin | |
.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('clipPath') // **BROWSER BUG** can't reselect camel cased elements | |
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-' + id + '-' + 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)' }) | |
//TODO: ****MUST REMOVE DUPLICATES**** ....figure out best equation for this | |
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('stroke', d3.rgb(230, 230, 230,0)) | |
.style('fill-opacity', 0); | |
pointPaths | |
.attr('clip-path', function(d,i) { return 'url(#clip-' + id + '-' + i +')'; }) | |
.attr('d', function(d) { return 'M' + d.join(',') + 'Z'; }) | |
.on('mouseover', function(d, i) { | |
wrap.select('circle.point-' + i) | |
.classed('hover', true) | |
//TODO: Figure out what broke point interaction in Firefox | |
//TODO: don't derive value, use ACTUAL... make sure to use axis tickFormat to format values in tooltip | |
//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: consider putting points ONTOP of voronoi paths, and adding hover toggle, this way there won't | |
// be any funky or unreachable points, while random with the voronoi, some points near the edge | |
// can be hard/impossible to get to (appears to be a very small cornor case) | |
var points = wrap.select('.points').selectAll('circle.point') | |
.data(vertices, function(d) { console.log(d[3]); return d[3] }) | |
points.enter().append('circle') | |
.attr('class', function(d,i) { return 'point point-' + i + ' series' + d[2] }) //TODO: consider using unique ID | |
.attr('cx', function(d) { return d[0] }) | |
.attr('cy', function(d) { return y.range()[0] }); | |
points.exit().remove(); | |
points | |
.style('fill', function(d, i){ return color[d[2] % 20] }) | |
.attr('r', dotRadius()) | |
.transition().duration(animate) | |
.attr('cx', function(d,i) { if (typeof d[0] == 'object') { console.log("Why? d[0]",d,i,vertices[i])} return vertices[i][0] }) | |
.attr('cy', function(d,i) { return vertices[i][1] }); | |
//TODO: Fix above workaround, likely causing points and paths to be mis align | |
// ***think it only occurs when shrinking the vertical, appears to be caused by | |
// the scroll bar being introduced, then removed, between the 2 calculations | |
var lines = wrap.select('.lines').selectAll('.line') | |
.data(function(d) { return d }, function(d,i) { return d.label }); | |
lines.enter().append('g').attr('class', function(d,i) { return 'line series' + i }); | |
lines.exit().remove(); | |
lines | |
.style('fill', function(d,i){ return color[i % 20] }) | |
.style('stroke', function(d,i){ return color[i % 20] }); | |
var paths = lines.selectAll('path') | |
.data(function(d, i) { return [d.data] }); | |
paths.enter().append('path') | |
.attr('d', d3.svg.line() | |
.x(function(d) { return x(d[0]) }) | |
.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[0]) }) | |
.y(function(d) { return y(d[1]) }) | |
); | |
/* | |
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()); | |
*/ | |
}); | |
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; | |
}; | |
chart.color = function(_) { | |
if (!arguments.length) return color; | |
color = _; | |
return chart; | |
}; | |
chart.id = function(_) { | |
if (!arguments.length) return id; | |
id = _; | |
return chart; | |
}; | |
return chart; | |
} | |
nv.models.lineWithLegend = function() { | |
var margin = {top: 20, right: 20, bottom: 50, 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(); | |
var x = d3.scale.linear(), | |
y = d3.scale.linear(), | |
xAxis = d3.svg.axis().scale(x).orient('bottom'), | |
yAxis = d3.svg.axis().scale(y).orient('left'), | |
legend = nv.models.legend().height(30), | |
lines = nv.models.line(); | |
function chart(selection) { | |
selection.each(function(data) { | |
var series = data.filter(function(d) { return !d.disabled }) | |
.map(function(d) { return d.data }); | |
x .domain(d3.extent(d3.merge(series), function(d) { return d[0] } )) | |
.range([0, width - margin.left - margin.right]); | |
y .domain(d3.extent(d3.merge(series), function(d) { return d[1] } )) | |
.range([height - margin.top - margin.bottom, 0]); | |
lines | |
.width(width - margin.left - margin.right) | |
.height(height - margin.top - margin.bottom) | |
.color(data.map(function(d,i) { | |
return d.color || color[i * 2 % 20]; | |
}).filter(function(d,i) { return !data[i].disabled })) | |
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 d3lineWithLegend').append('g'); | |
gEnter.append('g').attr('class', 'legendWrap'); | |
gEnter.append('g').attr('class', 'x axis'); | |
gEnter.append('g').attr('class', 'y axis'); | |
gEnter.append('g').attr('class', 'linesWrap'); | |
legend.dispatch.on('toggle', function(d,i) { chart(selection) }); | |
legend.width(width/2 - margin.right); | |
wrap.select('.legendWrap') | |
.datum(data) | |
.attr('transform', 'translate(' + (width/2 - margin.left) + ',' + (-legend.height()) +')') | |
.call(legend); | |
//TODO: margins should be adjusted based on what components are used: axes, axis labels, legend | |
var g = wrap.select('g') | |
.attr('transform', 'translate(' + margin.left + ',' + legend.height() + ')'); | |
wrap.select('.linesWrap') | |
.datum(data.filter(function(d) { return !d.disabled })) | |
.call(lines); | |
//TODO: Extract Axis component with Label for reuse | |
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', margin.bottom - 20); | |
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', 20 - margin.left); | |
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(_); | |
lines.dotRadius = d3.functor(_); | |
return chart; | |
}; | |
chart.animate = function(_) { | |
if (!arguments.length) return animate; | |
animate = _; | |
lines.animate(_); | |
return chart; | |
}; | |
//TODO: consider directly exposing both axes | |
//chart.xAxis = xAxis; | |
//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 = yAxis; | |
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 = nv.models.lineWithLegend() | |
.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([ i, Math.sin(i/10)]); | |
cos.push([ i, .5 * Math.cos(i/10)]); | |
} | |
return [ | |
{ | |
data: sin, | |
//color: "#ff7f0e", | |
label: "Sine Wave" | |
}, | |
{ | |
data: cos, | |
//color: "#2ca02c", | |
label: "Cosine Wave" | |
} | |
]; | |
/* | |
//WHY DOES DATA IN THIS ORDER RUIN TOGGLE AFTER FIRST CLICK IN CHROME?!?!?!? | |
return [ | |
{ | |
data: cos, | |
//color: "#2ca02c", | |
label: "Cosine Wave" | |
}, | |
{ | |
data: sin, | |
//color: "#ff7f0e", | |
label: "Sine Wave" | |
} | |
]; | |
*/ | |
} | |
}); | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment