Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: frame animations with time encoding and timer param #8921

Open
wants to merge 54 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
a8cc8f5
mvp timer signals
jonathanzong May 24, 2023
0666970
mvp datasets
jonathanzong May 24, 2023
e74bf6c
mvp rewrite data source in marks
jonathanzong May 24, 2023
f69d7bb
marginally better
jonathanzong May 24, 2023
7d65261
add time to encoding channel
jonathanzong May 24, 2023
aa932b8
whatever
jonathanzong May 24, 2023
b2fe9b2
transforms working
jonathanzong May 25, 2023
a27984b
cleanup
jonathanzong May 25, 2023
d711d2a
generalized the signals a bit
jonathanzong May 25, 2023
40ec2e5
add comment about easing
jonathanzong May 25, 2023
c410770
make existing test suites pass
jonathanzong May 25, 2023
6c49a53
cleanup
jonathanzong May 25, 2023
6f24c49
time rescale optional
jonathanzong May 25, 2023
c31101f
some point tests
jonathanzong May 25, 2023
370950b
more unit tests
jonathanzong May 25, 2023
350666c
runtime tests
jonathanzong May 25, 2023
0b8674e
lookup dataset name
jonathanzong May 25, 2023
128eec1
schema
jonathanzong May 25, 2023
835dcad
make tests pass
jonathanzong May 25, 2023
f6fb295
comment unused signals
jonathanzong May 25, 2023
f23114b
check for source data filter before copying
jonathanzong May 26, 2023
880188e
export signal constants in point
joshpoll Sep 19, 2023
b9527ba
move correctDataNames into unit model
joshpoll Sep 19, 2023
d50d73f
expose default values for time ranges
joshpoll Sep 19, 2023
2c2fd3b
time name no longer hardcoded
joshpoll Sep 20, 2023
6022dcf
remove vlSelectionIdTest check
jonathanzong Sep 25, 2023
8842e38
update defined for toggle and clear
jonathanzong Sep 25, 2023
a1b4fc6
cleanup
jonathanzong Sep 25, 2023
486583d
add example specs
jonathanzong Sep 25, 2023
8998aea
update example specs
jonathanzong Sep 25, 2023
b753d2a
update timer selection unit test
jonathanzong Sep 25, 2023
3a9220d
build schema
jonathanzong Sep 25, 2023
1596746
move clock signals to toplevelsignals
jonathanzong Sep 25, 2023
a35bd16
update test to reflect clock signal moved to top level
jonathanzong Sep 25, 2023
22f597c
wip
jonathanzong Jan 22, 2024
5ef84b1
with sleep
jonathanzong Jan 22, 2024
5239fc6
test for loop
jonathanzong Jan 22, 2024
b51895d
test for forward progress
jonathanzong Jan 22, 2024
7852ff7
correct 1965
jonathanzong Jan 22, 2024
c7f2b6a
get data and signals in the same await
jonathanzong Jan 22, 2024
8d59883
renamed variable
jonathanzong Jan 22, 2024
18c985b
add is_playing signal
jonathanzong Jan 23, 2024
75b7bfc
use pause signal to test frame rendering
jonathanzong Jan 23, 2024
67f821f
get time test to run
jonathanzong Jan 23, 2024
90648ae
rename mousemove to pointermove
jonathanzong Jan 23, 2024
4f1924c
reduce sleep on forward progress test
jonathanzong Jan 23, 2024
6096de3
coverage tests passing
jonathanzong Jan 26, 2024
5da3453
coverage actually passing
jonathanzong Jan 26, 2024
8bc4ffa
schema
jonathanzong Jan 26, 2024
e25a344
actually commit the file
jonathanzong Jan 26, 2024
de153b1
put linear time scales behind a todo
jonathanzong Jan 29, 2024
13e8c83
update tests
jonathanzong Jan 29, 2024
4508cd2
update clock extent for runtime test
jonathanzong Jan 29, 2024
7ce0c10
Empty-Commit
mattijn Apr 6, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
266 changes: 266 additions & 0 deletions build/vega-lite-schema.json

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions examples/specs/animated_gapminder.vl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"url": "data/gapminder.json"
},
"mark": "point",
"params": [
{
"name": "avl",
"select": {
"type": "point",
"fields": [
"year"
],
"on": "timer"
}
}
],
"transform": [
{
"filter": {
"param": "avl"
}
}
],
"encoding": {
"color": {
"field": "country"
},
"x": {
"field": "fertility",
"type": "quantitative"
},
"y": {
"field": "life_expect",
"type": "quantitative"
},
"time": {
"field": "year",
"type": "ordinal"
}
}
}
41 changes: 41 additions & 0 deletions examples/specs/animated_hop.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"$schema": "https://vega.github.io/schema/vega-lite/v5.json",
"data": {
"url": "data/seattle-weather.csv"
},
"mark": "tick",
"config": {
"tick": {
"thickness": 3
}
},
"params": [
{
"name": "date",
"select": {
"type": "point",
"fields": [
"date"
],
"on": "timer"
}
}
],
"transform": [
{
"filter": {
"param": "date"
}
}
],
"encoding": {
"y": {
"field": "precipitation",
"type": "quantitative"
},
"time": {
"field": "date",
"type": "ordinal"
}
}
}
19 changes: 19 additions & 0 deletions src/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ 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;

