/* * Constants and utilities for encoding channels (Visual variables) * such as 'x', 'y', 'color'. */ import {hasOwnProperty} from 'vega-util'; import {RangeType} from './compile/scale/type.js'; import {Encoding} from './encoding.js'; import {Mark} from './mark.js'; import {EncodingFacetMapping} from './spec/facet.js'; import {Flag, keys} from './util.js'; export type Channel = keyof Encoding; export type ExtendedChannel = Channel | FacetChannel; // Facet export const ROW = 'row' as const; export const COLUMN = 'column' as const; export const FACET = 'facet' as const; // Position export const X = 'x' as const; export const Y = 'y' as const; export const X2 = 'x2' as const; export const Y2 = 'y2' as const; // Position Offset export const XOFFSET = 'xOffset' as const; export const YOFFSET = 'yOffset' as const; // Arc-Position export const RADIUS = 'radius' as const; export const RADIUS2 = 'radius2' as const; export const THETA = 'theta' as const; export const THETA2 = 'theta2' as const; // Geo Position export const LATITUDE = 'latitude' as const; export const LONGITUDE = 'longitude' as const; export const LATITUDE2 = 'latitude2' as const; export const LONGITUDE2 = 'longitude2' as const; // Time export const TIME = 'time' as const; // Mark property with scale export const COLOR = 'color' as const; export const FILL = 'fill' as const; export const STROKE = 'stroke' as const; export const SHAPE = 'shape' as const; export const SIZE = 'size' as const; export const ANGLE = 'angle' as const; export const OPACITY = 'opacity' as const; export const FILLOPACITY = 'fillOpacity' as const; export const STROKEOPACITY = 'strokeOpacity' as const; export const STROKEWIDTH = 'strokeWidth' as const; export const STROKEDASH = 'strokeDash' as const; // Non-scale channel export const TEXT = 'text' as const; export const ORDER = 'order' as const; export const DETAIL = 'detail' as const; export const KEY = 'key' as const; export const TOOLTIP = 'tooltip' as const; export const HREF = 'href' as const; export const URL = 'url' as const; export const DESCRIPTION = 'description' as const; const POSITION_CHANNEL_INDEX = { x: 1, y: 1, x2: 1, y2: 1, } as const; export type PositionChannel = keyof typeof POSITION_CHANNEL_INDEX; const POLAR_POSITION_CHANNEL_INDEX = { theta: 1, theta2: 1, radius: 1, radius2: 1, } as const; export type PolarPositionChannel = keyof typeof POLAR_POSITION_CHANNEL_INDEX; export function isPolarPositionChannel(c: Channel): c is PolarPositionChannel { return hasOwnProperty(POLAR_POSITION_CHANNEL_INDEX, c); } const GEO_POSIITON_CHANNEL_INDEX = { longitude: 1, longitude2: 1, latitude: 1, latitude2: 1, } as const; export type GeoPositionChannel = keyof typeof GEO_POSIITON_CHANNEL_INDEX; export function getPositionChannelFromLatLong(channel: GeoPositionChannel): PositionChannel { switch (channel) { case LATITUDE: return 'y'; case LATITUDE2: return 'y2'; case LONGITUDE: return 'x'; case LONGITUDE2: return 'x2'; } } export function isGeoPositionChannel(c: Channel): c is GeoPositionChannel { return hasOwnProperty(GEO_POSIITON_CHANNEL_INDEX, c); } export const GEOPOSITION_CHANNELS = keys(GEO_POSIITON_CHANNEL_INDEX); const UNIT_CHANNEL_INDEX: Flag = { ...POSITION_CHANNEL_INDEX, ...POLAR_POSITION_CHANNEL_INDEX, ...GEO_POSIITON_CHANNEL_INDEX, xOffset: 1, yOffset: 1, // color color: 1, fill: 1, stroke: 1, // time time: 1, // other non-position with scale opacity: 1, fillOpacity: 1, strokeOpacity: 1, strokeWidth: 1, strokeDash: 1, size: 1, angle: 1, shape: 1, // channels without scales order: 1, text: 1, detail: 1, key: 1, tooltip: 1, href: 1, url: 1, description: 1, }; export type ColorChannel = 'color' | 'fill' | 'stroke'; export function isColorChannel(channel: Channel): channel is ColorChannel { return channel === COLOR || channel === FILL || channel === STROKE; } export type FacetChannel = keyof EncodingFacetMapping; const FACET_CHANNEL_INDEX: Flag> = { row: 1, column: 1, facet: 1, }; export const FACET_CHANNELS = keys(FACET_CHANNEL_INDEX); const CHANNEL_INDEX = { ...UNIT_CHANNEL_INDEX, ...FACET_CHANNEL_INDEX, }; export const CHANNELS = keys(CHANNEL_INDEX); const {order: _o, detail: _d, tooltip: _tt1, ...SINGLE_DEF_CHANNEL_INDEX} = CHANNEL_INDEX; const {row: _r, column: _c, facet: _f, ...SINGLE_DEF_UNIT_CHANNEL_INDEX} = SINGLE_DEF_CHANNEL_INDEX; /** * Channels that cannot have an array of channelDef. * model.fieldDef, getFieldDef only work for these channels. * * (The only two channels that can have an array of channelDefs are "detail" and "order". * Since there can be multiple fieldDefs for detail and order, getFieldDef/model.fieldDef * are not applicable for them. Similarly, selection projection won't work with "detail" and "order".) */ export const SINGLE_DEF_CHANNELS = keys(SINGLE_DEF_CHANNEL_INDEX); export type SingleDefChannel = (typeof SINGLE_DEF_CHANNELS)[number]; export const SINGLE_DEF_UNIT_CHANNELS = keys(SINGLE_DEF_UNIT_CHANNEL_INDEX); export type SingleDefUnitChannel = (typeof SINGLE_DEF_UNIT_CHANNELS)[number]; export function isSingleDefUnitChannel(str: string): str is SingleDefUnitChannel { return hasOwnProperty(SINGLE_DEF_UNIT_CHANNEL_INDEX, str); } export function isChannel(str: string): str is Channel { return hasOwnProperty(CHANNEL_INDEX, str); } export type SecondaryRangeChannel = 'x2' | 'y2' | 'latitude2' | 'longitude2' | 'theta2' | 'radius2'; export const SECONDARY_RANGE_CHANNEL: SecondaryRangeChannel[] = [X2, Y2, LATITUDE2, LONGITUDE2, THETA2, RADIUS2]; export function isSecondaryRangeChannel(c: ExtendedChannel): c is SecondaryRangeChannel { const main = getMainRangeChannel(c); return main !== c; } export type MainChannelOf = C extends 'x2' ? 'x' : C extends 'y2' ? 'y' : C extends 'latitude2' ? 'latitude' : C extends 'longitude2' ? 'longitude' : C extends 'theta2' ? 'theta' : C extends 'radius2' ? 'radius' : C; /** * Get the main channel for a range channel. E.g. `x` for `x2`. */ export function getMainRangeChannel(channel: C): MainChannelOf { switch (channel) { case X2: return X as MainChannelOf; case Y2: return Y as MainChannelOf; case LATITUDE2: return LATITUDE as MainChannelOf; case LONGITUDE2: return LONGITUDE as MainChannelOf; case THETA2: return THETA as MainChannelOf; case RADIUS2: return RADIUS as MainChannelOf; } return channel as MainChannelOf; } export type SecondaryChannelOf = C extends 'x' ? 'x2' : C extends 'y' ? 'y2' : C extends 'latitude' ? 'latitude2' : C extends 'longitude' ? 'longitude2' : C extends 'theta' ? 'theta2' : C extends 'radius' ? 'radius2' : undefined; export function getVgPositionChannel(channel: PolarPositionChannel | PositionChannel) { if (isPolarPositionChannel(channel)) { switch (channel) { case THETA: return 'startAngle'; case THETA2: return 'endAngle'; case RADIUS: return 'outerRadius'; case RADIUS2: return 'innerRadius'; } } return channel; } /** * Get the main channel for a range channel. E.g. `x` for `x2`. */ export function getSecondaryRangeChannel(channel: C): SecondaryChannelOf | undefined { switch (channel) { case X: return X2 as SecondaryChannelOf; case Y: return Y2 as SecondaryChannelOf; case LATITUDE: return LATITUDE2 as SecondaryChannelOf; case LONGITUDE: return LONGITUDE2 as SecondaryChannelOf; case THETA: return THETA2 as SecondaryChannelOf; case RADIUS: return RADIUS2 as SecondaryChannelOf; } return undefined; } export function getSizeChannel(channel: PositionChannel): 'width' | 'height'; export function getSizeChannel(channel: Channel): 'width' | 'height' | undefined; export function getSizeChannel(channel: Channel): 'width' | 'height' | undefined { switch (channel) { case X: case X2: return 'width'; case Y: case Y2: return 'height'; } return undefined; } /** * Get the main channel for a range channel. E.g. `x` for `x2`. */ export function getOffsetChannel(channel: Channel) { switch (channel) { case X: return 'xOffset'; case Y: return 'yOffset'; case X2: return 'x2Offset'; case Y2: return 'y2Offset'; case THETA: return 'thetaOffset'; case RADIUS: return 'radiusOffset'; case THETA2: return 'theta2Offset'; case RADIUS2: return 'radius2Offset'; } return undefined; } /** * Get the main channel for a range channel. E.g. `x` for `x2`. */ export function getOffsetScaleChannel(channel: Channel): OffsetScaleChannel { switch (channel) { case X: return 'xOffset'; case Y: return 'yOffset'; } return undefined; } export function getMainChannelFromOffsetChannel(channel: OffsetScaleChannel): PositionScaleChannel { switch (channel) { case 'xOffset': return 'x'; case 'yOffset': return 'y'; } } // CHANNELS without COLUMN, ROW export const UNIT_CHANNELS = keys(UNIT_CHANNEL_INDEX); // NONPOSITION_CHANNELS = UNIT_CHANNELS without X, Y, X2, Y2; const { x: _x, y: _y, // x2 and y2 share the same scale as x and y x2: _x2, y2: _y2, // xOffset: _xo, yOffset: _yo, latitude: _latitude, longitude: _longitude, latitude2: _latitude2, longitude2: _longitude2, theta: _theta, theta2: _theta2, radius: _radius, radius2: _radius2, // The rest of unit channels then have scale ...NONPOSITION_CHANNEL_INDEX } = UNIT_CHANNEL_INDEX; export const NONPOSITION_CHANNELS = keys(NONPOSITION_CHANNEL_INDEX); export type NonPositionChannel = (typeof NONPOSITION_CHANNELS)[number]; const POSITION_SCALE_CHANNEL_INDEX = { x: 1, y: 1, } as const; export const POSITION_SCALE_CHANNELS = keys(POSITION_SCALE_CHANNEL_INDEX); export type PositionScaleChannel = keyof typeof POSITION_SCALE_CHANNEL_INDEX; export function isXorY(channel: ExtendedChannel): channel is PositionScaleChannel { return hasOwnProperty(POSITION_SCALE_CHANNEL_INDEX, channel); } export const POLAR_POSITION_SCALE_CHANNEL_INDEX = { theta: 1, radius: 1, } as const; export const POLAR_POSITION_SCALE_CHANNELS = keys(POLAR_POSITION_SCALE_CHANNEL_INDEX); export type PolarPositionScaleChannel = keyof typeof POLAR_POSITION_SCALE_CHANNEL_INDEX; export function getPositionScaleChannel(sizeType: 'width' | 'height'): PositionScaleChannel { return sizeType === 'width' ? X : Y; } const OFFSET_SCALE_CHANNEL_INDEX: {xOffset: 1; yOffset: 1} = {xOffset: 1, yOffset: 1}; export const OFFSET_SCALE_CHANNELS = keys(OFFSET_SCALE_CHANNEL_INDEX); export type OffsetScaleChannel = (typeof OFFSET_SCALE_CHANNELS)[0]; export function isXorYOffset(channel: Channel): channel is OffsetScaleChannel { return hasOwnProperty(OFFSET_SCALE_CHANNEL_INDEX, channel); } const TIME_SCALE_CHANNEL_INDEX = { time: 1, } as const; export const TIME_SCALE_CHANNELS = keys(TIME_SCALE_CHANNEL_INDEX); export type TimeScaleChannel = keyof typeof TIME_SCALE_CHANNEL_INDEX; export function isTime(channel: ExtendedChannel): channel is TimeScaleChannel { return channel in TIME_SCALE_CHANNEL_INDEX; } // NON_POSITION_SCALE_CHANNEL = SCALE_CHANNELS without position / offset const { // x2 and y2 share the same scale as x and y // text and tooltip have format instead of scale, // href has neither format, nor scale text: _t, tooltip: _tt, href: _hr, url: _u, description: _al, // detail and order have no scale detail: _dd, key: _k, order: _oo, ...NONPOSITION_SCALE_CHANNEL_INDEX } = NONPOSITION_CHANNEL_INDEX; export const NONPOSITION_SCALE_CHANNELS = keys(NONPOSITION_SCALE_CHANNEL_INDEX); export type NonPositionScaleChannel = (typeof NONPOSITION_SCALE_CHANNELS)[number]; export function isNonPositionScaleChannel(channel: Channel): channel is NonPositionScaleChannel { return hasOwnProperty(NONPOSITION_CHANNEL_INDEX, channel); } /** * @returns whether Vega supports legends for a particular channel */ export function supportLegend(channel: NonPositionScaleChannel) { switch (channel) { case COLOR: case FILL: case STROKE: case SIZE: case SHAPE: case OPACITY: case STROKEWIDTH: case STROKEDASH: return true; case FILLOPACITY: case STROKEOPACITY: case ANGLE: case TIME: return false; } } // Declare SCALE_CHANNEL_INDEX const SCALE_CHANNEL_INDEX = { ...POSITION_SCALE_CHANNEL_INDEX, ...POLAR_POSITION_SCALE_CHANNEL_INDEX, ...OFFSET_SCALE_CHANNEL_INDEX, ...NONPOSITION_SCALE_CHANNEL_INDEX, }; /** List of channels with scales */ export const SCALE_CHANNELS = keys(SCALE_CHANNEL_INDEX); export type ScaleChannel = (typeof SCALE_CHANNELS)[number]; export function isScaleChannel(channel: ExtendedChannel): channel is ScaleChannel { return hasOwnProperty(SCALE_CHANNEL_INDEX, channel); } export type SupportedMark = Partial>; /** * Return whether a channel supports a particular mark type. * @param channel channel name * @param mark the mark type * @return whether the mark supports the channel */ export function supportMark(channel: ExtendedChannel, mark: Mark) { return getSupportedMark(channel)[mark]; } const ALL_MARKS: Record = { // all marks arc: 'always', area: 'always', bar: 'always', circle: 'always', geoshape: 'always', image: 'always', line: 'always', rule: 'always', point: 'always', rect: 'always', square: 'always', trail: 'always', text: 'always', tick: 'always', }; const {geoshape: _g, ...ALL_MARKS_EXCEPT_GEOSHAPE} = ALL_MARKS; /** * Return a dictionary showing whether a channel supports mark type. * @param channel * @return A dictionary mapping mark types to 'always', 'binned', or undefined */ function getSupportedMark(channel: ExtendedChannel): SupportedMark { switch (channel) { case COLOR: case FILL: case STROKE: // falls through case DESCRIPTION: case DETAIL: case KEY: case TOOLTIP: case HREF: case ORDER: // TODO: revise (order might not support rect, which is not stackable?) case OPACITY: case FILLOPACITY: case STROKEOPACITY: case STROKEWIDTH: // falls through case FACET: case ROW: // falls through case COLUMN: return ALL_MARKS; case X: case Y: case XOFFSET: case YOFFSET: case LATITUDE: case LONGITUDE: case TIME: // all marks except geoshape. geoshape does not use X, Y -- it uses a projection return ALL_MARKS_EXCEPT_GEOSHAPE; case X2: case Y2: case LATITUDE2: case LONGITUDE2: return { area: 'always', bar: 'always', image: 'always', rect: 'always', rule: 'always', circle: 'binned', point: 'binned', square: 'binned', tick: 'binned', line: 'binned', trail: 'binned', }; case SIZE: return { point: 'always', tick: 'always', rule: 'always', circle: 'always', square: 'always', bar: 'always', text: 'always', line: 'always', trail: 'always', }; case STROKEDASH: return { line: 'always', point: 'always', tick: 'always', rule: 'always', circle: 'always', square: 'always', bar: 'always', geoshape: 'always', }; case SHAPE: return {point: 'always', geoshape: 'always'}; case TEXT: return {text: 'always'}; case ANGLE: return {point: 'always', square: 'always', text: 'always'}; case URL: return {image: 'always'}; case THETA: return {text: 'always', arc: 'always'}; case RADIUS: return {text: 'always', arc: 'always'}; case THETA2: case RADIUS2: return {arc: 'always'}; } } export function rangeType(channel: ExtendedChannel): RangeType { switch (channel) { case X: case Y: case THETA: case RADIUS: case XOFFSET: case YOFFSET: case SIZE: case ANGLE: case STROKEWIDTH: case OPACITY: case FILLOPACITY: case STROKEOPACITY: case TIME: // X2 and Y2 use X and Y scales, so they similarly have continuous range. [falls through] case X2: case Y2: case THETA2: case RADIUS2: return undefined; case FACET: case ROW: case COLUMN: case SHAPE: case STROKEDASH: // TEXT, TOOLTIP, URL, and HREF have no scale but have discrete output [falls through] case TEXT: case TOOLTIP: case HREF: case URL: case DESCRIPTION: return 'discrete'; // Color can be either continuous or discrete, depending on scale type. case COLOR: case FILL: case STROKE: return 'flexible'; // No scale, no range type. case LATITUDE: case LONGITUDE: case LATITUDE2: case LONGITUDE2: case DETAIL: case KEY: case ORDER: return undefined; } }