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 zrUtil from 'zrender/src/core/util'; | |
import VisualMapModel, { VisualMapOption, VisualMeta } from './VisualMapModel'; | |
import * as numberUtil from '../../util/number'; | |
import { VisualMappingOption } from '../../visual/VisualMapping'; | |
import { inheritDefaultOption } from '../../util/component'; | |
import { ItemStyleOption } from '../../util/types'; | |
// Constant | |
const DEFAULT_BAR_BOUND = [20, 140]; | |
type RangeWithAuto = { | |
auto?: 0 | 1 | |
}; | |
type VisualState = VisualMapModel['stateList'][number]; | |
export interface ContinousVisualMapOption extends VisualMapOption { | |
align?: 'auto' | 'left' | 'right' | 'top' | 'bottom' | |
/** | |
* This prop effect default component type determine | |
* @see echarts/component/visualMap/typeDefaulter. | |
*/ | |
calculable?: boolean | |
/** | |
* selected range. In default case `range` is [min, max] | |
* and can auto change along with modification of min max, | |
* until user specified a range. | |
*/ | |
range?: number[] | |
/** | |
* Whether to enable hover highlight. | |
*/ | |
hoverLink?: boolean | |
/** | |
* The extent of hovered data. | |
*/ | |
hoverLinkDataSize?: number | |
/** | |
* Whether trigger hoverLink when hover handle. | |
* If not specified, follow the value of `realtime`. | |
*/ | |
hoverLinkOnHandle?: boolean, | |
handleIcon?: string, | |
// Percent of the item width | |
handleSize?: string | number, | |
handleStyle?: ItemStyleOption | |
indicatorIcon?: string, | |
// Percent of the item width | |
indicatorSize?: string | number, | |
indicatorStyle?: ItemStyleOption | |
emphasis?: { | |
handleStyle?: ItemStyleOption | |
} | |
} | |
class ContinuousModel extends VisualMapModel<ContinousVisualMapOption> { | |
static type = 'visualMap.continuous' as const; | |
type = ContinuousModel.type; | |
/** | |
* @override | |
*/ | |
optionUpdated(newOption: ContinousVisualMapOption, isInit: boolean) { | |
super.optionUpdated.apply(this, arguments as any); | |
this.resetExtent(); | |
this.resetVisual(function (mappingOption?: VisualMappingOption) { | |
mappingOption.mappingMethod = 'linear'; | |
mappingOption.dataExtent = this.getExtent(); | |
}); | |
this._resetRange(); | |
} | |
/** | |
* @protected | |
* @override | |
*/ | |
resetItemSize() { | |
super.resetItemSize.apply(this, arguments as any); | |
const itemSize = this.itemSize; | |
(itemSize[0] == null || isNaN(itemSize[0])) && (itemSize[0] = DEFAULT_BAR_BOUND[0]); | |
(itemSize[1] == null || isNaN(itemSize[1])) && (itemSize[1] = DEFAULT_BAR_BOUND[1]); | |
} | |
/** | |
* @private | |
*/ | |
_resetRange() { | |
const dataExtent = this.getExtent(); | |
const range = this.option.range; | |
if (!range || (range as RangeWithAuto).auto) { | |
// `range` should always be array (so we don't use other | |
// value like 'auto') for user-friend. (consider getOption). | |
(dataExtent as RangeWithAuto).auto = 1; | |
this.option.range = dataExtent; | |
} | |
else if (zrUtil.isArray(range)) { | |
if (range[0] > range[1]) { | |
range.reverse(); | |
} | |
range[0] = Math.max(range[0], dataExtent[0]); | |
range[1] = Math.min(range[1], dataExtent[1]); | |
} | |
} | |
/** | |
* @protected | |
* @override | |
*/ | |
completeVisualOption() { | |
super.completeVisualOption.apply(this, arguments as any); | |
zrUtil.each(this.stateList, function (state: VisualState) { | |
const symbolSize = this.option.controller[state].symbolSize; | |
if (symbolSize && symbolSize[0] !== symbolSize[1]) { | |
symbolSize[0] = symbolSize[1] / 3; // For good looking. | |
} | |
}, this); | |
} | |
/** | |
* @override | |
*/ | |
setSelected(selected: number[]) { | |
this.option.range = selected.slice(); | |
this._resetRange(); | |
} | |
/** | |
* @public | |
*/ | |
getSelected(): [number, number] { | |
const dataExtent = this.getExtent(); | |
const dataInterval = numberUtil.asc( | |
(this.get('range') || []).slice() | |
) as [number, number]; | |
// Clamp | |
dataInterval[0] > dataExtent[1] && (dataInterval[0] = dataExtent[1]); | |
dataInterval[1] > dataExtent[1] && (dataInterval[1] = dataExtent[1]); | |
dataInterval[0] < dataExtent[0] && (dataInterval[0] = dataExtent[0]); | |
dataInterval[1] < dataExtent[0] && (dataInterval[1] = dataExtent[0]); | |
return dataInterval; | |
} | |
/** | |
* @override | |
*/ | |
getValueState(value: number): VisualState { | |
const range = this.option.range; | |
const dataExtent = this.getExtent(); | |
// When range[0] === dataExtent[0], any value larger than dataExtent[0] maps to 'inRange'. | |
// range[1] is processed likewise. | |
return ( | |
(range[0] <= dataExtent[0] || range[0] <= value) | |
&& (range[1] >= dataExtent[1] || value <= range[1]) | |
) ? 'inRange' : 'outOfRange'; | |
} | |
findTargetDataIndices(range: number[]) { | |
type DataIndices = { | |
seriesId: string | |
dataIndex: number[] | |
}; | |
const result: DataIndices[] = []; | |
this.eachTargetSeries(function (seriesModel) { | |
const dataIndices: number[] = []; | |
const data = seriesModel.getData(); | |
data.each(this.getDataDimensionIndex(data), function (value, dataIndex) { | |
range[0] <= value && value <= range[1] && dataIndices.push(dataIndex); | |
}, this); | |
result.push({ | |
seriesId: seriesModel.id, | |
dataIndex: dataIndices | |
}); | |
}, this); | |
return result; | |
} | |
/** | |
* @implement | |
*/ | |
getVisualMeta( | |
getColorVisual: (value: number, valueState: VisualState) => string | |
) { | |
type ColorStop = VisualMeta['stops'][number]; | |
const oVals = getColorStopValues(this, 'outOfRange', this.getExtent()); | |
const iVals = getColorStopValues(this, 'inRange', this.option.range.slice()); | |
const stops: ColorStop[] = []; | |
function setStop(value: number, valueState: VisualState) { | |
stops.push({ | |
value: value, | |
color: getColorVisual(value, valueState) | |
}); | |
} | |
// Format to: outOfRange -- inRange -- outOfRange. | |
let iIdx = 0; | |
let oIdx = 0; | |
const iLen = iVals.length; | |
const oLen = oVals.length; | |
for (; oIdx < oLen && (!iVals.length || oVals[oIdx] <= iVals[0]); oIdx++) { | |
// If oVal[oIdx] === iVals[iIdx], oVal[oIdx] should be ignored. | |
if (oVals[oIdx] < iVals[iIdx]) { | |
setStop(oVals[oIdx], 'outOfRange'); | |
} | |
} | |
for (let first = 1; iIdx < iLen; iIdx++, first = 0) { | |
// If range is full, value beyond min, max will be clamped. | |
// make a singularity | |
first && stops.length && setStop(iVals[iIdx], 'outOfRange'); | |
setStop(iVals[iIdx], 'inRange'); | |
} | |
for (let first = 1; oIdx < oLen; oIdx++) { | |
if (!iVals.length || iVals[iVals.length - 1] < oVals[oIdx]) { | |
// make a singularity | |
if (first) { | |
stops.length && setStop(stops[stops.length - 1].value, 'outOfRange'); | |
first = 0; | |
} | |
setStop(oVals[oIdx], 'outOfRange'); | |
} | |
} | |
const stopsLen = stops.length; | |
return { | |
stops: stops, | |
outerColors: [ | |
stopsLen ? stops[0].color : 'transparent', | |
stopsLen ? stops[stopsLen - 1].color : 'transparent' | |
] as VisualMeta['outerColors'] | |
}; | |
} | |
static defaultOption = inheritDefaultOption(VisualMapModel.defaultOption, { | |
align: 'auto', // 'auto', 'left', 'right', 'top', 'bottom' | |
calculable: false, | |
hoverLink: true, | |
realtime: true, | |
handleIcon: 'path://M-11.39,9.77h0a3.5,3.5,0,0,1-3.5,3.5h-22a3.5,3.5,0,0,1-3.5-3.5h0a3.5,3.5,0,0,1,3.5-3.5h22A3.5,3.5,0,0,1-11.39,9.77Z', | |
handleSize: '120%', | |
handleStyle: { | |
borderColor: '#fff', | |
borderWidth: 1 | |
}, | |
indicatorIcon: 'circle', | |
indicatorSize: '50%', | |
indicatorStyle: { | |
borderColor: '#fff', | |
borderWidth: 2, | |
shadowBlur: 2, | |
shadowOffsetX: 1, | |
shadowOffsetY: 1, | |
shadowColor: 'rgba(0,0,0,0.2)' | |
} | |
// emphasis: { | |
// handleStyle: { | |
// shadowBlur: 3, | |
// shadowOffsetX: 1, | |
// shadowOffsetY: 1, | |
// shadowColor: 'rgba(0,0,0,0.2)' | |
// } | |
// } | |
}) as ContinousVisualMapOption; | |
} | |
function getColorStopValues( | |
visualMapModel: ContinuousModel, | |
valueState: VisualState, | |
dataExtent: number[] | |
) { | |
if (dataExtent[0] === dataExtent[1]) { | |
return dataExtent.slice(); | |
} | |
// When using colorHue mapping, it is not linear color any more. | |
// Moreover, canvas gradient seems not to be accurate linear. | |
// FIXME | |
// Should be arbitrary value 100? or based on pixel size? | |
const count = 200; | |
const step = (dataExtent[1] - dataExtent[0]) / count; | |
let value = dataExtent[0]; | |
const stopValues = []; | |
for (let i = 0; i <= count && value < dataExtent[1]; i++) { | |
stopValues.push(value); | |
value += step; | |
} | |
stopValues.push(dataExtent[1]); | |
return stopValues; | |
} | |
export default ContinuousModel; | |