c3
Version:
D3-based reusable chart library
803 lines (758 loc) • 22 kB
text/typescript
import CLASS from './class'
import { ChartInternal } from './core'
import { isFunction } from './util'
ChartInternal.prototype.initPie = function() {
var $$ = this,
d3 = $$.d3
$$.pie = d3
.pie()
.padAngle(this.getPadAngle.bind(this))
.value(function(d) {
return d.values.reduce(function(a, b) {
return a + b.value
}, 0)
})
let orderFct = $$.getOrderFunction()
// we need to reverse the returned order if asc or desc to have the slice in expected order.
if (orderFct && ($$.isOrderAsc() || $$.isOrderDesc())) {
let defaultSort = orderFct
orderFct = (t1, t2) => defaultSort(t1, t2) * -1
}
$$.pie.sort(orderFct || null)
}
ChartInternal.prototype.updateRadius = function() {
var $$ = this,
config = $$.config,
w = config.gauge_width || config.donut_width,
gaugeArcWidth =
$$.filterTargetsToShow($$.data.targets).length *
$$.config.gauge_arcs_minWidth
$$.radiusExpanded =
(Math.min($$.arcWidth, $$.arcHeight) / 2) * ($$.hasType('gauge') ? 0.85 : 1)
$$.radius = $$.radiusExpanded * 0.95
$$.innerRadiusRatio = w ? ($$.radius - w) / $$.radius : 0.6
$$.innerRadius =
$$.hasType('donut') || $$.hasType('gauge')
? $$.radius * $$.innerRadiusRatio
: 0
$$.gaugeArcWidth = w
? w
: gaugeArcWidth <= $$.radius - $$.innerRadius
? $$.radius - $$.innerRadius
: gaugeArcWidth <= $$.radius
? gaugeArcWidth
: $$.radius
}
ChartInternal.prototype.getPadAngle = function() {
if (this.hasType('pie')) {
return this.config.pie_padAngle || 0
} else if (this.hasType('donut')) {
return this.config.donut_padAngle || 0
} else {
return 0
}
}
ChartInternal.prototype.updateArc = function() {
var $$ = this
$$.svgArc = $$.getSvgArc()
$$.svgArcExpanded = $$.getSvgArcExpanded()
$$.svgArcExpandedSub = $$.getSvgArcExpanded(0.98)
}
ChartInternal.prototype.updateAngle = function(d) {
var $$ = this,
config = $$.config,
found = false,
index = 0,
gMin,
gMax,
gTic,
gValue
if (!config) {
return null
}
$$.pie($$.filterTargetsToShow($$.data.targets)).forEach(function(t) {
if (!found && t.data.id === d.data.id) {
found = true
d = t
d.index = index
}
index++
})
if (isNaN(d.startAngle)) {
d.startAngle = 0
}
if (isNaN(d.endAngle)) {
d.endAngle = d.startAngle
}
if ($$.isGaugeType(d.data)) {
gMin = config.gauge_min
gMax = config.gauge_max
gTic = (Math.PI * (config.gauge_fullCircle ? 2 : 1)) / (gMax - gMin)
gValue = d.value < gMin ? 0 : d.value < gMax ? d.value - gMin : gMax - gMin
d.startAngle = config.gauge_startingAngle
d.endAngle = d.startAngle + gTic * gValue
}
return found ? d : null
}
ChartInternal.prototype.getSvgArc = function() {
var $$ = this,
hasGaugeType = $$.hasType('gauge'),
singleArcWidth =
$$.gaugeArcWidth / $$.filterTargetsToShow($$.data.targets).length,
arc = $$.d3
.arc()
.outerRadius(function(d) {
return hasGaugeType ? $$.radius - singleArcWidth * d.index : $$.radius
})
.innerRadius(function(d) {
return hasGaugeType
? $$.radius - singleArcWidth * (d.index + 1)
: $$.innerRadius
}),
newArc = function(d, withoutUpdate) {
var updated
if (withoutUpdate) {
return arc(d)
} // for interpolate
updated = $$.updateAngle(d)
return updated ? arc(updated) : 'M 0 0'
}
// TODO: extends all function
;(newArc as any).centroid = arc.centroid
return newArc
}
ChartInternal.prototype.getSvgArcExpanded = function(rate) {
rate = rate || 1
var $$ = this,
hasGaugeType = $$.hasType('gauge'),
singleArcWidth =
$$.gaugeArcWidth / $$.filterTargetsToShow($$.data.targets).length,
expandWidth = Math.min(
$$.radiusExpanded * rate - $$.radius,
singleArcWidth * 0.8 - (1 - rate) * 100
),
arc = $$.d3
.arc()
.outerRadius(function(d) {
return hasGaugeType
? $$.radius - singleArcWidth * d.index + expandWidth
: $$.radiusExpanded * rate
})
.innerRadius(function(d) {
return hasGaugeType
? $$.radius - singleArcWidth * (d.index + 1)
: $$.innerRadius
})
return function(d) {
var updated = $$.updateAngle(d)
return updated ? arc(updated) : 'M 0 0'
}
}
ChartInternal.prototype.getArc = function(d, withoutUpdate, force) {
return force || this.isArcType(d.data)
? this.svgArc(d, withoutUpdate)
: 'M 0 0'
}
ChartInternal.prototype.transformForArcLabel = function(d) {
var $$ = this,
config = $$.config,
updated = $$.updateAngle(d),
c,
x,
y,
h,
ratio,
translate = '',
hasGauge = $$.hasType('gauge')
if (updated && !hasGauge) {
c = this.svgArc.centroid(updated)
x = isNaN(c[0]) ? 0 : c[0]
y = isNaN(c[1]) ? 0 : c[1]
h = Math.sqrt(x * x + y * y)
if ($$.hasType('donut') && config.donut_label_ratio) {
ratio = isFunction(config.donut_label_ratio)
? config.donut_label_ratio(d, $$.radius, h)
: config.donut_label_ratio
} else if ($$.hasType('pie') && config.pie_label_ratio) {
ratio = isFunction(config.pie_label_ratio)
? config.pie_label_ratio(d, $$.radius, h)
: config.pie_label_ratio
} else {
ratio =
$$.radius && h
? ((36 / $$.radius > 0.375 ? 1.175 - 36 / $$.radius : 0.8) *
$$.radius) /
h
: 0
}
translate = 'translate(' + x * ratio + ',' + y * ratio + ')'
} else if (
updated &&
hasGauge &&
$$.filterTargetsToShow($$.data.targets).length > 1
) {
var y1 = Math.sin(updated.endAngle - Math.PI / 2)
x = Math.cos(updated.endAngle - Math.PI / 2) * ($$.radiusExpanded + 25)
y = y1 * ($$.radiusExpanded + 15 - Math.abs(y1 * 10)) + 3
translate = 'translate(' + x + ',' + y + ')'
}
return translate
}
/**
* @deprecated Use `getRatio('arc', d)` instead.
*/
ChartInternal.prototype.getArcRatio = function(d) {
return this.getRatio('arc', d)
}
ChartInternal.prototype.convertToArcData = function(d) {
return this.addName({
id: d.data.id,
value: d.value,
ratio: this.getRatio('arc', d),
index: d.index
})
}
ChartInternal.prototype.textForArcLabel = function(d) {
var $$ = this,
updated,
value,
ratio,
id,
format
if (!$$.shouldShowArcLabel()) {
return ''
}
updated = $$.updateAngle(d)
value = updated ? updated.value : null
ratio = $$.getRatio('arc', updated)
id = d.data.id
if (!$$.hasType('gauge') && !$$.meetsArcLabelThreshold(ratio)) {
return ''
}
format = $$.getArcLabelFormat()
return format
? format(value, ratio, id)
: $$.defaultArcValueFormat(value, ratio)
}
ChartInternal.prototype.textForGaugeMinMax = function(value, isMax) {
var $$ = this,
format = $$.getGaugeLabelExtents()
return format ? format(value, isMax) : value
}
ChartInternal.prototype.expandArc = function(targetIds) {
var $$ = this,
interval
// MEMO: avoid to cancel transition
if ($$.transiting) {
interval = window.setInterval(function() {
if (!$$.transiting) {
window.clearInterval(interval)
if ($$.legend.selectAll('.c3-legend-item-focused').size() > 0) {
$$.expandArc(targetIds)
}
}
}, 10)
return
}
targetIds = $$.mapToTargetIds(targetIds)
$$.svg
.selectAll($$.selectorTargets(targetIds, '.' + CLASS.chartArc))
.each(function(d) {
if (!$$.shouldExpand(d.data.id)) {
return
}
$$.d3
.select(this)
.selectAll('path')
.transition()
.duration($$.expandDuration(d.data.id))
.attr('d', $$.svgArcExpanded)
.transition()
.duration($$.expandDuration(d.data.id) * 2)
.attr('d', $$.svgArcExpandedSub)
.each(function(d) {
if ($$.isDonutType(d.data)) {
// callback here
}
})
})
}
ChartInternal.prototype.unexpandArc = function(targetIds) {
var $$ = this
if ($$.transiting) {
return
}
targetIds = $$.mapToTargetIds(targetIds)
$$.svg
.selectAll($$.selectorTargets(targetIds, '.' + CLASS.chartArc))
.selectAll('path')
.transition()
.duration(function(d) {
return $$.expandDuration(d.data.id)
})
.attr('d', $$.svgArc)
$$.svg.selectAll('.' + CLASS.arc)
}
ChartInternal.prototype.expandDuration = function(id) {
var $$ = this,
config = $$.config
if ($$.isDonutType(id)) {
return config.donut_expand_duration
} else if ($$.isGaugeType(id)) {
return config.gauge_expand_duration
} else if ($$.isPieType(id)) {
return config.pie_expand_duration
} else {
return 50
}
}
ChartInternal.prototype.shouldExpand = function(id) {
var $$ = this,
config = $$.config
return (
($$.isDonutType(id) && config.donut_expand) ||
($$.isGaugeType(id) && config.gauge_expand) ||
($$.isPieType(id) && config.pie_expand)
)
}
ChartInternal.prototype.shouldShowArcLabel = function() {
var $$ = this,
config = $$.config,
shouldShow = true
if ($$.hasType('donut')) {
shouldShow = config.donut_label_show
} else if ($$.hasType('pie')) {
shouldShow = config.pie_label_show
}
// when gauge, always true
return shouldShow
}
ChartInternal.prototype.meetsArcLabelThreshold = function(ratio) {
var $$ = this,
config = $$.config,
threshold = $$.hasType('donut')
? config.donut_label_threshold
: config.pie_label_threshold
return ratio >= threshold
}
ChartInternal.prototype.getArcLabelFormat = function() {
var $$ = this,
config = $$.config,
format = config.pie_label_format
if ($$.hasType('gauge')) {
format = config.gauge_label_format
} else if ($$.hasType('donut')) {
format = config.donut_label_format
}
return format
}
ChartInternal.prototype.getGaugeLabelExtents = function() {
var $$ = this,
config = $$.config
return config.gauge_label_extents
}
ChartInternal.prototype.getArcTitle = function() {
var $$ = this
return $$.hasType('donut') ? $$.config.donut_title : ''
}
ChartInternal.prototype.updateTargetsForArc = function(targets) {
var $$ = this,
main = $$.main,
mainPies,
mainPieEnter,
classChartArc = $$.classChartArc.bind($$),
classArcs = $$.classArcs.bind($$),
classFocus = $$.classFocus.bind($$)
mainPies = main
.select('.' + CLASS.chartArcs)
.selectAll('.' + CLASS.chartArc)
.data($$.pie(targets))
.attr('class', function(d) {
return classChartArc(d) + classFocus(d.data)
})
mainPieEnter = mainPies
.enter()
.append('g')
.attr('class', classChartArc)
mainPieEnter.append('g').attr('class', classArcs)
mainPieEnter
.append('text')
.attr('dy', $$.hasType('gauge') ? '-.1em' : '.35em')
.style('opacity', 0)
.style('text-anchor', 'middle')
.style('pointer-events', 'none')
// MEMO: can not keep same color..., but not bad to update color in redraw
//mainPieUpdate.exit().remove();
}
ChartInternal.prototype.initArc = function() {
var $$ = this
$$.arcs = $$.main
.select('.' + CLASS.chart)
.append('g')
.attr('class', CLASS.chartArcs)
.attr('transform', $$.getTranslate('arc'))
$$.arcs
.append('text')
.attr('class', CLASS.chartArcsTitle)
.style('text-anchor', 'middle')
.text($$.getArcTitle())
}
ChartInternal.prototype.redrawArc = function(
duration,
durationForExit,
withTransform
) {
var $$ = this,
d3 = $$.d3,
config = $$.config,
main = $$.main,
arcs,
mainArc,
arcLabelLines,
mainArcLabelLine,
hasGaugeType = $$.hasType('gauge')
arcs = main
.selectAll('.' + CLASS.arcs)
.selectAll('.' + CLASS.arc)
.data($$.arcData.bind($$))
mainArc = arcs
.enter()
.append('path')
.attr('class', $$.classArc.bind($$))
.style('fill', function(d) {
return $$.color(d.data)
})
.style('cursor', function(d) {
return config.interaction_enabled && config.data_selection_isselectable(d)
? 'pointer'
: null
})
.each(function(d) {
if ($$.isGaugeType(d.data)) {
d.startAngle = d.endAngle = config.gauge_startingAngle
}
this._current = d
})
.merge(arcs)
if (hasGaugeType) {
arcLabelLines = main
.selectAll('.' + CLASS.arcs)
.selectAll('.' + CLASS.arcLabelLine)
.data($$.arcData.bind($$))
mainArcLabelLine = arcLabelLines
.enter()
.append('rect')
.attr('class', function(d) {
return (
CLASS.arcLabelLine +
' ' +
CLASS.target +
' ' +
CLASS.target +
'-' +
d.data.id
)
})
.merge(arcLabelLines)
if ($$.filterTargetsToShow($$.data.targets).length === 1) {
mainArcLabelLine.style('display', 'none')
} else {
mainArcLabelLine
.style('fill', function(d) {
return $$.levelColor
? $$.levelColor(
d.data.values.reduce(function(total, item) {
return total + item.value
}, 0)
)
: $$.color(d.data)
})
.style('display', config.gauge_labelLine_show ? '' : 'none')
.each(function(d) {
var lineLength = 0,
lineThickness = 2,
x = 0,
y = 0,
transform = ''
if ($$.hiddenTargetIds.indexOf(d.data.id) < 0) {
var updated = $$.updateAngle(d),
innerLineLength =
($$.gaugeArcWidth /
$$.filterTargetsToShow($$.data.targets).length) *
(updated.index + 1),
lineAngle = updated.endAngle - Math.PI / 2,
arcInnerRadius = $$.radius - innerLineLength,
linePositioningAngle =
lineAngle - (arcInnerRadius === 0 ? 0 : 1 / arcInnerRadius)
lineLength = $$.radiusExpanded - $$.radius + innerLineLength
x = Math.cos(linePositioningAngle) * arcInnerRadius
y = Math.sin(linePositioningAngle) * arcInnerRadius
transform =
'rotate(' +
(lineAngle * 180) / Math.PI +
', ' +
x +
', ' +
y +
')'
}
d3.select(this)
.attr('x', x)
.attr('y', y)
.attr('width', lineLength)
.attr('height', lineThickness)
.attr('transform', transform)
.style(
'stroke-dasharray',
'0, ' + (lineLength + lineThickness) + ', 0'
)
})
}
}
mainArc
.attr('transform', function(d) {
return !$$.isGaugeType(d.data) && withTransform ? 'scale(0)' : ''
})
.on(
'mouseover',
config.interaction_enabled
? function(d) {
var updated, arcData
if ($$.transiting) {
// skip while transiting
return
}
updated = $$.updateAngle(d)
if (updated) {
arcData = $$.convertToArcData(updated)
// transitions
$$.expandArc(updated.data.id)
$$.api.focus(updated.data.id)
$$.toggleFocusLegend(updated.data.id, true)
$$.config.data_onmouseover(arcData, this)
}
}
: null
)
.on(
'mousemove',
config.interaction_enabled
? function(d) {
var updated = $$.updateAngle(d),
arcData,
selectedData
if (updated) {
;(arcData = $$.convertToArcData(updated)),
(selectedData = [arcData])
$$.showTooltip(selectedData, this)
}
}
: null
)
.on(
'mouseout',
config.interaction_enabled
? function(d) {
var updated, arcData
if ($$.transiting) {
// skip while transiting
return
}
updated = $$.updateAngle(d)
if (updated) {
arcData = $$.convertToArcData(updated)
// transitions
$$.unexpandArc(updated.data.id)
$$.api.revert()
$$.revertLegend()
$$.hideTooltip()
$$.config.data_onmouseout(arcData, this)
}
}
: null
)
.on(
'click',
config.interaction_enabled
? function(d, i) {
var updated = $$.updateAngle(d),
arcData
if (updated) {
arcData = $$.convertToArcData(updated)
if ($$.toggleShape) {
$$.toggleShape(this, arcData, i)
}
$$.config.data_onclick.call($$.api, arcData, this)
}
}
: null
)
.each(function() {
$$.transiting = true
})
.transition()
.duration(duration)
.attrTween('d', function(d) {
var updated = $$.updateAngle(d),
interpolate
if (!updated) {
return function() {
return 'M 0 0'
}
}
// if (this._current === d) {
// this._current = {
// startAngle: Math.PI*2,
// endAngle: Math.PI*2,
// };
// }
if (isNaN(this._current.startAngle)) {
this._current.startAngle = 0
}
if (isNaN(this._current.endAngle)) {
this._current.endAngle = this._current.startAngle
}
interpolate = d3.interpolate(this._current, updated)
this._current = interpolate(0)
return function(t) {
// prevents crashing the charts once in transition and chart.destroy() has been called
if ($$.config === null) {
return 'M 0 0'
}
var interpolated = interpolate(t)
interpolated.data = d.data // data.id will be updated by interporator
return $$.getArc(interpolated, true)
}
})
.attr('transform', withTransform ? 'scale(1)' : '')
.style('fill', function(d) {
return $$.levelColor
? $$.levelColor(
d.data.values.reduce(function(total, item) {
return total + item.value
}, 0)
)
: $$.color(d.data.id)
}) // Where gauge reading color would receive customization.
.call($$.endall, function() {
$$.transiting = false
})
arcs
.exit()
.transition()
.duration(durationForExit)
.style('opacity', 0)
.remove()
main
.selectAll('.' + CLASS.chartArc)
.select('text')
.style('opacity', 0)
.attr('class', function(d) {
return $$.isGaugeType(d.data) ? CLASS.gaugeValue : ''
})
.text($$.textForArcLabel.bind($$))
.attr('transform', $$.transformForArcLabel.bind($$))
.style('font-size', function(d) {
return $$.isGaugeType(d.data) &&
$$.filterTargetsToShow($$.data.targets).length === 1
? Math.round($$.radius / 5) + 'px'
: ''
})
.transition()
.duration(duration)
.style('opacity', function(d) {
return $$.isTargetToShow(d.data.id) && $$.isArcType(d.data) ? 1 : 0
})
main
.select('.' + CLASS.chartArcsTitle)
.style('opacity', $$.hasType('donut') || hasGaugeType ? 1 : 0)
if (hasGaugeType) {
let index = 0
const backgroundArc = $$.arcs
.select('g.' + CLASS.chartArcsBackground)
.selectAll('path.' + CLASS.chartArcsBackground)
.data($$.data.targets)
backgroundArc
.enter()
.append('path')
.attr(
'class',
(d, i) =>
CLASS.chartArcsBackground + ' ' + CLASS.chartArcsBackground + '-' + i
)
.merge(backgroundArc)
.attr('d', d1 => {
if ($$.hiddenTargetIds.indexOf(d1.id) >= 0) {
return 'M 0 0'
}
var d = {
data: [{ value: config.gauge_max }],
startAngle: config.gauge_startingAngle,
endAngle:
-1 *
config.gauge_startingAngle *
(config.gauge_fullCircle ? Math.PI : 1),
index: index++
}
return $$.getArc(d, true, true)
})
backgroundArc.exit().remove()
$$.arcs
.select('.' + CLASS.chartArcsGaugeUnit)
.attr('dy', '.75em')
.text(config.gauge_label_show ? config.gauge_units : '')
$$.arcs
.select('.' + CLASS.chartArcsGaugeMin)
.attr(
'dx',
-1 *
($$.innerRadius +
($$.radius - $$.innerRadius) / (config.gauge_fullCircle ? 1 : 2)) +
'px'
)
.attr('dy', '1.2em')
.text(
config.gauge_label_show
? $$.textForGaugeMinMax(config.gauge_min, false)
: ''
)
$$.arcs
.select('.' + CLASS.chartArcsGaugeMax)
.attr(
'dx',
$$.innerRadius +
($$.radius - $$.innerRadius) / (config.gauge_fullCircle ? 1 : 2) +
'px'
)
.attr('dy', '1.2em')
.text(
config.gauge_label_show
? $$.textForGaugeMinMax(config.gauge_max, true)
: ''
)
}
}
ChartInternal.prototype.initGauge = function() {
var arcs = this.arcs
if (this.hasType('gauge')) {
arcs.append('g').attr('class', CLASS.chartArcsBackground)
arcs
.append('text')
.attr('class', CLASS.chartArcsGaugeUnit)
.style('text-anchor', 'middle')
.style('pointer-events', 'none')
arcs
.append('text')
.attr('class', CLASS.chartArcsGaugeMin)
.style('text-anchor', 'middle')
.style('pointer-events', 'none')
arcs
.append('text')
.attr('class', CLASS.chartArcsGaugeMax)
.style('text-anchor', 'middle')
.style('pointer-events', 'none')
}
}
ChartInternal.prototype.getGaugeLabelHeight = function() {
return this.config.gauge_label_show ? 20 : 0
}