@@ -9,21 +9,27 @@ const objectToString = Object.prototype.toString;
99/** @jsdoc valueof */
1010export function valueof ( data , value , type ) {
1111 const valueType = typeof value ;
12- const access =
13- type && Object . getPrototypeOf ( type ) === TypedArray
14- ? ( value ) => floatMap ( data , value , type )
15- : ( value ) => map ( data , value ) ;
1612 return valueType === "string"
17- ? access ( field ( value ) )
13+ ? maybeTypedMap ( data , field ( value ) , type )
1814 : valueType === "function"
19- ? access ( value )
15+ ? maybeTypedMap ( data , value , type )
2016 : valueType === "number" || value instanceof Date || valueType === "boolean"
21- ? access ( constant ( value ) )
17+ ? map ( data , constant ( value ) , type )
2218 : value && typeof value . transform === "function"
2319 ? arrayify ( value . transform ( data ) , type )
2420 : arrayify ( value , type ) ; // preserve undefined type
2521}
2622
23+ // When valueof is asked to produce a typed array (i.e., numbers) we implicitly
24+ // apply null-safe type coercion.
25+ function maybeTypedMap ( data , f , type ) {
26+ return map ( data , isTypedArray ( type ?. prototype ) ? floater ( f ) : f , type ) ;
27+ }
28+
29+ function floater ( f ) {
30+ return ( d , i ) => coerceNumber ( f ( d , i ) ) ;
31+ }
32+
2733export const field = ( name ) => ( d ) => d [ name ] ;
2834export const indexOf = ( d , i ) => i ;
2935/** @jsdoc identity */
@@ -46,6 +52,38 @@ export function percentile(reduce) {
4652 return ( I , f ) => quantile ( I , p , f ) ;
4753}
4854
55+ // If the values are specified as a typed array, no coercion is required.
56+ export function coerceNumbers ( values ) {
57+ return isTypedArray ( values ) ? values : map ( values , coerceNumber , Float64Array ) ;
58+ }
59+
60+ // Unlike Mark’s number, here we want to convert null and undefined to NaN since
61+ // the result will be stored in a Float64Array and we don’t want null to be
62+ // coerced to zero. We use Number instead of unary + to allow BigInt coercion.
63+ export function coerceNumber ( x ) {
64+ return x == null ? NaN : Number ( x ) ;
65+ }
66+
67+ export function coerceDates ( values ) {
68+ return map ( values , coerceDate ) ;
69+ }
70+
71+ // When coercing strings to dates, we only want to allow the ISO 8601 format
72+ // since the built-in string parsing of the Date constructor varies across
73+ // browsers. (In the future, this could be made more liberal if desired, though
74+ // it is still generally preferable to do date parsing yourself explicitly,
75+ // rather than rely on Plot.) Any non-string values are coerced to number first
76+ // and treated as milliseconds since UNIX epoch.
77+ export function coerceDate ( x ) {
78+ return x instanceof Date && ! isNaN ( x )
79+ ? x
80+ : typeof x === "string"
81+ ? isoParse ( x )
82+ : x == null || isNaN ( ( x = + x ) )
83+ ? undefined
84+ : new Date ( x ) ;
85+ }
86+
4987// Some channels may allow a string constant to be specified; to differentiate
5088// string constants (e.g., "red") from named fields (e.g., "date"), this
5189// function tests whether the given value is a CSS color string and returns a
@@ -79,7 +117,9 @@ export function keyword(input, name, allowed) {
79117// Promotes the specified data to an array or typed array as needed. If an array
80118// type is provided (e.g., Array), then the returned array will strictly be of
81119// the specified type; otherwise, any array or typed array may be returned. If
82- // the specified data is null or undefined, returns the value as-is.
120+ // the specified data is null or undefined, returns the value as-is. When
121+ // converting a non-typed array to a typed array, null-safe number coercion is
122+ // implicitly applied.
83123export function arrayify ( data , type ) {
84124 return data == null
85125 ? data
@@ -89,30 +129,13 @@ export function arrayify(data, type) {
89129 : Array . from ( data )
90130 : data instanceof type
91131 ? data
92- : type . from ( data ) ;
132+ : type . from ( data , isTypedArray ( type . prototype ) && ! isTypedArray ( data ) ? coerceNumber : undefined ) ;
93133}
94134
95135// An optimization of type.from(values, f): if the given values are already an
96136// instanceof the desired array type, the faster values.map method is used.
97- export function map ( values , f ) {
98- return values instanceof Array ? values . map ( f ) : Array . from ( values , f ) ;
99- }
100-
101- // Unlike Mark’s number, this converts null and undefined to NaN, since the
102- // result will be stored in a Float64Array and we don’t want null to be coerced
103- // to zero. Using Number to coerce BigInts.
104- function toFloat ( x ) {
105- return x == null ? NaN : Number ( x ) ;
106- }
107-
108- export function floatMap ( values , f , type = Float64Array ) {
109- return f === undefined
110- ? values instanceof type
111- ? values . map ( toFloat )
112- : type . from ( values , toFloat )
113- : values instanceof type
114- ? values . map ( ( d , i ) => toFloat ( f ( d , i ) ) )
115- : type . from ( values , ( d , i ) => toFloat ( f ( d , i ) ) ) ;
137+ export function map ( values , f , type = Array ) {
138+ return values instanceof type ? values . map ( f ) : type . from ( values , f ) ;
116139}
117140
118141// An optimization of type.from(values): if the given values are already an
@@ -252,7 +275,7 @@ export function mid(x1, x2) {
252275 const X2 = x2 . transform ( data ) ;
253276 return isTemporal ( X1 ) || isTemporal ( X2 )
254277 ? map ( X1 , ( _ , i ) => new Date ( ( + X1 [ i ] + + X2 [ i ] ) / 2 ) )
255- : floatMap ( X1 , ( _ , i ) => ( + X1 [ i ] + + X2 [ i ] ) / 2 ) ;
278+ : map ( X1 , ( _ , i ) => ( + X1 [ i ] + + X2 [ i ] ) / 2 , Float64Array ) ;
256279 } ,
257280 label : x1 . label
258281 } ;
0 commit comments