/* * 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 RadiusAxis from './RadiusAxis'; import AngleAxis from './AngleAxis'; import PolarModel from './PolarModel'; import { CoordinateSystem, CoordinateSystemMaster, CoordinateSystemClipArea } from '../CoordinateSystem'; import GlobalModel from '../../model/Global'; import { ParsedModelFinder, ParsedModelFinderKnown } from '../../util/model'; import { ScaleDataValue } from '../../util/types'; import ExtensionAPI from '../../core/ExtensionAPI'; export const polarDimensions = ['radius', 'angle']; interface Polar { update(ecModel: GlobalModel, api: ExtensionAPI): void } class Polar implements CoordinateSystem, CoordinateSystemMaster { readonly name: string; readonly dimensions = polarDimensions; readonly type = 'polar'; /** * x of polar center */ cx = 0; /** * y of polar center */ cy = 0; private _radiusAxis = new RadiusAxis(); private _angleAxis = new AngleAxis(); axisPointerEnabled = true; model: PolarModel; constructor(name: string) { this.name = name || ''; this._radiusAxis.polar = this._angleAxis.polar = this; } /** * If contain coord */ containPoint(point: number[]) { const coord = this.pointToCoord(point); return this._radiusAxis.contain(coord[0]) && this._angleAxis.contain(coord[1]); } /** * If contain data */ containData(data: number[]) { return this._radiusAxis.containData(data[0]) && this._angleAxis.containData(data[1]); } getAxis(dim: 'radius' | 'angle') { const key = ('_' + dim + 'Axis') as '_radiusAxis' | '_angleAxis'; return this[key]; } getAxes() { return [this._radiusAxis, this._angleAxis]; } /** * Get axes by type of scale */ getAxesByScale(scaleType: 'ordinal' | 'interval' | 'time' | 'log') { const axes = []; const angleAxis = this._angleAxis; const radiusAxis = this._radiusAxis; angleAxis.scale.type === scaleType && axes.push(angleAxis); radiusAxis.scale.type === scaleType && axes.push(radiusAxis); return axes; } getAngleAxis() { return this._angleAxis; } getRadiusAxis() { return this._radiusAxis; } getOtherAxis(axis: AngleAxis | RadiusAxis): AngleAxis | RadiusAxis { const angleAxis = this._angleAxis; return axis === angleAxis ? this._radiusAxis : angleAxis; } /** * Base axis will be used on stacking. * */ getBaseAxis() { return this.getAxesByScale('ordinal')[0] || this.getAxesByScale('time')[0] || this.getAngleAxis(); } getTooltipAxes(dim: 'radius' | 'angle' | 'auto') { const baseAxis = (dim != null && dim !== 'auto') ? this.getAxis(dim) : this.getBaseAxis(); return { baseAxes: [baseAxis], otherAxes: [this.getOtherAxis(baseAxis)] }; } /** * Convert a single data item to (x, y) point. * Parameter data is an array which the first element is radius and the second is angle */ dataToPoint(data: ScaleDataValue[], clamp?: boolean) { return this.coordToPoint([ this._radiusAxis.dataToRadius(data[0], clamp), this._angleAxis.dataToAngle(data[1], clamp) ]); } /** * Convert a (x, y) point to data */ pointToData(point: number[], clamp?: boolean) { const coord = this.pointToCoord(point); return [ this._radiusAxis.radiusToData(coord[0], clamp), this._angleAxis.angleToData(coord[1], clamp) ]; } /** * Convert a (x, y) point to (radius, angle) coord */ pointToCoord(point: number[]) { let dx = point[0] - this.cx; let dy = point[1] - this.cy; const angleAxis = this.getAngleAxis(); const extent = angleAxis.getExtent(); let minAngle = Math.min(extent[0], extent[1]); let maxAngle = Math.max(extent[0], extent[1]); // Fix fixed extent in polarCreator // FIXME angleAxis.inverse ? (minAngle = maxAngle - 360) : (maxAngle = minAngle + 360); const radius = Math.sqrt(dx * dx + dy * dy); dx /= radius; dy /= radius; let radian = Math.atan2(-dy, dx) / Math.PI * 180; // move to angleExtent const dir = radian < minAngle ? 1 : -1; while (radian < minAngle || radian > maxAngle) { radian += dir * 360; } return [radius, radian]; } /** * Convert a (radius, angle) coord to (x, y) point */ coordToPoint(coord: number[]) { const radius = coord[0]; const radian = coord[1] / 180 * Math.PI; const x = Math.cos(radian) * radius + this.cx; // Inverse the y const y = -Math.sin(radian) * radius + this.cy; return [x, y]; } /** * Get ring area of cartesian. * Area will have a contain function to determine if a point is in the coordinate system. */ getArea(): PolarArea { const angleAxis = this.getAngleAxis(); const radiusAxis = this.getRadiusAxis(); const radiusExtent = radiusAxis.getExtent().slice(); radiusExtent[0] > radiusExtent[1] && radiusExtent.reverse(); const angleExtent = angleAxis.getExtent(); const RADIAN = Math.PI / 180; return { cx: this.cx, cy: this.cy, r0: radiusExtent[0], r: radiusExtent[1], startAngle: -angleExtent[0] * RADIAN, endAngle: -angleExtent[1] * RADIAN, clockwise: angleAxis.inverse, contain(x: number, y: number) { // It's a ring shape. // Start angle and end angle don't matter const dx = x - this.cx; const dy = y - this.cy; // minus a tiny value 1e-4 to avoid being clipped unexpectedly const d2 = dx * dx + dy * dy - 1e-4; const r = this.r; const r0 = this.r0; return d2 <= r * r && d2 >= r0 * r0; } }; } convertToPixel(ecModel: GlobalModel, finder: ParsedModelFinder, value: ScaleDataValue[]) { const coordSys = getCoordSys(finder); return coordSys === this ? this.dataToPoint(value) : null; } convertFromPixel(ecModel: GlobalModel, finder: ParsedModelFinder, pixel: number[]) { const coordSys = getCoordSys(finder); return coordSys === this ? this.pointToData(pixel) : null; } } function getCoordSys(finder: ParsedModelFinderKnown) { const seriesModel = finder.seriesModel; const polarModel = finder.polarModel as PolarModel; return polarModel && polarModel.coordinateSystem || seriesModel && seriesModel.coordinateSystem as Polar; } interface PolarArea extends CoordinateSystemClipArea { cx: number cy: number r0: number r: number startAngle: number endAngle: number clockwise: boolean } export default Polar;