Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

symbol legend tests; fix symbol scale hint; symbol identity scale #680

Merged
merged 5 commits into from
Jan 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -844,6 +844,8 @@ The **r** option can be specified as either a channel or constant. When the radi

The **stroke** defaults to none. The **fill** defaults to currentColor if the stroke is none, and to none otherwise. The **strokeWidth** defaults to 1.5. The **rotate** and **symbol** options can be specified as either channels or constants. When rotate is specified as a number, it is interpreted as a constant; otherwise it is interpreted as a channel. When symbol is a valid symbol name or symbol object (implementing the draw method), it is interpreted as a constant; otherwise it is interpreted as a channel.

The built-in **symbol** types are: *asterisk*, *circle*, *cross*, *diamond*, *diamond2*, *plus*, *square*, *square2*, *star*, *times*, *triangle*, *triangle2*, and *wye*. You can also specify a D3 or custom symbol type as an object that implements the [*symbol*.draw(*context*, *size*)](https://github.com/d3/d3-shape/blob/main/README.md#custom-symbol-types) method.

Dots are drawn in input order, with the last data drawn on top. If sorting is needed, say to mitigate overplotting by drawing the smallest dots on top, consider a [sort and reverse transform](#transforms).

#### Plot.dot(*data*, *options*)
Expand Down
14 changes: 12 additions & 2 deletions src/legends.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,26 @@ import {legendRamp} from "./legends/ramp.js";
import {legendSwatches, legendSymbols} from "./legends/swatches.js";

const legendRegistry = new Map([
["color", legendColor],
["symbol", legendSymbols],
["color", legendColor],
["opacity", legendOpacity]
]);

export function legend(options = {}) {
for (const [key, value] of legendRegistry) {
const scale = options[key];
if (isObject(scale)) { // e.g., ignore {color: "red"}
return value(normalizeScale(key, scale), legendOptions(scale, options), key => isObject(options[key]) ? normalizeScale(key, options[key]) : null);
let hint;
// For symbol legends, pass a hint to the symbol scale.
if (key === "symbol") {
const {fill, stroke = fill === undefined && isObject(options.color) ? "color" : undefined} = options;
hint = {fill, stroke};
}
return value(
normalizeScale(key, scale, hint),
legendOptions(scale, options),
key => isObject(options[key]) ? normalizeScale(key, options[key]) : null
);
}
}
throw new Error("unknown legend type");
Expand Down
7 changes: 3 additions & 4 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 Expand Up @@ -42,8 +41,8 @@ export class Dot extends Mark {
const fillChannel = channels.find(({name}) => name === "fill");
const strokeChannel = channels.find(({name}) => name === "stroke");
symbolChannel.hint = {
fill: fillChannel?.value === symbolChannel.value ? "color" : this.fill,
stroke: strokeChannel?.value === symbolChannel.value ? "color" : this.stroke
fill: fillChannel ? (fillChannel.value === symbolChannel.value ? "color" : "currentColor") : this.fill,
stroke: strokeChannel ? (strokeChannel.value === symbolChannel.value ? "color" : "currentColor") : this.stroke
};
}
}
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
37 changes: 25 additions & 12 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 @@ -127,8 +127,8 @@ function piecewiseRange(scale) {
return Array.from({length}, (_, i) => start + i / (length - 1) * (end - start));
}

export function normalizeScale(key, scale) {
return Scale(key, undefined, {...scale});
export function normalizeScale(key, scale, hint) {
return Scale(key, hint === undefined ? undefined : [{hint}], {...scale});
}

function Scale(key, channels = [], options = {}) {
Expand All @@ -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.

50 changes: 50 additions & 0 deletions test/output/symbolLegendBasic.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<div class="plot" style="
--swatchWidth: 15px;
--swatchHeight: 15px;
">
<style>
.plot {
font-family: system-ui, sans-serif;
font-size: 10px;
margin-bottom: 0.5em;
margin-left: 0px;
}

.plot-swatch>svg {
width: var(--swatchWidth);
height: var(--swatchHeight);
margin-right: 0.5em;
overflow: visible;
fill: none;
fill-opacity: 1;
stroke: currentColor;
stroke-width: 1.5px;
stroke-opacity: 1;
}

.plot {
display: flex;
align-items: center;
min-height: 33px;
flex-wrap: wrap;
}

.plot-swatch {
display: inline-flex;
align-items: center;
margin-right: 1em;
}
</style><span class="plot-swatch"><svg viewBox="-8 -8 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M4.5,0A4.5,4.5,0,1,1,-4.5,0A4.5,4.5,0,1,1,4.5,0"></path>
</svg>A</span><span class="plot-swatch"><svg viewBox="-8 -8 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M-6.873088769818877,0L6.873088769818877,0M0,6.873088769818877L0,-6.873088769818877"></path>
</svg>B</span><span class="plot-swatch"><svg viewBox="-8 -8 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M-4.869970345289333,-4.869970345289333L4.869970345289333,4.869970345289333M-4.869970345289333,4.869970345289333L4.869970345289333,-4.869970345289333"></path>
</svg>C</span><span class="plot-swatch"><svg viewBox="-8 -8 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0,-5.442851285360659L4.713647482143115,2.7214256426803294L-4.713647482143115,2.7214256426803294Z"></path>
</svg>D</span><span class="plot-swatch"><svg viewBox="-8 -8 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0,4.768502950069832L0,-4.768502950069832M-4.129644692781513,-2.384251475034916L4.129644692781513,2.384251475034916M-4.129644692781513,2.384251475034916L4.129644692781513,-2.384251475034916"></path>
</svg>E</span><span class="plot-swatch"><svg viewBox="-8 -8 16 16" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M3.5341843560130535,3.5341843560130535L3.5341843560130535,-3.5341843560130535L-3.5341843560130535,-3.5341843560130535L-3.5341843560130535,3.5341843560130535Z"></path>
</svg>F</span>
</div>
50 changes: 50 additions & 0 deletions test/output/symbolLegendColorFill.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<div class="plot" style="
--swatchWidth: 15px;
--swatchHeight: 15px;
">
<style>
.plot {
font-family: system-ui, sans-serif;
font-size: 10px;
margin-bottom: 0.5em;
margin-left: 0px;
}

.plot-swatch>svg {
width: var(--swatchWidth);
height: var(--swatchHeight);
margin-right: 0.5em;
overflow: visible;
fill: undefined;
fill-opacity: 1;
stroke: none;
stroke-width: 1.5px;
stroke-opacity: 1;
}

.plot {
display: flex;
align-items: center;
min-height: 33px;
flex-wrap: wrap;
}

.plot-swatch {
display: inline-flex;
align-items: center;
margin-right: 1em;
}
</style><span class="plot-swatch"><svg viewBox="-8 -8 16 16" fill="#4e79a7" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M4.5,0A4.5,4.5,0,1,1,-4.5,0A4.5,4.5,0,1,1,4.5,0"></path>
</svg>A</span><span class="plot-swatch"><svg viewBox="-8 -8 16 16" fill="#f28e2c" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M-5.350491851768115,-1.7834972839227048L-1.7834972839227048,-1.7834972839227048L-1.7834972839227048,-5.350491851768115L1.7834972839227048,-5.350491851768115L1.7834972839227048,-1.7834972839227048L5.350491851768115,-1.7834972839227048L5.350491851768115,1.7834972839227048L1.7834972839227048,1.7834972839227048L1.7834972839227048,5.350491851768115L-1.7834972839227048,5.350491851768115L-1.7834972839227048,1.7834972839227048L-5.350491851768115,1.7834972839227048Z"></path>
</svg>B</span><span class="plot-swatch"><svg viewBox="-8 -8 16 16" fill="#e15759" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0,-7.422543747841057L4.285407630887808,0L0,7.422543747841057L-4.285407630887808,0Z"></path>
</svg>C</span><span class="plot-swatch"><svg viewBox="-8 -8 16 16" fill="#76b7b2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M-3.988021164537411,-3.988021164537411h7.976042329074822v7.976042329074822h-7.976042329074822Z"></path>
</svg>D</span><span class="plot-swatch"><svg viewBox="-8 -8 16 16" fill="#59a14f" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0,-7.528019676343693L1.6901457214599565,-2.326286013979192L7.159572167984802,-2.3262860139791925L2.7347132232624225,0.8885621897865381L4.42485894472238,6.090295852151038L6.661338147750939e-16,2.8754476483853075L-4.424858944722378,6.090295852151039L-2.734713223262422,0.8885621897865386L-7.159572167984803,-2.3262860139791908L-1.690145721459957,-2.3262860139791917Z"></path>
</svg>E</span><span class="plot-swatch"><svg viewBox="-8 -8 16 16" fill="#edc949" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M0,-6.998041357002963L6.060481591898691,3.4990206785014815L-6.060481591898691,3.4990206785014815Z"></path>
</svg>F</span>
</div>
Loading