Spaces:
Running
Running
| /* | |
| * 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); | |
| }); | |
| } | |