@jbrowse/cli 4.3.0 → 5.0.0-beta.10
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.
- package/README.md +451 -72
- package/dist/base.js +0 -3
- package/dist/bin.js +2 -1
- package/dist/cliFetch.js +3 -0
- package/dist/commands/add-assembly/index.js +46 -19
- package/dist/commands/add-assembly/utils.js +204 -290
- package/dist/commands/add-connection.js +73 -79
- package/dist/commands/add-track-json.js +24 -12
- package/dist/commands/add-track-utils/adapter-utils.js +189 -288
- package/dist/commands/add-track-utils/file-operations.js +27 -11
- package/dist/commands/add-track-utils/multiwig.js +47 -0
- package/dist/commands/add-track-utils/track-config.js +66 -25
- package/dist/commands/add-track-utils/validators.js +36 -20
- package/dist/commands/add-track.js +200 -62
- package/dist/commands/admin-server/index.js +39 -50
- package/dist/commands/admin-server/utils.js +238 -126
- package/dist/commands/create.js +18 -34
- package/dist/commands/make-density/density-generator.js +180 -0
- package/dist/commands/make-density/index.js +139 -0
- package/dist/commands/make-pif/index.js +133 -11
- package/dist/commands/make-pif/pif-generator.js +243 -43
- package/dist/commands/process-utils.js +13 -2
- package/dist/commands/remove-track.js +20 -13
- package/dist/commands/set-default-session.js +35 -31
- package/dist/commands/shared/config-operations.js +3 -10
- package/dist/commands/shared/density.js +12 -0
- package/dist/commands/shared/sort-utils.js +67 -10
- package/dist/commands/shared/validators.js +38 -3
- package/dist/commands/sort-bed.js +2 -33
- package/dist/commands/sort-gff.js +2 -33
- package/dist/commands/text-index/adapter-utils.js +19 -26
- package/dist/commands/text-index/aggregate.js +40 -34
- package/dist/commands/text-index/command.js +37 -7
- package/dist/commands/text-index/config-utils.js +62 -85
- package/dist/commands/text-index/file-list.js +32 -18
- package/dist/commands/text-index/index.js +4 -4
- package/dist/commands/text-index/indexing-utils.js +44 -96
- package/dist/commands/text-index/per-track.js +44 -31
- package/dist/commands/text-index/validators.js +1 -1
- package/dist/commands/upgrade.js +20 -32
- package/dist/commands/validate/configManifest.generated.js +7613 -0
- package/dist/commands/validate/configSchema.generated.js +20400 -0
- package/dist/commands/validate/displayDefaultKeys.js +28 -0
- package/dist/commands/validate/index.js +113 -0
- package/dist/commands/validate/liftConfig.js +34 -0
- package/dist/commands/validate/markRules/aggregateFieldName.js +9 -0
- package/dist/commands/validate/markRules/autoBin.js +27 -0
- package/dist/commands/validate/markRules/colorScale.js +244 -0
- package/dist/commands/validate/markRules/cssColorsLevel4.js +162 -0
- package/dist/commands/validate/markRules/jexlStrings.js +23 -0
- package/dist/commands/validate/markRules/markProblems.js +532 -0
- package/dist/commands/validate/markRules/markRuleFacts.js +6 -0
- package/dist/commands/validate/markRules/markSpecs.js +96 -0
- package/dist/commands/validate/markRules/markVocabulary.js +57 -0
- package/dist/commands/validate/markRules/shapeNameList.js +7 -0
- package/dist/commands/validate/markRules/stepChannels.js +55 -0
- package/dist/commands/validate/schemaValidate.js +425 -0
- package/dist/commands/validate/suggest.js +35 -0
- package/dist/commands/validate/types.js +6 -0
- package/dist/commands/validate/validateConfig.js +502 -0
- package/dist/index.js +104 -50
- package/dist/types/common.js +3 -27
- package/dist/utils.js +309 -128
- package/dist/version.js +1 -1
- package/package.json +8 -14
- package/bin/run +0 -4
- package/bundle/index.js +0 -3686
- package/dist/commands/make-pif/cigar-utils.js +0 -24
- package/dist/commands/make-pif/file-utils.js +0 -30
- package/dist/commands/track-utils.js +0 -74
- package/dist/util.js +0 -35
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { configManifest } from './configManifest.generated.js';
|
|
2
|
+
// `displayDefaults: {color: 'green'}` is a track-level shorthand
|
|
3
|
+
// (expandTrackConfigShorthand.ts) routing each key to whichever of the track's
|
|
4
|
+
// display types declares it, so the keys a track accepts are the union of those
|
|
5
|
+
// displays' slots rather than the track's own.
|
|
6
|
+
export function displayDefaultKeys(trackEntry, manifest) {
|
|
7
|
+
return (trackEntry.displayTypes ?? []).flatMap(name => manifest.displays[name]?.slots.map(slot => slot.name) ?? []);
|
|
8
|
+
}
|
|
9
|
+
// The same set for a track type named rather than resolved — add-track knows
|
|
10
|
+
// the type it is writing, not a manifest entry. The display types come back
|
|
11
|
+
// alongside, because most tracks offer exactly one and naming it is the whole
|
|
12
|
+
// difference between a message someone can act on and one they cannot: an
|
|
13
|
+
// AlignmentsTrack has only LinearAlignmentsDisplay, so "no AlignmentsTrack
|
|
14
|
+
// display declares color" sends the reader looking for the other displays.
|
|
15
|
+
//
|
|
16
|
+
// Both empty for a type the manifest does not carry (a plugin's), which no
|
|
17
|
+
// check here can speak to.
|
|
18
|
+
export function displayDefaultsForTrackType(trackType, manifest = configManifest) {
|
|
19
|
+
const entry = lookup(manifest.tracks, trackType);
|
|
20
|
+
return {
|
|
21
|
+
displayTypes: entry?.displayTypes ?? [],
|
|
22
|
+
keys: entry ? displayDefaultKeys(entry, manifest) : [],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function lookup(group, typeName) {
|
|
26
|
+
return (group[typeName] ??
|
|
27
|
+
Object.values(group).find(entry => entry.aliases?.includes(typeName)));
|
|
28
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import { printHelp, readConfigFile } from '../../utils.js';
|
|
3
|
+
import { validateConfig } from './validateConfig.js';
|
|
4
|
+
const description = 'Check a JBrowse configuration for errors, including the ones JBrowse itself accepts silently';
|
|
5
|
+
const notes = `A config key JBrowse does not recognize is ignored rather than reported, so a
|
|
6
|
+
misspelled slot leaves the track loading normally with the setting doing
|
|
7
|
+
nothing. That is what this command is mainly for.
|
|
8
|
+
|
|
9
|
+
Two levels are reported:
|
|
10
|
+
|
|
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.
|
|
16
|
+
|
|
17
|
+
warning JBrowse will complain by itself on load, or handles it — a type name
|
|
18
|
+
the core plugins do not register (expected if one of your plugins
|
|
19
|
+
registers it), or a legacy key a migration rewrites, such as a view
|
|
20
|
+
nesting its settings under "init". Never fails the run.
|
|
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
|
+
|
|
30
|
+
Types registered by plugins are not known to this command, so they come through
|
|
31
|
+
as warnings rather than errors.
|
|
32
|
+
|
|
33
|
+
The same checks are published as a JSON Schema at
|
|
34
|
+
https://jbrowse.org/jb2/schema/v5/config.json: a "$schema" line naming it at
|
|
35
|
+
the top of a config.json gives an editor completion and validation for every
|
|
36
|
+
slot. This command runs that schema first, then the cross-references a schema
|
|
37
|
+
cannot express — a trackId a session names that no track defines, an assembly
|
|
38
|
+
a track names that none defines.`;
|
|
39
|
+
const examples = [
|
|
40
|
+
'# check the config.json in the current directory',
|
|
41
|
+
'$ jbrowse validate',
|
|
42
|
+
'',
|
|
43
|
+
'# check a specific config or saved session',
|
|
44
|
+
'$ jbrowse validate /path/to/config.json',
|
|
45
|
+
'$ jbrowse validate mysession.jbrowse',
|
|
46
|
+
'',
|
|
47
|
+
'# machine-readable output',
|
|
48
|
+
'$ jbrowse validate config.json --json',
|
|
49
|
+
];
|
|
50
|
+
const options = {
|
|
51
|
+
json: {
|
|
52
|
+
type: 'boolean',
|
|
53
|
+
description: 'Output the findings as JSON instead of text',
|
|
54
|
+
},
|
|
55
|
+
quiet: {
|
|
56
|
+
type: 'boolean',
|
|
57
|
+
short: 'q',
|
|
58
|
+
description: 'Only print errors, suppressing warnings and notes',
|
|
59
|
+
},
|
|
60
|
+
help: { type: 'boolean', short: 'h', description: 'Show help' },
|
|
61
|
+
};
|
|
62
|
+
function format({ level, where, message }) {
|
|
63
|
+
return where ? `${level}: ${where}: ${message}` : `${level}: ${message}`;
|
|
64
|
+
}
|
|
65
|
+
export async function run(args) {
|
|
66
|
+
const { values: flags, positionals } = parseArgs({
|
|
67
|
+
options,
|
|
68
|
+
args,
|
|
69
|
+
allowPositionals: true,
|
|
70
|
+
});
|
|
71
|
+
if (flags.help) {
|
|
72
|
+
printHelp({
|
|
73
|
+
description,
|
|
74
|
+
examples,
|
|
75
|
+
notes,
|
|
76
|
+
usage: 'jbrowse validate [config.json] [options]',
|
|
77
|
+
options,
|
|
78
|
+
});
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const file = positionals[0] ?? 'config.json';
|
|
82
|
+
// readConfigFile, not readJsonFile: running from the wrong directory is the
|
|
83
|
+
// most common way this fails, and it says which path it looked at.
|
|
84
|
+
const config = await readConfigFile(file);
|
|
85
|
+
const result = validateConfig(config);
|
|
86
|
+
if (flags.json) {
|
|
87
|
+
console.log(JSON.stringify({ file, ok: result.errorCount === 0, ...result }, null, 2));
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const shown = flags.quiet
|
|
91
|
+
? result.problems.filter(p => p.level === 'error')
|
|
92
|
+
: result.problems;
|
|
93
|
+
if (!flags.quiet) {
|
|
94
|
+
for (const note of result.notes) {
|
|
95
|
+
console.log(`note: ${note}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
for (const problem of shown) {
|
|
99
|
+
console.log(format(problem));
|
|
100
|
+
}
|
|
101
|
+
if (result.problems.length === 0) {
|
|
102
|
+
console.log(`${file} looks good`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
console.log(`\n${result.errorCount} error(s), ${result.warningCount} warning(s) in ${file}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (result.errorCount > 0) {
|
|
109
|
+
// Non-zero so this can gate a deploy. Thrown rather than process.exit so it
|
|
110
|
+
// goes through the same error path as every other command.
|
|
111
|
+
throw new Error(`${result.errorCount} error(s) found in ${file}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
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) => {
|
|
8
|
+
const target = typeof item === 'string'
|
|
9
|
+
? shorthand.string
|
|
10
|
+
: typeof item === 'number'
|
|
11
|
+
? shorthand.number
|
|
12
|
+
: undefined;
|
|
13
|
+
return liftToSnapshot(target === undefined ? item : { [target]: item }, subSlots);
|
|
14
|
+
};
|
|
15
|
+
return Array.isArray(member) ? member.map(lift) : lift(member);
|
|
16
|
+
}
|
|
17
|
+
const list = slot.liftsString && typeof member === 'string' ? [member] : member;
|
|
18
|
+
return slot.liftsNumbers && Array.isArray(list) ? list.map(String) : list;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A config object as its schema holds it once loaded, defaults still left off:
|
|
22
|
+
* a bare string or number lifted into the sub-schema's `shorthand` slot taking it or a string into a list of
|
|
23
|
+
* one, and a number in a list of strings carried as a string. Which slot lifts
|
|
24
|
+
* what is the manifest's record of the live schemas, so a rule list written
|
|
25
|
+
* against a config snapshot reads a file the way it reads the app's.
|
|
26
|
+
*/
|
|
27
|
+
export function liftToSnapshot(value, slots) {
|
|
28
|
+
return isRecord(value)
|
|
29
|
+
? Object.fromEntries(Object.entries(value).map(([key, member]) => {
|
|
30
|
+
const slot = slots.find(s => s.name === key);
|
|
31
|
+
return [key, slot ? liftSlot(member, slot) : member];
|
|
32
|
+
}))
|
|
33
|
+
: value;
|
|
34
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Generated by scripts/generateMarkRules.ts from packages/core/src/util/aggregateFieldName.ts: do not edit, `pnpm autogen` rewrites it.
|
|
2
|
+
/**
|
|
3
|
+
* The field an aggregate op writes: `as` where named, else `count` for a
|
|
4
|
+
* count, else `<op>_<field>`. Imports nothing, so the validator's copy of the
|
|
5
|
+
* mark rule list carries it unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export function aggregateFieldName({ op, field, as, }) {
|
|
8
|
+
return as || (op === 'count' || !field ? op : `${op}_${field}`);
|
|
9
|
+
}
|
|
@@ -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,244 @@
|
|
|
1
|
+
// Generated by scripts/generateMarkRules.ts from packages/core/src/util/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
|
+
/**
|
|
16
|
+
* The scale that maps no field: each feature paints the colour it carries, and
|
|
17
|
+
* the key names the colours `domain` lists. Only a display whose features
|
|
18
|
+
* carry colours of their own declares it.
|
|
19
|
+
*/
|
|
20
|
+
export const IDENTITY_SCALE = 'identity';
|
|
21
|
+
const CATEGORICAL_PRESET = { scale: 'categorical' };
|
|
22
|
+
/**
|
|
23
|
+
* The fields whose values have names, an order and colours of their own on
|
|
24
|
+
* every display, beneath each display's own presets. Red forward and blue
|
|
25
|
+
* reverse is what the synteny ribbons paint too. The SV classes are
|
|
26
|
+
* `svClassOf`'s, deletion red and duplication blue as dbVar and gnomAD-SV
|
|
27
|
+
* paint them, insertion the pileup's purple, and the rest kept apart under
|
|
28
|
+
* deuteranopia and protanopia. A record with no class files under `''`, so
|
|
29
|
+
* each display keeps its own no-value colour for it.
|
|
30
|
+
*/
|
|
31
|
+
export const UNIVERSAL_FIELD_PRESETS = {
|
|
32
|
+
strand: {
|
|
33
|
+
scale: 'categorical',
|
|
34
|
+
missing: '0',
|
|
35
|
+
domain: ['1', '-1', '0'],
|
|
36
|
+
range: ['tomato', 'cornflowerblue', 'goldenrod'],
|
|
37
|
+
labels: ['Forward strand', 'Reverse strand', 'No strand'],
|
|
38
|
+
},
|
|
39
|
+
svType: {
|
|
40
|
+
scale: 'categorical',
|
|
41
|
+
domain: ['DEL', 'DUP', 'INS', 'INV', 'CNV', 'TR', 'BND', 'CPX', 'OTHER'],
|
|
42
|
+
range: [
|
|
43
|
+
'#e41a1c',
|
|
44
|
+
'#377eb8',
|
|
45
|
+
'#800080',
|
|
46
|
+
'#ff7f00',
|
|
47
|
+
'#7e6148',
|
|
48
|
+
'#e7298a',
|
|
49
|
+
'#17becf',
|
|
50
|
+
'#66a61e',
|
|
51
|
+
'#000000',
|
|
52
|
+
],
|
|
53
|
+
labels: [
|
|
54
|
+
'Deletion',
|
|
55
|
+
'Duplication',
|
|
56
|
+
'Insertion',
|
|
57
|
+
'Inversion',
|
|
58
|
+
'Copy-number variable',
|
|
59
|
+
'Tandem repeat',
|
|
60
|
+
'Breakend / translocation',
|
|
61
|
+
'Complex',
|
|
62
|
+
'Other / mixed',
|
|
63
|
+
],
|
|
64
|
+
title: 'SV type',
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
/** The universal preset of `field`, if it has one. */
|
|
68
|
+
export function universalPresetOf(field) {
|
|
69
|
+
return Object.hasOwn(UNIVERSAL_FIELD_PRESETS, field)
|
|
70
|
+
? UNIVERSAL_FIELD_PRESETS[field]
|
|
71
|
+
: undefined;
|
|
72
|
+
}
|
|
73
|
+
/** Every field categorical while `scale` is unset, with nothing preset. */
|
|
74
|
+
export const CATEGORICAL_FIELD_PRESETS = {
|
|
75
|
+
'*': CATEGORICAL_PRESET,
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* What a feature's field paints through while `scale` is unset: `score` a
|
|
79
|
+
* ramp, any other field categories.
|
|
80
|
+
*/
|
|
81
|
+
export const FEATURE_FIELD_PRESETS = {
|
|
82
|
+
score: { scale: 'linear' },
|
|
83
|
+
'*': CATEGORICAL_PRESET,
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* A field's preset under `presets`: its own, else a universal one, else
|
|
87
|
+
* `*`'s, else categorical.
|
|
88
|
+
*/
|
|
89
|
+
export function presetOf(presets, field) {
|
|
90
|
+
return ((Object.hasOwn(presets, field)
|
|
91
|
+
? presets[field]
|
|
92
|
+
: (universalPresetOf(field) ?? presets['*'])) ?? CATEGORICAL_PRESET);
|
|
93
|
+
}
|
|
94
|
+
/** What a field paints through under `presets` while `scale` is unset. */
|
|
95
|
+
export function fieldScaleOf(presets, field) {
|
|
96
|
+
return presetOf(presets, field).scale;
|
|
97
|
+
}
|
|
98
|
+
function unwritten(value) {
|
|
99
|
+
return value === undefined || (Array.isArray(value) && value.length === 0);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* A colour object with its field's preset under it: each member the preset
|
|
103
|
+
* names and the config leaves unwritten, an empty list counting as unwritten.
|
|
104
|
+
* Only while the colour paints through the preset's own scale, so the cuts of
|
|
105
|
+
* a threshold preset never reach a field read through a ramp. A categorical
|
|
106
|
+
* preset's `range` and `labels` name its `domain`'s values in order, so they
|
|
107
|
+
* fill only while `domain` does too: a written order keeps each value's own
|
|
108
|
+
* colour and name.
|
|
109
|
+
*/
|
|
110
|
+
export function withPreset(color, presets) {
|
|
111
|
+
const field = color.field ?? '';
|
|
112
|
+
const preset = presetOf(presets, field);
|
|
113
|
+
if (!field || (color.scale ?? preset.scale) !== preset.scale) {
|
|
114
|
+
return color;
|
|
115
|
+
}
|
|
116
|
+
const written = new Map(Object.entries(color));
|
|
117
|
+
const ordered = preset.scale !== 'categorical' ||
|
|
118
|
+
!preset.domain ||
|
|
119
|
+
unwritten(written.get('domain'));
|
|
120
|
+
const filled = Object.entries(preset).filter(([key]) => key !== 'scale' &&
|
|
121
|
+
key !== 'missing' &&
|
|
122
|
+
(ordered || (key !== 'range' && key !== 'labels')) &&
|
|
123
|
+
unwritten(written.get(key)));
|
|
124
|
+
return filled.length > 0 ? { ...color, ...Object.fromEntries(filled) } : color;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The scale a colour object paints through: `none` while no `field` is
|
|
128
|
+
* named, else its own `scale`, or `fieldScale` where that is unset.
|
|
129
|
+
*/
|
|
130
|
+
export function paintedScale({ scale, field }, fieldScale) {
|
|
131
|
+
return field ? (scale ?? fieldScale) : 'none';
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* What a quantitative scale's ends say together that neither can refuse
|
|
135
|
+
* alone: a `domainMax` below its `domainMin`, which every such scale draws in
|
|
136
|
+
* order either way, and a `domainQuantile` outside 0.5 to 1, where the quantile
|
|
137
|
+
* an open end follows is clamped. `reverse` is whether the scale turns round by
|
|
138
|
+
* a member of its own, which is then the spelling to point at.
|
|
139
|
+
*/
|
|
140
|
+
export function scaleEndProblems({ domainMin, domainMax, domainQuantile }, { reverse = false } = {}) {
|
|
141
|
+
return [
|
|
142
|
+
...(domainMin !== undefined &&
|
|
143
|
+
domainMax !== undefined &&
|
|
144
|
+
domainMin > domainMax
|
|
145
|
+
? [
|
|
146
|
+
{
|
|
147
|
+
rule: 'domain-ends',
|
|
148
|
+
slot: 'domainMax',
|
|
149
|
+
message: `domainMax is below domainMin: the scale spans the two in order either way${reverse ? ', and reverse is what turns it round' : ''}`,
|
|
150
|
+
},
|
|
151
|
+
]
|
|
152
|
+
: []),
|
|
153
|
+
...(domainQuantile !== undefined &&
|
|
154
|
+
!(domainQuantile >= 0.5 && domainQuantile <= 1)
|
|
155
|
+
? [
|
|
156
|
+
{
|
|
157
|
+
rule: 'domain-quantile',
|
|
158
|
+
slot: 'domainQuantile',
|
|
159
|
+
message: 'domainQuantile is a fraction from 0.5 to 1, not a percent: 1 follows the extremes and 0.99 clips the outermost 1% at each end; above 1 reads as 1 and below 0.5 as 0.5',
|
|
160
|
+
},
|
|
161
|
+
]
|
|
162
|
+
: []),
|
|
163
|
+
];
|
|
164
|
+
}
|
|
165
|
+
/** Problems as the lines a display's corner notice lists, under the setting that holds the scale. */
|
|
166
|
+
export function noticeLines(setting, problems) {
|
|
167
|
+
return problems.map(({ slot, message }) => `${setting}.${slot}: ${message}`);
|
|
168
|
+
}
|
|
169
|
+
function pinned(entry) {
|
|
170
|
+
return entry !== '' && Number.isFinite(Number(entry));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* What a colour object's slots say together that no single slot can refuse,
|
|
174
|
+
* since the config editor writes one slot at a time (ADR-133), read with its
|
|
175
|
+
* field's preset under it. A display shows these as a notice and draws what
|
|
176
|
+
* it can. A threshold naming no cuts leaves its intervals to the display —
|
|
177
|
+
* the alignments insert-size band samples its cuts, the wiggle plot cuts at
|
|
178
|
+
* its origin — so its `labels` go uncounted.
|
|
179
|
+
*/
|
|
180
|
+
export function colorProblems(written, presets) {
|
|
181
|
+
const color = withPreset(written, presets);
|
|
182
|
+
const { domain = [] } = color;
|
|
183
|
+
const { range = [] } = written;
|
|
184
|
+
const scale = color.scale === IDENTITY_SCALE
|
|
185
|
+
? IDENTITY_SCALE
|
|
186
|
+
: paintedScale({ scale: color.scale, field: color.field ?? '' }, fieldScaleOf(presets, color.field ?? ''));
|
|
187
|
+
const problems = [];
|
|
188
|
+
if (scale === 'threshold') {
|
|
189
|
+
if (domain.some((cut, i) => !pinned(cut) || Number(cut) <= Number(domain[i - 1]))) {
|
|
190
|
+
problems.push({
|
|
191
|
+
rule: 'threshold-cuts',
|
|
192
|
+
slot: 'domain',
|
|
193
|
+
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',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
if (domain.length > 0 &&
|
|
197
|
+
range.length > 0 &&
|
|
198
|
+
range.length !== domain.length + 1) {
|
|
199
|
+
problems.push({
|
|
200
|
+
rule: 'threshold-range',
|
|
201
|
+
slot: 'range',
|
|
202
|
+
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`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (scale === 'linear' || scale === 'log') {
|
|
207
|
+
if (domain.length > 0) {
|
|
208
|
+
problems.push({
|
|
209
|
+
rule: 'ramp-domain',
|
|
210
|
+
slot: 'domain',
|
|
211
|
+
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",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
problems.push(...scaleEndProblems(color, { reverse: true }));
|
|
215
|
+
}
|
|
216
|
+
const { labels = [] } = color;
|
|
217
|
+
const named = scale === 'categorical' || scale === IDENTITY_SCALE
|
|
218
|
+
? domain.length
|
|
219
|
+
: scale === 'threshold'
|
|
220
|
+
? domain.length + 1
|
|
221
|
+
: undefined;
|
|
222
|
+
const cutsFromData = scale === 'threshold' && domain.length === 0;
|
|
223
|
+
if (labels.length > 0 &&
|
|
224
|
+
!cutsFromData &&
|
|
225
|
+
(named === undefined || labels.length > named)) {
|
|
226
|
+
const what = scale === 'categorical'
|
|
227
|
+
? 'value'
|
|
228
|
+
: scale === IDENTITY_SCALE
|
|
229
|
+
? 'colour'
|
|
230
|
+
: 'interval';
|
|
231
|
+
problems.push({
|
|
232
|
+
rule: 'labels-domain',
|
|
233
|
+
slot: 'labels',
|
|
234
|
+
message: named === undefined
|
|
235
|
+
? "labels names a categorical scale's values, a threshold scale's intervals or an identity scale's colours, and this colour paints none of them"
|
|
236
|
+
: `labels names one ${what} each, and ${labels.length} ${labels.length === 1 ? 'label names' : 'labels name'} ${named} ${named === 1 ? what : `${what}s`}: a label past them names nothing`,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return problems;
|
|
240
|
+
}
|
|
241
|
+
/** `colorProblems` as the lines a display's corner notice lists, under `color`. */
|
|
242
|
+
export function colorNotices(color, presets) {
|
|
243
|
+
return noticeLines('color', colorProblems(color, presets));
|
|
244
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Generated by scripts/generateMarkRules.ts from packages/core/src/util/color/cssColorsLevel4.ts: do not edit, `pnpm autogen` rewrites it.
|
|
2
|
+
// from https://www.w3.org/TR/css-color-4/#named-colors
|
|
3
|
+
const colors = {
|
|
4
|
+
aliceblue: '#F0F8FF',
|
|
5
|
+
antiquewhite: '#FAEBD7',
|
|
6
|
+
aqua: '#00FFFF',
|
|
7
|
+
aquamarine: '#7FFFD4',
|
|
8
|
+
azure: '#F0FFFF',
|
|
9
|
+
beige: '#F5F5DC',
|
|
10
|
+
bisque: '#FFE4C4',
|
|
11
|
+
black: '#000000',
|
|
12
|
+
blanchedalmond: '#FFEBCD',
|
|
13
|
+
blue: '#0000FF',
|
|
14
|
+
blueviolet: '#8A2BE2',
|
|
15
|
+
brown: '#A52A2A',
|
|
16
|
+
burlywood: '#DEB887',
|
|
17
|
+
cadetblue: '#5F9EA0',
|
|
18
|
+
chartreuse: '#7FFF00',
|
|
19
|
+
chocolate: '#D2691E',
|
|
20
|
+
coral: '#FF7F50',
|
|
21
|
+
cornflowerblue: '#6495ED',
|
|
22
|
+
cornsilk: '#FFF8DC',
|
|
23
|
+
crimson: '#DC143C',
|
|
24
|
+
cyan: '#00FFFF',
|
|
25
|
+
darkblue: '#00008B',
|
|
26
|
+
darkcyan: '#008B8B',
|
|
27
|
+
darkgoldenrod: '#B8860B',
|
|
28
|
+
darkgray: '#A9A9A9',
|
|
29
|
+
darkgreen: '#006400',
|
|
30
|
+
darkgrey: '#A9A9A9',
|
|
31
|
+
darkkhaki: '#BDB76B',
|
|
32
|
+
darkmagenta: '#8B008B',
|
|
33
|
+
darkolivegreen: '#556B2F',
|
|
34
|
+
darkorange: '#FF8C00',
|
|
35
|
+
darkorchid: '#9932CC',
|
|
36
|
+
darkred: '#8B0000',
|
|
37
|
+
darksalmon: '#E9967A',
|
|
38
|
+
darkseagreen: '#8FBC8F',
|
|
39
|
+
darkslateblue: '#483D8B',
|
|
40
|
+
darkslategray: '#2F4F4F',
|
|
41
|
+
darkslategrey: '#2F4F4F',
|
|
42
|
+
darkturquoise: '#00CED1',
|
|
43
|
+
darkviolet: '#9400D3',
|
|
44
|
+
deeppink: '#FF1493',
|
|
45
|
+
deepskyblue: '#00BFFF',
|
|
46
|
+
dimgray: '#696969',
|
|
47
|
+
dimgrey: '#696969',
|
|
48
|
+
dodgerblue: '#1E90FF',
|
|
49
|
+
firebrick: '#B22222',
|
|
50
|
+
floralwhite: '#FFFAF0',
|
|
51
|
+
forestgreen: '#228B22',
|
|
52
|
+
fuchsia: '#FF00FF',
|
|
53
|
+
gainsboro: '#DCDCDC',
|
|
54
|
+
ghostwhite: '#F8F8FF',
|
|
55
|
+
gold: '#FFD700',
|
|
56
|
+
goldenrod: '#DAA520',
|
|
57
|
+
gray: '#808080',
|
|
58
|
+
green: '#008000',
|
|
59
|
+
greenyellow: '#ADFF2F',
|
|
60
|
+
grey: '#808080',
|
|
61
|
+
honeydew: '#F0FFF0',
|
|
62
|
+
hotpink: '#FF69B4',
|
|
63
|
+
indianred: '#CD5C5C',
|
|
64
|
+
indigo: '#4B0082',
|
|
65
|
+
ivory: '#FFFFF0',
|
|
66
|
+
khaki: '#F0E68C',
|
|
67
|
+
lavender: '#E6E6FA',
|
|
68
|
+
lavenderblush: '#FFF0F5',
|
|
69
|
+
lawngreen: '#7CFC00',
|
|
70
|
+
lemonchiffon: '#FFFACD',
|
|
71
|
+
lightblue: '#ADD8E6',
|
|
72
|
+
lightcoral: '#F08080',
|
|
73
|
+
lightcyan: '#E0FFFF',
|
|
74
|
+
lightgoldenrodyellow: '#FAFAD2',
|
|
75
|
+
lightgray: '#D3D3D3',
|
|
76
|
+
lightgreen: '#90EE90',
|
|
77
|
+
lightgrey: '#D3D3D3',
|
|
78
|
+
lightpink: '#FFB6C1',
|
|
79
|
+
lightsalmon: '#FFA07A',
|
|
80
|
+
lightseagreen: '#20B2AA',
|
|
81
|
+
lightskyblue: '#87CEFA',
|
|
82
|
+
lightslategray: '#778899',
|
|
83
|
+
lightslategrey: '#778899',
|
|
84
|
+
lightsteelblue: '#B0C4DE',
|
|
85
|
+
lightyellow: '#FFFFE0',
|
|
86
|
+
lime: '#00FF00',
|
|
87
|
+
limegreen: '#32CD32',
|
|
88
|
+
linen: '#FAF0E6',
|
|
89
|
+
magenta: '#FF00FF',
|
|
90
|
+
maroon: '#800000',
|
|
91
|
+
mediumaquamarine: '#66CDAA',
|
|
92
|
+
mediumblue: '#0000CD',
|
|
93
|
+
mediumorchid: '#BA55D3',
|
|
94
|
+
mediumpurple: '#9370DB',
|
|
95
|
+
mediumseagreen: '#3CB371',
|
|
96
|
+
mediumslateblue: '#7B68EE',
|
|
97
|
+
mediumspringgreen: '#00FA9A',
|
|
98
|
+
mediumturquoise: '#48D1CC',
|
|
99
|
+
mediumvioletred: '#C71585',
|
|
100
|
+
midnightblue: '#191970',
|
|
101
|
+
mintcream: '#F5FFFA',
|
|
102
|
+
mistyrose: '#FFE4E1',
|
|
103
|
+
moccasin: '#FFE4B5',
|
|
104
|
+
navajowhite: '#FFDEAD',
|
|
105
|
+
navy: '#000080',
|
|
106
|
+
oldlace: '#FDF5E6',
|
|
107
|
+
olive: '#808000',
|
|
108
|
+
olivedrab: '#6B8E23',
|
|
109
|
+
orange: '#FFA500',
|
|
110
|
+
orangered: '#FF4500',
|
|
111
|
+
orchid: '#DA70D6',
|
|
112
|
+
palegoldenrod: '#EEE8AA',
|
|
113
|
+
palegreen: '#98FB98',
|
|
114
|
+
paleturquoise: '#AFEEEE',
|
|
115
|
+
palevioletred: '#DB7093',
|
|
116
|
+
papayawhip: '#FFEFD5',
|
|
117
|
+
peachpuff: '#FFDAB9',
|
|
118
|
+
peru: '#CD853F',
|
|
119
|
+
pink: '#FFC0CB',
|
|
120
|
+
plum: '#DDA0DD',
|
|
121
|
+
powderblue: '#B0E0E6',
|
|
122
|
+
purple: '#800080',
|
|
123
|
+
rebeccapurple: '#663399',
|
|
124
|
+
red: '#FF0000',
|
|
125
|
+
rosybrown: '#BC8F8F',
|
|
126
|
+
royalblue: '#4169E1',
|
|
127
|
+
saddlebrown: '#8B4513',
|
|
128
|
+
salmon: '#FA8072',
|
|
129
|
+
sandybrown: '#F4A460',
|
|
130
|
+
seagreen: '#2E8B57',
|
|
131
|
+
seashell: '#FFF5EE',
|
|
132
|
+
sienna: '#A0522D',
|
|
133
|
+
silver: '#C0C0C0',
|
|
134
|
+
skyblue: '#87CEEB',
|
|
135
|
+
slateblue: '#6A5ACD',
|
|
136
|
+
slategray: '#708090',
|
|
137
|
+
slategrey: '#708090',
|
|
138
|
+
snow: '#FFFAFA',
|
|
139
|
+
springgreen: '#00FF7F',
|
|
140
|
+
steelblue: '#4682B4',
|
|
141
|
+
tan: '#D2B48C',
|
|
142
|
+
teal: '#008080',
|
|
143
|
+
thistle: '#D8BFD8',
|
|
144
|
+
tomato: '#FF6347',
|
|
145
|
+
turquoise: '#40E0D0',
|
|
146
|
+
violet: '#EE82EE',
|
|
147
|
+
wheat: '#F5DEB3',
|
|
148
|
+
white: '#FFFFFF',
|
|
149
|
+
whitesmoke: '#F5F5F5',
|
|
150
|
+
yellow: '#FFFF00',
|
|
151
|
+
yellowgreen: '#9ACD32',
|
|
152
|
+
};
|
|
153
|
+
// Object.hasOwn rather than a bare index: the table is a plain object, so
|
|
154
|
+
// `colors.constructor` and `colors.toString` come back as functions typed
|
|
155
|
+
// `string`, and isNamedColor('toString') was true.
|
|
156
|
+
export function namedColorToHex(name) {
|
|
157
|
+
return Object.hasOwn(colors, name) ? colors[name] : undefined;
|
|
158
|
+
}
|
|
159
|
+
export function isNamedColor(name) {
|
|
160
|
+
return Object.hasOwn(colors, name);
|
|
161
|
+
}
|
|
162
|
+
export const CSS_COLOR_NAMES = Object.keys(colors);
|
|
@@ -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
|
+
}
|