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