Expand Down Expand Up @@ -135,6 +138,9 @@ const UNIT_CHANNEL_INDEX: Flag<Channel> = {
fill: 1,
stroke: 1,

// time
time: 1,

// other non-position with scale
opacity: 1,
fillOpacity: 1,
Expand Down Expand Up @@ -424,6 +430,16 @@ export function isXorYOffset(channel: Channel): channel is OffsetScaleChannel {
return channel in OFFSET_SCALE_CHANNEL_INDEX;
}

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
Expand Down Expand Up @@ -464,6 +480,7 @@ export function supportLegend(channel: NonPositionScaleChannel) {
case FILLOPACITY:
case STROKEOPACITY:
case ANGLE:
case TIME:
return false;
}
}
Expand Down Expand Up @@ -551,6 +568,7 @@ function getSupportedMark(channel: ExtendedChannel): SupportedMark {
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:
Expand Down Expand Up @@ -625,6 +643,7 @@ export function rangeType(channel: ExtendedChannel): RangeType {
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:
Expand Down
12 changes: 10 additions & 2 deletions src/channeldef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import {
TEXT,
THETA,
THETA2,
TIME,
TOOLTIP,
URL,
X,
Expand Down Expand Up @@ -517,6 +518,12 @@ export interface PositionMixins {

export type PolarDef<F extends Field> = PositionFieldDefBase<F> | PositionDatumDefBase<F> | PositionValueDef;

export type TimeDef<F extends Field> = TimeFieldDef<F>;
export interface TimeMixins {
rescale?: boolean;
}
export type TimeFieldDef<F extends Field> = ScaleFieldDef<F, StandardType> & TimeMixins;

export function getBandPosition({
fieldDef,
fieldDef2,
Expand Down Expand Up @@ -1303,6 +1310,7 @@ export function channelCompatibility(
case RADIUS2:
case X2:
case Y2:
case TIME:
if (type === 'nominal' && !fieldDef['sort']) {
return {
compatible: false,
Expand Down Expand Up @@ -1338,13 +1346,13 @@ export function channelCompatibility(
*/
export function isFieldOrDatumDefForTimeFormat(fieldOrDatumDef: FieldDef<string> | DatumDef): boolean {
const {formatType} = getFormatMixins(fieldOrDatumDef);
return formatType === 'time' || (!formatType && isTimeFieldDef(fieldOrDatumDef));
return formatType === 'time' || (!formatType && isTemporalFieldDef(fieldOrDatumDef));
}

/**
* Check if field def has type `temporal`. If you want to also cover field defs that use a time format, use `isTimeFormatFieldDef`.
*/
export function isTimeFieldDef(def: FieldDef<any> | DatumDef): boolean {
export function isTemporalFieldDef(def: FieldDef<any> | DatumDef): boolean {
return def && (def['type'] === 'temporal' || (isFieldDef(def) && !!def.timeUnit));
}

Expand Down
7 changes: 2 additions & 5 deletions src/compile/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,8 @@ function assembleTopLevelModel(
// Config with Vega-Lite only config removed.
const vgConfig = model.config ? stripAndRedirectConfig(model.config) : undefined;

const data = [].concat(
model.assembleSelectionData([]),
// only assemble data in the root
assembleRootData(model.component.data, datasets)
);
const rootData = assembleRootData(model.component.data, datasets);
const data = model.assembleSelectionData(rootData);

const projections = model.assembleProjections();
const title = model.assembleTitle();
Expand Down
19 changes: 0 additions & 19 deletions src/compile/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,25 +598,6 @@ export abstract class Model {
return undefined;
}

/**
* Corrects the data references in marks after assemble.
*/
public correctDataNames = (mark: VgMarkGroup) => {
// TODO: make this correct

// for normal data references
if (mark.from?.data) {
mark.from.data = this.lookupDataSource(mark.from.data);
}

// for access to facet data
if (mark.from?.facet?.data) {
mark.from.facet.data = this.lookupDataSource(mark.from.facet.data);
}

return mark;
};

/**
* Traverse a model's hierarchy to get the scale component for a particular channel.
*/
Expand Down
10 changes: 9 additions & 1 deletion src/compile/scale/range.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
X,
XOFFSET,
Y,
YOFFSET
YOFFSET,
TIME
} from '../../channel';
import {
getBandPosition,
Expand Down Expand Up @@ -318,6 +319,13 @@
];
}

case TIME: {
if (scaleType === 'band') {
return {step: 1000 / config.scale.framesPerSecond};
}
return [0, config.scale.animationDuration * 1000]; // e.g. linear

Check warning on line 326 in src/compile/scale/range.ts

View check run for this annotation

Codecov / codecov/patch

src/compile/scale/range.ts#L326

Added line #L326 was not covered by tests
}

case STROKEWIDTH:
// TODO: support custom rangeMin, rangeMax
return [config.scale.minStrokeWidth, config.scale.maxStrokeWidth];
Expand Down
12 changes: 12 additions & 0 deletions src/compile/scale/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
getSizeChannel,
isColorChannel,
isScaleChannel,
isTime,
isXorY,
isXorYOffset,
rangeType,
Expand Down Expand Up @@ -76,6 +77,10 @@
return 'ordinal';
}

if (isTime(channel)) {
return 'band';
}

if (isXorY(channel) || isXorYOffset(channel)) {
if (util.contains(['rect', 'bar', 'image', 'rule'], mark.type)) {
// The rect/bar mark should fit into a band.
Expand Down Expand Up @@ -111,7 +116,11 @@
return 'ordinal';
} else if (isFieldDef(fieldDef) && fieldDef.timeUnit && normalizeTimeUnit(fieldDef.timeUnit).utc) {
return 'utc';
} else if (isTime(channel)) {
// return 'linear';
return 'band'; // TODO(jzong): when interpolation is implemented, this should be 'linear'
}

return 'time';

case 'quantitative':
Expand All @@ -125,6 +134,9 @@
log.warn(log.message.discreteChannelCannotEncode(channel, 'quantitative'));
// TODO: consider using quantize (equivalent to binning) once we have it
return 'ordinal';
} else if (isTime(channel)) {
// return 'linear';
return 'band'; // TODO(jzong): when interpolation is implemented, this should be 'linear'

Check warning on line 139 in src/compile/scale/type.ts

View check run for this annotation

Codecov / codecov/patch

src/compile/scale/type.ts#L139

Added line #L139 was not covered by tests
}

return 'linear';
Expand Down
37 changes: 32 additions & 5 deletions src/compile/selection/assemble.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {Signal, SignalRef} from 'vega';
import {parseSelector} from 'vega-event-selector';
import {identity, isArray, stringValue} from 'vega-util';
import {MODIFY, STORE, unitName, VL_SELECTION_RESOLVE, TUPLE, selectionCompilers} from '.';
import {MODIFY, STORE, unitName, VL_SELECTION_RESOLVE, TUPLE, selectionCompilers, isTimerSelection} from '.';
import {dateTimeToExpr, isDateTime, dateTimeToTimestamp} from '../../datetime';
import {hasContinuousDomain} from '../../scale';
import {SelectionInit, SelectionInitInterval, ParameterExtent, SELECTION_ID} from '../../selection';
Expand All @@ -14,6 +14,8 @@ import {ScaleComponent} from '../scale/component';
import {UnitModel} from '../unit';
import {parseSelectionExtent} from './parse';
import {SelectionProjection} from './project';
import {CURR} from './point';
import {DataSourceType} from '../../data';

export function assembleProjection(proj: SelectionProjection) {
const {signals, hasLegend, index, ...rest} = proj;
Expand Down Expand Up @@ -120,7 +122,8 @@ export function assembleTopLevelSignals(model: UnitModel, signals: Signal[]) {
}

export function assembleUnitSelectionData(model: UnitModel, data: readonly VgData[]): VgData[] {
const dataCopy = [...data];
const selectionData = [];
const animationData = [];
const unit = unitName(model, {escape: false});

for (const selCmpt of vals(model.component.selection ?? {})) {
Expand All @@ -138,13 +141,37 @@ export function assembleUnitSelectionData(model: UnitModel, data: readonly VgDat
: selCmpt.init.map(i => ({unit, fields, values: assembleInit(i, false)}));
}

const contains = dataCopy.filter(d => d.name === selCmpt.name + STORE);
const contains = [...selectionData, ...data].filter(d => d.name === selCmpt.name + STORE);
if (!contains.length) {
dataCopy.push(store);
selectionData.push(store);
}

if (isTimerSelection(selCmpt) && data.length) {
const sourceName = model.lookupDataSource(model.getDataName(DataSourceType.Main));
const sourceData = data.find(d => d.name === sourceName);

// find the filter transform for the current selection
const sourceDataFilter = sourceData.transform.find(
t => t.type === 'filter' && t.expr.includes('vlSelectionTest')
);

if (sourceDataFilter) {
// remove it from the original dataset
sourceData.transform = sourceData.transform.filter(t => t !== sourceDataFilter);

// create dataset to hold current animation frame
const currentFrame: VgData = {
name: sourceData.name + CURR,
source: sourceData.name,
transform: [sourceDataFilter] // add the selection filter to the animation dataset
};

animationData.push(currentFrame);
}
}
}

return dataCopy;
return selectionData.concat(data, animationData);
}

export function assembleUnitSelectionMarks(model: UnitModel, marks: any[]): any[] {
Expand Down
4 changes: 2 additions & 2 deletions src/compile/selection/clear.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {Update} from 'vega';
import {parseSelector} from 'vega-event-selector';
import {isString} from 'vega-util';
import {TUPLE} from '.';
import {TUPLE, isTimerSelection} from '.';
import {varName} from '../../util';
import inputBindings from './inputs';
import toggle, {TOGGLE} from './toggle';
import {SelectionCompiler} from '.';

const clear: SelectionCompiler = {
defined: selCmpt => {
return selCmpt.clear !== undefined && selCmpt.clear !== false;
return selCmpt.clear !== undefined && selCmpt.clear !== false && !isTimerSelection(selCmpt);
},

parse: (model, selCmpt) => {
Expand Down
4 changes: 4 additions & 0 deletions src/compile/selection/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,7 @@ export function disableDirectManipulation(selCmpt: SelectionComponent, selDef: S
if (isString(selDef.select) || !selDef.select.clear) delete selCmpt.clear;
if (isString(selDef.select) || !selDef.select.toggle) delete selCmpt.toggle;
}

export function isTimerSelection<T extends SelectionType>(selCmpt: SelectionComponent<T>) {
return selCmpt.events?.find(e => 'type' in e && e.type === 'timer');
}