echarts-liquidfill
Version:
ECharts liquid fill extension
508 lines (433 loc) • 17.5 kB
JavaScript
import * as echarts from 'echarts/lib/echarts';
import * as numberUtil from 'echarts/lib/util/number';
import LiquidShape from './liquidFillShape';
var parsePercent = numberUtil.parsePercent;
function isPathSymbol(symbol) {
return symbol && symbol.indexOf('path://') === 0
}
echarts.extendChartView({
type: 'liquidFill',
render: function (seriesModel, ecModel, api) {
var self = this;
var group = this.group;
group.removeAll();
var data = seriesModel.getData();
var itemModel = data.getItemModel(0);
var center = itemModel.get('center');
var radius = itemModel.get('radius');
var width = api.getWidth();
var height = api.getHeight();
var size = Math.min(width, height);
// itemStyle
var outlineDistance = 0;
var outlineBorderWidth = 0;
var showOutline = seriesModel.get('outline.show');
if (showOutline) {
outlineDistance = seriesModel.get('outline.borderDistance');
outlineBorderWidth = parsePercent(
seriesModel.get('outline.itemStyle.borderWidth'), size
);
}
var cx = parsePercent(center[0], width);
var cy = parsePercent(center[1], height);
var outterRadius;
var innerRadius;
var paddingRadius;
var isFillContainer = false;
var symbol = seriesModel.get('shape');
if (symbol === 'container') {
// a shape that fully fills the container
isFillContainer = true;
outterRadius = [
width / 2,
height / 2
];
innerRadius = [
outterRadius[0] - outlineBorderWidth / 2,
outterRadius[1] - outlineBorderWidth / 2
];
paddingRadius = [
parsePercent(outlineDistance, width),
parsePercent(outlineDistance, height)
];
radius = [
Math.max(innerRadius[0] - paddingRadius[0], 0),
Math.max(innerRadius[1] - paddingRadius[1], 0)
];
}
else {
outterRadius = parsePercent(radius, size) / 2;
innerRadius = outterRadius - outlineBorderWidth / 2;
paddingRadius = parsePercent(outlineDistance, size);
radius = Math.max(innerRadius - paddingRadius, 0);
}
if (showOutline) {
var outline = getOutline();
outline.style.lineWidth = outlineBorderWidth;
group.add(getOutline());
}
var left = isFillContainer ? 0 : cx - radius;
var top = isFillContainer ? 0 : cy - radius;
var wavePath = null;
group.add(getBackground());
// each data item for a wave
var oldData = this._data;
var waves = [];
data.diff(oldData)
.add(function (idx) {
var wave = getWave(idx, false);
var waterLevel = wave.shape.waterLevel;
wave.shape.waterLevel = isFillContainer ? height / 2 : radius;
echarts.graphic.initProps(wave, {
shape: {
waterLevel: waterLevel
}
}, seriesModel);
wave.z2 = 2;
setWaveAnimation(idx, wave, null);
group.add(wave);
data.setItemGraphicEl(idx, wave);
waves.push(wave);
})
.update(function (newIdx, oldIdx) {
var waveElement = oldData.getItemGraphicEl(oldIdx);
// new wave is used to calculate position, but not added
var newWave = getWave(newIdx, false, waveElement);
// changes with animation
var shape = {};
var shapeAttrs = ['amplitude', 'cx', 'cy', 'phase', 'radius', 'radiusY', 'waterLevel', 'waveLength'];
for (var i = 0; i < shapeAttrs.length; ++i) {
var attr = shapeAttrs[i];
if (newWave.shape.hasOwnProperty(attr)) {
shape[attr] = newWave.shape[attr];
}
}
var style = {};
var styleAttrs = ['fill', 'opacity', 'shadowBlur', 'shadowColor'];
for (var i = 0; i < styleAttrs.length; ++i) {
var attr = styleAttrs[i];
if (newWave.style.hasOwnProperty(attr)) {
style[attr] = newWave.style[attr];
}
}
if (isFillContainer) {
shape.radiusY = height / 2;
}
// changes with animation
echarts.graphic.updateProps(waveElement, {
shape: shape,
x: newWave.x,
y: newWave.y
}, seriesModel);
if (seriesModel.isUniversalTransitionEnabled && seriesModel.isUniversalTransitionEnabled()) {
echarts.graphic.updateProps(waveElement, {
style: style
}, seriesModel);
}
else {
waveElement.useStyle(style);
}
// instant changes
var oldWaveClipPath = waveElement.getClipPath();
var newWaveClipPath = newWave.getClipPath();
waveElement.setClipPath(newWave.getClipPath());
waveElement.shape.inverse = newWave.inverse;
if (oldWaveClipPath && newWaveClipPath
&& self._shape === symbol
// TODO use zrender morphing to apply complex symbol animation.
&& !isPathSymbol(symbol)
) {
// Can be animated.
echarts.graphic.updateProps(newWaveClipPath, {
shape: oldWaveClipPath.shape
}, seriesModel, { isFrom: true });
}
setWaveAnimation(newIdx, waveElement, waveElement);
group.add(waveElement);
data.setItemGraphicEl(newIdx, waveElement);
waves.push(waveElement);
})
.remove(function (idx) {
var wave = oldData.getItemGraphicEl(idx);
group.remove(wave);
})
.execute();
if (itemModel.get('label.show')) {
group.add(getText(waves));
}
this._shape = symbol;
this._data = data;
/**
* Get path for outline, background and clipping
*
* @param {number} r outter radius of shape
* @param {boolean|undefined} isForClipping if the shape is used
* for clipping
*/
function getPath(r, isForClipping) {
if (symbol) {
// customed symbol path
if (isPathSymbol(symbol)) {
var path = echarts.graphic.makePath(symbol.slice(7), {});
var bouding = path.getBoundingRect();
var w = bouding.width;
var h = bouding.height;
if (w > h) {
h = r * 2 / w * h;
w = r * 2;
}
else {
w = r * 2 / h * w;
h = r * 2;
}
var left = isForClipping ? 0 : cx - w / 2;
var top = isForClipping ? 0 : cy - h / 2;
path = echarts.graphic.makePath(
symbol.slice(7),
{},
new echarts.graphic.BoundingRect(left, top, w, h)
);
if (isForClipping) {
path.x = -w / 2;
path.y = -h / 2;
}
return path;
}
else if (isFillContainer) {
// fully fill the container
var x = isForClipping ? -r[0] : cx - r[0];
var y = isForClipping ? -r[1] : cy - r[1];
return echarts.helper.createSymbol(
'rect', x, y, r[0] * 2, r[1] * 2
);
}
else {
var x = isForClipping ? -r : cx - r;
var y = isForClipping ? -r : cy - r;
if (symbol === 'pin') {
y += r;
}
else if (symbol === 'arrow') {
y -= r;
}
return echarts.helper.createSymbol(symbol, x, y, r * 2, r * 2);
}
}
return new echarts.graphic.Circle({
shape: {
cx: isForClipping ? 0 : cx,
cy: isForClipping ? 0 : cy,
r: r
}
});
}
/**
* Create outline
*/
function getOutline() {
var outlinePath = getPath(outterRadius);
outlinePath.style.fill = null;
outlinePath.setStyle(seriesModel.getModel('outline.itemStyle')
.getItemStyle());
return outlinePath;
}
/**
* Create background
*/
function getBackground() {
// Seperate stroke and fill, so we can use stroke to cover the alias of clipping.
var strokePath = getPath(radius);
strokePath.setStyle(seriesModel.getModel('backgroundStyle')
.getItemStyle());
strokePath.style.fill = null;
// Stroke is front of wave
strokePath.z2 = 5;
var fillPath = getPath(radius);
fillPath.setStyle(seriesModel.getModel('backgroundStyle')
.getItemStyle());
fillPath.style.stroke = null;
var group = new echarts.graphic.Group();
group.add(strokePath);
group.add(fillPath);
return group;
}
/**
* wave shape
*/
function getWave(idx, isInverse, oldWave) {
var radiusX = isFillContainer ? radius[0] : radius;
var radiusY = isFillContainer ? height / 2 : radius;
var itemModel = data.getItemModel(idx);
var itemStyleModel = itemModel.getModel('itemStyle');
var phase = itemModel.get('phase');
var amplitude = parsePercent(itemModel.get('amplitude'),
radiusY * 2);
var waveLength = parsePercent(itemModel.get('waveLength'),
radiusX * 2);
var value = data.get('value', idx);
var waterLevel = radiusY - value * radiusY * 2;
phase = oldWave ? oldWave.shape.phase
: (phase === 'auto' ? idx * Math.PI / 4 : phase);
var normalStyle = itemStyleModel.getItemStyle();
if (!normalStyle.fill) {
var seriesColor = seriesModel.get('color');
var id = idx % seriesColor.length;
normalStyle.fill = seriesColor[id];
}
var x = radiusX * 2;
var wave = new LiquidShape({
shape: {
waveLength: waveLength,
radius: radiusX,
radiusY: radiusY,
cx: x,
cy: 0,
waterLevel: waterLevel,
amplitude: amplitude,
phase: phase,
inverse: isInverse
},
style: normalStyle,
x: cx,
y: cy,
});
wave.shape._waterLevel = waterLevel;
var hoverStyle = itemModel.getModel('emphasis.itemStyle')
.getItemStyle();
hoverStyle.lineWidth = 0;
wave.ensureState('emphasis').style = hoverStyle;
echarts.helper.enableHoverEmphasis(wave);
// clip out the part outside the circle
var clip = getPath(radius, true);
// set fill for clipPath, otherwise it will not trigger hover event
clip.setStyle({
fill: 'white'
});
wave.setClipPath(clip);
return wave;
}
function setWaveAnimation(idx, wave, oldWave) {
var itemModel = data.getItemModel(idx);
var maxSpeed = itemModel.get('period');
var direction = itemModel.get('direction');
var value = data.get('value', idx);
var phase = itemModel.get('phase');
phase = oldWave ? oldWave.shape.phase
: (phase === 'auto' ? idx * Math.PI / 4 : phase);
var defaultSpeed = function (maxSpeed) {
var cnt = data.count();
return cnt === 0 ? maxSpeed : maxSpeed *
(0.2 + (cnt - idx) / cnt * 0.8);
};
var speed = 0;
if (maxSpeed === 'auto') {
speed = defaultSpeed(5000);
}
else {
speed = typeof maxSpeed === 'function'
? maxSpeed(value, idx) : maxSpeed;
}
// phase for moving left/right
var phaseOffset = 0;
if (direction === 'right' || direction == null) {
phaseOffset = Math.PI;
}
else if (direction === 'left') {
phaseOffset = -Math.PI;
}
else if (direction === 'none') {
phaseOffset = 0;
}
else {
console.error('Illegal direction value for liquid fill.');
}
// wave animation of moving left/right
if (direction !== 'none' && itemModel.get('waveAnimation')) {
wave
.animate('shape', true)
.when(0, {
phase: phase
})
.when(speed / 2, {
phase: phaseOffset + phase
})
.when(speed, {
phase: phaseOffset * 2 + phase
})
.during(function () {
if (wavePath) {
wavePath.dirty(true);
}
})
.start();
}
}
/**
* text on wave
*/
function getText(waves) {
var labelModel = itemModel.getModel('label');
function formatLabel() {
var formatted = seriesModel.getFormattedLabel(0, 'normal');
var defaultVal = (data.get('value', 0) * 100);
var defaultLabel = data.getName(0) || seriesModel.name;
if (!isNaN(defaultVal)) {
defaultLabel = defaultVal.toFixed(0) + '%';
}
return formatted == null ? defaultLabel : formatted;
}
var textRectOption = {
z2: 10,
shape: {
x: left,
y: top,
width: (isFillContainer ? radius[0] : radius) * 2,
height: (isFillContainer ? radius[1] : radius) * 2
},
style: {
fill: 'transparent'
},
textConfig: {
position: labelModel.get('position') || 'inside'
},
silent: true
};
var textOption = {
style: {
text: formatLabel(),
textAlign: labelModel.get('align'),
textVerticalAlign: labelModel.get('baseline')
}
};
Object.assign(textOption.style, echarts.helper.createTextStyle(labelModel));
var outsideTextRect = new echarts.graphic.Rect(textRectOption);
var insideTextRect = new echarts.graphic.Rect(textRectOption);
insideTextRect.disableLabelAnimation = true;
outsideTextRect.disableLabelAnimation = true;
var outsideText = new echarts.graphic.Text(textOption);
var insideText = new echarts.graphic.Text(textOption);
outsideTextRect.setTextContent(outsideText);
insideTextRect.setTextContent(insideText);
var insColor = labelModel.get('insideColor');
insideText.style.fill = insColor;
var group = new echarts.graphic.Group();
group.add(outsideTextRect);
group.add(insideTextRect);
// clip out waves for insideText
var boundingCircle = getPath(radius, true);
wavePath = new echarts.graphic.CompoundPath({
shape: {
paths: waves
},
x: cx,
y: cy
});
wavePath.setClipPath(boundingCircle);
insideTextRect.setClipPath(wavePath);
return group;
}
},
dispose: function () {
// dispose nothing here
}
});