c3
Version:
D3-based reusable chart library
593 lines (574 loc) • 16.2 kB
text/typescript
import CLASS from './class'
import { ChartInternal } from './core'
import { isValue, isFunction, isUndefined, isDefined } from './util'
ChartInternal.prototype.initLine = function() {
var $$ = this
$$.main
.select('.' + CLASS.chart)
.append('g')
.attr('class', CLASS.chartLines)
}
ChartInternal.prototype.updateTargetsForLine = function(targets) {
var $$ = this,
config = $$.config,
mainLines,
mainLineEnter,
classChartLine = $$.classChartLine.bind($$),
classLines = $$.classLines.bind($$),
classAreas = $$.classAreas.bind($$),
classCircles = $$.classCircles.bind($$),
classFocus = $$.classFocus.bind($$)
mainLines = $$.main
.select('.' + CLASS.chartLines)
.selectAll('.' + CLASS.chartLine)
.data(targets)
.attr('class', function(d) {
return classChartLine(d) + classFocus(d)
})
mainLineEnter = mainLines
.enter()
.append('g')
.attr('class', classChartLine)
.style('opacity', 0)
.style('pointer-events', 'none')
// Lines for each data
mainLineEnter.append('g').attr('class', classLines)
// Areas
mainLineEnter.append('g').attr('class', classAreas)
// Circles for each data point on lines
mainLineEnter.append('g').attr('class', function(d) {
return $$.generateClass(CLASS.selectedCircles, d.id)
})
mainLineEnter
.append('g')
.attr('class', classCircles)
.style('cursor', function(d) {
return config.data_selection_isselectable(d) ? 'pointer' : null
})
// Update date for selected circles
targets.forEach(function(t) {
$$.main
.selectAll('.' + CLASS.selectedCircles + $$.getTargetSelectorSuffix(t.id))
.selectAll('.' + CLASS.selectedCircle)
.each(function(d) {
d.value = t.values[d.index].value
})
})
// MEMO: can not keep same color...
//mainLineUpdate.exit().remove();
}
ChartInternal.prototype.updateLine = function(durationForExit) {
var $$ = this
var mainLine = $$.main
.selectAll('.' + CLASS.lines)
.selectAll('.' + CLASS.line)
.data($$.lineData.bind($$))
var mainLineEnter = mainLine
.enter()
.append('path')
.attr('class', $$.classLine.bind($$))
.style('stroke', $$.color)
$$.mainLine = mainLineEnter
.merge(mainLine)
.style('opacity', $$.initialOpacity.bind($$))
.style('shape-rendering', function(d) {
return $$.isStepType(d) ? 'crispEdges' : ''
})
.attr('transform', null)
mainLine
.exit()
.transition()
.duration(durationForExit)
.style('opacity', 0)
}
ChartInternal.prototype.redrawLine = function(
drawLine,
withTransition,
transition
) {
return [
(withTransition ? this.mainLine.transition(transition) : this.mainLine)
.attr('d', drawLine)
.style('stroke', this.color)
.style('opacity', 1)
]
}
ChartInternal.prototype.generateDrawLine = function(lineIndices, isSub) {
var $$ = this,
config = $$.config,
line = $$.d3.line(),
getPoints = $$.generateGetLinePoints(lineIndices, isSub),
yScaleGetter = isSub ? $$.getSubYScale : $$.getYScale,
xValue = function(d) {
return (isSub ? $$.subxx : $$.xx).call($$, d)
},
yValue = function(d, i) {
return config.data_groups.length > 0
? getPoints(d, i)[0][1]
: yScaleGetter.call($$, d.id)(d.value)
}
line = config.axis_rotated
? line.x(yValue).y(xValue)
: line.x(xValue).y(yValue)
if (!config.line_connectNull) {
line = line.defined(function(d) {
return d.value != null
})
}
return function(d) {
var values = config.line_connectNull
? $$.filterRemoveNull(d.values)
: d.values,
x = isSub ? $$.subX : $$.x,
y = yScaleGetter.call($$, d.id),
x0 = 0,
y0 = 0,
path
if ($$.isLineType(d)) {
if (config.data_regions[d.id]) {
path = $$.lineWithRegions(values, x, y, config.data_regions[d.id])
} else {
if ($$.isStepType(d)) {
values = $$.convertValuesToStep(values)
}
path = line.curve($$.getInterpolate(d))(values)
}
} else {
if (values[0]) {
x0 = x(values[0].x)
y0 = y(values[0].value)
}
path = config.axis_rotated ? 'M ' + y0 + ' ' + x0 : 'M ' + x0 + ' ' + y0
}
return path ? path : 'M 0 0'
}
}
ChartInternal.prototype.generateGetLinePoints = function(lineIndices, isSub) {
// partial duplication of generateGetBarPoints
var $$ = this,
config = $$.config,
lineTargetsNum = lineIndices.__max__ + 1,
x = $$.getShapeX(0, lineTargetsNum, lineIndices, !!isSub),
y = $$.getShapeY(!!isSub),
lineOffset = $$.getShapeOffset($$.isLineType, lineIndices, !!isSub),
yScale = isSub ? $$.getSubYScale : $$.getYScale
return function(d, i) {
var y0 = yScale.call($$, d.id)(0),
offset = lineOffset(d, i) || y0, // offset is for stacked area chart
posX = x(d),
posY = y(d)
// fix posY not to overflow opposite quadrant
if (config.axis_rotated) {
if ((0 < d.value && posY < y0) || (d.value < 0 && y0 < posY)) {
posY = y0
}
}
// 1 point that marks the line position
return [
[posX, posY - (y0 - offset)],
[posX, posY - (y0 - offset)], // needed for compatibility
[posX, posY - (y0 - offset)], // needed for compatibility
[posX, posY - (y0 - offset)] // needed for compatibility
]
}
}
ChartInternal.prototype.lineWithRegions = function(d, x, y, _regions) {
var $$ = this,
config = $$.config,
prev = -1,
i,
j,
s = 'M',
sWithRegion,
xp,
yp,
dx,
dy,
dd,
diff,
diffx2,
xOffset = $$.isCategorized() ? 0.5 : 0,
xValue,
yValue,
regions = []
function isWithinRegions(x, regions) {
var i
for (i = 0; i < regions.length; i++) {
if (regions[i].start < x && x <= regions[i].end) {
return true
}
}
return false
}
// Check start/end of regions
if (isDefined(_regions)) {
for (i = 0; i < _regions.length; i++) {
regions[i] = {}
if (isUndefined(_regions[i].start)) {
regions[i].start = d[0].x
} else {
regions[i].start = $$.isTimeSeries()
? $$.parseDate(_regions[i].start)
: _regions[i].start
}
if (isUndefined(_regions[i].end)) {
regions[i].end = d[d.length - 1].x
} else {
regions[i].end = $$.isTimeSeries()
? $$.parseDate(_regions[i].end)
: _regions[i].end
}
}
}
// Set scales
xValue = config.axis_rotated
? function(d) {
return y(d.value)
}
: function(d) {
return x(d.x)
}
yValue = config.axis_rotated
? function(d) {
return x(d.x)
}
: function(d) {
return y(d.value)
}
// Define svg generator function for region
function generateM(points) {
return (
'M' +
points[0][0] +
' ' +
points[0][1] +
' ' +
points[1][0] +
' ' +
points[1][1]
)
}
if ($$.isTimeSeries()) {
sWithRegion = function(d0, d1, j, diff) {
var x0 = d0.x.getTime(),
x_diff = d1.x - d0.x,
xv0 = new Date(x0 + x_diff * j),
xv1 = new Date(x0 + x_diff * (j + diff)),
points
if (config.axis_rotated) {
points = [
[y(yp(j)), x(xv0)],
[y(yp(j + diff)), x(xv1)]
]
} else {
points = [
[x(xv0), y(yp(j))],
[x(xv1), y(yp(j + diff))]
]
}
return generateM(points)
}
} else {
sWithRegion = function(d0, d1, j, diff) {
var points
if (config.axis_rotated) {
points = [
[y(yp(j), true), x(xp(j))],
[y(yp(j + diff), true), x(xp(j + diff))]
]
} else {
points = [
[x(xp(j), true), y(yp(j))],
[x(xp(j + diff), true), y(yp(j + diff))]
]
}
return generateM(points)
}
}
// Generate
for (i = 0; i < d.length; i++) {
// Draw as normal
if (isUndefined(regions) || !isWithinRegions(d[i].x, regions)) {
s += ' ' + xValue(d[i]) + ' ' + yValue(d[i])
}
// Draw with region // TODO: Fix for horizotal charts
else {
xp = $$.getScale(
d[i - 1].x + xOffset,
d[i].x + xOffset,
$$.isTimeSeries()
)
yp = $$.getScale(d[i - 1].value, d[i].value)
dx = x(d[i].x) - x(d[i - 1].x)
dy = y(d[i].value) - y(d[i - 1].value)
dd = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2))
diff = 2 / dd
diffx2 = diff * 2
for (j = diff; j <= 1; j += diffx2) {
s += sWithRegion(d[i - 1], d[i], j, diff)
}
}
prev = d[i].x
}
return s
}
ChartInternal.prototype.updateArea = function(durationForExit) {
var $$ = this,
d3 = $$.d3
var mainArea = $$.main
.selectAll('.' + CLASS.areas)
.selectAll('.' + CLASS.area)
.data($$.lineData.bind($$))
var mainAreaEnter = mainArea
.enter()
.append('path')
.attr('class', $$.classArea.bind($$))
.style('fill', $$.color)
.style('opacity', function() {
$$.orgAreaOpacity = +d3.select(this).style('opacity')
return 0
})
$$.mainArea = mainAreaEnter
.merge(mainArea)
.style('opacity', $$.orgAreaOpacity)
mainArea
.exit()
.transition()
.duration(durationForExit)
.style('opacity', 0)
}
ChartInternal.prototype.redrawArea = function(
drawArea,
withTransition,
transition
) {
return [
(withTransition ? this.mainArea.transition(transition) : this.mainArea)
.attr('d', drawArea)
.style('fill', this.color)
.style('opacity', this.orgAreaOpacity)
]
}
ChartInternal.prototype.generateDrawArea = function(areaIndices, isSub) {
var $$ = this,
config = $$.config,
area = $$.d3.area(),
getPoints = $$.generateGetAreaPoints(areaIndices, isSub),
yScaleGetter = isSub ? $$.getSubYScale : $$.getYScale,
xValue = function(d) {
return (isSub ? $$.subxx : $$.xx).call($$, d)
},
value0 = function(d, i) {
return config.data_groups.length > 0
? getPoints(d, i)[0][1]
: yScaleGetter.call($$, d.id)($$.getAreaBaseValue(d.id))
},
value1 = function(d, i) {
return config.data_groups.length > 0
? getPoints(d, i)[1][1]
: yScaleGetter.call($$, d.id)(d.value)
}
area = config.axis_rotated
? area
.x0(value0)
.x1(value1)
.y(xValue)
: area
.x(xValue)
.y0(config.area_above ? 0 : value0)
.y1(value1)
if (!config.line_connectNull) {
area = area.defined(function(d) {
return d.value !== null
})
}
return function(d) {
var values = config.line_connectNull
? $$.filterRemoveNull(d.values)
: d.values,
x0 = 0,
y0 = 0,
path
if ($$.isAreaType(d)) {
if ($$.isStepType(d)) {
values = $$.convertValuesToStep(values)
}
path = area.curve($$.getInterpolate(d))(values)
} else {
if (values[0]) {
x0 = $$.x(values[0].x)
y0 = $$.getYScale(d.id)(values[0].value)
}
path = config.axis_rotated ? 'M ' + y0 + ' ' + x0 : 'M ' + x0 + ' ' + y0
}
return path ? path : 'M 0 0'
}
}
ChartInternal.prototype.getAreaBaseValue = function() {
return 0
}
ChartInternal.prototype.generateGetAreaPoints = function(areaIndices, isSub) {
// partial duplication of generateGetBarPoints
var $$ = this,
config = $$.config,
areaTargetsNum = areaIndices.__max__ + 1,
x = $$.getShapeX(0, areaTargetsNum, areaIndices, !!isSub),
y = $$.getShapeY(!!isSub),
areaOffset = $$.getShapeOffset($$.isAreaType, areaIndices, !!isSub),
yScale = isSub ? $$.getSubYScale : $$.getYScale
return function(d, i) {
var y0 = yScale.call($$, d.id)(0),
offset = areaOffset(d, i) || y0, // offset is for stacked area chart
posX = x(d),
posY = y(d)
// fix posY not to overflow opposite quadrant
if (config.axis_rotated) {
if ((0 < d.value && posY < y0) || (d.value < 0 && y0 < posY)) {
posY = y0
}
}
// 1 point that marks the area position
return [
[posX, offset],
[posX, posY - (y0 - offset)],
[posX, posY - (y0 - offset)], // needed for compatibility
[posX, offset] // needed for compatibility
]
}
}
ChartInternal.prototype.updateCircle = function(cx, cy) {
var $$ = this
var mainCircle = $$.main
.selectAll('.' + CLASS.circles)
.selectAll('.' + CLASS.circle)
.data($$.lineOrScatterOrStanfordData.bind($$))
var mainCircleEnter = mainCircle
.enter()
.append('circle')
.attr('shape-rendering', $$.isStanfordGraphType() ? 'crispEdges' : '')
.attr('class', $$.classCircle.bind($$))
.attr('cx', cx)
.attr('cy', cy)
.attr('r', $$.pointR.bind($$))
.style(
'color',
$$.isStanfordGraphType() ? $$.getStanfordPointColor.bind($$) : $$.color
)
$$.mainCircle = mainCircleEnter
.merge(mainCircle)
.style(
'opacity',
$$.isStanfordGraphType() ? 1 : $$.initialOpacityForCircle.bind($$)
)
mainCircle.exit().style('opacity', 0)
}
ChartInternal.prototype.redrawCircle = function(
cx,
cy,
withTransition,
transition
) {
var $$ = this,
selectedCircles = $$.main.selectAll('.' + CLASS.selectedCircle)
return [
(withTransition ? $$.mainCircle.transition(transition) : $$.mainCircle)
.style('opacity', this.opacityForCircle.bind($$))
.style(
'color',
$$.isStanfordGraphType() ? $$.getStanfordPointColor.bind($$) : $$.color
)
.attr('cx', cx)
.attr('cy', cy),
(withTransition ? selectedCircles.transition(transition) : selectedCircles)
.attr('cx', cx)
.attr('cy', cy)
]
}
ChartInternal.prototype.circleX = function(d) {
return d.x || d.x === 0 ? this.x(d.x) : null
}
ChartInternal.prototype.updateCircleY = function() {
var $$ = this,
lineIndices,
getPoints
if ($$.config.data_groups.length > 0) {
;(lineIndices = $$.getShapeIndices($$.isLineType)),
(getPoints = $$.generateGetLinePoints(lineIndices))
$$.circleY = function(d, i) {
return getPoints(d, i)[0][1]
}
} else {
$$.circleY = function(d) {
return $$.getYScale(d.id)(d.value)
}
}
}
ChartInternal.prototype.getCircles = function(i, id) {
var $$ = this
return (id
? $$.main.selectAll('.' + CLASS.circles + $$.getTargetSelectorSuffix(id))
: $$.main
).selectAll('.' + CLASS.circle + (isValue(i) ? '-' + i : ''))
}
ChartInternal.prototype.expandCircles = function(i, id, reset) {
var $$ = this,
r = $$.pointExpandedR.bind($$)
if (reset) {
$$.unexpandCircles()
}
$$.getCircles(i, id)
.classed(CLASS.EXPANDED, true)
.attr('r', r)
}
ChartInternal.prototype.unexpandCircles = function(i) {
var $$ = this,
r = $$.pointR.bind($$)
$$.getCircles(i)
.filter(function() {
return $$.d3.select(this).classed(CLASS.EXPANDED)
})
.classed(CLASS.EXPANDED, false)
.attr('r', r)
}
ChartInternal.prototype.pointR = function(d) {
var $$ = this,
config = $$.config
return $$.isStepType(d)
? 0
: isFunction(config.point_r)
? config.point_r(d)
: config.point_r
}
ChartInternal.prototype.pointExpandedR = function(d) {
var $$ = this,
config = $$.config
if (config.point_focus_expand_enabled) {
return isFunction(config.point_focus_expand_r)
? config.point_focus_expand_r(d)
: config.point_focus_expand_r
? config.point_focus_expand_r
: $$.pointR(d) * 1.75
} else {
return $$.pointR(d)
}
}
ChartInternal.prototype.pointSelectR = function(d) {
var $$ = this,
config = $$.config
return isFunction(config.point_select_r)
? config.point_select_r(d)
: config.point_select_r
? config.point_select_r
: $$.pointR(d) * 4
}
ChartInternal.prototype.isWithinCircle = function(that, r) {
var d3 = this.d3,
mouse = d3.mouse(that),
d3_this = d3.select(that),
cx = +d3_this.attr('cx'),
cy = +d3_this.attr('cy')
return Math.sqrt(Math.pow(cx - mouse[0], 2) + Math.pow(cy - mouse[1], 2)) < r
}
ChartInternal.prototype.isWithinStep = function(that, y) {
return Math.abs(y - this.d3.mouse(that)[1]) < 30
}