@opendata-ai/openchart-core 6.1.4 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +10 -2
- package/dist/index.js +14 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/locale/__tests__/format.test.ts +79 -1
- package/src/locale/format.ts +21 -4
- package/src/locale/index.ts +7 -1
- package/src/types/spec.ts +3 -1
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
abbreviateNumber,
|
|
4
|
+
buildD3Formatter,
|
|
5
|
+
buildTemporalFormatter,
|
|
6
|
+
formatDate,
|
|
7
|
+
formatNumber,
|
|
8
|
+
} from '../format';
|
|
3
9
|
|
|
4
10
|
describe('formatNumber', () => {
|
|
5
11
|
it('formats integers with commas', () => {
|
|
@@ -126,4 +132,76 @@ describe('formatDate', () => {
|
|
|
126
132
|
const result = formatDate('not-a-date');
|
|
127
133
|
expect(result).toBe('not-a-date');
|
|
128
134
|
});
|
|
135
|
+
|
|
136
|
+
it('infers year granularity for bare year strings regardless of timezone', () => {
|
|
137
|
+
// "2020" parses as 2020-01-01T00:00:00Z. In negative-offset timezones,
|
|
138
|
+
// local getHours()/getDate() would see Dec 31 2019 — the UTC fix prevents that.
|
|
139
|
+
const result = formatDate('2020');
|
|
140
|
+
expect(result).toBe('2020');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('infers year granularity for Jan 1 ISO dates', () => {
|
|
144
|
+
const result = formatDate('2020-01-01');
|
|
145
|
+
expect(result).toBe('2020');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('infers month granularity for first-of-month dates', () => {
|
|
149
|
+
const result = formatDate('2020-06-01');
|
|
150
|
+
expect(result).toContain('Jun');
|
|
151
|
+
expect(result).toContain('2020');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('infers day granularity for mid-month dates', () => {
|
|
155
|
+
const result = formatDate('2020-06-15');
|
|
156
|
+
expect(result).toContain('15');
|
|
157
|
+
expect(result).toContain('Jun');
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe('buildTemporalFormatter', () => {
|
|
162
|
+
it('returns null for undefined format', () => {
|
|
163
|
+
expect(buildTemporalFormatter(undefined)).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('returns null for empty string', () => {
|
|
167
|
+
expect(buildTemporalFormatter('')).toBeNull();
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('formats dates with %Y to just the year', () => {
|
|
171
|
+
const fmt = buildTemporalFormatter('%Y');
|
|
172
|
+
expect(fmt).not.toBeNull();
|
|
173
|
+
expect(fmt!('2020-01-01')).toBe('2020');
|
|
174
|
+
expect(fmt!(new Date('2020-06-15'))).toBe('2020');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('formats dates with %b %Y to month and year', () => {
|
|
178
|
+
const fmt = buildTemporalFormatter('%b %Y');
|
|
179
|
+
expect(fmt).not.toBeNull();
|
|
180
|
+
expect(fmt!('2020-06-01')).toBe('Jun 2020');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('formats dates with full date format', () => {
|
|
184
|
+
const fmt = buildTemporalFormatter('%Y-%m-%d');
|
|
185
|
+
expect(fmt).not.toBeNull();
|
|
186
|
+
expect(fmt!('2020-06-15')).toBe('2020-06-15');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('handles invalid date input gracefully', () => {
|
|
190
|
+
const fmt = buildTemporalFormatter('%Y');
|
|
191
|
+
expect(fmt).not.toBeNull();
|
|
192
|
+
expect(fmt!('not-a-date')).toBe('not-a-date');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('handles Date objects', () => {
|
|
196
|
+
const fmt = buildTemporalFormatter('%Y');
|
|
197
|
+
expect(fmt).not.toBeNull();
|
|
198
|
+
expect(fmt!(new Date('2025-01-01T00:00:00Z'))).toBe('2025');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('handles numeric timestamps', () => {
|
|
202
|
+
const fmt = buildTemporalFormatter('%Y');
|
|
203
|
+
expect(fmt).not.toBeNull();
|
|
204
|
+
// 2020-01-01T00:00:00Z
|
|
205
|
+
expect(fmt!(1577836800000)).toBe('2020');
|
|
206
|
+
});
|
|
129
207
|
});
|
package/src/locale/format.ts
CHANGED
|
@@ -161,15 +161,32 @@ export function formatDate(
|
|
|
161
161
|
return timeFormat(formatStr)(date);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Build a formatter for temporal values using a d3-time-format string (e.g. "%Y", "%b %Y").
|
|
166
|
+
* Returns a function that accepts a Date, string, or number and returns the formatted string.
|
|
167
|
+
* Returns null if the format string is falsy.
|
|
168
|
+
*/
|
|
169
|
+
export function buildTemporalFormatter(
|
|
170
|
+
formatStr: string | undefined,
|
|
171
|
+
): ((value: Date | string | number) => string) | null {
|
|
172
|
+
if (!formatStr) return null;
|
|
173
|
+
const fmt = utcFormat(formatStr);
|
|
174
|
+
return (value: Date | string | number) => {
|
|
175
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
176
|
+
if (Number.isNaN(date.getTime())) return String(value);
|
|
177
|
+
return fmt(date);
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
164
181
|
/**
|
|
165
182
|
* Infer the appropriate granularity from a date value.
|
|
166
183
|
* If time components are all zero, assume day or higher.
|
|
167
184
|
*/
|
|
168
185
|
function inferGranularity(date: Date): DateGranularity {
|
|
169
|
-
if (date.
|
|
170
|
-
return date.
|
|
186
|
+
if (date.getUTCHours() !== 0 || date.getUTCMinutes() !== 0) {
|
|
187
|
+
return date.getUTCMinutes() !== 0 ? 'minute' : 'hour';
|
|
171
188
|
}
|
|
172
|
-
if (date.
|
|
173
|
-
if (date.
|
|
189
|
+
if (date.getUTCDate() !== 1) return 'day';
|
|
190
|
+
if (date.getUTCMonth() !== 0) return 'month';
|
|
174
191
|
return 'year';
|
|
175
192
|
}
|
package/src/locale/index.ts
CHANGED
|
@@ -3,4 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export type { DateGranularity } from './format';
|
|
6
|
-
export {
|
|
6
|
+
export {
|
|
7
|
+
abbreviateNumber,
|
|
8
|
+
buildD3Formatter,
|
|
9
|
+
buildTemporalFormatter,
|
|
10
|
+
formatDate,
|
|
11
|
+
formatNumber,
|
|
12
|
+
} from './format';
|
package/src/types/spec.ts
CHANGED
|
@@ -273,12 +273,14 @@ export interface Encoding {
|
|
|
273
273
|
// Graph-specific encoding
|
|
274
274
|
// ---------------------------------------------------------------------------
|
|
275
275
|
|
|
276
|
-
/** Encoding channel for graph nodes and edges.
|
|
276
|
+
/** Encoding channel for graph nodes and edges. */
|
|
277
277
|
export interface GraphEncodingChannel {
|
|
278
278
|
/** Data field name on the node/edge object. */
|
|
279
279
|
field: string;
|
|
280
280
|
/** How to interpret the field values. */
|
|
281
281
|
type?: FieldType;
|
|
282
|
+
/** Scale configuration. Auto-derived from data if omitted. */
|
|
283
|
+
scale?: ScaleConfig;
|
|
282
284
|
}
|
|
283
285
|
|
|
284
286
|
/** Graph-specific encoding mapping visual properties to node/edge data fields. */
|