Skip to content

Commit a017e1f

Browse files
committed
warnings!
1 parent 776d3ed commit a017e1f

10 files changed

+229
-265
lines changed

src/options.js

Lines changed: 12 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,17 @@ 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+
213225
export function isNumeric(values) {
214226
for (const value of values) {
215227
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, 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: 15 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} 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,14 @@ 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 misuse of implicit band scales.
139+
if (options.type === undefined && type === "band") {
140+
const values = channels.map(({value}) => value).filter(value => value !== undefined);
141+
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 "band" scale. If you are using a bar mark, you probably want to switch to a rect mark with the interval option; if you are using a group transform, you probably want to switch to a bin transform. If you intend to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "band".`);
142+
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 and convert them to Date objects. Dates are typically associated with a "utc" or "time" scale rather than a "band" scale. If you are using a bar mark, you probably want to switch to a rect mark with the interval option; if you are using a group transform, you probably want to switch to a bin transform. If you intend to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${typeof type === "symbol" ? type.description : type}".`);
143+
}
144+
136145
options.type = type; // Mutates input!
137146

138147
// Once the scale type is known, coerce the associated channel values and any
@@ -246,7 +255,11 @@ function inferScaleType(key, channels, {type, domain, range, scheme}) {
246255

247256
// If any channel is ordinal or temporal, it takes priority.
248257
const values = channels.map(({value}) => value).filter(value => value !== undefined);
249-
if (values.some(isOrdinal)) return asOrdinalType(kind);
258+
if (values.some(isOrdinal)) {
259+
const type = asOrdinalType(kind);
260+
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 and convert them to Date objects. If you intend to treat this data as ordinal, you can suppress this warning by setting the type of the ${key} scale to "${typeof type === "symbol" ? type.description : type}".`);
261+
return type;
262+
}
250263
if (values.some(isTemporal)) return "utc";
251264
return "linear";
252265
}

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)