/*
|
* Licensed to the Apache Software Foundation (ASF) under one
|
* or more contributor license agreements. See the NOTICE file
|
* distributed with this work for additional information
|
* regarding copyright ownership. The ASF licenses this file
|
* to you under the Apache License, Version 2.0 (the
|
* "License"); you may not use this file except in compliance
|
* with the License. You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing,
|
* software distributed under the License is distributed on an
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
* KIND, either express or implied. See the License for the
|
* specific language governing permissions and limitations
|
* under the License.
|
*/
|
|
import * as layout from '../../util/layout';
|
import {parsePercent, linearMap} from '../../util/number';
|
import FunnelSeriesModel, { FunnelSeriesOption, FunnelDataItemOption } from './FunnelSeries';
|
import ExtensionAPI from '../../core/ExtensionAPI';
|
import List from '../../data/List';
|
import GlobalModel from '../../model/Global';
|
|
function getViewRect(seriesModel: FunnelSeriesModel, api: ExtensionAPI) {
|
return layout.getLayoutRect(
|
seriesModel.getBoxLayoutParams(), {
|
width: api.getWidth(),
|
height: api.getHeight()
|
}
|
);
|
}
|
|
function getSortedIndices(data: List, sort: FunnelSeriesOption['sort']) {
|
const valueDim = data.mapDimension('value');
|
const valueArr = data.mapArray(valueDim, function (val: number) {
|
return val;
|
});
|
const indices: number[] = [];
|
const isAscending = sort === 'ascending';
|
for (let i = 0, len = data.count(); i < len; i++) {
|
indices[i] = i;
|
}
|
|
// Add custom sortable function & none sortable opetion by "options.sort"
|
if (typeof sort === 'function') {
|
indices.sort(sort);
|
}
|
else if (sort !== 'none') {
|
indices.sort(function (a, b) {
|
return isAscending
|
? valueArr[a] - valueArr[b]
|
: valueArr[b] - valueArr[a];
|
});
|
}
|
return indices;
|
}
|
|
function labelLayout(data: List) {
|
const seriesModel = data.hostModel;
|
const orient = seriesModel.get('orient');
|
data.each(function (idx) {
|
const itemModel = data.getItemModel<FunnelDataItemOption>(idx);
|
const labelModel = itemModel.getModel('label');
|
let labelPosition = labelModel.get('position');
|
|
const labelLineModel = itemModel.getModel('labelLine');
|
|
const layout = data.getItemLayout(idx);
|
const points = layout.points;
|
|
const isLabelInside = labelPosition === 'inner'
|
|| labelPosition === 'inside' || labelPosition === 'center'
|
|| labelPosition === 'insideLeft' || labelPosition === 'insideRight';
|
|
let textAlign;
|
let textX;
|
let textY;
|
let linePoints;
|
|
if (isLabelInside) {
|
if (labelPosition === 'insideLeft') {
|
textX = (points[0][0] + points[3][0]) / 2 + 5;
|
textY = (points[0][1] + points[3][1]) / 2;
|
textAlign = 'left';
|
}
|
else if (labelPosition === 'insideRight') {
|
textX = (points[1][0] + points[2][0]) / 2 - 5;
|
textY = (points[1][1] + points[2][1]) / 2;
|
textAlign = 'right';
|
}
|
else {
|
textX = (points[0][0] + points[1][0] + points[2][0] + points[3][0]) / 4;
|
textY = (points[0][1] + points[1][1] + points[2][1] + points[3][1]) / 4;
|
textAlign = 'center';
|
}
|
linePoints = [
|
[textX, textY], [textX, textY]
|
];
|
}
|
else {
|
let x1;
|
let y1;
|
let x2;
|
let y2;
|
const labelLineLen = labelLineModel.get('length');
|
if (__DEV__) {
|
if (orient === 'vertical' && ['top', 'bottom'].indexOf(labelPosition as string) > -1) {
|
labelPosition = 'left';
|
console.warn('Position error: Funnel chart on vertical orient dose not support top and bottom.');
|
}
|
if (orient === 'horizontal' && ['left', 'right'].indexOf(labelPosition as string) > -1) {
|
labelPosition = 'bottom';
|
console.warn('Position error: Funnel chart on horizontal orient dose not support left and right.');
|
}
|
}
|
if (labelPosition === 'left') {
|
// Left side
|
x1 = (points[3][0] + points[0][0]) / 2;
|
y1 = (points[3][1] + points[0][1]) / 2;
|
x2 = x1 - labelLineLen;
|
textX = x2 - 5;
|
textAlign = 'right';
|
}
|
else if (labelPosition === 'right') {
|
// Right side
|
x1 = (points[1][0] + points[2][0]) / 2;
|
y1 = (points[1][1] + points[2][1]) / 2;
|
x2 = x1 + labelLineLen;
|
textX = x2 + 5;
|
textAlign = 'left';
|
}
|
else if (labelPosition === 'top') {
|
// Top side
|
x1 = (points[3][0] + points[0][0]) / 2;
|
y1 = (points[3][1] + points[0][1]) / 2;
|
y2 = y1 - labelLineLen;
|
textY = y2 - 5;
|
textAlign = 'center';
|
}
|
else if (labelPosition === 'bottom') {
|
// Bottom side
|
x1 = (points[1][0] + points[2][0]) / 2;
|
y1 = (points[1][1] + points[2][1]) / 2;
|
y2 = y1 + labelLineLen;
|
textY = y2 + 5;
|
textAlign = 'center';
|
}
|
else if (labelPosition === 'rightTop') {
|
// RightTop side
|
x1 = orient === 'horizontal' ? points[3][0] : points[1][0];
|
y1 = orient === 'horizontal' ? points[3][1] : points[1][1];
|
if (orient === 'horizontal') {
|
y2 = y1 - labelLineLen;
|
textY = y2 - 5;
|
textAlign = 'center';
|
}
|
else {
|
x2 = x1 + labelLineLen;
|
textX = x2 + 5;
|
textAlign = 'top';
|
}
|
}
|
else if (labelPosition === 'rightBottom') {
|
// RightBottom side
|
x1 = points[2][0];
|
y1 = points[2][1];
|
if (orient === 'horizontal') {
|
y2 = y1 + labelLineLen;
|
textY = y2 + 5;
|
textAlign = 'center';
|
}
|
else {
|
x2 = x1 + labelLineLen;
|
textX = x2 + 5;
|
textAlign = 'bottom';
|
}
|
}
|
else if (labelPosition === 'leftTop') {
|
// LeftTop side
|
x1 = points[0][0];
|
y1 = orient === 'horizontal' ? points[0][1] : points[1][1];
|
if (orient === 'horizontal') {
|
y2 = y1 - labelLineLen;
|
textY = y2 - 5;
|
textAlign = 'center';
|
}
|
else {
|
x2 = x1 - labelLineLen;
|
textX = x2 - 5;
|
textAlign = 'right';
|
}
|
}
|
else if (labelPosition === 'leftBottom') {
|
// LeftBottom side
|
x1 = orient === 'horizontal' ? points[1][0] : points[3][0];
|
y1 = orient === 'horizontal' ? points[1][1] : points[2][1];
|
if (orient === 'horizontal') {
|
y2 = y1 + labelLineLen;
|
textY = y2 + 5;
|
textAlign = 'center';
|
}
|
else {
|
x2 = x1 - labelLineLen;
|
textX = x2 - 5;
|
textAlign = 'right';
|
}
|
}
|
else {
|
// Right side or Bottom side
|
x1 = (points[1][0] + points[2][0]) / 2;
|
y1 = (points[1][1] + points[2][1]) / 2;
|
if (orient === 'horizontal') {
|
y2 = y1 + labelLineLen;
|
textY = y2 + 5;
|
textAlign = 'center';
|
}
|
else {
|
x2 = x1 + labelLineLen;
|
textX = x2 + 5;
|
textAlign = 'left';
|
}
|
}
|
if (orient === 'horizontal') {
|
x2 = x1;
|
textX = x2;
|
}
|
else {
|
y2 = y1;
|
textY = y2;
|
}
|
linePoints = [[x1, y1], [x2, y2]];
|
}
|
|
layout.label = {
|
linePoints: linePoints,
|
x: textX,
|
y: textY,
|
verticalAlign: 'middle',
|
textAlign: textAlign,
|
inside: isLabelInside
|
};
|
});
|
}
|
|
export default function funnelLayout(ecModel: GlobalModel, api: ExtensionAPI) {
|
ecModel.eachSeriesByType('funnel', function (seriesModel: FunnelSeriesModel) {
|
const data = seriesModel.getData();
|
const valueDim = data.mapDimension('value');
|
const sort = seriesModel.get('sort');
|
const viewRect = getViewRect(seriesModel, api);
|
const orient = seriesModel.get('orient');
|
const viewWidth = viewRect.width;
|
const viewHeight = viewRect.height;
|
let indices = getSortedIndices(data, sort);
|
let x = viewRect.x;
|
let y = viewRect.y;
|
|
const sizeExtent = orient === 'horizontal' ? [
|
parsePercent(seriesModel.get('minSize'), viewHeight),
|
parsePercent(seriesModel.get('maxSize'), viewHeight)
|
] : [
|
parsePercent(seriesModel.get('minSize'), viewWidth),
|
parsePercent(seriesModel.get('maxSize'), viewWidth)
|
];
|
const dataExtent = data.getDataExtent(valueDim);
|
let min = seriesModel.get('min');
|
let max = seriesModel.get('max');
|
if (min == null) {
|
min = Math.min(dataExtent[0], 0);
|
}
|
if (max == null) {
|
max = dataExtent[1];
|
}
|
|
const funnelAlign = seriesModel.get('funnelAlign');
|
let gap = seriesModel.get('gap');
|
const viewSize = orient === 'horizontal' ? viewWidth : viewHeight;
|
let itemSize = (viewSize - gap * (data.count() - 1)) / data.count();
|
|
const getLinePoints = function (idx: number, offset: number) {
|
// End point index is data.count() and we assign it 0
|
if (orient === 'horizontal') {
|
const val = data.get(valueDim, idx) as number || 0;
|
const itemHeight = linearMap(val, [min, max], sizeExtent, true);
|
let y0;
|
switch (funnelAlign) {
|
case 'top':
|
y0 = y;
|
break;
|
case 'center':
|
y0 = y + (viewHeight - itemHeight) / 2;
|
break;
|
case 'bottom':
|
y0 = y + (viewHeight - itemHeight);
|
break;
|
}
|
|
return [
|
[offset, y0],
|
[offset, y0 + itemHeight]
|
];
|
}
|
const val = data.get(valueDim, idx) as number || 0;
|
const itemWidth = linearMap(val, [min, max], sizeExtent, true);
|
let x0;
|
switch (funnelAlign) {
|
case 'left':
|
x0 = x;
|
break;
|
case 'center':
|
x0 = x + (viewWidth - itemWidth) / 2;
|
break;
|
case 'right':
|
x0 = x + viewWidth - itemWidth;
|
break;
|
}
|
return [
|
[x0, offset],
|
[x0 + itemWidth, offset]
|
];
|
};
|
|
if (sort === 'ascending') {
|
// From bottom to top
|
itemSize = -itemSize;
|
gap = -gap;
|
if (orient === 'horizontal') {
|
x += viewWidth;
|
}
|
else {
|
y += viewHeight;
|
}
|
indices = indices.reverse();
|
}
|
|
for (let i = 0; i < indices.length; i++) {
|
const idx = indices[i];
|
const nextIdx = indices[i + 1];
|
const itemModel = data.getItemModel<FunnelDataItemOption>(idx);
|
|
if (orient === 'horizontal') {
|
let width = itemModel.get(['itemStyle', 'width']);
|
if (width == null) {
|
width = itemSize;
|
}
|
else {
|
width = parsePercent(width, viewWidth);
|
if (sort === 'ascending') {
|
width = -width;
|
}
|
}
|
|
const start = getLinePoints(idx, x);
|
const end = getLinePoints(nextIdx, x + width);
|
|
x += width + gap;
|
|
data.setItemLayout(idx, {
|
points: start.concat(end.slice().reverse())
|
});
|
}
|
else {
|
let height = itemModel.get(['itemStyle', 'height']);
|
if (height == null) {
|
height = itemSize;
|
}
|
else {
|
height = parsePercent(height, viewHeight);
|
if (sort === 'ascending') {
|
height = -height;
|
}
|
}
|
|
const start = getLinePoints(idx, y);
|
const end = getLinePoints(nextIdx, y + height);
|
|
y += height + gap;
|
|
data.setItemLayout(idx, {
|
points: start.concat(end.slice().reverse())
|
});
|
}
|
}
|
|
labelLayout(data);
|
});
|
}
|