Skip to content

Instantly share code, notes, and snippets.

@bobmonteverde
Created March 11, 2012 22:39
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 bobmonteverde/2018493 to your computer and use it in GitHub Desktop.
Save bobmonteverde/2018493 to your computer and use it in GitHub Desktop.
<!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) {
x .domain(d3.extent(d3.merge(data), function(d) { return d[0] } ))
.range([0, width - margin.left - margin.right]);
y .domain(d3.extent(d3.merge(data), 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(series, index) {
return series.map(function(point) {
return [x(point[0]), y(point[1]), index]; //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,i) { return d[0] + '-' + d[2] }) //TODO: FIX KEY FUNCTION!!
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 color[i] ? color[i].substr(1) : undefined }); //TODO: Fix key function!!!
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) { return [d] });
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(series)
.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