|
function graphBarChart(data, parentNode, options) { |
|
var margins = options.margins || { |
|
top: 20, |
|
right: 20, |
|
bottom: 20, |
|
left: 20 |
|
}, |
|
width = options.width, |
|
height = options.height - margins.top - margins.bottom, |
|
padding = options.padding; |
|
|
|
var line; |
|
var x = d3.scale.ordinal() |
|
.rangeRoundBands([padding, width - padding], 0.1); |
|
|
|
var y = d3.scale.linear() |
|
.range([height, 0]); |
|
|
|
var xAxis = d3.svg.axis() |
|
.scale(x) |
|
.orient('bottom'); |
|
|
|
var yAxis = d3.svg.axis() |
|
.scale(y) |
|
.orient('left'); |
|
|
|
var svg = d3.select(parentNode).append('svg') |
|
.attr('width', width) |
|
.attr('height', height + margins.top + margins.bottom) |
|
.style('display', options.display ? '' : 'none') |
|
.append('g') |
|
.attr('transform', 'translate(' + margins.left + ',' + margins.top + ')'); |
|
|
|
configureAxesDomains(data, options, x, y); |
|
|
|
var tip = d3.tip() |
|
.attr('class', 'd3-tip') |
|
.offset([-10, 0]) |
|
.html(function(d) { |
|
var fn = typeof options.tips.parse === 'function' ? |
|
options.tips.parse : Math.round.bind(); |
|
var tip = '{{options.yAxis.text}}: <span style="color:white">' + fn(options.yAxis.value(d)) + '</span>'; |
|
|
|
if(d.count) { |
|
tip += '<br>Cantidad: <span> ' + d.count + '</span>'; |
|
} |
|
tip = tip.replace('{{options.yAxis.text}}', options.yAxis.text); |
|
return tip; |
|
}); |
|
|
|
svg.append('g') |
|
.attr('class', 'x axis') |
|
.attr('transform', 'translate(0,' + height + ')') |
|
.call(xAxis) |
|
.selectAll('text') |
|
.style('text-anchor', 'end') |
|
.attr('transform', function() { |
|
if(options.xAxis && options.xAxis.transform) { |
|
d3.select(this).attr('x', -5).attr('y', 5); |
|
return options.xAxis.transform; |
|
} |
|
}); |
|
|
|
svg.append('g') |
|
.attr('class', 'y axis') |
|
.attr('transform', 'translate('+padding+',0)') |
|
.call(yAxis) |
|
.selectAll('text') |
|
.attr('transform', function() { |
|
if(options.yAxis && options.yAxis.transform) { |
|
return options.yAxis.transform; |
|
} |
|
}); |
|
|
|
svg.selectAll('.bar') |
|
.data(data) |
|
.enter().append('rect') |
|
.attr('class', 'bar') |
|
.attr('x', compose(x, options.xAxis.value)) |
|
.attr('width', x.rangeBand()) |
|
.attr('y', compose(y, options.yAxis.value)) |
|
.attr('height', function(d) { |
|
return height - y(options.yAxis.value(d)); |
|
}) |
|
.on('mouseover', tip.show) |
|
.on('mouseout', tip.hide); |
|
|
|
svg.append('text') |
|
.attr('text-anchor', 'middle') |
|
.attr('transform', 'translate('+ (padding/3) +','+(height/2)+')rotate(-90)') |
|
.text(options.yAxis.text); |
|
|
|
svg.append('text') |
|
.attr('text-anchor', 'middle') |
|
.attr('transform', 'translate('+ (width/2) +','+(height + margins.top + margins.bottom-(padding/3))+')') |
|
.text(options.xAxis.text); |
|
|
|
svg.call(tip); |
|
|
|
if(options.textLabel) { |
|
addText({ |
|
elem: svg, |
|
x: (width - 80), |
|
y: (margins.bottom / 2), |
|
textAnchor: 'end', |
|
fontSize: '16px', |
|
text: options.textLabel |
|
}); |
|
} |
|
|
|
// add interpolation |
|
if(options.interpolation) { |
|
line = d3.svg.line() |
|
.x(function(d, i) {return x(options.xAxis.value(d)) + i;}) // composing twice is more pure but who cares? |
|
.y(compose(y, options.yAxis.value)) |
|
.interpolate(options.interpolation); |
|
svg.append('path') |
|
.datum(data) |
|
.attr('class', 'line') |
|
.attr('d', line); |
|
} |
|
} |
|
|
|
function addText(obj) { |
|
if(!obj || !obj.elem || typeof obj.elem.append !== 'function') { |
|
console.log('Invalid call to addText.'); |
|
return; |
|
} |
|
|
|
obj.elem.append('text') |
|
.attr('x', obj.x) |
|
.attr('y', obj.y) |
|
.attr('text-anchor', obj.textAnchor) |
|
.style('font-size', obj.fontSize) |
|
.text(obj.text); |
|
} |
|
|
|
function configureAxesDomains(data, options, x, y) { |
|
function configureAxisDomain(axis, axisFn) { |
|
if(options && options[axis] && options[axis].domain) { |
|
if(typeof options[axis].domain === 'function') { |
|
axisFn.domain(data.map(options[axis].domain)); |
|
} else { |
|
axisFn.domain(options[axis].domain); |
|
} |
|
} else { |
|
axisFn.domain([d3.min(data), d3.max(data)]); |
|
} |
|
} |
|
|
|
x && configureAxisDomain('xAxis', x); |
|
y && configureAxisDomain('yAxis', y); |
|
} |
|
|
|
function graphHistogram(values, parentNode, options) { |
|
function addBarText(data) { |
|
bar.append('text') |
|
.style('cursor', 'default') |
|
.attr('dy', '.75em') |
|
.attr('y', 4) |
|
.attr('x', Math.ceil((x(data[0].dx) - x(0)) / 2)) |
|
.attr('text-anchor', 'middle') |
|
.style('font-size', options.barTextSize) |
|
.attr('class', 'histogram-text') |
|
.text(function(d) { |
|
if(d.height < 17) { |
|
d3.select(this) |
|
.attr('class', null) |
|
.attr('y', -15); |
|
} |
|
return formatCount(d.y); |
|
}); |
|
} |
|
|
|
var formatCount = d3.format(',.0f'); |
|
|
|
var margin = options.margins || { |
|
top: 20, |
|
right: 20, |
|
bottom: 100, |
|
left: 60 |
|
}, |
|
width = options.width - margin.left - margin.right, |
|
height = options.height - margin.top - margin.bottom, |
|
numOfBins = options.bins || 20; |
|
|
|
var x = d3.scale.linear() |
|
.range([0, width]); |
|
|
|
configureAxesDomains(values, options, x); |
|
|
|
var line; |
|
var data = d3.layout.histogram() |
|
.bins(x.ticks(numOfBins))(values); |
|
|
|
var y = d3.scale.linear() |
|
.domain([0, d3.max(data, pick('y'))]) |
|
.range([height, 0]); |
|
|
|
var xAxis = d3.svg.axis() |
|
.scale(x) |
|
.orient('bottom'); |
|
|
|
var svg = d3.select(parentNode).append('svg') |
|
.attr('width', width + margin.left + margin.right) |
|
.attr('height', height + margin.top + margin.bottom) |
|
.attr('id', options.id) |
|
.style('display', options.display ? '' : 'none') |
|
.append('g') |
|
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')'); |
|
|
|
saveGraphInfo(options.id, { |
|
height: height, |
|
width: width, |
|
options: options, |
|
addBarText: addBarText, |
|
configureAxesDomains: configureAxesDomains, |
|
x: x |
|
}); |
|
|
|
var bar = svg.selectAll('.bar') |
|
.data(data) |
|
.enter().append('g') |
|
.attr('class', 'bar') |
|
.attr('transform', function(d) { |
|
return 'translate(' + x(d.x) + ',' + y(d.y) + ')'; |
|
}); |
|
|
|
bar.append('rect') |
|
.attr('x', 1) |
|
.attr('width', x(data[0].dx) - x(0) - 1) |
|
.attr('height', function(d) {d.height= height - y(d.y); return height - y(d.y);}); |
|
|
|
addBarText(data); |
|
|
|
svg.append('g') |
|
.attr('class', 'x axis') |
|
.attr('transform', 'translate(0,' + height + ')') |
|
.call(xAxis); |
|
|
|
svg.append('text') |
|
.attr('text-anchor', 'middle') |
|
.attr('transform', 'translate('+ (width/2) +','+(height + margin.top + (20/100 *margin.bottom))+')') |
|
.text(options.xAxis.text); |
|
|
|
// add interpolation |
|
if(options.interpolation) { |
|
line = d3.svg.line() |
|
.x(function(d, i) {return x(d[0]) + i;}) // composing twice is more pure but who cares? |
|
.y(compose(y, options.yAxis.value)) |
|
.interpolate(options.interpolation); |
|
svg.append('path') |
|
.datum(data) |
|
.attr('class', 'line') |
|
.attr('d', line); |
|
} |
|
} |
|
|
|
function graphPieChart(data, parentNode, options) { |
|
function addSlices(data, color) { |
|
var slice = svg.select('.slices').selectAll('path.slice') |
|
.data(pie(data), key); |
|
|
|
slice.enter() |
|
.insert('path') |
|
.attr('fill', compose(color, compose(options.name, pick('data')))) |
|
.attr('class', 'slice') |
|
.transition().duration(1000) |
|
.attrTween('d', arcTween); |
|
|
|
slice.exit().remove(); |
|
} |
|
|
|
function removeSlices() { |
|
svg.select('.slices').selectAll('path.slice') |
|
.remove(); |
|
} |
|
|
|
function arcTween(d) { |
|
var interpolate; |
|
|
|
interpolate = d3.interpolate({startAngle: 0, endAngle: 0}, d); |
|
return compose(arc, interpolate); |
|
} |
|
|
|
var width = options.width, |
|
height = options.height, |
|
radius = Math.min(width, height) / 2, |
|
svg, |
|
pie, |
|
arc, |
|
tip, |
|
outerArc, |
|
key, |
|
color, |
|
xOffset = options.xOffset || 0; |
|
|
|
svg = d3.select(parentNode) |
|
.append('svg') |
|
.attr('width', width) |
|
.attr('height', height) |
|
.attr('id', options.id) |
|
.style('display', options.display ? '' : 'none') |
|
.append('g'); |
|
|
|
svg.append('g') |
|
.attr('class', 'slices'); |
|
svg.append('g') |
|
.attr('class', 'labels'); |
|
svg.append('g') |
|
.attr('class', 'lines'); |
|
|
|
pie = d3.layout.pie() |
|
.sort(null) // this avoids further sorting, we assume data it's already sorted in the right way |
|
.value(options.value); |
|
|
|
arc = d3.svg.arc() |
|
.outerRadius(radius * 0.8) |
|
.innerRadius(radius * 0.4); |
|
|
|
tip = d3.tip() |
|
.attr('class', 'd3-tip') |
|
.offset(options.tip ? options.tip.offset : [0, 0]) |
|
.html(function(d) { |
|
return options.name(d.data) + ': ' + options.value(d.data); |
|
}); |
|
|
|
outerArc = d3.svg.arc() |
|
.innerRadius(radius * 0.9) |
|
.outerRadius(radius * 0.9); |
|
|
|
svg.attr('transform', 'translate(' + ((width / 2) + xOffset) + ',' + height / 2 + ')'); |
|
|
|
key = compose(options.name, pick('data')); |
|
|
|
color = d3.scale.ordinal() |
|
.domain(data.map(options.name)) |
|
.range(options.colorsArray); |
|
|
|
/* ------- PIE SLICES -------*/ |
|
svg.select('.slices').selectAll('path.slice') |
|
.data(pie(data), key); |
|
|
|
var addLabelsParams = { |
|
tip: tip, |
|
outerArc: outerArc, |
|
key: key, |
|
options: options, |
|
pie: pie, |
|
svg: svg, |
|
radius: radius, |
|
arc: arc |
|
}; |
|
|
|
saveGraphInfo(options.id, { |
|
height: height, |
|
width: width, |
|
options: options, |
|
pie: pie, |
|
arcTween: arcTween, |
|
addLabelsParams: addLabelsParams, |
|
removeSlices: removeSlices, |
|
addSlices: addSlices, |
|
arc: arc |
|
}); |
|
|
|
addSlices(data, color); |
|
addLabelsParams.data = data; |
|
addPieChartLabels(addLabelsParams, options); |
|
|
|
if(options.title) { |
|
addText({ |
|
elem: svg, |
|
x: 0, |
|
y: 8, |
|
textAnchor: 'middle', |
|
fontSize: '21px', |
|
text: options.title |
|
}); |
|
} |
|
|
|
return svg; |
|
} |
|
|
|
function addPieChartLabels(params, options) { |
|
var svg = params.svg; |
|
var pie = params.pie; |
|
var data = params.data; |
|
var key = params.key; |
|
var tip = params.tip; |
|
var outerArc = params.outerArc; |
|
var radius = params.radius; |
|
var arc = params.arc; |
|
|
|
var text = svg.select('.labels').selectAll('text') |
|
.data(pie(data), key); |
|
|
|
text.enter() |
|
.append('text') |
|
.attr('dy', '.35em') |
|
.text(compose(options.name, pick('data'))); |
|
|
|
function midAngle(d) { |
|
return d.startAngle + (d.endAngle - d.startAngle)/2; |
|
} |
|
|
|
text.transition().duration(1000) |
|
.attrTween('transform', function(d) { |
|
var interpolate; |
|
|
|
this._current = this._current || d; |
|
interpolate = d3.interpolate(this._current, d); |
|
this._current = interpolate(0); |
|
return function(t) { |
|
var d2 = interpolate(t); |
|
var pos = outerArc.centroid(d2); |
|
|
|
pos[0] = radius * (midAngle(d2) < Math.PI ? 1 : -1); |
|
pos[0] += pos[0] < 0 ? 20 : -20; |
|
return 'translate('+ pos +')'; |
|
}; |
|
}) |
|
.styleTween('text-anchor', function(d) { |
|
var interpolate; |
|
|
|
this._current = this._current || d; |
|
interpolate = d3.interpolate(this._current, d); |
|
this._current = interpolate(0); |
|
return function(t) { |
|
var d2 = interpolate(t); |
|
|
|
return midAngle(d2) < Math.PI ? 'start':'end'; |
|
}; |
|
}); |
|
|
|
text.exit() |
|
.remove(); |
|
|
|
/* ------- SLICE TO TEXT POLYLINES -------*/ |
|
|
|
var polyline = svg.select('.lines').selectAll('polyline') |
|
.data(pie(data), key); |
|
|
|
polyline.enter() |
|
.append('polyline'); |
|
|
|
polyline.transition().duration(1000) |
|
.attrTween('points', function(d) { |
|
var interpolate; |
|
|
|
this._current = this._current || d; |
|
interpolate = d3.interpolate(this._current, d); |
|
this._current = interpolate(0); |
|
return function(t) { |
|
var d2 = interpolate(t); |
|
var pos = outerArc.centroid(d2); |
|
|
|
pos[0] = radius * 0.85 * (midAngle(d2) < Math.PI ? 1 : -1); |
|
return [arc.centroid(d2), outerArc.centroid(d2), pos]; |
|
}; |
|
}); |
|
|
|
polyline.exit() |
|
.remove(); |
|
|
|
svg.call(tip); |
|
svg.select('.slices').selectAll('path.slice') |
|
.on('mouseover', tip.show) |
|
.on('mouseout', tip.hide); |
|
} |
|
|
|
function graphScatterPlot(data, parentNode, options) { |
|
var margin = options.margins || {top: 20, right: 20, bottom: 40, left: 60}, |
|
width = options.width - margin.left - margin.right, |
|
height = options.height - margin.top - margin.bottom; |
|
|
|
/* |
|
* value accessor - returns the value to encode for a given data object. |
|
* scale - maps value to a visual display encoding, such as a pixel position. |
|
* map function - maps from data value to display value |
|
* axis - setsup axis |
|
*/ |
|
// setup x |
|
var xValue = pick('xValue'), // data -> value |
|
xScale = d3.scale.linear().range([0, width]), // value -> display |
|
xMap = compose(xScale, xValue), // data -> display |
|
xAxis = d3.svg.axis().scale(xScale).orient('bottom'); |
|
|
|
// setup y |
|
var yValue = pick('yValue'), // data -> value |
|
yScale = d3.scale.linear().range([height, 0]), // value -> display |
|
yMap = compose(yScale, yValue), // data -> display |
|
yAxis = d3.svg.axis().scale(yScale).orient('left'); |
|
|
|
// add the graph canvas to the body of the webpage |
|
var svg = d3.select(parentNode).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 + ')'); |
|
|
|
// add the tooltip area to the webpage |
|
d3.select('body').append('div') |
|
.attr('class', 'tooltip') |
|
.style('opacity', 0); |
|
|
|
// don't want dots overlapping axis, so add in buffer to data domain |
|
if(options.xAxis.domain) { |
|
xScale.domain(options.xAxis.domain); |
|
} else { |
|
xScale.domain([d3.min(data, xValue) -1, d3.max(data, xValue)+1]); |
|
} |
|
|
|
if(options.yAxis.domain) { |
|
yScale.domain(options.yAxis.domain); |
|
} else { |
|
yScale.domain([d3.min(data, yValue)-1, d3.max(data, yValue)+1]); |
|
} |
|
|
|
// x-axis |
|
svg.append('g') |
|
.attr('class', 'x axis') |
|
.attr('transform', 'translate(0,' + height + ')') |
|
.call(xAxis) |
|
.append('text') |
|
.attr('class', 'label') |
|
.attr('x', width) |
|
.attr('y', -6) |
|
.style('text-anchor', 'end') |
|
.text(options.xAxis.text); |
|
|
|
// y-axis |
|
svg.append('g') |
|
.attr('class', 'y axis') |
|
.call(yAxis) |
|
.append('text') |
|
.attr('class', 'label') |
|
.attr('transform', 'rotate(-90)') |
|
.attr('y', 6) |
|
.attr('dy', '.71em') |
|
.style('text-anchor', 'end') |
|
.text(options.yAxis.text); |
|
|
|
// draw dots |
|
svg.selectAll('.dot') |
|
.data(data) |
|
.enter().append('circle') |
|
.attr('class', 'dot') |
|
.attr('r', 3.5) |
|
.attr('cx', xMap) |
|
.attr('cy', yMap) |
|
.style('fill', 'black'); |
|
} |
|
|
|
function saveGraphInfo(id, info) { |
|
var selector; |
|
|
|
if(!info || !info.options) { |
|
console.log('Invalid call to saveGraphInfo.'); |
|
return; |
|
} |
|
|
|
selector = '#' + id; |
|
if(document.querySelector(selector)) { |
|
document.querySelector(selector).graph = info; |
|
} else { |
|
console.log('Invalid selector:', id); |
|
} |
|
} |
|
|
|
function updateHistogram(domElement, data) { |
|
var svg, |
|
width, |
|
height, |
|
options, |
|
histData, |
|
x, |
|
y; |
|
|
|
if(!domElement || !data) { |
|
console.log('Invalid call to updateHistogram.'); |
|
return; |
|
} |
|
|
|
svg = d3.select(domElement); |
|
|
|
width = domElement.graph.width; |
|
height = domElement.graph.height; |
|
options = domElement.graph.options; |
|
x = domElement.graph.x; |
|
|
|
configureAxesDomains(data, options, x); |
|
|
|
if(!data.length) { |
|
svg |
|
.append('text') |
|
.attr('x', width / 2) |
|
.attr('y', height / 2) |
|
.attr('class', 'no-data') |
|
.text('No hay datos'); |
|
|
|
svg.selectAll('.bar rect') |
|
.transition() |
|
.duration(400) |
|
.ease('linear') |
|
.attr('height', 0); |
|
|
|
svg.selectAll('.bar text').remove(); |
|
return; |
|
} |
|
|
|
svg.selectAll('.no-data').remove(); |
|
|
|
histData = d3.layout.histogram() |
|
.bins(x.ticks(options.bins))(data); |
|
|
|
y = d3.scale.linear() |
|
.domain([0, d3.max(histData, pick('y'))]) |
|
.range([height, 0]); |
|
|
|
svg.selectAll('.bar') |
|
.data(histData) |
|
.attr('transform', function(d) { |
|
return 'translate(' + x(d.x) + ',' + y(d.y) + ')'; |
|
}) |
|
.select('rect') |
|
.attr('height', 0) |
|
.transition() |
|
.duration(400) |
|
.ease('linear') |
|
.attr('height', function(d) {d.height= height - y(d.y); return height - y(d.y);}); |
|
|
|
svg.selectAll('.bar text').remove(); |
|
|
|
domElement.graph.addBarText(histData); |
|
} |
|
|
|
function updatePieChart(domElement, data) { |
|
var svg, |
|
pie, |
|
path, |
|
addLabelsParams, |
|
allZeros, |
|
color; |
|
|
|
if(!domElement || !data) { |
|
console.log(domElement, data); |
|
console.log('Invalid call to updatePieChart.'); |
|
return; |
|
} |
|
|
|
svg = d3.select(domElement); |
|
|
|
allZeros = data.filter(function(e) { |
|
return domElement.graph.options.value(e) !== 0; |
|
}).length === 0; |
|
|
|
if(!data.length || allZeros) { |
|
domElement.graph.removeSlices(); |
|
svg |
|
.selectAll('.labels text') |
|
.transition() |
|
.duration(700) |
|
.style('fill', 'white') |
|
.remove(); |
|
svg |
|
.selectAll('.lines polyline') |
|
.transition() |
|
.duration(700) |
|
.style('stroke', 'white') |
|
.remove(); |
|
|
|
svg.select('g') |
|
.append('text') |
|
.attr('x', 0) |
|
.attr('y', 20) |
|
.attr('text-anchor', 'middle') |
|
.style('font-size', '16px') |
|
.attr('class', 'no-data') |
|
.text('(No hay datos)'); |
|
return; |
|
} |
|
|
|
svg.selectAll('g .no-data').remove(); |
|
|
|
// here I would sort the data so not too many small slices are placed together |
|
// in order to improve readability but it's optional |
|
|
|
pie = domElement.graph.pie; |
|
path = svg.datum(data).selectAll('path'); |
|
|
|
path = path.data(pie); |
|
|
|
color = d3.scale.ordinal() |
|
.domain(data.map(domElement.graph.options.name)) |
|
.range(domElement.graph.options.colorsArray); |
|
|
|
domElement.graph.removeSlices(); |
|
domElement.graph.addSlices(data, color); |
|
addLabelsParams = domElement.graph.addLabelsParams; |
|
addLabelsParams.data = data; |
|
addPieChartLabels(addLabelsParams, domElement.graph.options); |
|
} |
|
|
|
function pick(prop) { |
|
if(typeof prop !== 'string' && typeof prop !== 'number') { |
|
throw Error('Properties should be able to be coersed to string properly.'); |
|
} |
|
|
|
return function(obj) { |
|
var dots, curObj, curKey; |
|
|
|
if(typeof obj !== 'object') { |
|
throw Error('Invalid parameter received to partial application, should have type of object'); |
|
} |
|
|
|
dots = (prop + '').split('.'); |
|
curObj = obj; |
|
|
|
while (dots.length) { |
|
curKey = dots.shift(); |
|
curObj = curObj[curKey]; |
|
} |
|
return curObj; |
|
}; |
|
} |
|
|
|
function compose(x, y) { |
|
if(typeof x !== 'function' || typeof y !== 'function') { |
|
throw Error('x and y parameters should be functions'); |
|
} |
|
|
|
if(arguments.length > 2) { |
|
throw Error('compose(fn1,fn2...fn) not implemented'); |
|
} |
|
|
|
return function() { |
|
return x(y.apply(null, arguments)); |
|
}; |
|
} |