import {Orientation, SignalRef, Text} from 'vega';
import {isArray, isBoolean, isString} from 'vega-util';
import {CompositeMark, CompositeMarkDef} from './index.js';
import {
Field,
FieldDefBase,
isContinuousFieldOrDatumDef,
isFieldDef,
isFieldOrDatumDefForTimeFormat,
PositionFieldDef,
SecondaryFieldDef,
StringFieldDef,
StringFieldDefWithCondition,
StringValueDefWithCondition,
} from '../channeldef.js';
import {Encoding, fieldDefs} from '../encoding.js';
import {ExprRef} from '../expr.js';
import * as log from '../log/index.js';
import {ColorMixins, GenericMarkDef, isMarkDef, Mark, AnyMarkConfig, MarkDef} from '../mark.js';
import {GenericUnitSpec, NormalizedUnitSpec} from '../spec/index.js';
import {getFirstDefined, hash, unique} from '../util.js';
import {isSignalRef} from '../vega.schema.js';
import {toStringFieldDef} from './../channeldef.js';
// Parts mixins can be any mark type. We could make a more specific type for each part.
export type PartsMixins
= Partial>>;
export type GenericCompositeMarkDef = GenericMarkDef &
ColorMixins & {
/**
* The opacity (value between [0,1]) of the mark.
*
* @minimum 0
* @maximum 1
*/
opacity?: number;
/**
* Whether a composite mark be clipped to the enclosing group’s width and height.
*/
clip?: boolean;
};
export interface CompositeMarkTooltipSummary {
/**
* The prefix of the field to be shown in tooltip
*/
fieldPrefix: string;
/**
* The title prefix to show, corresponding to the field with field prefix `fieldPrefix`
*/
titlePrefix: Text | SignalRef;
}
export function filterTooltipWithAggregatedField(
oldEncoding: Encoding,
): {
customTooltipWithoutAggregatedField?:
| StringFieldDefWithCondition
| StringValueDefWithCondition
| StringFieldDef[];
filteredEncoding: Encoding;
} {
const {tooltip, ...filteredEncoding} = oldEncoding;
if (!tooltip) {
return {filteredEncoding};
}
let customTooltipWithAggregatedField:
| StringFieldDefWithCondition
| StringValueDefWithCondition
| StringFieldDef[];
let customTooltipWithoutAggregatedField:
| StringFieldDefWithCondition
| StringValueDefWithCondition
| StringFieldDef[];
if (isArray(tooltip)) {
for (const t of tooltip) {
if (t.aggregate) {
if (!customTooltipWithAggregatedField) {
customTooltipWithAggregatedField = [];
}
(customTooltipWithAggregatedField as StringFieldDef[]).push(t);
} else {
if (!customTooltipWithoutAggregatedField) {
customTooltipWithoutAggregatedField = [];
}
(customTooltipWithoutAggregatedField as StringFieldDef[]).push(t);
}
}
if (customTooltipWithAggregatedField) {
(filteredEncoding as Encoding).tooltip = customTooltipWithAggregatedField;
}
} else {
if ((tooltip as any).aggregate) {
(filteredEncoding as Encoding).tooltip = tooltip;
} else {
customTooltipWithoutAggregatedField = tooltip;
}
}
if (isArray(customTooltipWithoutAggregatedField) && customTooltipWithoutAggregatedField.length === 1) {
customTooltipWithoutAggregatedField = customTooltipWithoutAggregatedField[0];
}
return {customTooltipWithoutAggregatedField, filteredEncoding};
}
export function getCompositeMarkTooltip(
tooltipSummary: CompositeMarkTooltipSummary[],
continuousAxisChannelDef: PositionFieldDef,
encodingWithoutContinuousAxis: Encoding,
withFieldName = true,
): Encoding {
if ('tooltip' in encodingWithoutContinuousAxis) {
return {tooltip: encodingWithoutContinuousAxis.tooltip};
}
const fiveSummaryTooltip: StringFieldDef[] = tooltipSummary.map(
({fieldPrefix, titlePrefix}): StringFieldDef => {
const mainTitle = withFieldName ? ` of ${getTitle(continuousAxisChannelDef)}` : '';
return {
field: fieldPrefix + continuousAxisChannelDef.field,
type: continuousAxisChannelDef.type,
title: isSignalRef(titlePrefix) ? {signal: `${titlePrefix}"${escape(mainTitle)}"`} : titlePrefix + mainTitle,
};
},
);
const tooltipFieldDefs = fieldDefs(encodingWithoutContinuousAxis).map(toStringFieldDef);
return {
tooltip: [
...fiveSummaryTooltip,
// need to cast because TextFieldDef supports fewer types of bin
...unique(tooltipFieldDefs, hash),
],
};
}
export function getTitle(continuousAxisChannelDef: PositionFieldDef) {
const {title, field} = continuousAxisChannelDef;
return getFirstDefined(title, field);
}
export function makeCompositeAggregatePartFactory>(
compositeMarkDef: GenericCompositeMarkDef & P,
continuousAxis: 'x' | 'y',
continuousAxisChannelDef: PositionFieldDef,
sharedEncoding: Encoding,
compositeMarkConfig: P,
) {
const {scale, axis} = continuousAxisChannelDef;
return ({
partName,
mark,
positionPrefix,
endPositionPrefix = undefined,
extraEncoding = {},
}: {
partName: keyof P;
mark: Mark | MarkDef;
positionPrefix: string;
endPositionPrefix?: string;
extraEncoding?: Encoding;
}) => {
const title = getTitle(continuousAxisChannelDef);
return partLayerMixins(compositeMarkDef, partName, compositeMarkConfig, {
mark, // TODO better remove this method and just have mark as a parameter of the method
encoding: {
[continuousAxis]: {
field: `${positionPrefix}_${continuousAxisChannelDef.field}`,
type: continuousAxisChannelDef.type,
...(title !== undefined ? {title} : {}),
...(scale !== undefined ? {scale} : {}),
...(axis !== undefined ? {axis} : {}),
},
...(isString(endPositionPrefix)
? {
[`${continuousAxis}2`]: {
field: `${endPositionPrefix}_${continuousAxisChannelDef.field}`,
},
}
: {}),
...sharedEncoding,
...extraEncoding,
},
});
};
}
export function partLayerMixins
>(
markDef: GenericCompositeMarkDef & P,
part: keyof P,
compositeMarkConfig: P,
partBaseSpec: NormalizedUnitSpec,
): NormalizedUnitSpec[] {
const {clip, color, opacity} = markDef;
const mark = markDef.type;
if (markDef[part] || (markDef[part] === undefined && compositeMarkConfig[part])) {
return [
{
...partBaseSpec,
mark: {
...(compositeMarkConfig[part] as AnyMarkConfig),
...(clip ? {clip} : {}),
...(color ? {color} : {}),
...(opacity ? {opacity} : {}),
...(isMarkDef(partBaseSpec.mark) ? partBaseSpec.mark : {type: partBaseSpec.mark}),
style: `${mark}-${String(part)}`,
...(isBoolean(markDef[part]) ? {} : (markDef[part] as AnyMarkConfig)),
},
},
];
}
return [];
}
export function compositeMarkContinuousAxis(
spec: GenericUnitSpec, CompositeMark | CompositeMarkDef>,
orient: Orientation,
compositeMark: M,
): {
continuousAxisChannelDef: PositionFieldDef;
continuousAxisChannelDef2: SecondaryFieldDef;
continuousAxisChannelDefError: SecondaryFieldDef;
continuousAxisChannelDefError2: SecondaryFieldDef;
continuousAxis: 'x' | 'y';
} {
const {encoding} = spec;
const continuousAxis: 'x' | 'y' = orient === 'vertical' ? 'y' : 'x';
const continuousAxisChannelDef = encoding[continuousAxis] as PositionFieldDef; // Safe to cast because if x is not continuous fielddef, the orient would not be horizontal.
const continuousAxisChannelDef2 = encoding[`${continuousAxis}2`] as SecondaryFieldDef;
const continuousAxisChannelDefError = (encoding as any)[`${continuousAxis}Error`] as SecondaryFieldDef;
const continuousAxisChannelDefError2 = (encoding as any)[`${continuousAxis}Error2`] as SecondaryFieldDef;
return {
continuousAxisChannelDef: filterAggregateFromChannelDef(continuousAxisChannelDef, compositeMark),
continuousAxisChannelDef2: filterAggregateFromChannelDef(continuousAxisChannelDef2, compositeMark),
continuousAxisChannelDefError: filterAggregateFromChannelDef(continuousAxisChannelDefError, compositeMark),
continuousAxisChannelDefError2: filterAggregateFromChannelDef(continuousAxisChannelDefError2, compositeMark),
continuousAxis,
};
}
function filterAggregateFromChannelDef>(
continuousAxisChannelDef: F,
compositeMark: M,
): F {
if (continuousAxisChannelDef?.aggregate) {
const {aggregate, ...continuousAxisWithoutAggregate} = continuousAxisChannelDef;
if (aggregate !== compositeMark) {
log.warn(log.message.errorBarContinuousAxisHasCustomizedAggregate(aggregate, compositeMark));
}
return continuousAxisWithoutAggregate as F;
} else {
return continuousAxisChannelDef;
}
}
export function compositeMarkOrient(
spec: GenericUnitSpec, CompositeMark | CompositeMarkDef>,
compositeMark: M,
): Orientation {
const {mark, encoding} = spec;
const {x, y} = encoding;
if (isMarkDef(mark) && mark.orient) {
return mark.orient;
}
if (isContinuousFieldOrDatumDef(x)) {
// x is continuous
if (isContinuousFieldOrDatumDef(y)) {
// both x and y are continuous
const xAggregate = isFieldDef(x) && x.aggregate;
const yAggregate = isFieldDef(y) && y.aggregate;
if (!xAggregate && yAggregate === compositeMark) {
return 'vertical';
} else if (!yAggregate && xAggregate === compositeMark) {
return 'horizontal';
} else if (xAggregate === compositeMark && yAggregate === compositeMark) {
throw new Error('Both x and y cannot have aggregate');
} else {
if (isFieldOrDatumDefForTimeFormat(y) && !isFieldOrDatumDefForTimeFormat(x)) {
// y is temporal but x is not
return 'horizontal';
}
// default orientation for two continuous
return 'vertical';
}
}
return 'horizontal';
} else if (isContinuousFieldOrDatumDef(y)) {
// y is continuous but x is not
return 'vertical';
} else {
// Neither x nor y is continuous.
throw new Error(`Need a valid continuous axis for ${compositeMark}s`);
}
}