vega-lite
Version:
Vega-Lite is a concise high-level language for interactive visualization.
167 lines (143 loc) • 4.99 kB
text/typescript
import type {SignalRef} from 'vega';
import {hasOwnProperty} from 'vega-util';
import {LATITUDE, LATITUDE2, LONGITUDE, LONGITUDE2, SHAPE} from '../../channel.js';
import {getFieldOrDatumDef} from '../../channeldef.js';
import {DataSourceType} from '../../data.js';
import {replaceExprRef} from '../../expr.js';
import {PROJECTION_PROPERTIES} from '../../projection.js';
import {GEOJSON} from '../../type.js';
import {deepEqual, duplicate, every} from '../../util.js';
import {isUnitModel, Model} from '../model.js';
import {UnitModel} from '../unit.js';
import {ProjectionComponent} from './component.js';
export function parseProjection(model: Model) {
model.component.projection = isUnitModel(model) ? parseUnitProjection(model) : parseNonUnitProjections(model);
}
function parseUnitProjection(model: UnitModel): ProjectionComponent {
if (model.hasProjection) {
const proj = replaceExprRef(model.specifiedProjection);
const fit = !(proj && (proj.scale != null || proj.translate != null));
const size = fit ? [model.getSizeSignalRef('width'), model.getSizeSignalRef('height')] : undefined;
const data = fit ? gatherFitData(model) : undefined;
const projComp = new ProjectionComponent(
model.projectionName(true),
{
...replaceExprRef(model.config.projection),
...proj,
},
size,
data,
);
if (!projComp.get('type')) {
projComp.set('type', 'equalEarth', false);
}
return projComp;
}
return undefined;
}
function gatherFitData(model: UnitModel) {
const data: (SignalRef | string)[] = [];
const {encoding} = model;
for (const posssiblePair of [
[LONGITUDE, LATITUDE],
[LONGITUDE2, LATITUDE2],
]) {
if (getFieldOrDatumDef(encoding[posssiblePair[0]]) || getFieldOrDatumDef(encoding[posssiblePair[1]])) {
data.push({
signal: model.getName(`geojson_${data.length}`),
});
}
}
if (model.channelHasField(SHAPE) && model.typedFieldDef(SHAPE).type === GEOJSON) {
data.push({
signal: model.getName(`geojson_${data.length}`),
});
}
if (data.length === 0) {
// main source is geojson, so we can just use that
data.push(model.requestDataName(DataSourceType.Main));
}
return data;
}
function mergeIfNoConflict(first: ProjectionComponent, second: ProjectionComponent): ProjectionComponent {
const allPropertiesShared = every(PROJECTION_PROPERTIES, (prop) => {
// neither has the property
if (!hasOwnProperty(first.explicit, prop) && !hasOwnProperty(second.explicit, prop)) {
return true;
}
// both have property and an equal value for property
if (
hasOwnProperty(first.explicit, prop) &&
hasOwnProperty(second.explicit, prop) &&
// some properties might be signals or objects and require hashing for comparison
deepEqual(first.get(prop), second.get(prop))
) {
return true;
}
return false;
});
const size = deepEqual(first.size, second.size);
if (size) {
if (allPropertiesShared) {
return first;
} else if (deepEqual(first.explicit, {})) {
return second;
} else if (deepEqual(second.explicit, {})) {
return first;
}
}
// if all properties don't match, let each unit spec have its own projection
return null;
}
function parseNonUnitProjections(model: Model): ProjectionComponent {
if (model.children.length === 0) {
return undefined;
}
let nonUnitProjection: ProjectionComponent;
// parse all children first
for (const child of model.children) {
parseProjection(child);
}
// analyze parsed projections, attempt to merge
const mergable = every(model.children, (child) => {
const projection = child.component.projection;
if (!projection) {
// child layer does not use a projection
return true;
} else if (!nonUnitProjection) {
// cached 'projection' is null, cache this one
nonUnitProjection = projection;
return true;
} else {
const merge = mergeIfNoConflict(nonUnitProjection, projection);
if (merge) {
nonUnitProjection = merge;
}
return !!merge;
}
});
// if cached one and all other children share the same projection,
if (nonUnitProjection && mergable) {
// so we can elevate it to the layer level
const name = model.projectionName(true);
const modelProjection = new ProjectionComponent(
name,
nonUnitProjection.specifiedProjection,
nonUnitProjection.size,
duplicate(nonUnitProjection.data),
);
// rename and assign all others as merged
for (const child of model.children) {
const projection = child.component.projection;
if (projection) {
if (projection.isFit) {
modelProjection.data.push(...child.component.projection.data);
}
child.renameProjection(projection.get('name'), name);
projection.merged = true;
}
}
return modelProjection;
}
return undefined;
}