Skip to content

Commit de90f3f

Browse files
committed
per-channel scale override
1 parent 28b1a4c commit de90f3f

5 files changed

Lines changed: 930 additions & 926 deletions

File tree

src/channel.js

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,44 @@
11
import {ascending, descending, rollup, sort} from "d3";
2-
import {first, isIterable, labelof, map, maybeValue, range, valueof} from "./options.js";
2+
import {first, isColor, isFirst, isEvery, isIterable, labelof, map, maybeValue, range, valueof} from "./options.js";
33
import {registry} from "./scales/index.js";
4+
import {isSymbol, maybeSymbol} from "./symbols.js";
45
import {maybeReduce} from "./transforms/group.js";
56

67
// TODO Type coercion?
7-
export function Channel(data, {scale, type, value, filter, hint}) {
8+
export function Channel(data, {scale, scaleLock, type, value, filter, hint}) {
9+
let V = valueof(data, value);
10+
11+
// For color and symbol scales, if a scale is not explicitly specified when
12+
// the channel is declared, ignore the channel’s default scale if the channel
13+
// values are literal. In the case of symbols, we must further promote symbol
14+
// names (e.g., "plus") to a symbol instance (symbolPlus). TODO Instead of a
15+
// scaleLock property, use an "auto" scale?
16+
const is = scaleLock || !V ? null : scale === "color" ? isColor : scale === "symbol" ? isSymbol : null;
17+
if (is && isFirst(V, is) && isEvery(V, is)) {
18+
if (is === isSymbol) V = map(V, maybeSymbol);
19+
scale = undefined;
20+
}
21+
822
return {
923
scale,
1024
type,
11-
value: valueof(data, value),
25+
value: V,
1226
label: labelof(value),
1327
filter,
1428
hint
1529
};
1630
}
1731

18-
export function Channels(descriptors, data) {
19-
return Object.fromEntries(Object.entries(descriptors).map(([name, channel]) => [name, Channel(data, channel)]));
32+
export function Channels(channels, data) {
33+
return Object.fromEntries(Object.entries(channels).map(([name, channel]) => [name, Channel(data, channel)]));
2034
}
2135

2236
// TODO Use Float64Array for scales with numeric ranges, e.g. position?
2337
export function valueObject(channels, scales) {
2438
return Object.fromEntries(
2539
Object.entries(channels).map(([name, {scale: scaleName, value}]) => {
2640
let scale;
27-
if (scaleName !== undefined) {
28-
scale = scales[scaleName];
29-
}
41+
if (scaleName != null) scale = scales[scaleName];
3042
return [name, scale === undefined ? value : map(value, scale)];
3143
})
3244
);

src/mark.js

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Channels, channelDomain, valueObject} from "./channel.js";
22
import {defined} from "./defined.js";
33
import {maybeFacetAnchor} from "./facet.js";
4-
import {arrayify, isDomainSort, range} from "./options.js";
4+
import {arrayify, isDomainSort, isOptions, range} from "./options.js";
55
import {keyword, maybeNamed} from "./options.js";
66
import {maybeProject} from "./projection.js";
77
import {maybeClip, styles} from "./style.js";
@@ -41,11 +41,24 @@ export class Mark {
4141
if (extraChannels !== undefined) channels = {...maybeNamed(extraChannels), ...channels};
4242
if (defaults !== undefined) channels = {...styles(this, options, defaults), ...channels};
4343
this.channels = Object.fromEntries(
44-
Object.entries(channels).filter(([name, {value, optional}]) => {
45-
if (value != null) return true;
46-
if (optional) return false;
47-
throw new Error(`missing channel value: ${name}`);
48-
})
44+
Object.entries(channels)
45+
.map(([name, channel]) => {
46+
let {value} = channel;
47+
if (isOptions(value)) {
48+
channel = {...channel};
49+
if (value.scale !== undefined) {
50+
channel.scale = value.scale;
51+
channel.scaleLock = true;
52+
}
53+
channel.value = value.value;
54+
}
55+
return [name, channel];
56+
})
57+
.filter(([name, {value, optional}]) => {
58+
if (value != null) return true;
59+
if (optional) return false;
60+
throw new Error(`missing channel value: ${name}`);
61+
})
4962
);
5063
this.dx = +dx;
5164
this.dy = +dy;

src/scales.js

Lines changed: 2 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import {parse as isoParse} from "isoformat";
22
import {
3-
isColor,
4-
isEvery,
53
isOrdinal,
6-
isFirst,
74
isTemporal,
85
isTemporalString,
96
isNumericString,
@@ -34,7 +31,7 @@ import {
3431
import {isDivergingScheme} from "./scales/schemes.js";
3532
import {ScaleTime, ScaleUtc} from "./scales/temporal.js";
3633
import {ScaleOrdinal, ScalePoint, ScaleBand, ordinalImplicit} from "./scales/ordinal.js";
37-
import {isSymbol, maybeSymbol} from "./symbols.js";
34+
import {maybeSymbol} from "./symbols.js";
3835
import {warn} from "./warnings.js";
3936

4037
export function Scales(
@@ -406,20 +403,8 @@ function inferScaleType(key, channels, {type, domain, range, scheme, pivot, proj
406403
// If there’s no data (and no type) associated with this scale, don’t create a scale.
407404
if (domain === undefined && !channels.some(({value}) => value !== undefined)) return;
408405

409-
const kind = registry.get(key);
410-
411-
// For color scales, if no range or scheme is specified and all associated
412-
// defined values (from the domain if present, and otherwise from channels)
413-
// are valid colors, then default to the identity scale. This allows, for
414-
// example, a fill channel to return literal colors; without this, the colors
415-
// would be remapped to a categorical scheme!
416-
if (kind === color && range === undefined && scheme === undefined && isAll(domain, channels, isColor))
417-
return "identity";
418-
419-
// Similarly for symbols…
420-
if (kind === symbol && range === undefined && isAll(domain, channels, isSymbol)) return "identity";
421-
422406
// Some scales have default types.
407+
const kind = registry.get(key);
423408
if (kind === radius) return "sqrt";
424409
if (kind === opacity || kind === length) return "linear";
425410
if (kind === symbol) return "ordinal";
@@ -461,13 +446,6 @@ function asOrdinalType(kind) {
461446
}
462447
}
463448

464-
function isAll(domain, channels, is) {
465-
return domain !== undefined
466-
? isFirst(domain, is) && isEvery(domain, is)
467-
: channels.some(({value}) => value !== undefined && isFirst(value, is)) &&
468-
channels.every(({value}) => value === undefined || isEvery(value, is));
469-
}
470-
471449
export function isTemporalScale({type}) {
472450
return type === "time" || type === "utc";
473451
}

0 commit comments

Comments
 (0)