Added another layer of abstraction to take care of all the glue and make the chart api extremely simple. First try, so not entirely complete, but does function.

This commit is contained in:
Bob Monteverde 2012-04-05 01:24:39 -04:00
parent 1880f11775
commit 4e7475da73
11 changed files with 394 additions and 115 deletions

81
examples/lineChart.html Normal file
View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<meta charset="utf-8">
<link href="../src/d3.css" rel="stylesheet" type="text/css">
<style>
body {
overflow-y:scroll;
}
text {
font: 12px sans-serif;
}
#chart1 {
height: 500px;
margin: 10px;
min-width: 100px;
min-height: 100px;
/*
Minimum height and width is a good idea to prevent negative SVG dimensions...
For example width should be =< margin.left + margin.right + 1,
of course 1 pixel for the entire chart would not be very useful, BUT should not have errors
*/
}
</style>
<body>
<div id="chart1">
</div>
<script src="../lib/d3.v2.js"></script>
<script src="../lib/jquery.min.js"></script>
<script src="../nv.d3.js"></script>
<script src="../src/nvtooltip.js"></script>
<script src="../src/models/legend.js"></script>
<script src="../src/models/xaxis.js"></script>
<script src="../src/models/yaxis.js"></script>
<script src="../src/models/line.js"></script>
<script src="../src/models/lineWithLegend.js"></script>
<script src="../src/charts/lineChart.js"></script>
<script>
nv.charts.line()
.data(sinAndCos())
.selector('#chart1')
.yAxisLabel('Voltage (v)')
.build();
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 [
{
values: sin,
key: "Sine Wave",
color: "#ff7f0e"
},
{
values: cos,
key: "Cosine Wave",
color: "#2ca02c"
}
];
}
</script>

View File

