custom animations for faceted specs #18

This commit is contained in:
Giorgi 2022-02-13 13:30:28 +04:00
Родитель fa775b98d8
Коммит 90a8a20e89
10 изменённых файлов: 60982 добавлений и 3436 удалений

Просмотреть файл

@ -143,8 +143,8 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
/**
* Plays animation
*/
function play() {
frameIndex = 0;
function play(startIndex) {
frameIndex = startIndex || 0;
const tick = () => {
animateFrame(frameIndex);
frameIndex++;
@ -162,7 +162,7 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
clearInterval(intervalId);
frameIndex = 0;
}
}, frameDuration + frameDelay);
}, frameDuration + frameDelay + 500);
}
/**
@ -209,7 +209,7 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
d3.select(controls).style("width", spec.width + transformX + 10 + "px");
// draw vis
return drawChart(spec, (vegaSpec && vegaSpec.custom) ? null : vegaSpec);
return drawChart(spec, vegaSpec && vegaSpec.custom ? null : vegaSpec);
}
/**
@ -247,11 +247,10 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
res();
}
// ensure facet translations match in axisSelector and otherLayers
setTimeout(() => {
adjustAxisAndErrorbars();
}, 100);
// ensure facet translations match in axisSelector and otherLayers
setTimeout(() => {
adjustAxisAndErrorbars();
}, 100);
});
});
});
@ -262,8 +261,14 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
function adjustAxisAndErrorbars() {
const { axisSelector, otherLayers } = getSelectors(id);
const axisCells = d3.select(axisSelector).selectAll(".mark-group.cell>g").nodes();
const otherLayersCells = d3.select(otherLayers).selectAll(".mark-group.cell>g").nodes();
const axisCells = d3
.select(axisSelector)
.selectAll(".mark-group.cell>g")
.nodes();
const otherLayersCells = d3
.select(otherLayers)
.selectAll(".mark-group.cell>g")
.nodes();
if (axisCells.length === otherLayersCells.length) {
for (let i = 0; i < axisCells.length; i++) {
@ -334,8 +339,10 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
* @param {Number} index specification index in vegaLiteSpecs
* @returns a promise of gemini.animate
*/
let animating = false;
async function animateFrame(index) {
if (!frames[index]) return;
if (animating) return;
const { axisSelector, visSelector, otherLayers, descr, slider, controls } =
getSelectors(id);
@ -365,8 +372,9 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
drawSpec(index, source).then(() => {
timeoutId = setTimeout(() => {
d3.select(descr).html(currMeta.description);
animating = true;
anim.play(visSelector).then(() => {
animating = false;
d3.select(slider).property("value", index + 1);
});
@ -476,8 +484,11 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
if (meta.custom_animation) {
let funName = meta.custom_animation;
let p = null;
if (Array.isArray(meta.custom_animation) && meta.custom_animation[0] === "quantile") {
if (
Array.isArray(meta.custom_animation) &&
meta.custom_animation[0] === "quantile"
) {
p = meta.custom_animation[1];
funName = "median";
}
@ -490,24 +501,23 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
// fake facets
if (rawSpecsDontChange[i - 1].facet) {
source = {
...vegaLiteSpecs[i - 1],
meta: {
...vegaLiteSpecs[i - 1].meta,
hasFacet: true,
columnFacet: rawSpecsDontChange[i - 1].facet.column,
rowFacet: rawSpecsDontChange[i - 1].facet.row
rowFacet: rawSpecsDontChange[i - 1].facet.row,
},
data: {
values: vegaLiteSpecs[i - 1].data.values.map(d => {
values: vegaLiteSpecs[i - 1].data.values.map((d) => {
return {
...d,
[CONF.X_FIELD]: d[CONF.X_FIELD+"_num"],
}
})
}
}
[CONF.X_FIELD]: d[CONF.X_FIELD + "_num"],
};
}),
},
};
}
if (vegaLiteSpecs[i].facet) {
@ -519,12 +529,7 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
const fn = CustomAnimations[funName];
if (fn) {
const sequence = await fn(
source,
target,
vegaLiteSpecs[i - 1],
p
);
const sequence = await fn(source, target, vegaLiteSpecs[i - 1], p);
vegaLiteSpecs[i] = {
custom: meta.custom_animation,
sequence,
@ -638,13 +643,7 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
};
for (let i = 1; i < vegaSpecs.length; i++) {
let prev = vegaSpecs[i - 1];
if (prev.custom) {
const seq = prev.sequence;
prev = seq[seq.length - 1];
}
const prev = vegaSpecs[i - 1];
const curr = vegaSpecs[i];
const prevMeta = metas[i - 1];
@ -657,12 +656,12 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
resp = await gemini.recommendForSeq(curr.sequence, {
...options,
stageN: curr.sequence.length - 1,
totalDuration: options.totalDuration,
totalDuration: frameDuration,
});
const _gemSpec = resp[0].specs.map((d) => d.spec);
// make sure to add gemini_id to data change.
// make sure to add gemini_id to data change.
// gemini recommend does not add it by itself.
_gemSpec.forEach((d) => {
if (d.timeline.concat.length) {
@ -685,9 +684,13 @@ function App(id, { specUrls, specs, autoPlay = false, frameDur, frameDel }) {
prevMeta,
currMeta,
});
} else {
resp = await gemini.recommend(prev, curr, options);
} else {
resp = await gemini.recommend(
prev.custom ? prev.sequence[prev.sequence.length - 1] : prev,
curr,
options
);
if (prev.custom) console.log(resp);
const _gemSpec = resp[0] ? resp[0].spec : gemSpec;
const sync = _gemSpec.timeline.concat[0].sync;

Просмотреть файл

@ -148,8 +148,8 @@ const getMedianStep = (source, target, step = 0, p = 0.5) => {
groupId,
median_pos: y_median_pos,
rankDiff: Math.abs(max_rank - median_rank),
rule_start: d3.min(sorted, d => d[CONF.X_FIELD + "_pos"]),
rule_end: d3.max(sorted, d => d[CONF.X_FIELD + "_pos"]),
rule_start: d3.min(sorted, d => d[CONF.X_FIELD + "_pos"]) + 1,
rule_end: d3.max(sorted, d => d[CONF.X_FIELD + "_pos"]) - 1,
});
values.push(...sorted);
@ -168,7 +168,7 @@ const getMedianStep = (source, target, step = 0, p = 0.5) => {
}
d3
.rollups(
.rollup(
source.data.values.slice(),
reduce,
...groupKeys.map((key) => {
@ -178,8 +178,15 @@ const getMedianStep = (source, target, step = 0, p = 0.5) => {
const rules = [];
let ruleField = isLast ? "y_median" : CONF.Y_FIELD;
if (hasFacet) {
ruleField = isLast ? "y_median_pos" : CONF.Y_FIELD;
}
all_groups.forEach((d, i) => {
const n = all_groups.length;
const top_rule = {
transform: isLast ? [{ filter: d.groupFilter }] : [{ filter: d.filter }],
@ -191,7 +198,7 @@ const getMedianStep = (source, target, step = 0, p = 0.5) => {
},
encoding: {
y: {
field: isLast ? "y_median" : hasFacet ? "y_median_pos" : CONF.Y_FIELD,
field: ruleField,
type: "quantitative",
aggregate: "max",
axis: null,
@ -209,7 +216,7 @@ const getMedianStep = (source, target, step = 0, p = 0.5) => {
},
encoding: {
y: {
field: isLast ? "y_median" : hasFacet ? "y_median_pos" : CONF.Y_FIELD,
field: ruleField,
type: "quantitative",
aggregate: "min",
axis: null,
@ -429,37 +436,100 @@ const getMinMaxStep = (source, target, minOrMax = "min") => {
const domain = source.encoding.y.scale.domain;
const minMaxPoints = {};
const all_groups = d3.groups(
source.data.values.slice(),
(d) => d[CONF.X_FIELD])
.map(([key, data]) => {
const groupValue = key;
const filter = `datum['${CONF.X_FIELD}'] === ${groupValue}`;
const aggr = aggrFn(data, d => d[CONF.Y_FIELD]);
const groupKeys = [CONF.X_FIELD];
const hasFacet = source.meta.hasFacet;
const meta = source.meta;
minMaxPoints[groupValue] = data.find(d => d[CONF.Y_FIELD] === aggr);
const values = [];
return {
if (hasFacet) {
if (meta.columnFacet) {
groupKeys.push(meta.columnFacet.field);
}
if (meta.rowFacet) {
groupKeys.push(meta.rowFacet.field);
}
}
const all_groups = [];
d3.rollup(
source.data.values.slice(),
data => {
const groupValue = data[0][CONF.X_FIELD];
let filter = `datum['${CONF.X_FIELD}'] === ${groupValue}`;
let groupId = groupValue;
if (hasFacet) {
filter += ' && ';
groupId += "_";
if (meta.columnFacet) {
filter += `datum['${meta.columnFacet.field}'] === '${data[0][meta.columnFacet.field]}'`;
groupId += data[0][meta.columnFacet.field];
}
if (meta.columnFacet && meta.rowFacet) {
filter += ' && ';
groupId += "_";
}
if (meta.rowFacet) {
filter += `datum['${meta.rowFacet.field}'] === '${data[0][meta.rowFacet.field]}'`
groupId += data[0][meta.rowFacet.field];
}
}
const aggr = aggrFn(data, d => {
return hasFacet ? d.oldY : d[CONF.Y_FIELD];
});
const aggr_pos = hasFacet ? data[0].scaleY(aggr) : aggr;
all_groups.push({
filter,
groupValue,
groupKey: CONF.X_FIELD,
aggr,
}
});
aggr_pos,
groupId,
rule_start: d3.min(data, d => d[CONF.X_FIELD] - 2),
rule_end: d3.max(data, d => d[CONF.X_FIELD] + 2),
});
const g = data.find(d => {
const v = hasFacet ? d.oldY : d[CONF.Y_FIELD];
return v === aggr;
});
values.push(...data.map(d => {
const isAggr = g && g.gemini_id === d.gemini_id;
return {
...d,
isAggr,
aggr_pos
}
}))
},
...groupKeys.map((key) => {
return (d) => d[key];
})
);
const rules = all_groups.map((group, i) => {
const n = all_groups.length;
return {
transform: [{ filter: group.filter }],
name: `rule_${group.groupValue}`,
name: `rule_${group.groupId}`,
mark: {
type: "rule",
x: { expr: `${i + 1} * (width / ${n + 1}) - 5` },
x2: { expr: `${i + 1} * (width / ${n + 1}) + 5` },
x: hasFacet ? group.rule_start : { expr: `${i + 1} * (width / ${n + 1}) - 5` },
x2: hasFacet ? group.rule_end : { expr: `${i + 1} * (width / ${n + 1}) + 5` },
},
encoding: {
y: {
field: CONF.Y_FIELD,
field: hasFacet ? "aggr_pos" : CONF.Y_FIELD,
type: "quantitative",
aggregate: minOrMax,
axis: null,
@ -469,16 +539,6 @@ const getMinMaxStep = (source, target, minOrMax = "min") => {
}
});
const values = source.data.values.map(d => {
const g = minMaxPoints[d[CONF.X_FIELD]];
const isAggr = g && g.gemini_id === d.gemini_id;
return {
...d,
isAggr,
}
})
return {
$schema: CONF.SCHEME,
width,
@ -518,7 +578,7 @@ const CustomAnimations = {
},
min: (rawSource, target, source) => {
const step_1 = getMinMaxStep(rawSource, target, "min");
const groups = step_1.meta.all_groups;
// const groups = step_1.meta.all_groups;
const step_2 = {
...step_1,
@ -542,20 +602,20 @@ const CustomAnimations = {
// this is for test, it should be passed from R or Python side..
// but can also keep this. it will work!!!
target.data.values.forEach((d) => {
const group = groups.find((x) => x.groupValue === d[x.groupKey]);
d[CONF.Y_FIELD] = group.aggr;
});
// target.data.values.forEach((d) => {
// const group = groups.find((x) => x.groupValue === d[x.groupKey]);
// d[CONF.Y_FIELD] = group.aggr;
// });
const domain = d3.extent(groups, (d) => d.aggr);
target.encoding.y.scale.domain = domain;
// const domain = d3.extent(groups, (d) => d.aggr);
// target.encoding.y.scale.domain = domain;
/// end of test ////
return [rawSource, step_1, step_2, target];
},
max: (rawSource, target, source) => {
const step_1 = getMinMaxStep(rawSource, target, "max");
const groups = step_1.meta.all_groups;
// const groups = step_1.meta.all_groups;
const step_2 = {
...step_1,
@ -692,7 +752,7 @@ const CustomAnimations = {
}
};
return [rawSource, intermediate, step_1, step_2, step_3, step_4];
return [rawSource, intermediate, step_1, step_2, step_3, step_4, target];
},
median: (rawSource, target, calculatedSource, p) => {
const initial = getMedianStep(rawSource, target, 0, p ?? 0.5);
@ -700,8 +760,6 @@ const CustomAnimations = {
// const groups = initial.meta.all_groups;
const last_with_points = getMedianStep(rawSource, target, null, p ?? 0.5)
console.log(last_with_points);
// this is for test, it should be passed from R or Python side..
// target.data.values.forEach((d) => {
// const group = groups.find((x) => x.groupValue === d[x.groupKey]);
@ -726,7 +784,23 @@ const CustomAnimations = {
}
},
resolve: { axis: { y: "independent" } },
}
};
const source = {
$schema: rawSource.$schema,
data: rawSource.data,
width: rawSource.width,
height: rawSource.height,
meta: rawSource.meta,
layer: [
{
name: 'main',
mark: rawSource.mark,
encoding: rawSource.encoding,
}
],
resolve: { axis: { y: "independent" } },
};
return [rawSource, initial, last_with_points, last, target];
},

Просмотреть файл

@ -214,10 +214,6 @@ function getHackedSpec({ view, spec, width = 600, height = 600 }) {
const xStart = colMap.get(col) || 0;
const yStart = rowMap.get(row) || 0;
if (spec.meta.custom_animation) {
console.log(yStart);
}
const xField = spec.meta.parse === "jitter" ? "x" : CONF.X_FIELD;
const yField = spec.meta.parse === "jitter" ? "y" : CONF.Y_FIELD;

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -176,14 +176,14 @@
<script src="../js/custom-animations.js"></script>
<script src="../js/app.js"></script>
<script>
const animationType = "quantile";
const animationType = "median";
const base = "../../../sandbox/custom_animations";
let app1 = null;
d3.json(
`${base}/custom-animations-${animationType}-R.json`
`${base}/custom-animations-${animationType}-facet.json`
).then(specs => {
app1 = App("app", { specs: specs.slice(0), frameDur: 3000 });
app1 = App("app", { specs: specs, frameDur: 3000, frameDel: 1000 });
});
</script>
</body>

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -67,7 +67,7 @@
},
"spec": {
"height": 300,
"width": 150,
"width": 100,
"mark": {
"type": "point",
"filled": true,
@ -146,8 +146,8 @@
}
},
"spec": {
"height": 150,
"width": 150,
"height": 100,
"width": 100,
"mark": {
"type": "point",
"filled": true,
@ -293,8 +293,8 @@
}
},
"spec": {
"height": 150,
"width": 150,
"height": 100,
"width": 100,
"mark": {
"type": "point",
"filled": true,
@ -3452,8 +3452,8 @@
}
},
"spec": {
"height": 150,
"width": 150,
"height": 100,
"width": 100,
"mark": {
"type": "point",
"filled": true,
@ -3476,7 +3476,7 @@
"y": {
"field": "datamations_y",
"type": "quantitative",
"title": "mean(bill_length_mm + something)",
"title": "mean bill_length",
"scale": {
"domain": [32.1, 59.6]
}
@ -6630,8 +6630,8 @@
}
},
"spec": {
"height": 150,
"width": 150,
"height": 100,
"width": 100,
"mark": {
"type": "point",
"filled": true,
@ -6654,7 +6654,7 @@
"y": {
"field": "datamations_y",
"type": "quantitative",
"title": "mean(bill_length_mm + something)",
"title": "mean bill_length",
"scale": {
"domain": [32.1, 59.6]
}
@ -10837,8 +10837,8 @@
}
},
"spec": {
"height": 150,
"width": 150,
"height": 100,
"width": 100,
"layer": [
{
"mark": "errorbar",
@ -10859,7 +10859,7 @@
"y": {
"field": "datamations_y_raw",
"type": "quantitative",
"title": "mean(bill_length_mm + something)",
"title": "mean bill_length",
"scale": {
"domain": [32.1, 59.6]
}
@ -10925,7 +10925,7 @@
"y": {
"field": "datamations_y",
"type": "quantitative",
"title": "mean(bill_length_mm + something)",
"title": "mean bill_length",
"scale": {
"domain": [32.1, 59.6]
}
@ -15120,8 +15120,8 @@
}
},
"spec": {
"height": 150,
"width": 150,
"height": 100,
"width": 100,
"layer": [
{
"mark": "errorbar",
@ -15142,7 +15142,7 @@
"y": {
"field": "datamations_y_raw",
"type": "quantitative",
"title": "mean(bill_length_mm + something)",
"title": "mean bill_length",
"scale": {
"domain": [36.481220238402, 51.3624372072661]
}
@ -15208,7 +15208,7 @@
"y": {
"field": "datamations_y",
"type": "quantitative",
"title": "mean(bill_length_mm + something)",
"title": "mean bill_length",
"scale": {
"domain": [36.481220238402, 51.3624372072661]
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1041,7 +1041,10 @@
"axes": false,
"description": "Plot 10% quantile of each group",
"splitField": "Degree",
"custom_animation": "quantile(0.1)"
"custom_animation": [
"quantile",
0.1
]
},
"data": {
"values": [