vega-lite
Version:
Vega-Lite is a concise high-level language for interactive visualization.
101 lines (85 loc) • 3.96 kB
text/typescript
import {AxisOrient, SignalRef} from 'vega';
import {isArray} from 'vega-util';
import {FacetChannel, FACET_CHANNELS} from '../../channel.js';
import {title as fieldDefTitle} from '../../channeldef.js';
import {contains, getFirstDefined} from '../../util.js';
import {isSignalRef} from '../../vega.schema.js';
import {assembleAxis} from '../axis/assemble.js';
import {FacetModel} from '../facet.js';
import {parseGuideResolve} from '../resolve.js';
import {getHeaderProperty} from './common.js';
import {HeaderChannel, HeaderComponent} from './component.js';
export function getHeaderType(orient: AxisOrient | SignalRef) {
if (orient === 'top' || orient === 'left' || isSignalRef(orient)) {
// we always use header for orient signal since we can't dynamically make header becomes footer
return 'header';
}
return 'footer';
}
export function parseFacetHeaders(model: FacetModel) {
for (const channel of FACET_CHANNELS) {
parseFacetHeader(model, channel);
}
mergeChildAxis(model, 'x');
mergeChildAxis(model, 'y');
}
function parseFacetHeader(model: FacetModel, channel: FacetChannel) {
const {facet, config, child, component} = model;
if (model.channelHasField(channel)) {
const fieldDef = facet[channel];
const titleConfig = getHeaderProperty('title', null, config, channel);
let title = fieldDefTitle(fieldDef, config, {
allowDisabling: true,
includeDefault: titleConfig === undefined || !!titleConfig,
});
if (child.component.layoutHeaders[channel].title) {
// TODO: better handle multiline titles
title = isArray(title) ? title.join(', ') : title;
// merge title with child to produce "Title / Subtitle / Sub-subtitle"
title += ` / ${child.component.layoutHeaders[channel].title}`;
child.component.layoutHeaders[channel].title = null;
}
const labelOrient = getHeaderProperty('labelOrient', fieldDef.header, config, channel);
const labels =
fieldDef.header !== null ? getFirstDefined(fieldDef.header?.labels, config.header.labels, true) : false;
const headerType = contains(['bottom', 'right'], labelOrient) ? 'footer' : 'header';
component.layoutHeaders[channel] = {
title: fieldDef.header !== null ? title : null,
facetFieldDef: fieldDef,
[headerType]: channel === 'facet' ? [] : [makeHeaderComponent(model, channel, labels)],
};
}
}
function makeHeaderComponent(model: FacetModel, channel: HeaderChannel, labels: boolean): HeaderComponent {
const sizeType = channel === 'row' ? 'height' : 'width';
return {
labels,
sizeSignal: model.child.component.layoutSize.get(sizeType) ? model.child.getSizeSignalRef(sizeType) : undefined,
axes: [],
};
}
function mergeChildAxis(model: FacetModel, channel: 'x' | 'y') {
const {child} = model;
if (child.component.axes[channel]) {
const {layoutHeaders, resolve} = model.component;
resolve.axis[channel] = parseGuideResolve(resolve, channel);
if (resolve.axis[channel] === 'shared') {
// For shared axis, move the axes to facet's header or footer
const headerChannel = channel === 'x' ? 'column' : 'row';
const layoutHeader = layoutHeaders[headerChannel];
for (const axisComponent of child.component.axes[channel]) {
const headerType = getHeaderType(axisComponent.get('orient'));
layoutHeader[headerType] ??= [makeHeaderComponent(model, headerChannel, false)];
// FIXME: assemble shouldn't be called here, but we do it this way so we only extract the main part of the axes
const mainAxis = assembleAxis(axisComponent, 'main', model.config, {header: true});
if (mainAxis) {
// LayoutHeader no longer keep track of property precedence, thus let's combine.
layoutHeader[headerType][0].axes.push(mainAxis);
}
axisComponent.mainExtracted = true;
}
} else {
// Otherwise do nothing for independent axes
}
}
}