Skip to content

Commit 36df05d

Browse files
mbostockFil
andauthored
warnings! (#751)
* warnings! * uplift an old warning * numeric string warning Co-authored-by: Philippe Rivière <[email protected]>
1 parent f8d39b3 commit 36df05d

13 files changed

+290
-302
lines changed

src/options.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import {parse as isoParse} from "isoformat";
12
import {color, descending} from "d3";
23
import {symbolAsterisk, symbolDiamond2, symbolPlus, symbolSquare2, symbolTriangle2, symbolX as symbolTimes} from "d3";
34
import {symbolCircle, symbolCross, symbolDiamond, symbolSquare, symbolStar, symbolTriangle, symbolWye} from "d3";
@@ -210,6 +211,26 @@ export function isTemporal(values) {
210211
}
211212
}
212213

214+
// Are these strings that might represent dates? This is stricter than ISO 8601
215+
// because we want to ignore false positives on numbers; for example, the string
216+
// "1192" is more likely to represent a number than a date even though it is
217+
// valid ISO 8601 representing 1192-01-01.
218+
export function isTemporalString(values) {
219+
for (const value of values) {
220+
if (value == null) continue;
221+
return typeof value === "string" && isNaN(value) && isoParse(value);
222+
}
223+
}
224+
225+
// Are these strings that might represent numbers? This is stricter than
226+
// coercion because we want to ignore false positives on e.g. empty strings.
227+
export function isNumericString(values) {
228+
for (const value of values) {
229+
if (value == null || value === "") continue;
230+
return typeof value === "string" && !isNaN(value);
231+
}
232+
}
233+
213234
export function isNumeric(values) {
214235
for (const value of values) {
215236
if (value == null) continue;

src/plot.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {create, cross, difference, groups, InternMap} from "d3";
1+
import {create, cross, difference, groups, InternMap, select} from "d3";
22
import {Axes, autoAxisTicks, autoScaleLabels} from "./axes.js";
33
import {Channel, channelSort} from "./channel.js";
44
import {defined} from "./defined.js";
@@ -8,6 +8,7 @@ import {arrayify, isOptions, keyword, range, first, second, where} from "./optio
88
import {Scales, ScaleFunctions, autoScaleRange, applyScales, exposeScales} from "./scales.js";
99
import {applyInlineStyles, maybeClassName, maybeClip, styles} from "./style.js";
1010
import {basic} from "./transforms/basic.js";
11+
import {consumeWarnings} from "./warnings.js";
1112

1213
export function plot(options = {}) {
1314
const {facet, style, caption, ariaLabel, ariaDescription} = options;
@@ -119,6 +120,19 @@ export function plot(options = {}) {
119120

120121
figure.scale = exposeScales(scaleDescriptors);
121122
figure.legend = exposeLegends(scaleDescriptors, options);
123+
124+
const w = consumeWarnings();
125+
if (w > 0) {
126+
select(svg).append("text")
127+
.attr("x", width)
128+
.attr("y", 20)
129+
.attr("dy", "-1em")
130+
.attr("text-anchor", "end")
131+
.text("⚠️")
132+
.append("title")
133+
.text(`${w.toLocaleString("en-US")} warning${w === 1 ? "" : "s"}. Please check the console.`);
134+
}
135+
122136
return figure;
123137
}
124138

src/scales.js

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import {parse as isoParse} from "isoformat";
2-
import {isColor, isEvery, isOrdinal, isFirst, isSymbol, isTemporal, maybeSymbol, order} from "./options.js";
2+
import {isColor, isEvery, isOrdinal, isFirst, isSymbol, isTemporal, maybeSymbol, order, isTemporalString, isNumericString} from "./options.js";
33
import {registry, color, position, radius, opacity, symbol, length} from "./scales/index.js";
44
import {ScaleLinear, ScaleSqrt, ScalePow, ScaleLog, ScaleSymlog, ScaleQuantile, ScaleThreshold, ScaleIdentity} from "./scales/quantitative.js";
55
import {ScaleDiverging, ScaleDivergingSqrt, ScaleDivergingPow, ScaleDivergingLog, ScaleDivergingSymlog} from "./scales/diverging.js";
66
import {ScaleTime, ScaleUtc} from "./scales/temporal.js";
77
import {ScaleOrdinal, ScalePoint, ScaleBand, ordinalImplicit} from "./scales/ordinal.js";
8+
import {warn} from "./warnings.js";
89

910
export function Scales(channels, {
1011
inset: globalInset = 0,
@@ -133,6 +134,24 @@ export function normalizeScale(key, scale, hint) {
133134

134135
function Scale(key, channels = [], options = {}) {
135136
const type = inferScaleType(key, channels, options);
137+
138+
// Warn for common misuses of implicit ordinal scales. We disable this test if
139+
// you set the domain or range explicitly, since setting the domain or range
140+
// (typically with a cardinality of more than two) is another indication that
141+
// you intended for the scale to be ordinal; we also disable it for facet
142+
// scales since these are always band scales.
143+
if (options.type === undefined
144+
&& options.domain === undefined
145+
&& options.range === undefined
146+
&& key !== "fx"
147+
&& key !== "fy"
148+
&& isOrdinalScale({type})) {
149+
const values = channels.map(({value}) => value).filter(value => value !== undefined);
150+
if (values.some(isTemporal)) warn(`Warning: some data associated with the ${key} scale are dates. Dates are typically associated with a "utc" or "time" scale rather than a "${formatScaleType(type)}" scale. If you are using a bar mark, you probably want a rect mark with the interval option instead; if you are using a group transform, you probably want a bin transform instead. If you want to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${formatScaleType(type)}".`);
151+
else if (values.some(isTemporalString)) warn(`Warning: some data associated with the ${key} scale are strings that appear to be dates (e.g., YYYY-MM-DD). If these strings represent dates, you should parse them to Date objects. Dates are typically associated with a "utc" or "time" scale rather than a "${formatScaleType(type)}" scale. If you are using a bar mark, you probably want a rect mark with the interval option instead; if you are using a group transform, you probably want a bin transform instead. If you want to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${formatScaleType(type)}".`);
152+
else if (values.some(isNumericString)) warn(`Warning: some data associated with the ${key} scale are strings that appear to be numbers. If these strings represent numbers, you should parse or coerce them to numbers. Numbers are typically associated with a "linear" scale rather than a "${formatScaleType(type)}" scale. If you want to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${formatScaleType(type)}".`);
153+
}
154+
136155
options.type = type; // Mutates input!
137156

138157
// Once the scale type is known, coerce the associated channel values and any
@@ -190,6 +209,10 @@ function Scale(key, channels = [], options = {}) {
190209
}
191210
}
192211

212+
function formatScaleType(type) {
213+
return typeof type === "symbol" ? type.description : type;
214+
}
215+
193216
function inferScaleType(key, channels, {type, domain, range, scheme}) {
194217
// The facet scales are always band scales; this cannot be changed.
195218
if (key === "fx" || key === "fy") return "band";
@@ -272,7 +295,7 @@ export function isTemporalScale({type}) {
272295
}
273296

274297
export function isOrdinalScale({type}) {
275-
return type === "ordinal" || type === "point" || type === "band";
298+
return type === "ordinal" || type === "point" || type === "band" || type === ordinalImplicit;
276299
}
277300

278301
function isThresholdScale({type}) {

src/transforms/window.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {mapX, mapY} from "./map.js";
22
import {deviation, max, min, median, mode, variance} from "d3";
3+
import {warn} from "../warnings.js";
34

45
export function windowX(windowOptions = {}, options) {
56
if (arguments.length === 1) options = windowOptions;
@@ -13,7 +14,11 @@ export function windowY(windowOptions = {}, options) {
1314

1415
export function window(options = {}) {
1516
if (typeof options === "number") options = {k: options};
16-
let {k, reduce, shift, anchor = maybeShift(shift)} = options;
17+
let {k, reduce, shift, anchor} = options;
18+
if (anchor === undefined && shift !== undefined) {
19+
anchor = maybeShift(shift);
20+
warn(`Warning: the shift option is deprecated; please use anchor "${anchor}" instead.`);
21+
}
1722
if (!((k = Math.floor(k)) > 0)) throw new Error("invalid k");
1823
return maybeReduce(reduce)(k, maybeAnchor(anchor, k));
1924
}
@@ -28,8 +33,6 @@ function maybeAnchor(anchor = "middle", k) {
2833
}
2934

3035
function maybeShift(shift) {
31-
if (shift === undefined) return;
32-
console.warn("shift is deprecated; please use anchor instead");
3336
switch (`${shift}`.toLowerCase()) {
3437
case "centered": return "middle";
3538
case "leading": return "start";

src/warnings.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
let warnings = 0;
2+
3+
export function consumeWarnings() {
4+
const w = warnings;
5+
warnings = 0;
6+
return w;
7+
}
8+
9+
export function warn(message) {
10+
console.warn(message);
11+
++warnings;
12+
}

0 commit comments

Comments
 (0)