Skip to content

Commit

Permalink
symbol identity scale
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Jan 19, 2022
1 parent 6414e08 commit 7d4dd20
Show file tree
Hide file tree
Showing 8 changed files with 2,836 additions and 58 deletions.
3 changes: 1 addition & 2 deletions src/marks/dot.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import {create, path, symbolCircle} from "d3";
import {positive} from "../defined.js";
import {identity, maybeNumberChannel, maybeTuple} from "../options.js";
import {identity, maybeNumberChannel, maybeSymbolChannel, maybeTuple} from "../options.js";
import {Mark} from "../plot.js";
import {maybeSymbolChannel} from "../scales/symbol.js";
import {applyChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, offset} from "../style.js";

const defaults = {
Expand Down
58 changes: 51 additions & 7 deletions src/options.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {color, descending} from "d3";
import {symbolAsterisk, symbolDiamond2, symbolPlus, symbolSquare2, symbolTriangle2, symbolX as symbolTimes} from "d3";
import {symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye} from "d3";

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray
const TypedArray = Object.getPrototypeOf(Uint8Array);
Expand Down Expand Up @@ -207,21 +209,21 @@ export function isNumeric(values) {
}
}

export function isColors(values) {
export function isFirst(values, is) {
for (const value of values) {
if (value == null) continue;
return isColor(value);
return is(value);
}
}

// Whereas isColors only tests the first defined value and returns undefined for
// Whereas isFirst only tests the first defined value and returns undefined for
// an empty array, this tests all defined values and only returns true if all of
// them are valid colors. It also returns true for an empty array, and thus
// should generally be used in conjunction with isColors.
export function isAllColors(values) {
// should generally be used in conjunction with isFirst.
export function isEvery(values, is) {
for (const value of values) {
if (value == null) continue;
if (!isColor(value)) return false;
if (!is(value)) return false;
}
return true;
}
Expand All @@ -231,11 +233,53 @@ export function isAllColors(values) {
// coercion here, though note that d3-color instances would need to support
// valueOf to work correctly with InternMap.
export function isColor(value) {
if (!(typeof value === "string")) return false;
if (typeof value !== "string") return false;
value = value.toLowerCase();
return value === "currentcolor" || value === "none" || color(value) !== null;
}

const symbols = new Map([
["asterisk", symbolAsterisk],
["circle", symbolCircle],
["cross", symbolCross],
["diamond", symbolDiamond],
["diamond2", symbolDiamond2],
["plus", symbolPlus],
["square", symbolSquare],
["square2", symbolSquare2],
["star", symbolStar],
["times", symbolTimes],
["triangle", symbolTriangle],
["triangle2", symbolTriangle2],
["wye", symbolWye]
]);

function isSymbolObject(value) {
return value && typeof value.draw === "function";
}

export function isSymbol(value) {
if (isSymbolObject(value)) return true;
if (typeof value !== "string") return false;
return symbols.has(value.toLowerCase());
}

export function maybeSymbol(symbol) {
if (symbol == null || isSymbolObject(symbol)) return symbol;
const value = symbols.get(`${symbol}`.toLowerCase());
if (value) return value;
throw new Error(`invalid symbol: ${symbol}`);
}

export function maybeSymbolChannel(symbol) {
if (symbol == null || isSymbolObject(symbol)) return [undefined, symbol];
if (typeof symbol === "string") {
const value = symbols.get(`${symbol}`.toLowerCase());
if (value) return [undefined, value];
}
return [symbol, undefined];
}

// Like a sort comparator, returns a positive value if the given array of values
// is in ascending order, a negative value if the values are in descending
// order. Assumes monotonicity; only tests the first and last values.
Expand Down
33 changes: 23 additions & 10 deletions src/scales.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {parse as isoParse} from "isoformat";
import {isAllColors, isColors, isOrdinal, isTemporal, order} from "./options.js";
import {isColor, isEvery, isOrdinal, isFirst, isSymbol, isTemporal, maybeSymbol, order} from "./options.js";
import {registry, color, position, radius, opacity, symbol, length} from "./scales/index.js";
import {ScaleLinear, ScaleSqrt, ScalePow, ScaleLog, ScaleSymlog, ScaleQuantile, ScaleThreshold, ScaleIdentity} from "./scales/quantitative.js";
import {ScaleDiverging, ScaleDivergingSqrt, ScaleDivergingPow, ScaleDivergingLog, ScaleDivergingSymlog} from "./scales/diverging.js";
Expand Down Expand Up @@ -155,7 +155,10 @@ function Scale(key, channels = [], options = {}) {
options = coerceType(channels, options, coerceNumber, Float64Array);
break;
case "identity":
if (registry.get(key) === position) options = coerceType(channels, options, coerceNumber, Float64Array);
switch (registry.get(key)) {
case position: options = coerceType(channels, options, coerceNumber, Float64Array); break;
case symbol: options = coerceType(channels, options, maybeSymbol); break;
}
break;
case "utc":
case "time":
Expand Down Expand Up @@ -203,11 +206,7 @@ function inferScaleType(key, channels, {type, domain, range, scheme}) {
// If the scale, a channel, or user specified a (consistent) type, return it.
if (type !== undefined) return type;

// Some scales have default types.
const kind = registry.get(key);
if (kind === radius) return "sqrt";
if (kind === opacity || kind === length) return "linear";
if (kind === symbol) return "ordinal";

// For color scales, if no range or scheme is specified and all associated
// defined values (from the domain if present, and otherwise from channels)
Expand All @@ -217,10 +216,17 @@ function inferScaleType(key, channels, {type, domain, range, scheme}) {
if (kind === color
&& range === undefined
&& scheme === undefined
&& (domain !== undefined
? isColors(domain) && isAllColors(domain)
: channels.some(({value}) => value !== undefined && isColors(value))
&& channels.every(({value}) => value === undefined || isAllColors(value)))) return "identity";
&& isAll(domain, channels, isColor)) return "identity";

// Similarly for symbols…
if (kind === symbol
&& range === undefined
&& isAll(domain, channels, isSymbol)) return "identity";

// Some scales have default types.
if (kind === radius) return "sqrt";
if (kind === opacity || kind === length) return "linear";
if (kind === symbol) return "ordinal";

// If the domain or range has more than two values, assume it’s ordinal. You
// can still use a “piecewise” (or “polylinear”) scale, but you must set the
Expand Down Expand Up @@ -254,6 +260,13 @@ function asOrdinalType(kind) {
}
}

function isAll(domain, channels, is) {
return domain !== undefined
? isFirst(domain, is) && isEvery(domain, is)
: channels.some(({value}) => value !== undefined && isFirst(value, is))
&& channels.every(({value}) => value === undefined || isEvery(value, is));
}

export function isTemporalScale({type}) {
return type === "time" || type === "utc";
}
Expand Down
2 changes: 1 addition & 1 deletion src/scales/ordinal.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {InternSet, quantize, reverse as reverseof, sort, symbolsFill, symbolsStroke} from "d3";
import {scaleBand, scaleOrdinal, scalePoint, scaleImplicit} from "d3";
import {ascendingDefined} from "../defined.js";
import {maybeSymbol} from "../options.js";
import {none} from "../style.js";
import {registry, color, symbol} from "./index.js";
import {ordinalScheme, quantitativeScheme} from "./schemes.js";
import {maybeSymbol} from "./symbol.js";

export function ScaleO(scale, channels, {
type,
Expand Down
38 changes: 0 additions & 38 deletions src/scales/symbol.js

This file was deleted.

Loading

0 comments on commit 7d4dd20

Please sign in to comment.