@jbrowse/cli 5.0.0-beta.8 → 5.0.0-beta.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,16 +8,25 @@ nothing. That is what this command is mainly for.
8
8
 
9
9
  Two levels are reported:
10
10
 
11
- error JBrowse accepts it and silently does the wrong thing — an unknown
12
- slot, a key a defaultSession view or display does not declare, a
13
- track naming an assembly the config never defines, a defaultSession
14
- naming a trackId that does not exist, a duplicate trackId. Exits 1.
11
+ error JBrowse silently does the wrong thing, or refuses to load it — an
12
+ unknown slot, a key a defaultSession view or display does not
13
+ declare, a track naming an assembly the config never defines, a
14
+ defaultSession naming a trackId that does not exist, a duplicate
15
+ trackId. Exits 1.
15
16
 
16
17
  warning JBrowse will complain by itself on load, or handles it — a type name
17
18
  the core plugins do not register (expected if one of your plugins
18
19
  registers it), or a legacy key a migration rewrites, such as a view
19
20
  nesting its settings under "init". Never fails the run.
20
21
 
22
+ A LinearMarkDisplay loads whenever its keys and value types are right and says
23
+ what it cannot draw in a corner notice rather than refusing the track.
24
+ This command runs the display's own rule list over every "marks" list in the
25
+ file: an error where a mark draws nothing, never draws or a step cannot run,
26
+ and a warning where a slot waits unread or the marks draw in an arrangement
27
+ the author may not have meant. With --json each finding carries the stable
28
+ "rule" id of the rule that made it.
29
+
21
30
  Types registered by plugins are not known to this command, so they come through
22
31
  as warnings rather than errors.
23
32
 
