| /* | 
 | * 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. | 
 | */ | 
 |  | 
 | // TODO clockwise | 
 |  | 
 | import IndicatorAxis from './IndicatorAxis'; | 
 | import IntervalScale from '../../scale/Interval'; | 
 | import * as numberUtil from '../../util/number'; | 
 | import { CoordinateSystemMaster, CoordinateSystem } from '../CoordinateSystem'; | 
 | import RadarModel from './RadarModel'; | 
 | import GlobalModel from '../../model/Global'; | 
 | import ExtensionAPI from '../../core/ExtensionAPI'; | 
 | import { ScaleDataValue } from '../../util/types'; | 
 | import { ParsedModelFinder } from '../../util/model'; | 
 | import { map, each, isString, isNumber } from 'zrender/src/core/util'; | 
 | import { alignScaleTicks } from '../axisAlignTicks'; | 
 |  | 
 |  | 
 | class Radar implements CoordinateSystem, CoordinateSystemMaster { | 
 |  | 
 |     readonly type: 'radar'; | 
 |     /** | 
 |      * | 
 |      * Radar dimensions | 
 |      */ | 
 |     readonly dimensions: string[] = []; | 
 |  | 
 |     cx: number; | 
 |  | 
 |     cy: number; | 
 |  | 
 |     r: number; | 
 |  | 
 |     r0: number; | 
 |  | 
 |     startAngle: number; | 
 |  | 
 |     private _model: RadarModel; | 
 |  | 
 |     private _indicatorAxes: IndicatorAxis[]; | 
 |  | 
 |     constructor(radarModel: RadarModel, ecModel: GlobalModel, api: ExtensionAPI) { | 
 |         this._model = radarModel; | 
 |  | 
 |         this._indicatorAxes = map(radarModel.getIndicatorModels(), function (indicatorModel, idx) { | 
 |             const dim = 'indicator_' + idx; | 
 |             const indicatorAxis = new IndicatorAxis(dim, | 
 |                 new IntervalScale() | 
 |                 // (indicatorModel.get('axisType') === 'log') ? new LogScale() : new IntervalScale() | 
 |             ); | 
 |             indicatorAxis.name = indicatorModel.get('name'); | 
 |             // Inject model and axis | 
 |             indicatorAxis.model = indicatorModel; | 
 |             indicatorModel.axis = indicatorAxis; | 
 |             this.dimensions.push(dim); | 
 |             return indicatorAxis; | 
 |         }, this); | 
 |  | 
 |         this.resize(radarModel, api); | 
 |     } | 
 |  | 
 |     getIndicatorAxes() { | 
 |         return this._indicatorAxes; | 
 |     } | 
 |  | 
 |     dataToPoint(value: ScaleDataValue, indicatorIndex: number) { | 
 |         const indicatorAxis = this._indicatorAxes[indicatorIndex]; | 
 |  | 
 |         return this.coordToPoint(indicatorAxis.dataToCoord(value), indicatorIndex); | 
 |     } | 
 |  | 
 |     // TODO: API should be coordToPoint([coord, indicatorIndex]) | 
 |     coordToPoint(coord: number, indicatorIndex: number) { | 
 |         const indicatorAxis = this._indicatorAxes[indicatorIndex]; | 
 |         const angle = indicatorAxis.angle; | 
 |         const x = this.cx + coord * Math.cos(angle); | 
 |         const y = this.cy - coord * Math.sin(angle); | 
 |         return [x, y]; | 
 |     } | 
 |  | 
 |     pointToData(pt: number[]) { | 
 |         let dx = pt[0] - this.cx; | 
 |         let dy = pt[1] - this.cy; | 
 |         const radius = Math.sqrt(dx * dx + dy * dy); | 
 |         dx /= radius; | 
 |         dy /= radius; | 
 |  | 
 |         const radian = Math.atan2(-dy, dx); | 
 |  | 
 |         // Find the closest angle | 
 |         // FIXME index can calculated directly | 
 |         let minRadianDiff = Infinity; | 
 |         let closestAxis; | 
 |         let closestAxisIdx = -1; | 
 |         for (let i = 0; i < this._indicatorAxes.length; i++) { | 
 |             const indicatorAxis = this._indicatorAxes[i]; | 
 |             const diff = Math.abs(radian - indicatorAxis.angle); | 
 |             if (diff < minRadianDiff) { | 
 |                 closestAxis = indicatorAxis; | 
 |                 closestAxisIdx = i; | 
 |                 minRadianDiff = diff; | 
 |             } | 
 |         } | 
 |  | 
 |         return [closestAxisIdx, +(closestAxis && closestAxis.coordToData(radius))]; | 
 |     } | 
 |  | 
 |     resize(radarModel: RadarModel, api: ExtensionAPI) { | 
 |         const center = radarModel.get('center'); | 
 |         const viewWidth = api.getWidth(); | 
 |         const viewHeight = api.getHeight(); | 
 |         const viewSize = Math.min(viewWidth, viewHeight) / 2; | 
 |         this.cx = numberUtil.parsePercent(center[0], viewWidth); | 
 |         this.cy = numberUtil.parsePercent(center[1], viewHeight); | 
 |  | 
 |         this.startAngle = radarModel.get('startAngle') * Math.PI / 180; | 
 |  | 
 |         // radius may be single value like `20`, `'80%'`, or array like `[10, '80%']` | 
 |         let radius = radarModel.get('radius'); | 
 |         if (isString(radius) || isNumber(radius)) { | 
 |             radius = [0, radius]; | 
 |         } | 
 |         this.r0 = numberUtil.parsePercent(radius[0], viewSize); | 
 |         this.r = numberUtil.parsePercent(radius[1], viewSize); | 
 |  | 
 |         each(this._indicatorAxes, function (indicatorAxis, idx) { | 
 |             indicatorAxis.setExtent(this.r0, this.r); | 
 |             let angle = (this.startAngle + idx * Math.PI * 2 / this._indicatorAxes.length); | 
 |             // Normalize to [-PI, PI] | 
 |             angle = Math.atan2(Math.sin(angle), Math.cos(angle)); | 
 |             indicatorAxis.angle = angle; | 
 |         }, this); | 
 |     } | 
 |  | 
 |     update(ecModel: GlobalModel, api: ExtensionAPI) { | 
 |         const indicatorAxes = this._indicatorAxes; | 
 |         const radarModel = this._model; | 
 |         each(indicatorAxes, function (indicatorAxis) { | 
 |             indicatorAxis.scale.setExtent(Infinity, -Infinity); | 
 |         }); | 
 |         ecModel.eachSeriesByType('radar', function (radarSeries, idx) { | 
 |             if (radarSeries.get('coordinateSystem') !== 'radar' | 
 |                 // @ts-ignore | 
 |                 || ecModel.getComponent('radar', radarSeries.get('radarIndex')) !== radarModel | 
 |             ) { | 
 |                 return; | 
 |             } | 
 |             const data = radarSeries.getData(); | 
 |             each(indicatorAxes, function (indicatorAxis) { | 
 |                 indicatorAxis.scale.unionExtentFromData(data, data.mapDimension(indicatorAxis.dim)); | 
 |             }); | 
 |         }, this); | 
 |  | 
 |         const splitNumber = radarModel.get('splitNumber'); | 
 |         const dummyScale = new IntervalScale(); | 
 |         dummyScale.setExtent(0, splitNumber); | 
 |         dummyScale.setInterval(1); | 
 |         // Force all the axis fixing the maxSplitNumber. | 
 |         each(indicatorAxes, function (indicatorAxis, idx) { | 
 |             alignScaleTicks( | 
 |                 indicatorAxis.scale as IntervalScale, | 
 |                 indicatorAxis.model, | 
 |                 dummyScale | 
 |             ); | 
 |         }); | 
 |     } | 
 |  | 
 |     convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue[]): never { | 
 |         console.warn('Not implemented.'); | 
 |         return null as never; | 
 |     } | 
 |     convertFromPixel(ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[]): never { | 
 |         console.warn('Not implemented.'); | 
 |         return null as never; | 
 |     } | 
 |     containPoint(point: number[]): boolean { | 
 |         console.warn('Not implemented.'); | 
 |         return false; | 
 |     } | 
 |     /** | 
 |      * Radar dimensions is based on the data | 
 |      */ | 
 |     static dimensions: string[] = []; | 
 |  | 
 |     static create(ecModel: GlobalModel, api: ExtensionAPI) { | 
 |         const radarList: Radar[] = []; | 
 |         ecModel.eachComponent('radar', function (radarModel: RadarModel) { | 
 |             const radar = new Radar(radarModel, ecModel, api); | 
 |             radarList.push(radar); | 
 |             radarModel.coordinateSystem = radar; | 
 |         }); | 
 |         ecModel.eachSeriesByType('radar', function (radarSeries) { | 
 |             if (radarSeries.get('coordinateSystem') === 'radar') { | 
 |                 // Inject coordinate system | 
 |                 // @ts-ignore | 
 |                 radarSeries.coordinateSystem = radarList[radarSeries.get('radarIndex') || 0]; | 
 |             } | 
 |         }); | 
 |         return radarList; | 
 |     } | 
 | } | 
 |  | 
 |  | 
 | export default Radar; |