|
/** |
|
* module for reusable linechart |
|
* good ref at http://bost.ocks.org/mike/chart/time-series-chart.js |
|
* (blog post is at http://bost.ocks.org/mike/chart |
|
*/ |
|
|
|
//todo check if need selection.data([values[, key]]) |
|
|
|
function lineplot(){ |
|
var width = 500, |
|
height = 500, |
|
margins = {top: 40, bottom: 40, left:40, right:40}, |
|
line = d3.svg.line().x(X).y(Y), |
|
xScale = d3.scale.ordinal(), |
|
yScale = d3.scale.linear(), |
|
xAxis = d3.svg.axis().scale(xScale).orient("bottom"), |
|
yAxis = d3.svg.axis().scale(yScale).orient("left"), |
|
xValue = function(d) { return d[0]; }, |
|
yValue = function(d) { return d[1]; }, |
|
datapointRadius = 0; |
|
|
|
|
|
// The x-accessor for the path generator; xScale ∘ xValue. |
|
function X(d) { |
|
return xScale(d[0]) + xScale.rangeBand()/2 |
|
} |
|
|
|
function Y(d){ |
|
return yScale(d[1]) |
|
} |
|
|
|
function chart(selection){ |
|
var svg, lineSVG, xAxisSVG, yAxisSVG; |
|
|
|
selection.each((data) => { |
|
let dataType = "single" |
|
if(Array.isArray(data[0])){ |
|
data = data.map(d => { |
|
let outArr = dataMapping(d); |
|
// if the array has a property 'name' then add that to the mapped array to label the lines by |
|
if(d.name) outArr.name = d.name; |
|
//if array has a property 'color' add that to mapped array for linecolor |
|
if(d.color) outArr.color = d.color; |
|
return outArr |
|
}); |
|
dataType = "multi" |
|
}else{ |
|
data = dataMapping(data); |
|
} |
|
// Convert data to standard [[x1,y1], [x2, y2], [x3, y3]...] per line |
|
// this is needed for nondeterministic accessors. |
|
function dataMapping(data){ |
|
return data.map((d, i) => { |
|
return [xValue.call(data, d, i), yValue.call(data, d, i)]; |
|
}); |
|
} |
|
|
|
//for an array of arrays, get all the possible unique x values (data parsed as per dataMapping func) |
|
function getOrdinalRange(data){ |
|
var outArr = []; |
|
data.forEach(d => { |
|
d.forEach(i => { |
|
if(outArr.indexOf(i[0]) < 0){ |
|
outArr.push(i[0]) |
|
} |
|
}) |
|
}); |
|
|
|
outArr.sort((a,b) => (a-b)); |
|
return outArr |
|
} |
|
|
|
function drawLineGroup(data, svg){ |
|
let plottingData; |
|
if(dataType == "single"){ |
|
plottingData = [data]; |
|
}else { |
|
plottingData = data; |
|
} |
|
|
|
let lineFunc = d3.svg.line().x(X).y(Y); |
|
let lineG = svg.selectAll('g.data-lineG').data(plottingData); |
|
let newG = lineG.enter().append('g').attr('class', 'data-lineG'); |
|
|
|
let line = lineG.selectAll('path.data-line').data(d => [d]); |
|
line.enter().append('path').attr('id', (d,i) => d.name || "linechart-line") |
|
.attr('class', 'data-line'); |
|
line.attr('d', lineFunc).attr("fill", "none") |
|
.attr("stroke", (d,i) => d.color || "black"); |
|
line.exit().remove(); |
|
|
|
let datapoints = lineG.selectAll('.data-point') |
|
.data(d => {d.forEach(item => {item.color = d.color}); return d}); |
|
datapoints.enter().append("circle") |
|
.attr("r", datapointRadius).attr("class", "data-point") |
|
.attr('fill', (d) => { |
|
return d.color || "black" |
|
}); |
|
datapoints.attr("cx", (d, i) => {return xScale(d[0]) + xScale.rangeBand()/2}) |
|
.attr("cy", d=> yScale(d[1])) |
|
.attr("r", datapointRadius) |
|
.attr('fill', (d, i) => { |
|
return d.color || "black" |
|
}); |
|
datapoints.exit().remove(); |
|
|
|
lineG.exit().remove(); |
|
} |
|
|
|
|
|
/** |
|
* for an array of arrays, get the max value of Y |
|
* @param data parsed as [er dataMapping func) |
|
*/ |
|
function getMaxLinear(data){ |
|
d3.max(data, d => { d3.max(i => i[1]) }) |
|
} |
|
let xDomain, yDomain; |
|
|
|
//have |
|
if(dataType === "multi"){ |
|
xDomain = getOrdinalRange(data); |
|
yDomain = [0, d3.max(data.map(d => 1.2 * d3.max(d, i => i[1])))]; |
|
}else { |
|
xDomain = data.map( d=> d[0] ).sort((a,b) => (a-b)); |
|
yDomain = [0, 1.2 * d3.max(data, d => d[1])]; //add in a bit of padding to the yMax so the lines don't bang against the top of the graph |
|
} |
|
xScale |
|
.domain(xDomain) |
|
.rangeBands([0, (width - margins.left - margins.right)]); |
|
|
|
// Update the y-scale. |
|
yScale |
|
.domain(yDomain) |
|
.range([height - margins.top - margins.bottom, 0]); |
|
|
|
//select the svg if it exists |
|
//svg = d3.select(this).selectAll("svg.linechart").data([data]); |
|
svg = this.selectAll("svg.linechart").data([data]); |
|
//or add a new one if not |
|
var newSVG = svg.enter().append('svg').attr('class', 'linechart'); |
|
|
|
lineSVG = newSVG.append("g").attr('class', 'lines') |
|
.attr('transform', `translate(${margins.left} ,${margins.top})`); |
|
|
|
xAxisSVG = newSVG.append('g').attr('class', 'x axis') |
|
.attr('transform', `translate( ${margins.left} , ${height - margins.bottom})`); |
|
|
|
yAxisSVG = newSVG.append('g').attr('class', 'y axis') |
|
.attr('transform', `translate( ${margins.left} , ${margins.top})`); |
|
|
|
svg.attr("width" , width) |
|
.attr("height", height); |
|
|
|
let plottingData = data; |
|
svg.select('.x.axis').call(xAxis); |
|
svg.select('.y.axis').call(yAxis); |
|
if(!Array.isArray(data[0])){ |
|
plottingData = [data] |
|
} |
|
drawLineGroup(plottingData, svg.select('g.lines')); |
|
//drawLines(plottingData, svg.select('g.lines')); |
|
//drawDataPoints(plottingData, svg.select('g.lines')); |
|
|
|
}) |
|
} |
|
|
|
|
|
|
|
//getters and setters |
|
chart.width = function(val) { |
|
if (!arguments.length) { |
|
return width; |
|
} |
|
width = val; |
|
return chart; |
|
}; |
|
|
|
chart.height = function(val) { |
|
if (!arguments.length) { |
|
return height; |
|
} |
|
height = val; |
|
return chart; |
|
}; |
|
|
|
chart.margins = function(val) { |
|
if (!arguments.length) { |
|
return margins; |
|
} |
|
margins = val; |
|
return chart; |
|
}; |
|
|
|
chart.xAxis = function(axis){ |
|
if (!arguments.length) { |
|
return xAxis |
|
} |
|
|
|
xAxis = axis; |
|
return chart; |
|
}; |
|
|
|
chart.yAxis = function(axis){ |
|
if (!arguments.length) { |
|
return yAxis |
|
} |
|
yAxis = axis; |
|
return chart; |
|
}; |
|
|
|
chart.x = function(xAccessor) { |
|
if (!arguments.length) return xValue; |
|
xValue = xAccessor; |
|
return chart; |
|
}; |
|
|
|
chart.y = function(yAccessor) { |
|
if (!arguments.length) return yValue; |
|
yValue = yAccessor; |
|
return chart; |
|
}; |
|
|
|
chart.datapointRadius = function(radius) { |
|
if (!arguments.length) return datapointRadius; |
|
datapointRadius = radius; |
|
return chart; |
|
}; |
|
|
|
return chart |
|
} |
|
|
|
|
|
export {lineplot} |