@ -13,11 +13,22 @@ text {
font: 12px sans-serif;
}
#chart1 {
height: 500px;
margin: 10px;
min-width: 100px;
min-height: 100px;
/*
Minimum height and width is a good idea to prevent negative SVG dimensions...
For example width should be =< margin.left + margin.right + 1,
of course 1 pixel for the entire chart would not be very useful, BUT should not have errors
*/
}
</style>
<body>
<div id="test1">
<svg></svg>
<div id="chart1">
</div>
<script src="../lib/d3.v2.js"></script>
@ -31,86 +42,76 @@ text {
<script src="../src/models/lineWithLegend.js"></script>
<script>
/************
* Considering making an nv.charts object which will contain abstractions similar to the one below,
* but with the usual d3 reusable style.
* I could make this abstraction inside the layer above, but this layer incorporates some jQuery, so
* thinking nv.charts can be the glue that's not 100% d3
************/
var selector = '#chart1',
chart = nv.models.lineWithLegend(),
data = sinAndCos(),
xTickFormat = d3.format(',r'),
yTickFormat = d3.format(',.2f'),
xAxisLabel = null,
yAxisLabel = 'Voltage (v)',
duration = 500;
//Format A
nv.addGraph({
generate: function() {
var width = $(window).width() - 40,
height = $(window).height() - 40;
var container = d3.select(selector),
width = function() { return parseInt(container.style('width')) },
height = function() { return parseInt(container.style('height')) },
svg = container.append('svg');
var chart = nv.models.lineWithLegend()
.width(width)
.height(height)
//.margin({top: 20, right: 10, bottom: 50, left: 80})
chart
.width(width)
.height(height)
//chart.yAxis.axisLabel('Cumulative');
//chart.xAxis.axisLabel('Date');
chart.xAxis
.tickFormat(xTickFormat);
chart.yAxis
.tickFormat(yTickFormat)
.axisLabel(yAxisLabel);
chart.yAxis.tickFormat(d3.format(',%'))
chart.xAxis.tickFormat(function(d) {
return d3.time.format('%x')(new Date(d))
})
//chart.xaxis.tickFormat(d3.format(".02f"))
var svg = d3.select('#test1 svg')
.attr('width', width)
.attr('height', height)
.datum(sinAndCos())
svg.transition().duration(500).call(chart);
svg
.attr('width', width())
.attr('height', height())
.datum(data)
.transition().duration(duration).call(chart);
return chart;
},
callback: function(graph) {
callback: function(chart) {
var showTooltip = function(e) {
var offset = $(selector).offset(),
left = e.pos[0] + offset.left,
top = e.pos[1] + offset.top,
formatY = chart.yAxis.tickFormat(), //Assumes using same format as axis, can customize to show higher precision, etc.
formatX = chart.xAxis.tickFormat();
graph.dispatch.on('tooltipShow', function(e) {
var offset = $('#test1').offset(),
left = e.pos[0] + offset.left,
top = e.pos[1] + offset.top,
formatterY = d3.format(",.2%"),
formatterX = function(d) {
return d3.time.format('%x')(new Date(d))
};
// uses the chart's getX and getY, you may customize if x position is not the same as the value you want
// ex. daily data without weekends, x is the index, while you want the date
var content = '<h3>' + e.series.key + '</h3>' +
'<p>' +
formatY(chart.y()(e.point)) + ' at ' + formatX(chart.x()(e.point)) +
'</p>';
var content = '<h3>' + e.series.key + '</h3>' +
'<p>' +
formatterY(graph.y()(e.point)) + ' on ' + formatterX(graph.x()(e.point)) +
'</p>';
//$('#positionTest').css({'left': left, 'top': top});
nvtooltip.show([left, top], content);
});
graph.dispatch.on('tooltipHide', function(e) {
nvtooltip.cleanup();
});
nvtooltip.show([left, top], content);
};
chart.dispatch.on('tooltipShow', showTooltip);
chart.dispatch.on('tooltipHide', nvtooltip.cleanup);
$(window).resize(function() {
var width = $(window).width() - 40,
height = $(window).height() - 40,
margin = graph.margin();
if (width < margin.left + margin.right + 20)
width = margin.left + margin.right + 20;
if (height < margin.top + margin.bottom + 20)
height = margin.top + margin.bottom + 20;
graph
.width(width)
.height(height);
d3.select('#test1 svg')
.attr('width', width)
.attr('height', height)
.call(graph);
// now that width and height are functions, should be automatic..of course you can always override them
d3.select('#chart1 svg')
.attr('width', chart.width()()) //need to set SVG dimensions, chart is not aware of the SVG component
.attr('height', chart.height()())
.call(chart);
});
}
});

View File

@ -67,15 +67,18 @@ nv.addGraph({
},
callback: function(graph) {
/*
graph.dispatch.on('tooltipShow', function(e) {
var offset = $('#chart').offset(),
left = e.pos[0] + offset.left,
top = e.pos[1] + offset.top,
formatterY = graph.stacked.offset() == 'expand' ? d3.format(',.2%') : d3.format(',.2f'), //TODO: stacked format should be set by caller
formatterX = function(d) { return d };
/*
formatterY = d3.format(",.2%"),
formatterX = function(d) {
return d3.time.format('%x')(new Date(d))
};
*/
var content = '<h3>' + e.series.key + '</h3>' +
'<p>' +
@ -89,7 +92,6 @@ nv.addGraph({
graph.dispatch.on('tooltipHide', function(e) {
nvtooltip.cleanup();
});
*/

View File

@ -91,9 +91,10 @@ var nv = {version: "0.0.1"};
window.nv = nv;
nv.models = {};
nv.graphs = [];
nv.log = {};
nv.models = {}; //stores all the possible models/components
nv.charts = {}; //stores all the ready to use charts
nv.graphs = []; //stores all the graphs currently on the page
nv.log = {}; //stores some statistics and potential error messages
nv.dispatch = d3.dispatch("render_start", "render_end");
@ -124,7 +125,7 @@ nv.dispatch.on("render_end", function(e) {
nv.log.endTime = +new Date;
nv.log.totalTime = nv.log.endTime - nv.log.startTime;
//log('end', nv.log.endTime);
log('total', nv.log.totalTime);
log('total', nv.log.totalTime); //used for development, to keep track of graph generation times
});
@ -1242,8 +1243,8 @@ nv.models.lineWithFocus = function() {
nv.models.lineWithLegend = function() {
var margin = {top: 30, right: 20, bottom: 50, left: 60},
width = 960,
height = 500,
getWidth = function() { return 960 },
getHeight = function() { return 500 },
dotRadius = function() { return 2.5 },
color = d3.scale.category10().range(),
dispatch = d3.dispatch('tooltipShow', 'tooltipHide');
@ -1260,6 +1261,9 @@ nv.models.lineWithLegend = function() {
function chart(selection) {
selection.each(function(data) {
var width = getWidth(),
height = getHeight();
var series = data.filter(function(d) { return !d.disabled })
.map(function(d) { return d.values });
@ -1395,14 +1399,14 @@ nv.models.lineWithLegend = function() {
};
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
if (!arguments.length) return getWidth;
getWidth = d3.functor(_);
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
if (!arguments.length) return getHeight;
getHeight = d3.functor(_);
return chart;
};
@ -2042,6 +2046,7 @@ nv.models.stackedArea = function() {
* 'default' (input order)
************************************/
var lines = nv.models.line();
function chart(selection) {
selection.each(function(data) {
@ -2049,30 +2054,44 @@ nv.models.stackedArea = function() {
// Need to leave data alone to switch between stacked, stream, and expanded
var dataCopy = JSON.parse(JSON.stringify(data));
//log(dataCopy);
dataCopy = dataCopy.map(function(series) { return series.values })
//compute the data based on offset and order (calc's y0 for every point)
//dataCopy = d3.layout.stack().offset(offset).order(order).values(function(d){ return d.values })(dataCopy);
dataCopy = d3.layout.stack().offset(offset).order(order)(dataCopy);
dataCopy = d3.layout.stack().offset(offset).order(order).values(function(d){ return d.values })(dataCopy);
var mx = dataCopy[0].length - 1, // assumes that all layers have same # of samples & that there is at least one layer
var mx = dataCopy[0].values.length - 1, // assumes that all layers have same # of samples & that there is at least one layer
my = d3.max(dataCopy, function(d) {
return d3.max(d, function(d) {
return d3.max(d.values, function(d) {
return d.y0 + d.y;
});
});
lines
.width(width - margin.left - margin.right)
.height(height - margin.top - margin.bottom)
.y(function(d) { return d.y + d.y0 })
.color(data.map(function(d,i) {
return d.color || color[i % 10];
}).filter(function(d,i) { return !data[i].disabled }));
// Select the wrapper g, if it exists.
var wrap = d3.select(this).selectAll('g.d3stream').data([dataCopy]);
var wrap = d3.select(this).selectAll('g.d3stackedarea').data([dataCopy]);
// Create the skeletal chart on first load.
var gEnter = wrap.enter().append('g').attr('class', 'd3stream').append('g');
var gEnter = wrap.enter().append('g').attr('class', 'd3stackedarea').append('g');
gEnter.append('g').attr('class', 'areaWrap');
gEnter.append('g').attr('class', 'linesWrap');
var g = wrap.select('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var linesWrap = g.select('.linesWrap')
.datum(dataCopy.filter(function(d) { return !d.disabled }))
d3.transition(linesWrap).call(lines);
// Update the stacked graph
var availableWidth = width - margin.left - margin.right,
@ -2088,20 +2107,17 @@ nv.models.stackedArea = function() {
.y0(function(d) { return availableHeight - d.y0 * availableHeight / my; })
.y1(function(d) { return availableHeight - d.y0 * availableHeight / my; })
var path = g.selectAll('path')
var path = g.select('.areaWrap').selectAll('path.area')
.data(function(d) { return d });
//.data(dataCopy);
path.enter().append('path');
path.enter().append('path').attr('class', 'area');
d3.transition(path.exit())
.attr('d', zeroArea)
.attr('d', function(d,i) { return zeroArea(d.values,i) })
.remove();
path
.style('fill-opacity', .75)
.style('stroke-opacity', .75)
.style('fill', function(d,i){ return color[i % 10] })
.style('stroke', function(d,i){ return color[i % 10] });
d3.transition(path)
.attr('d', area);
.attr('d', function(d,i) { return area(d.values,i) })
});
@ -2168,6 +2184,7 @@ nv.models.stackedArea = function() {
return chart;
};
chart.dispatch = lines.dispatch;
return chart;
}
@ -2214,9 +2231,8 @@ nv.models.stackedAreaWithLegend = function() {
x .domain(d3.extent(d3.merge(series), getX ))
.range([0, width - margin.left - margin.right]);
//TODO: remove if stream
y .domain(stacked.offset() == 'zero' ?
d3.extent(d3.merge(series), getY ) :
y .domain(stacked.offset() == 'zero' ?
[0, d3.max(d3.merge(series), getY )] :
[0, 1] // 0 - 100%
)
.range([height - margin.top - margin.bottom, 0]);
@ -2291,6 +2307,7 @@ nv.models.stackedAreaWithLegend = function() {
d.hover = false;
selection.transition().call(chart)
});
*/
stacked.dispatch.on('pointMouseover.tooltip', function(e) {
dispatch.tooltipShow({
@ -2306,7 +2323,6 @@ nv.models.stackedAreaWithLegend = function() {
dispatch.tooltipHide(e);
});
*/
//TODO: margins should be adjusted based on what components are used: axes, axis labels, legend
@ -2358,7 +2374,7 @@ nv.models.stackedAreaWithLegend = function() {
.range(y.range())
.ticks( stacked.offset() == 'wiggle' ? 0 : height / 36 )
.tickSize(-(width - margin.right - margin.left), 0)
.tickFormat(stacked.offset() == 'zero' ? d3.format(',2f') : d3.format('%')); //TODO: stacked format should be set by caller
.tickFormat(stacked.offset() == 'zero' ? d3.format(',.2f') : d3.format('%')); //TODO: stacked format should be set by caller
d3.transition(g.select('.y.axis'))
.call(yAxis);
@ -2409,6 +2425,7 @@ nv.models.stackedAreaWithLegend = function() {
return chart;
};
chart.stacked = stacked;
// Expose the x-axis' tickFormat method.
//chart.xAxis = {};

4
nv.d3.min.js vendored

File diff suppressed because one or more lines are too long

137
src/charts/lineChart.js Normal file
View File

@ -0,0 +1,137 @@
//may make these more specific, like 'time series line with month end data points', etc.
// or may make that yet another layer of abstraction.... trying to not get too crazy
nv.charts.line = function() {
var selector = null,
data = [],
xTickFormat = d3.format(',r'),
yTickFormat = d3.format(',.2f'),
xAxisLabel = null,
yAxisLabel = null,
duration = 500;
var graph = nv.models.lineWithLegend(),
showTooltip = function(e) { //TODO: simplify so all the calcualtions don't need to be done by the user.
var offset = $(selector).offset(),
left = e.pos[0] + offset.left,
top = e.pos[1] + offset.top,
formatY = graph.yAxis.tickFormat(), //Assumes using same format as axis, can customize to show higher precision, etc.
formatX = graph.xAxis.tickFormat();
// uses the chart's getX and getY, you may customize if x position is not the same as the value you want
// ex. daily data without weekends, x is the index, while you want the date
var content = '<h3>' + e.series.key + '</h3>' +
'<p>' +
formatY(graph.y()(e.point)) + ' at ' + formatX(graph.x()(e.point)) +
'</p>';
nvtooltip.show([left, top], content);
};
function chart() {
return chart;
}
chart.build = function() {
if (!selector || !data.length) return chart; //do nothing if you have nothing to work with
nv.addGraph({
generate: function() {
var container = d3.select(selector),
width = function() { return parseInt(container.style('width')) },
height = function() { return parseInt(container.style('height')) },
svg = container.append('svg');
graph
.width(width)
.height(height);
graph.xAxis
.tickFormat(xTickFormat);
graph.yAxis
.tickFormat(yTickFormat)
.axisLabel(yAxisLabel);
svg
.attr('width', width())
.attr('height', height())
.datum(data)
.transition().duration(duration).call(graph);
return graph;
},
callback: function(graph) {
graph.dispatch.on('tooltipShow', showTooltip);
graph.dispatch.on('tooltipHide', nvtooltip.cleanup);
$(window).resize(function() {
// now that width and height are functions, should be automatic..of course you can always override them
d3.select(selector + ' svg')
.attr('width', graph.width()()) //need to set SVG dimensions, chart is not aware of the SVG component
.attr('height', graph.height()())
.call(graph);
});
}
});
return chart;
};
chart.update = function() {
//TODO: create update code
return chart;
};
chart.data = function(_) {
if (!arguments.length) return data;
data = _;
return chart;
};
chart.selector = function(_) {
if (!arguments.length) return selector;
selector = _;
return chart;
};
chart.duration = function(_) {
if (!arguments.length) return duration;
duration = _;
return chart;
};
chart.xTickFormat = function(_) {
if (!arguments.length) return xTickFormat;
xTickFormat = _;
return chart;
};
chart.yTickFormat = function(_) {
if (!arguments.length) return yTickFormat;
yTickFormat = _;
return chart;
};
chart.xAxisLabel = function(_) {
if (!arguments.length) return xAxisLabel;
xAxisLabel = _;
return chart;
};
chart.yAxisLabel = function(_) {
if (!arguments.length) return yAxisLabel;
yAxisLabel = _;
return chart;
};
return chart;
}

View File

@ -2,9 +2,10 @@ var nv = {version: "0.0.1"};
window.nv = nv;
nv.models = {};
nv.graphs = [];
nv.log = {};
nv.models = {}; //stores all the possible models/components
nv.charts = {}; //stores all the ready to use charts
nv.graphs = []; //stores all the graphs currently on the page
nv.log = {}; //stores some statistics and potential error messages
nv.dispatch = d3.dispatch("render_start", "render_end");
@ -35,7 +36,7 @@ nv.dispatch.on("render_end", function(e) {
nv.log.endTime = +new Date;
nv.log.totalTime = nv.log.endTime - nv.log.startTime;
//log('end', nv.log.endTime);
log('total', nv.log.totalTime);
log('total', nv.log.totalTime); //used for development, to keep track of graph generation times
});

View File

@ -49,6 +49,7 @@
.nvtooltip p {
margin: 0;
padding: 0;
text-align: center;
}
.nvtooltip span {
@ -235,10 +236,26 @@ text {
* Stacked Area
*/
.d3stackedarea path {
.d3stackedarea path.area {
fill-opacity: .75;
stroke-opacity: .75;
}
.d3stackedarea .lines path {
stroke-opacity: 0;
}
.d3stackedarea .lines .point {
stroke-opacity: 0;
fill-opacity: 0;
transition: all 250ms linear;
-moz-transition: all 250ms linear;
-webkit-transition: all 250ms linear;
}
.d3stackedarea .lines .point.hover {
stroke-width: 20px;
stroke-opacity: .75;
fill-opacity: 1;
}

View File

@ -1,8 +1,8 @@
nv.models.lineWithLegend = function() {
var margin = {top: 30, right: 20, bottom: 50, left: 60},
width = 960,
height = 500,
getWidth = function() { return 960 },
getHeight = function() { return 500 },
dotRadius = function() { return 2.5 },
color = d3.scale.category10().range(),
dispatch = d3.dispatch('tooltipShow', 'tooltipHide');
@ -19,6 +19,9 @@ nv.models.lineWithLegend = function() {
function chart(selection) {
selection.each(function(data) {
var width = getWidth(),
height = getHeight();
var series = data.filter(function(d) { return !d.disabled })
.map(function(d) { return d.values });
@ -154,14 +157,14 @@ nv.models.lineWithLegend = function() {
};
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
if (!arguments.length) return getWidth;
getWidth = d3.functor(_);
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
if (!arguments.length) return getHeight;
getHeight = d3.functor(_);
return chart;
};

View File

@ -20,6 +20,7 @@ nv.models.stackedArea = function() {
* 'default' (input order)
************************************/
var lines = nv.models.line();
function chart(selection) {
selection.each(function(data) {
@ -38,16 +39,33 @@ nv.models.stackedArea = function() {
});
});
lines
.width(width - margin.left - margin.right)
.height(height - margin.top - margin.bottom)
.y(function(d) { return d.y + d.y0 })
.color(data.map(function(d,i) {
return d.color || color[i % 10];
}).filter(function(d,i) { return !data[i].disabled }));
// Select the wrapper g, if it exists.
var wrap = d3.select(this).selectAll('g.d3stackedarea').data([dataCopy]);
// Create the skeletal chart on first load.
var gEnter = wrap.enter().append('g').attr('class', 'd3stackedarea').append('g');
gEnter.append('g').attr('class', 'areaWrap');
gEnter.append('g').attr('class', 'linesWrap');
var g = wrap.select('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
var linesWrap = g.select('.linesWrap')
.datum(dataCopy.filter(function(d) { return !d.disabled }))
d3.transition(linesWrap).call(lines);
// Update the stacked graph
var availableWidth = width - margin.left - margin.right,
@ -63,9 +81,9 @@ nv.models.stackedArea = function() {
.y0(function(d) { return availableHeight - d.y0 * availableHeight / my; })
.y1(function(d) { return availableHeight - d.y0 * availableHeight / my; })
var path = g.selectAll('path')
var path = g.select('.areaWrap').selectAll('path.area')
.data(function(d) { return d });
path.enter().append('path');
path.enter().append('path').attr('class', 'area');
d3.transition(path.exit())
.attr('d', function(d,i) { return zeroArea(d.values,i) })
.remove();
@ -140,6 +158,7 @@ nv.models.stackedArea = function() {
return chart;
};
chart.dispatch = lines.dispatch;
return chart;
}

View File

@ -117,6 +117,7 @@ nv.models.stackedAreaWithLegend = function() {
d.hover = false;
selection.transition().call(chart)
});
*/
stacked.dispatch.on('pointMouseover.tooltip', function(e) {
dispatch.tooltipShow({
@ -132,7 +133,6 @@ nv.models.stackedAreaWithLegend = function() {
dispatch.tooltipHide(e);
});
*/
//TODO: margins should be adjusted based on what components are used: axes, axis labels, legend
@ -184,7 +184,7 @@ nv.models.stackedAreaWithLegend = function() {
.range(y.range())
.ticks( stacked.offset() == 'wiggle' ? 0 : height / 36 )
.tickSize(-(width - margin.right - margin.left), 0)
.tickFormat(stacked.offset() == 'zero' ? d3.format(',2f') : d3.format('%')); //TODO: stacked format should be set by caller
.tickFormat(stacked.offset() == 'zero' ? d3.format(',.2f') : d3.format('%')); //TODO: stacked format should be set by caller
d3.transition(g.select('.y.axis'))
.call(yAxis);
@ -235,6 +235,7 @@ nv.models.stackedAreaWithLegend = function() {
return chart;
};
chart.stacked = stacked;
// Expose the x-axis' tickFormat method.
//chart.xAxis = {};