| /* |
| * 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 * as zrColor from 'zrender/src/tool/color'; |
| import {linearMap} from '../util/number'; |
| import { AllPropTypes, Dictionary } from 'zrender/src/core/types'; |
| import { |
| ColorString, |
| BuiltinVisualProperty, |
| VisualOptionPiecewise, |
| VisualOptionCategory, |
| VisualOptionLinear, |
| VisualOptionUnit, |
| ParsedValue |
| } from '../util/types'; |
| |
| const each = zrUtil.each; |
| const isObject = zrUtil.isObject; |
| |
| const CATEGORY_DEFAULT_VISUAL_INDEX = -1; |
| |
| // Type of raw value |
| type RawValue = ParsedValue; |
| // Type of mapping visual value |
| type VisualValue = AllPropTypes<VisualOptionUnit>; |
| // Type of value after normalized. 0 - 1 |
| type NormalizedValue = number; |
| |
| type MappingMethod = 'linear' | 'piecewise' | 'category' | 'fixed'; |
| |
| // May include liftZ. wich is not provided to developers. |
| |
| interface Normalizer { |
| (this: VisualMapping, value?: RawValue): NormalizedValue |
| } |
| interface ColorMapper { |
| (this: VisualMapping, value: RawValue | NormalizedValue, isNormalized?: boolean, out?: number[]) |
| : ColorString | number[] |
| } |
| interface DoMap { |
| (this: VisualMapping, normalzied?: NormalizedValue, value?: RawValue): VisualValue |
| } |
| interface VisualValueGetter { |
| (key: string): VisualValue |
| } |
| interface VisualValueSetter { |
| (key: string, value: VisualValue): void |
| } |
| interface VisualHandler { |
| applyVisual( |
| this: VisualMapping, |
| value: RawValue, |
| getter: VisualValueGetter, |
| setter: VisualValueSetter |
| ): void |
| |
| _normalizedToVisual: { |
| linear(this: VisualMapping, normalized: NormalizedValue): VisualValue |
| category(this: VisualMapping, normalized: NormalizedValue): VisualValue |
| piecewise(this: VisualMapping, normalzied: NormalizedValue, value: RawValue): VisualValue |
| fixed(this: VisualMapping): VisualValue |
| } |
| /** |
| * Get color mapping for the outside usage. |
| * Currently only used in `color` visual. |
| * |
| * The last parameter out is cached color array. |
| */ |
| getColorMapper?: (this: VisualMapping) => ColorMapper |
| } |
| |
| interface VisualMappingPiece { |
| index?: number |
| |
| value?: number | string |
| interval?: [number, number] |
| close?: [0 | 1, 0 | 1] |
| |
| text?: string |
| |
| visual?: VisualOptionPiecewise |
| } |
| |
| export interface VisualMappingOption { |
| type?: BuiltinVisualProperty |
| |
| mappingMethod?: MappingMethod |
| |
| /** |
| * required when mappingMethod is 'linear' |
| */ |
| dataExtent?: [number, number] |
| /** |
| * required when mappingMethod is 'piecewise'. |
| * Visual for only each piece can be specified |
| * [ |
| * {value: someValue}, |
| * {interval: [min1, max1], visual: {...}}, |
| * {interval: [min2, max2]} |
| * ],. |
| */ |
| pieceList?: VisualMappingPiece[] |
| /** |
| * required when mappingMethod is 'category'. If no option.categories, categories is set as [0, 1, 2, ...]. |
| */ |
| categories?: (string | number)[] |
| /** |
| * Whether loop mapping when mappingMethod is 'category'. |
| * @default false |
| */ |
| loop?: boolean |
| /** |
| * Visual data |
| * when mappingMethod is 'category', visual data can be array or object |
| * (like: {cate1: '#222', none: '#fff'}) |
| * or primary types (which represents default category visual), otherwise visual |
| * can be array or primary (which will be normalized to array). |
| */ |
| visual?: VisualValue[] | Dictionary<VisualValue> | VisualValue |
| } |
| |
| interface VisualMappingInnerPiece extends VisualMappingPiece { |
| originIndex: number |
| } |
| interface VisualMappingInnerOption extends VisualMappingOption { |
| hasSpecialVisual: boolean |
| pieceList: VisualMappingInnerPiece[] |
| /** |
| * Map to get category index |
| */ |
| categoryMap: Dictionary<number> |
| /** |
| * Cached parsed rgba array from string to avoid parse every time. |
| */ |
| parsedVisual: number[][] |
| |
| // Have converted primary value to array. |
| visual?: VisualValue[] | Dictionary<VisualValue> |
| } |
| |
| class VisualMapping<VisualOption |
| extends VisualOptionPiecewise | VisualOptionCategory | VisualOptionUnit | VisualOptionLinear = {} |
| > { |
| |
| option: VisualMappingInnerOption; |
| |
| type: BuiltinVisualProperty; |
| |
| mappingMethod: MappingMethod; |
| |
| applyVisual: VisualHandler['applyVisual']; |
| |
| getColorMapper: VisualHandler['getColorMapper']; |
| |
| _normalizeData: Normalizer; |
| |
| _normalizedToVisual: DoMap; |
| |
| constructor(option: VisualMappingOption) { |
| const mappingMethod = option.mappingMethod; |
| const visualType = option.type; |
| |
| const thisOption: VisualMappingInnerOption = this.option = zrUtil.clone(option) as VisualMappingInnerOption; |
| |
| this.type = visualType; |
| this.mappingMethod = mappingMethod; |
| |
| this._normalizeData = normalizers[mappingMethod]; |
| const visualHandler = VisualMapping.visualHandlers[visualType]; |
| |
| this.applyVisual = visualHandler.applyVisual; |
| |
| this.getColorMapper = visualHandler.getColorMapper; |
| |
| this._normalizedToVisual = visualHandler._normalizedToVisual[mappingMethod]; |
| |
| if (mappingMethod === 'piecewise') { |
| normalizeVisualRange(thisOption); |
| preprocessForPiecewise(thisOption); |
| } |
| else if (mappingMethod === 'category') { |
| thisOption.categories |
| ? preprocessForSpecifiedCategory(thisOption) |
| // categories is ordinal when thisOption.categories not specified, |
| // which need no more preprocess except normalize visual. |
| : normalizeVisualRange(thisOption, true); |
| } |
| else { // mappingMethod === 'linear' or 'fixed' |
| zrUtil.assert(mappingMethod !== 'linear' || thisOption.dataExtent); |
| normalizeVisualRange(thisOption); |
| } |
| } |
| |
| mapValueToVisual(value: RawValue): VisualValue { |
| const normalized = this._normalizeData(value); |
| return this._normalizedToVisual(normalized, value); |
| } |
| |
| getNormalizer() { |
| return zrUtil.bind(this._normalizeData, this); |
| } |
| |
| static visualHandlers: {[key in BuiltinVisualProperty]: VisualHandler} = { |
| color: { |
| applyVisual: makeApplyVisual('color'), |
| getColorMapper: function () { |
| const thisOption = this.option; |
| |
| return zrUtil.bind( |
| thisOption.mappingMethod === 'category' |
| ? function ( |
| this: VisualMapping, |
| value: NormalizedValue | RawValue, |
| isNormalized?: boolean |
| ): ColorString { |
| !isNormalized && (value = this._normalizeData(value)); |
| return doMapCategory.call(this, value) as ColorString; |
| } |
| : function ( |
| this: VisualMapping, |
| value: NormalizedValue | RawValue, |
| isNormalized?: boolean, |
| out?: number[] |
| ): number[] | string { |
| // If output rgb array |
| // which will be much faster and useful in pixel manipulation |
| const returnRGBArray = !!out; |
| !isNormalized && (value = this._normalizeData(value)); |
| out = zrColor.fastLerp(value as NormalizedValue, thisOption.parsedVisual, out); |
| return returnRGBArray ? out : zrColor.stringify(out, 'rgba'); |
| }, |
| this |
| ); |
| }, |
| |
| _normalizedToVisual: { |
| linear: function (normalized) { |
| return zrColor.stringify( |
| zrColor.fastLerp(normalized, this.option.parsedVisual), |
| 'rgba' |
| ); |
| }, |
| category: doMapCategory, |
| piecewise: function (normalized, value) { |
| let result = getSpecifiedVisual.call(this, value); |
| if (result == null) { |
| result = zrColor.stringify( |
| zrColor.fastLerp(normalized, this.option.parsedVisual), |
| 'rgba' |
| ); |
| } |
| return result; |
| }, |
| fixed: doMapFixed |
| } |
| }, |
| |
| colorHue: makePartialColorVisualHandler(function (color: ColorString, value: number) { |
| return zrColor.modifyHSL(color, value); |
| }), |
| |
| colorSaturation: makePartialColorVisualHandler(function (color: ColorString, value: number) { |
| return zrColor.modifyHSL(color, null, value); |
| }), |
| |
| colorLightness: makePartialColorVisualHandler(function (color: ColorString, value: number) { |
| return zrColor.modifyHSL(color, null, null, value); |
| }), |
| |
| colorAlpha: makePartialColorVisualHandler(function (color: ColorString, value: number) { |
| return zrColor.modifyAlpha(color, value); |
| }), |
| |
| decal: { |
| applyVisual: makeApplyVisual('decal'), |
| _normalizedToVisual: { |
| linear: null, |
| category: doMapCategory, |
| piecewise: null, |
| fixed: null |
| } |
| }, |
| |
| opacity: { |
| applyVisual: makeApplyVisual('opacity'), |
| _normalizedToVisual: createNormalizedToNumericVisual([0, 1]) |
| }, |
| |
| liftZ: { |
| applyVisual: makeApplyVisual('liftZ'), |
| _normalizedToVisual: { |
| linear: doMapFixed, |
| category: doMapFixed, |
| piecewise: doMapFixed, |
| fixed: doMapFixed |
| } |
| }, |
| |
| symbol: { |
| applyVisual: function (value, getter, setter) { |
| const symbolCfg = this.mapValueToVisual(value); |
| setter('symbol', symbolCfg as string); |
| }, |
| _normalizedToVisual: { |
| linear: doMapToArray, |
| category: doMapCategory, |
| piecewise: function (normalized, value) { |
| let result = getSpecifiedVisual.call(this, value); |
| if (result == null) { |
| result = doMapToArray.call(this, normalized); |
| } |
| return result; |
| }, |
| fixed: doMapFixed |
| } |
| }, |
| |
| symbolSize: { |
| applyVisual: makeApplyVisual('symbolSize'), |
| _normalizedToVisual: createNormalizedToNumericVisual([0, 1]) |
| } |
| }; |
| |
| |
| /** |
| * List available visual types. |
| * |
| * @public |
| * @return {Array.<string>} |
| */ |
| static listVisualTypes() { |
| return zrUtil.keys(VisualMapping.visualHandlers); |
| } |
| |
| // /** |
| // * @public |
| // */ |
| // static addVisualHandler(name, handler) { |
| // visualHandlers[name] = handler; |
| // } |
| |
| /** |
| * @public |
| */ |
| static isValidType(visualType: string): boolean { |
| return VisualMapping.visualHandlers.hasOwnProperty(visualType); |
| } |
| |
| /** |
| * Convinent method. |
| * Visual can be Object or Array or primary type. |
| */ |
| static eachVisual<Ctx, T>( |
| visual: T | T[] | Dictionary<T>, |
| callback: (visual: T, key?: string | number) => void, |
| context?: Ctx |
| ) { |
| if (zrUtil.isObject(visual)) { |
| zrUtil.each(visual as Dictionary<T>, callback, context); |
| } |
| else { |
| callback.call(context, visual); |
| } |
| } |
| |
| static mapVisual<Ctx, T>(visual: T, callback: (visual: T, key?: string | number) => T, context?: Ctx): T |
| static mapVisual<Ctx, T>(visual: T[], callback: (visual: T, key?: string | number) => T[], context?: Ctx): T[] |
| static mapVisual<Ctx, T>( |
| visual: Dictionary<T>, |
| callback: (visual: T, key?: string | number) => Dictionary<T>, |
| context?: Ctx |
| ): Dictionary<T> |
| static mapVisual<Ctx, T>( |
| visual: T | T[] | Dictionary<T>, |
| callback: (visual: T, key?: string | number) => T | T[] | Dictionary<T>, |
| context?: Ctx |
| ) { |
| let isPrimary: boolean; |
| let newVisual: T | T[] | Dictionary<T> = zrUtil.isArray(visual) |
| ? [] |
| : zrUtil.isObject(visual) |
| ? {} |
| : (isPrimary = true, null); |
| |
| VisualMapping.eachVisual(visual, function (v, key) { |
| const newVal = callback.call(context, v, key); |
| isPrimary ? (newVisual = newVal) : ((newVisual as Dictionary<T>)[key as string] = newVal as T); |
| }); |
| return newVisual; |
| } |
| |
| /** |
| * Retrieve visual properties from given object. |
| */ |
| static retrieveVisuals(obj: Dictionary<any>): VisualOptionPiecewise { |
| const ret: VisualOptionPiecewise = {}; |
| let hasVisual: boolean; |
| |
| obj && each(VisualMapping.visualHandlers, function (h, visualType: BuiltinVisualProperty) { |
| if (obj.hasOwnProperty(visualType)) { |
| (ret as any)[visualType] = obj[visualType]; |
| hasVisual = true; |
| } |
| }); |
| |
| return hasVisual ? ret : null; |
| } |
| |
| /** |
| * Give order to visual types, considering colorSaturation, colorAlpha depends on color. |
| * |
| * @public |
| * @param {(Object|Array)} visualTypes If Object, like: {color: ..., colorSaturation: ...} |
| * IF Array, like: ['color', 'symbol', 'colorSaturation'] |
| * @return {Array.<string>} Sorted visual types. |
| */ |
| static prepareVisualTypes( |
| visualTypes: {[key in BuiltinVisualProperty]?: any} | BuiltinVisualProperty[] |
| ) { |
| if (zrUtil.isArray(visualTypes)) { |
| visualTypes = visualTypes.slice(); |
| } |
| else if (isObject(visualTypes)) { |
| const types: BuiltinVisualProperty[] = []; |
| each(visualTypes, function (item: unknown, type: BuiltinVisualProperty) { |
| types.push(type); |
| }); |
| visualTypes = types; |
| } |
| else { |
| return []; |
| } |
| |
| visualTypes.sort(function (type1: BuiltinVisualProperty, type2: BuiltinVisualProperty) { |
| // color should be front of colorSaturation, colorAlpha, ... |
| // symbol and symbolSize do not matter. |
| return (type2 === 'color' && type1 !== 'color' && type1.indexOf('color') === 0) |
| ? 1 : -1; |
| }); |
| |
| return visualTypes; |
| } |
| |
| /** |
| * 'color', 'colorSaturation', 'colorAlpha', ... are depends on 'color'. |
| * Other visuals are only depends on themself. |
| */ |
| static dependsOn(visualType1: BuiltinVisualProperty, visualType2: BuiltinVisualProperty) { |
| return visualType2 === 'color' |
| ? !!(visualType1 && visualType1.indexOf(visualType2) === 0) |
| : visualType1 === visualType2; |
| } |
| |
| /** |
| * @param value |
| * @param pieceList [{value: ..., interval: [min, max]}, ...] |
| * Always from small to big. |
| * @param findClosestWhenOutside Default to be false |
| * @return index |
| */ |
| static findPieceIndex(value: number, pieceList: VisualMappingPiece[], findClosestWhenOutside?: boolean): number { |
| let possibleI: number; |
| let abs = Infinity; |
| |
| // value has the higher priority. |
| for (let i = 0, len = pieceList.length; i < len; i++) { |
| const pieceValue = pieceList[i].value; |
| if (pieceValue != null) { |
| if (pieceValue === value |
| // FIXME |
| // It is supposed to compare value according to value type of dimension, |
| // but currently value type can exactly be string or number. |
| // Compromise for numeric-like string (like '12'), especially |
| // in the case that visualMap.categories is ['22', '33']. |
| || (typeof pieceValue === 'string' && pieceValue === value + '') |
| ) { |
| return i; |
| } |
| findClosestWhenOutside && updatePossible(pieceValue as number, i); |
| } |
| } |
| |
| for (let i = 0, len = pieceList.length; i < len; i++) { |
| const piece = pieceList[i]; |
| const interval = piece.interval; |
| const close = piece.close; |
| |
| if (interval) { |
| if (interval[0] === -Infinity) { |
| if (littleThan(close[1], value, interval[1])) { |
| return i; |
| } |
| } |
| else if (interval[1] === Infinity) { |
| if (littleThan(close[0], interval[0], value)) { |
| return i; |
| } |
| } |
| else if ( |
| littleThan(close[0], interval[0], value) |
| && littleThan(close[1], value, interval[1]) |
| ) { |
| return i; |
| } |
| findClosestWhenOutside && updatePossible(interval[0], i); |
| findClosestWhenOutside && updatePossible(interval[1], i); |
| } |
| } |
| |
| if (findClosestWhenOutside) { |
| return value === Infinity |
| ? pieceList.length - 1 |
| : value === -Infinity |
| ? 0 |
| : possibleI; |
| } |
| |
| function updatePossible(val: number, index: number) { |
| const newAbs = Math.abs(val - value); |
| if (newAbs < abs) { |
| abs = newAbs; |
| possibleI = index; |
| } |
| } |
| |
| } |
| } |
| |
| function preprocessForPiecewise(thisOption: VisualMappingInnerOption) { |
| const pieceList = thisOption.pieceList; |
| thisOption.hasSpecialVisual = false; |
| |
| zrUtil.each(pieceList, function (piece, index) { |
| piece.originIndex = index; |
| // piece.visual is "result visual value" but not |
| // a visual range, so it does not need to be normalized. |
| if (piece.visual != null) { |
| thisOption.hasSpecialVisual = true; |
| } |
| }); |
| } |
| |
| function preprocessForSpecifiedCategory(thisOption: VisualMappingInnerOption) { |
| // Hash categories. |
| const categories = thisOption.categories; |
| const categoryMap: VisualMappingInnerOption['categoryMap'] = thisOption.categoryMap = {}; |
| |
| let visual = thisOption.visual; |
| each(categories, function (cate, index) { |
| categoryMap[cate] = index; |
| }); |
| |
| // Process visual map input. |
| if (!zrUtil.isArray(visual)) { |
| const visualArr: VisualValue[] = []; |
| |
| if (zrUtil.isObject(visual)) { |
| each(visual, function (v, cate) { |
| const index = categoryMap[cate]; |
| visualArr[index != null ? index : CATEGORY_DEFAULT_VISUAL_INDEX] = v; |
| }); |
| } |
| else { // Is primary type, represents default visual. |
| visualArr[CATEGORY_DEFAULT_VISUAL_INDEX] = visual; |
| } |
| |
| visual = setVisualToOption(thisOption, visualArr); |
| } |
| |
| // Remove categories that has no visual, |
| // then we can mapping them to CATEGORY_DEFAULT_VISUAL_INDEX. |
| for (let i = categories.length - 1; i >= 0; i--) { |
| if (visual[i] == null) { |
| delete categoryMap[categories[i]]; |
| categories.pop(); |
| } |
| } |
| } |
| |
| function normalizeVisualRange(thisOption: VisualMappingInnerOption, isCategory?: boolean) { |
| const visual = thisOption.visual; |
| const visualArr: VisualValue[] = []; |
| |
| if (zrUtil.isObject(visual)) { |
| each(visual, function (v) { |
| visualArr.push(v); |
| }); |
| } |
| else if (visual != null) { |
| visualArr.push(visual); |
| } |
| |
| const doNotNeedPair = {color: 1, symbol: 1}; |
| |
| if (!isCategory |
| && visualArr.length === 1 |
| && !doNotNeedPair.hasOwnProperty(thisOption.type) |
| ) { |
| // Do not care visualArr.length === 0, which is illegal. |
| visualArr[1] = visualArr[0]; |
| } |
| |
| setVisualToOption(thisOption, visualArr); |
| } |
| |
| function makePartialColorVisualHandler( |
| applyValue: (prop: VisualValue, value: NormalizedValue) => VisualValue |
| ): VisualHandler { |
| return { |
| applyVisual: function (value, getter, setter) { |
| // Only used in HSL |
| const colorChannel = this.mapValueToVisual(value); |
| // Must not be array value |
| setter('color', applyValue(getter('color'), colorChannel as number)); |
| }, |
| _normalizedToVisual: createNormalizedToNumericVisual([0, 1]) |
| }; |
| } |
| |
| function doMapToArray(this: VisualMapping<VisualOptionLinear>, normalized: NormalizedValue): VisualValue { |
| const visual = this.option.visual as VisualValue[]; |
| return visual[ |
| Math.round(linearMap(normalized, [0, 1], [0, visual.length - 1], true)) |
| ] || {} as any; // TODO {}? |
| } |
| |
| function makeApplyVisual(visualType: string): VisualHandler['applyVisual'] { |
| return function (value, getter, setter) { |
| setter(visualType, this.mapValueToVisual(value)); |
| }; |
| } |
| |
| function doMapCategory(this: VisualMapping<VisualOptionCategory>, normalized: NormalizedValue): VisualValue { |
| const visual = this.option.visual as Dictionary<any>; |
| return visual[ |
| (this.option.loop && normalized !== CATEGORY_DEFAULT_VISUAL_INDEX) |
| ? normalized % visual.length |
| : normalized |
| ]; |
| } |
| |
| function doMapFixed(this: VisualMapping): VisualValue { |
| // visual will be convert to array. |
| return (this.option.visual as VisualValue[])[0]; |
| } |
| |
| /** |
| * Create mapped to numeric visual |
| */ |
| function createNormalizedToNumericVisual(sourceExtent: [number, number]): VisualHandler['_normalizedToVisual'] { |
| return { |
| linear: function (normalized) { |
| return linearMap(normalized, sourceExtent, this.option.visual as [number, number], true); |
| }, |
| category: doMapCategory, |
| piecewise: function (normalized, value) { |
| let result = getSpecifiedVisual.call(this, value); |
| if (result == null) { |
| result = linearMap(normalized, sourceExtent, this.option.visual as [number, number], true); |
| } |
| return result; |
| }, |
| fixed: doMapFixed |
| }; |
| } |
| |
| function getSpecifiedVisual(this: VisualMapping, value: number) { |
| const thisOption = this.option; |
| const pieceList = thisOption.pieceList; |
| if (thisOption.hasSpecialVisual) { |
| const pieceIndex = VisualMapping.findPieceIndex(value, pieceList); |
| const piece = pieceList[pieceIndex]; |
| if (piece && piece.visual) { |
| return piece.visual[this.type]; |
| } |
| } |
| } |
| |
| function setVisualToOption(thisOption: VisualMappingInnerOption, visualArr: VisualValue[]) { |
| thisOption.visual = visualArr; |
| if (thisOption.type === 'color') { |
| thisOption.parsedVisual = zrUtil.map(visualArr, function (item: string) { |
| return zrColor.parse(item); |
| }); |
| } |
| return visualArr; |
| } |
| |
| |
| /** |
| * Normalizers by mapping methods. |
| */ |
| const normalizers: { [key in MappingMethod]: Normalizer } = { |
| linear: function (value: RawValue): NormalizedValue { |
| return linearMap(value as number, this.option.dataExtent, [0, 1], true); |
| }, |
| |
| piecewise: function (value: RawValue): NormalizedValue { |
| const pieceList = this.option.pieceList; |
| const pieceIndex = VisualMapping.findPieceIndex(value as number, pieceList, true); |
| if (pieceIndex != null) { |
| return linearMap(pieceIndex, [0, pieceList.length - 1], [0, 1], true); |
| } |
| }, |
| |
| category: function (value: RawValue): NormalizedValue { |
| const index: number = this.option.categories |
| ? this.option.categoryMap[value] |
| : value as number; // ordinal value |
| return index == null ? CATEGORY_DEFAULT_VISUAL_INDEX : index; |
| }, |
| |
| fixed: zrUtil.noop as Normalizer |
| }; |
| |
| |
| function littleThan(close: boolean | 0 | 1, a: number, b: number): boolean { |
| return close ? a <= b : a < b; |
| } |
| |
| export default VisualMapping; |