| /* |
| * 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 SeriesData from '../../data/SeriesData'; |
| import GlobalModel from '../../model/Global'; |
| import { isFunction } from 'zrender/src/core/util'; |
| |
| function getViewRect(seriesModel: FunnelSeriesModel, api: ExtensionAPI) { |
| return layout.getLayoutRect( |
| seriesModel.getBoxLayoutParams(), { |
| width: api.getWidth(), |
| height: api.getHeight() |
| } |
| ); |
| } |
| |
| function getSortedIndices(data: SeriesData, 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 (isFunction(sort)) { |
| indices.sort(sort as any); |
| } |
| else if (sort !== 'none') { |
| indices.sort(function (a, b) { |
| return isAscending |
| ? valueArr[a] - valueArr[b] |
| : valueArr[b] - valueArr[a]; |
| }); |
| } |
| return indices; |
| } |
| |
| function labelLayout(data: SeriesData) { |
| 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); |
| }); |
| } |