@@ -0,0 +1,29 @@
1
+ export function isRecord(value) {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }
4
+ function liftSlot(member, slot) {
5
+ if (slot.subSlots) {
6
+ const { subSlots, shorthand } = slot;
7
+ const lift = (item) => liftToSnapshot(typeof item === 'string' && shorthand !== undefined
8
+ ? { [shorthand]: item }
9
+ : item, subSlots);
10
+ return Array.isArray(member) ? member.map(lift) : lift(member);
11
+ }
12
+ const list = slot.liftsString && typeof member === 'string' ? [member] : member;
13
+ return slot.liftsNumbers && Array.isArray(list) ? list.map(String) : list;
14
+ }
15
+ /**
16
+ * A config object as its schema holds it once loaded, defaults still left off:
17
+ * a bare string lifted into a sub-schema's `shorthand` slot or into a list of
18
+ * one, and a number in a list of strings carried as a string. Which slot lifts
19
+ * what is the manifest's record of the live schemas, so a rule list written
20
+ * against a config snapshot reads a file the way it reads the app's.
21
+ */
22
+ export function liftToSnapshot(value, slots) {
23
+ return isRecord(value)
24
+ ? Object.fromEntries(Object.entries(value).map(([key, member]) => {
25
+ const slot = slots.find(s => s.name === key);
26
+ return [key, slot ? liftSlot(member, slot) : member];
27
+ }))
28
+ : value;
29
+ }
@@ -0,0 +1,27 @@
1
+ // Generated by scripts/generateMarkRules.ts from plugins/marks/src/LinearMarkDisplay/autoBin.ts: do not edit, `pnpm autogen` rewrites it.
2
+ /** The `step` value a `bin` step writes to follow the view's zoom. */
3
+ export const AUTO_BIN = 'auto';
4
+ /**
5
+ * How wide an automatic bin aims to be on screen. Four pixels is a bar the eye
6
+ * reads as a bar rather than a hairline, and it keeps a screenful of a
7
+ * 1000px-wide view at ~250 instances.
8
+ */
9
+ export const AUTO_BIN_TARGET_PX = 4;
10
+ const LADDER = [1, 2, 5];
11
+ /**
12
+ * The bin width an `auto` step resolves to at this zoom: the target width in bp
13
+ * ({@link AUTO_BIN_TARGET_PX} pixels' worth), snapped up to the next 1/2/5
14
+ * rung — 1, 2, 5, 10, 20, 50, 100 bp and so on, the ladder an axis picks its
15
+ * ticks from. Snapping stops most zoom steps from refetching: every bp/px
16
+ * inside a rung resolves to one width, so a zoom step within a rung refetches nothing
17
+ * and one across it refetches at the width the new zoom asks for.
18
+ */
19
+ export function autoBinStep(bpPerPx) {
20
+ const target = Math.max(1, bpPerPx * AUTO_BIN_TARGET_PX);
21
+ const decade = 10 ** Math.floor(Math.log10(target));
22
+ return (LADDER.find(m => m * decade >= target) ?? 10) * decade;
23
+ }
24
+ /** A `bin` step's declared width in bp, with `auto` resolved at this zoom. */
25
+ export function binStepWidth(step, bpPerPx) {
26
+ return step === AUTO_BIN ? autoBinStep(bpPerPx) : Number(step);
27
+ }
@@ -0,0 +1,81 @@
1
+ // Generated by scripts/generateMarkRules.ts from packages/display-kit/src/colorScale.ts: do not edit, `pnpm autogen` rewrites it.
2
+ /**
3
+ * The scales a colour object paints through, in a module that imports
4
+ * nothing: `jbrowse validate` carries a copy of it, so the validator and the
5
+ * displays read one statement.
6
+ */
7
+ /** How a colour object maps its `field`; `none` paints `value`. */
8
+ export const COLOR_SCALES = [
9
+ 'none',
10
+ 'categorical',
11
+ 'linear',
12
+ 'log',
13
+ 'threshold',
14
+ ];
15
+ /** What a field paints through under `scales` while `scale` is unset. */
16
+ export function fieldScaleOf(scales, field) {
17
+ return ((Object.hasOwn(scales, field) ? scales[field] : scales['*']) ??
18
+ 'categorical');
19
+ }
20
+ /**
21
+ * The scale a colour object paints through: `none` while no `field` is
22
+ * named, else its own `scale`, or `fieldScale` where that is unset.
23
+ */
24
+ export function paintedScale({ scale, field }, fieldScale) {
25
+ return field ? (scale ?? fieldScale) : 'none';
26
+ }
27
+ function pinned(entry) {
28
+ return entry !== '' && Number.isFinite(Number(entry));
29
+ }
30
+ /**
31
+ * What a colour object's slots say together that no single slot can refuse,
32
+ * since the config editor writes one slot at a time (ADR-133). A display shows
33
+ * these as a notice and draws what it can. `fieldScale` is the scale a named
34
+ * field paints through when `scale` is unset.
35
+ */
36
+ export function colorProblems(color, fieldScale) {
37
+ const { domain = [], range = [], domainMin, domainMax } = color;
38
+ const scale = paintedScale({ scale: color.scale, field: color.field ?? '' }, fieldScale);
39
+ const problems = [];
40
+ if (scale === 'threshold') {
41
+ if (domain.some((cut, i) => !pinned(cut) || Number(cut) <= Number(domain[i - 1]))) {
42
+ problems.push({
43
+ rule: 'threshold-cuts',
44
+ slot: 'domain',
45
+ message: 'threshold cuts are distinct numbers, read in ascending order, with the range running from the lowest interval; a repeated cut leaves an interval no value falls in',
46
+ });
47
+ }
48
+ if (domain.length > 0 &&
49
+ range.length > 0 &&
50
+ range.length !== domain.length + 1) {
51
+ problems.push({
52
+ rule: 'threshold-range',
53
+ slot: 'range',
54
+ message: `${domain.length} threshold ${domain.length === 1 ? 'cut makes' : 'cuts make'} ${domain.length + 1} intervals, one colour each, and range lists ${range.length}: a missing colour comes from the default palette and an extra one is never read`,
55
+ });
56
+ }
57
+ }
58
+ if (scale === 'linear' || scale === 'log') {
59
+ if (domain.length > 0) {
60
+ problems.push({
61
+ rule: 'ramp-domain',
62
+ slot: 'domain',
63
+ message: "a linear or log scale reads no domain, which is a categorical scale's order and a threshold scale's cuts; a ramp's ends are domainMin and domainMax where the colour has them",
64
+ });
65
+ }
66
+ if (domainMin !== undefined &&
67
+ domainMax !== undefined &&
68
+ domainMin > domainMax) {
69
+ problems.push({
70
+ rule: 'ramp-ends',
71
+ slot: 'domainMax',
72
+ message: 'domainMax is below domainMin: the ramp spans the two either way, and reverse is what turns it round',
73
+ });
74
+ }
75
+ }
76
+ return problems;
77
+ }
78
+ /** `colorProblems` as the lines a display's corner notice lists, under the setting's own name. */
79
+ export function colorNotices(color, fieldScale, name = 'color') {
80
+ return colorProblems(color, fieldScale).map(({ slot, message }) => `${name}.${slot}: ${message}`);
81
+ }
@@ -0,0 +1,23 @@
1
+ /** The `jexl:` prefix marks a config value as a deferred callback expression. */
2
+ export const JEXL_PREFIX = 'jexl:';
3
+ export function isJexl(str) {
4
+ return typeof str === 'string' && str.startsWith(JEXL_PREFIX);
5
+ }
6
+ /** Add the `jexl:` prefix unless the string already carries it. */
7
+ export function ensureJexlPrefix(code) {
8
+ return isJexl(code) ? code : `${JEXL_PREFIX}${code}`;
9
+ }
10
+ /**
11
+ * Compile a `jexl:...` string to an Expression. The jexl instance caches
12
+ * compilation, so this only strips the prefix.
13
+ *
14
+ * @param str - string of code like `jexl:...`
15
+ * @param jexl - the instance whose registered functions the expression may call
16
+ * (`pluginManager.jexl` on eval paths)
17
+ */
18
+ export function stringToJexlExpression(str, jexl) {
19
+ if (!isJexl(str)) {
20
+ throw new Error('string does not appear to be in jexl format');
21
+ }
22
+ return jexl.compile(str.slice(JEXL_PREFIX.length));
23
+ }
@@ -0,0 +1,253 @@
1
+ // Generated by scripts/generateMarkRules.ts from plugins/marks/src/LinearMarkDisplay/markProblems.ts: do not edit, `pnpm autogen` rewrites it.
2
+ import { AUTO_BIN } from './autoBin.js';
3
+ import { colorProblems, isJexl, paintedScale } from './markRuleFacts.js';
4
+ import { DEFAULT_AGGREGATE_OP, DEFAULT_BIN_AS, DEFAULT_COVERAGE_AS, DEFAULT_FORMULA_AS, DEFAULT_MARK_SHAPE, DEFAULT_PILEUP_AS, DEFAULT_PILEUP_FIELDS, } from './markVocabulary.js';
5
+ import { SHAPE_SPECS, rampResolvesPerRegion, readsValue } from './shapeSpecs.js';
6
+ /**
7
+ * The level of every rule of the list, by the stable id a report and a test
8
+ * refer to it by. A single mark's combination that a JSON schema can state is
9
+ * the mark schema's `requires` and is not restated here.
10
+ */
11
+ export const MARK_RULES = {
12
+ 'unread-channel': 'warning',
13
+ 'span-density-source': 'warning',
14
+ 'threshold-cuts': 'warning',
15
+ 'threshold-range': 'warning',
16
+ 'ramp-domain': 'warning',
17
+ 'ramp-ends': 'warning',
18
+ 'unpinned-span-ramp': 'warning',
19
+ 'empty-zoom-range': 'error',
20
+ 'step-expression': 'error',
21
+ 'bin-width': 'error',
22
+ 'step-pair': 'warning',
23
+ 'op-field': 'error',
24
+ 'step-field-expression': 'error',
25
+ 'unwritten-y': 'error',
26
+ 'value-beside-rows': 'warning',
27
+ 'two-packings': 'warning',
28
+ 'second-density-mark': 'warning',
29
+ };
30
+ function found(rule, slot, message) {
31
+ return { rule, level: MARK_RULES[rule], slot, message };
32
+ }
33
+ function shapeOf(mark) {
34
+ return mark.shape ?? DEFAULT_MARK_SHAPE;
35
+ }
36
+ function stepsOf(mark) {
37
+ return mark.transform ?? [];
38
+ }
39
+ function rampColor(mark) {
40
+ const color = mark.encoding?.color ?? {};
41
+ const painted = paintedScale({ scale: color.scale, field: color.field ?? '' }, 'categorical');
42
+ return painted === 'linear' || painted === 'log' ? color : undefined;
43
+ }
44
+ function drawTogether(a, b) {
45
+ const upper = (m) => m.maxBpPerPx || Infinity;
46
+ const lower = (m) => m.minBpPerPx ?? 0;
47
+ return lower(a) < upper(b) && lower(b) < upper(a);
48
+ }
49
+ // The field names a step reads, by the slot each is written in.
50
+ function fieldRefs(step) {
51
+ const list = (slot, refs = []) => refs.map((ref, k) => [`${slot}.${k}`, ref]);
52
+ switch (step.type) {
53
+ case 'bin':
54
+ case 'flatten':
55
+ return [['field', step.field]];
56
+ case 'pileup':
57
+ return list('fields', step.fields);
58
+ case 'aggregate':
59
+ return [
60
+ ...list('groupby', step.groupby),
61
+ ...(step.ops ?? []).map((o, k) => [
62
+ `ops.${k}.field`,
63
+ o.field,
64
+ ]),
65
+ ];
66
+ default:
67
+ return [];
68
+ }
69
+ }
70
+ // A slot naming two fields, and the default it reads as when it names another
71
+ // number of them.
72
+ function pairSlot(step) {
73
+ return step.type === 'bin'
74
+ ? { slot: 'as', names: step.as, fallback: DEFAULT_BIN_AS }
75
+ : step.type === 'pileup'
76
+ ? { slot: 'fields', names: step.fields, fallback: DEFAULT_PILEUP_FIELDS }
77
+ : undefined;
78
+ }
79
+ function packs(mark) {
80
+ return stepsOf(mark).some(s => s.type === 'pileup');
81
+ }
82
+ function banded(mark) {
83
+ return packs(mark) || !!mark.encoding?.row;
84
+ }
85
+ // The fields the last step that makes its features from nothing leaves behind,
86
+ // with what the steps after it add; undefined where no step does, and every
87
+ // field of the file is still there.
88
+ function madeFields(mark) {
89
+ const steps = stepsOf(mark);
90
+ const made = steps.findLastIndex(s => s.type === 'aggregate' || s.type === 'coverage');
91
+ const last = steps[made];
92
+ if (!last) {
93
+ return undefined;
94
+ }
95
+ const fields = new Set(['refName', 'start', 'end']);
96
+ if (last.type === 'coverage') {
97
+ fields.add(last.as ?? DEFAULT_COVERAGE_AS);
98
+ }
99
+ else if (last.type === 'aggregate') {
100
+ const bin = steps
101
+ .slice(0, made)
102
+ .findLast((s) => s.type === 'bin');
103
+ const edges = bin?.as ?? DEFAULT_BIN_AS;
104
+ for (const field of last.groupby?.length
105
+ ? last.groupby
106
+ : edges.length === 2
107
+ ? edges
108
+ : []) {
109
+ fields.add(field);
110
+ }
111
+ for (const { op = DEFAULT_AGGREGATE_OP, field, as } of last.ops ?? []) {
112
+ fields.add(as || (op === 'count' || !field ? op : `${op}_${field}`));
113
+ }
114
+ }
115
+ for (const step of steps.slice(made + 1)) {
116
+ if (step.type === 'formula') {
117
+ fields.add(step.as ?? DEFAULT_FORMULA_AS);
118
+ }
119
+ else if (step.type === 'pileup') {
120
+ fields.add(step.as ?? DEFAULT_PILEUP_AS);
121
+ }
122
+ else if (step.type === 'flatten' && step.index) {
123
+ fields.add(step.index);
124
+ }
125
+ }
126
+ return fields;
127
+ }
128
+ // What a list of steps cannot run as written, whether a mark's or the display's
129
+ function stepProblems(steps) {
130
+ const problems = [];
131
+ for (const [i, step] of steps.entries()) {
132
+ if (!step) {
133
+ continue;
134
+ }
135
+ const { type } = step;
136
+ if ((type === 'filter' || type === 'formula') && !isJexl(step.expr ?? '')) {
137
+ problems.push(found('step-expression', `transform.${i}.expr`, `a ${type} reads a jexl: expression`));
138
+ }
139
+ if (type === 'bin' &&
140
+ step.step !== undefined &&
141
+ step.step !== AUTO_BIN &&
142
+ !(Number(step.step) > 0)) {
143
+ problems.push(found('bin-width', `transform.${i}.step`, 'a bin is a positive width in bp'));
144
+ }
145
+ const pair = pairSlot(step);
146
+ if (pair?.names && pair.names.length !== 2) {
147
+ problems.push(found('step-pair', `transform.${i}.${pair.slot}`, `a ${type} reads two field names from ${pair.slot} and this names ${pair.names.length}, so it reads ${pair.fallback.join(' and ')}`));
148
+ }
149
+ if (type === 'aggregate') {
150
+ for (const [k, { op = DEFAULT_AGGREGATE_OP, field }] of (step.ops ?? []).entries()) {
151
+ if (op !== 'count' && !field) {
152
+ problems.push(found('op-field', `transform.${i}.ops.${k}.field`, `${op} reads a field and names none`));
153
+ }
154
+ }
155
+ }
156
+ for (const [slot, ref] of fieldRefs(step)) {
157
+ if (ref && isJexl(ref)) {
158
+ problems.push(found('step-field-expression', `transform.${i}.${slot}`, `the ${type} step reads a field name or a dotted path; a formula step in front computes one`));
159
+ }
160
+ }
161
+ }
162
+ return problems;
163
+ }
164
+ function ownProblems(mark) {
165
+ const shape = shapeOf(mark);
166
+ const problems = [];
167
+ const y = mark.encoding?.y;
168
+ const channels = new Set(['x', 'x2', ...SHAPE_SPECS[shape].channels]);
169
+ for (const channel of Object.keys(mark.encoding ?? {})) {
170
+ if (!channels.has(channel)) {
171
+ problems.push(found('unread-channel', `encoding.${channel}`, `a ${shape} does not read ${channel}`));
172
+ }
173
+ }
174
+ if (!readsValue(shape) && mark.source === 'density') {
175
+ problems.push(found('span-density-source', 'source', `a ${shape} does not draw the density sidecar`));
176
+ }
177
+ const color = mark.encoding?.color ?? {};
178
+ for (const { rule, slot, message } of colorProblems(color, 'categorical')) {
179
+ problems.push(found(rule, `encoding.color.${slot}`, message));
180
+ }
181
+ const ramp = rampColor(mark);
182
+ if (ramp) {
183
+ const { domainMin, domainMax } = ramp;
184
+ if (rampResolvesPerRegion(shape) &&
185
+ (domainMin === undefined || domainMax === undefined)) {
186
+ problems.push(found('unpinned-span-ramp', `encoding.color.${domainMin === undefined ? 'domainMin' : 'domainMax'}`, "a span's ramp resolves an open end against each region's own extremes, so its colours agree across regions only with domainMin and domainMax both pinned"));
187
+ }
188
+ }
189
+ const { minBpPerPx = 0, maxBpPerPx = 0 } = mark;
190
+ if (minBpPerPx > 0 && maxBpPerPx > 0 && minBpPerPx >= maxBpPerPx) {
191
+ problems.push(found('empty-zoom-range', 'minBpPerPx', `never draws: minBpPerPx ${minBpPerPx} is not below maxBpPerPx ${maxBpPerPx}`));
192
+ }
193
+ problems.push(...stepProblems(stepsOf(mark)));
194
+ const fields = madeFields(mark);
195
+ if (fields && y && !isJexl(y) && !fields.has(y)) {
196
+ problems.push(found('unwritten-y', 'encoding.y', `reads "${y}", which its steps do not write; they leave ${[...fields].join(', ')}`));
197
+ }
198
+ return problems;
199
+ }
200
+ /**
201
+ * The problems of a `marks` list as a config snapshot holds it: shorthands
202
+ * lifted, defaults left off. `facet` is the display's facet as written, naming
203
+ * the field its sections stack by; under one, a rowless mark stands on each
204
+ * section's first row by design. `transform` is the display's own steps,
205
+ * checked as a mark's are and reported with no mark. An `undefined` mark or
206
+ * step is one the caller could not read, such as one a file's schema refuses:
207
+ * it keeps its index, so the others are reported where they are, and is
208
+ * checked for nothing. A file's own spelling goes through the schema's lift
209
+ * first, which `jbrowse validate` does from the generated manifest.
210
+ */
211
+ export function markProblems(marks, facet, transform = []) {
212
+ const faceted = typeof facet?.field === 'string' && facet.field !== '';
213
+ const problems = [
214
+ ...stepProblems(transform),
215
+ ...marks.flatMap((mark, i) => mark ? ownProblems(mark).map(p => ({ mark: i, ...p })) : []),
216
+ ];
217
+ for (const [i, mark] of marks.entries()) {
218
+ for (const [j, other] of marks.entries()) {
219
+ if (i === j || !mark || !other || !drawTogether(mark, other)) {
220
+ continue;
221
+ }
222
+ if (!faceted &&
223
+ shapeOf(mark) !== 'span' &&
224
+ !banded(mark) &&
225
+ banded(other)) {
226
+ problems.push({
227
+ mark: i,
228
+ ...found('value-beside-rows', 'encoding.row', `stands in the first of the rows mark ${j} bands the plot into, its axis repeated per row`),
229
+ });
230
+ }
231
+ if (i > j && packs(mark) && packs(other)) {
232
+ problems.push({
233
+ mark: i,
234
+ ...found('two-packings', 'transform', `packs rows of its own, as mark ${j} does, and the two share row numbers; one pileup in the display's transform packs them together, ${faceted ? 'though across every section at once, leaving each section the rows the others fill' : "with each mark's encoding.row naming the field the pileup writes"}`),
235
+ });
236
+ }
237
+ if (i > j && mark.source === 'density' && other.source === 'density') {
238
+ problems.push({
239
+ mark: i,
240
+ ...found('second-density-mark', 'source', `mark ${j} already stands in for the density sidecar here`),
241
+ });
242
+ }
243
+ }
244
+ }
245
+ return problems;
246
+ }
247
+ /**
248
+ * A problem as one line of a notice: `mark 0 encoding.y: …`, or
249
+ * `transform.0.step: …` for the display's own step.
250
+ */
251
+ export function problemText({ mark, slot, message }) {
252
+ return `${mark === undefined ? '' : `mark ${mark} `}${slot}: ${message}`;
253
+ }
@@ -0,0 +1,3 @@
1
+ // Generated by scripts/generateMarkRules.ts from plugins/marks/src/LinearMarkDisplay/markRuleFacts.ts: do not edit, `pnpm autogen` rewrites it.
2
+ export { isJexl } from './jexlStrings.js';
3
+ export { colorProblems, paintedScale } from './colorScale.js';
@@ -0,0 +1,20 @@
1
+ // Generated by scripts/generateMarkRules.ts from plugins/marks/src/LinearMarkDisplay/markVocabulary.ts: do not edit, `pnpm autogen` rewrites it.
2
+ /**
3
+ * The names a `marks` list is written in and the ones a slot left off reads
4
+ * as, in a module that imports nothing: the config schema declares its
5
+ * enumerations and defaults from here, the rule list reads the same constants,
6
+ * and `jbrowse validate` carries a copy of both.
7
+ */
8
+ export const MARK_SHAPES = ['bar', 'point', 'span'];
9
+ export const DEFAULT_MARK_SHAPE = 'bar';
10
+ export const MARK_SOURCES = ['features', 'density'];
11
+ export const DEFAULT_MARK_SOURCE = 'features';
12
+ export const DEFAULT_FORMULA_AS = 'value';
13
+ export const DEFAULT_BIN_FIELD = 'start';
14
+ export const DEFAULT_BIN_AS = ['start', 'end'];
15
+ export const DEFAULT_COVERAGE_AS = 'coverage';
16
+ export const DEFAULT_FLATTEN_FIELD = 'subfeatures';
17
+ export const DEFAULT_PILEUP_AS = 'row';
18
+ export const DEFAULT_PILEUP_FIELDS = ['start', 'end'];
19
+ export const AGGREGATE_OPS = ['count', 'sum', 'mean', 'min', 'max'];
20
+ export const DEFAULT_AGGREGATE_OP = 'count';
@@ -0,0 +1,41 @@
1
+ /**
2
+ * What each shape reads: the one statement the lane request, the rule list and
3
+ * the model's value tests derive theirs from.
4
+ *
5
+ * Its own module, reaching only the vocabulary beside it: the rule list reads
6
+ * it and `jbrowse validate` carries a copy, and markList.ts takes render-core's
7
+ * marks barrel — shader sources included — which the examples sites' eager
8
+ * budget then pays for. `layerRequests` in model.ts is where each lane is
9
+ * checked to be one the encoder fills.
10
+ */
11
+ export const SHAPE_SPECS = {
12
+ bar: { channels: ['y', 'row', 'color'], ramp: 'display' },
13
+ point: { channels: ['y', 'row', 'color', 'glyph'], ramp: 'display' },
14
+ span: { channels: ['row', 'color'], ramp: 'worker' },
15
+ };
16
+ function specOf(shape) {
17
+ return SHAPE_SPECS[shape];
18
+ }
19
+ /** Whether a shape stands at a `y` value, so a mark of it naming none draws nothing. */
20
+ export function readsValue(shape) {
21
+ return specOf(shape).channels.includes('y');
22
+ }
23
+ /** Whether a shape's ramp colour resolves per region, in the worker. */
24
+ export function rampResolvesPerRegion(shape) {
25
+ return specOf(shape).ramp === 'worker';
26
+ }
27
+ /**
28
+ * The lanes a mark asks the worker to fill: its shape's channels, a ramp's raw
29
+ * values beside the colour where the display resolves the ramp, and the hit
30
+ * index. The encoder fills whichever of `color` and `colorValue` the colour
31
+ * declaration calls for.
32
+ */
33
+ export function markLanes(shape) {
34
+ const spec = specOf(shape);
35
+ return [
36
+ ...spec.channels.flatMap((channel) => channel === 'color' && spec.ramp === 'display'
37
+ ? [channel, 'colorValue']
38
+ : [channel]),
39
+ 'index',
40
+ ];
41
+ }