-
Notifications
You must be signed in to change notification settings - Fork 626
/
Copy pathlegends.ts
137 lines (121 loc) · 4.98 KB
/
legends.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import {isObject, MergedStream, NewSignal, Stream} from 'vega';
import {parseSelector} from 'vega-event-selector';
import {array, isString} from 'vega-util';
import {disableDirectManipulation, TUPLE} from '.';
import {NonPositionScaleChannel} from '../../channel';
import * as log from '../../log';
import {isLegendBinding, isLegendStreamBinding, SELECTION_ID} from '../../selection';
import {duplicate, vals, varName} from '../../util';
import {LegendComponent} from '../legend/component';
import {UnitModel} from '../unit';
import {TUPLE_FIELDS} from './project';
import {TOGGLE} from './toggle';
import {SelectionCompiler} from '.';
const legendBindings: SelectionCompiler<'point'> = {
defined: selCmpt => {
const spec = selCmpt.resolve === 'global' && selCmpt.bind && isLegendBinding(selCmpt.bind);
const projLen = selCmpt.project.items.length === 1 && selCmpt.project.items[0].field !== SELECTION_ID;
if (spec && !projLen) {
log.warn(log.message.LEGEND_BINDINGS_MUST_HAVE_PROJECTION);
}
return spec && projLen;
},
parse: (model, selCmpt, selDef) => {
// Allow legend items to be toggleable by default even though direct manipulation is disabled.
const selDef_ = duplicate(selDef);
selDef_.select = isString(selDef_.select)
? {type: selDef_.select, toggle: selCmpt.toggle}
: {...selDef_.select, toggle: selCmpt.toggle};
disableDirectManipulation(selCmpt, selDef_);
if (isObject(selDef.select) && (selDef.select.on || selDef.select.clear)) {
const legendFilter = 'event.item && indexof(event.item.mark.role, "legend") < 0';
for (const evt of selCmpt.events) {
evt.filter = array(evt.filter ?? []);
if (!evt.filter.includes(legendFilter)) {
evt.filter.push(legendFilter);
}
}
}
const evt = isLegendStreamBinding(selCmpt.bind) ? selCmpt.bind.legend : 'click';
const stream: Stream[] = isString(evt) ? parseSelector(evt, 'view') : array(evt);
selCmpt.bind = {legend: {merge: stream}};
},
topLevelSignals: (model, selCmpt, signals) => {
const selName = selCmpt.name;
const stream = isLegendStreamBinding(selCmpt.bind) && (selCmpt.bind.legend as MergedStream);
const markName = (name: string) => (s: Stream) => {
const ds = duplicate(s);
ds.markname = name;
return ds;
};
for (const proj of selCmpt.project.items) {
if (!proj.hasLegend) continue;
const prefix = `${varName(proj.field)}_legend`;
const sgName = `${selName}_${prefix}`;
const hasSignal = signals.filter(s => s.name === sgName);
if (hasSignal.length === 0) {
const events = stream.merge
.map(markName(`${prefix}_symbols`))
.concat(stream.merge.map(markName(`${prefix}_labels`)))
.concat(stream.merge.map(markName(`${prefix}_entries`)));
signals.unshift({
name: sgName,
...(!selCmpt.init ? {value: null} : {}),
on: [
// Legend entries do not store values, so we need to walk the scenegraph to the symbol datum.
{
events,
update: 'isDefined(datum.value) ? datum.value : item().items[0].items[0].datum.value',
force: true
},
{events: stream.merge, update: `!event.item || !datum ? null : ${sgName}`, force: true}
]
});
}
}
return signals;
},
signals: (model, selCmpt, signals) => {
const name = selCmpt.name;
const proj = selCmpt.project;
const tuple: NewSignal = signals.find(s => s.name === name + TUPLE);
const fields = name + TUPLE_FIELDS;
const values = proj.items.filter(p => p.hasLegend).map(p => varName(`${name}_${varName(p.field)}_legend`));
const valid = values.map(v => `${v} !== null`).join(' && ');
const update = `${valid} ? {fields: ${fields}, values: [${values.join(', ')}]} : null`;
if (selCmpt.events && values.length > 0) {
tuple.on.push({
events: values.map(signal => ({signal})),
update
});
} else if (values.length > 0) {
tuple.update = update;
delete tuple.value;
delete tuple.on;
}
const toggle = signals.find(s => s.name === name + TOGGLE);
const events = isLegendStreamBinding(selCmpt.bind) && selCmpt.bind.legend;
if (toggle) {
if (!selCmpt.events) toggle.on[0].events = events;
else toggle.on.push({...toggle.on[0], events});
}
return signals;
}
};
export default legendBindings;
export function parseInteractiveLegend(
model: UnitModel,
channel: NonPositionScaleChannel,
legendCmpt: LegendComponent
) {
const field = model.fieldDef(channel)?.field;
for (const selCmpt of vals(model.component.selection ?? {})) {
const proj = selCmpt.project.hasField[field] ?? selCmpt.project.hasChannel[channel];
if (proj && legendBindings.defined(selCmpt)) {
const legendSelections = legendCmpt.get('selections') ?? [];
legendSelections.push(selCmpt.name);
legendCmpt.set('selections', legendSelections, false);
proj.hasLegend = true;
}
}
}