/* * 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(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(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); }); }