@opendata-ai/openchart-core 7.9.3 → 7.10.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opendata-ai/openchart-core",
3
- "version": "7.9.3",
3
+ "version": "7.10.0",
4
4
  "description": "Types, theme, colors, accessibility, and utilities for openchart",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Riley Hilliard",
@@ -171,6 +171,54 @@ describe('formatDate', () => {
171
171
  expect(result).toContain('15');
172
172
  expect(result).toContain('Jun');
173
173
  });
174
+
175
+ describe('compact formats', () => {
176
+ it('formats months as bare month name', () => {
177
+ expect(formatDate('2025-02-01', undefined, undefined, true, true)).toBe('Feb');
178
+ });
179
+
180
+ it('infers year granularity at year boundaries (compact stays %Y)', () => {
181
+ expect(formatDate('2025-01-01', undefined, undefined, true, true)).toBe('2025');
182
+ });
183
+
184
+ it('formats days without the year', () => {
185
+ expect(formatDate('2025-03-05', undefined, undefined, true, true)).toBe('Mar 05');
186
+ });
187
+
188
+ it('formats quarters with apostrophe year', () => {
189
+ expect(formatDate('2025-04-15', undefined, 'quarter', true, true)).toBe("Q2 '25");
190
+ });
191
+
192
+ it('formats quarters with local-time year when useUtc is false', () => {
193
+ // Mid-quarter local date: quarter and year agree between local and UTC,
194
+ // exercising the getFullYear() arm of the compact branch.
195
+ expect(formatDate(new Date(2025, 4, 15), undefined, 'quarter', false, true)).toBe("Q2 '25");
196
+ });
197
+
198
+ it('formats weeks as month and day', () => {
199
+ expect(formatDate('2025-03-10', undefined, 'week', true, true)).toBe('Mar 10');
200
+ });
201
+
202
+ it('formats hours as time only', () => {
203
+ expect(formatDate('2025-03-05T14:00:00Z', undefined, undefined, true, true)).toBe('14:00');
204
+ });
205
+
206
+ it('formats minutes as time only', () => {
207
+ expect(formatDate('2025-03-05T14:30:00Z', undefined, undefined, true, true)).toBe('14:30');
208
+ });
209
+
210
+ it('default (compact: false) output is unchanged', () => {
211
+ expect(formatDate('2025-02-01')).toBe('Feb 2025');
212
+ expect(formatDate('2025-03-05')).toBe('Mar 05, 2025');
213
+ expect(formatDate('2025-04-15', undefined, 'quarter')).toBe('Q2 2025');
214
+ });
215
+
216
+ it('full quarter year follows useUtc at year boundaries', () => {
217
+ // In negative-offset timezones this instant is still Dec 31 2025 locally;
218
+ // the UTC quarter (Q1) must pair with the UTC year (2026), not the local one.
219
+ expect(formatDate(new Date('2026-01-01T00:00:00Z'), undefined, 'quarter')).toBe('Q1 2026');
220
+ });
221
+ });
174
222
  });
175
223
 
176
224
  describe('buildTemporalFormatter', () => {
@@ -135,6 +135,22 @@ const GRANULARITY_FORMATS: Record<DateGranularity, string> = {
135
135
  minute: '%H:%M',
136
136
  };
137
137
 
138
+ /**
139
+ * Compact format strings used by axis ticks when full-format labels overlap.
140
+ * Month is bare '%b': granularity inference runs per tick, so year-boundary
141
+ * ticks already render as '2025' and the axis reads "2025 · Feb · Mar".
142
+ * Year stays '%Y' — abbreviating to "'25" saves too little for the ambiguity.
143
+ */
144
+ const GRANULARITY_FORMATS_COMPACT: Record<DateGranularity, string> = {
145
+ year: '%Y',
146
+ quarter: '', // special-cased below, like GRANULARITY_FORMATS
147
+ month: '%b',
148
+ week: '%b %d',
149
+ day: '%b %d',
150
+ hour: '%H:%M',
151
+ minute: '%H:%M',
152
+ };
153
+
138
154
  /**
139
155
  * Format a date value for display.
140
156
  *
@@ -145,12 +161,15 @@ const GRANULARITY_FORMATS: Record<DateGranularity, string> = {
145
161
  * Pass `false` when formatting ticks from a local-time scale (d3 scaleTime),
146
162
  * so that e.g. midnight local isn't misread as an intra-day UTC time.
147
163
  * Defaults to `true` for backward compatibility.
164
+ * @param compact - Use the compact format table (axis ticks whose full-format
165
+ * labels would overlap). Defaults to `false`.
148
166
  */
149
167
  export function formatDate(
150
168
  value: Date | string | number,
151
169
  _locale?: string,
152
170
  granularity?: DateGranularity,
153
171
  useUtc: boolean = true,
172
+ compact: boolean = false,
154
173
  ): string {
155
174
  const date = value instanceof Date ? value : new Date(value);
156
175
  if (Number.isNaN(date.getTime())) return String(value);
@@ -162,10 +181,14 @@ export function formatDate(
162
181
  const q = useUtc
163
182
  ? Math.ceil((date.getUTCMonth() + 1) / 3)
164
183
  : Math.ceil((date.getMonth() + 1) / 3);
165
- return `Q${q} ${date.getFullYear()}`;
184
+ const year = useUtc ? date.getUTCFullYear() : date.getFullYear();
185
+ if (compact) {
186
+ return `Q${q} '${String(year).slice(-2)}`;
187
+ }
188
+ return `Q${q} ${year}`;
166
189
  }
167
190
 
168
- const formatStr = GRANULARITY_FORMATS[gran];
191
+ const formatStr = (compact ? GRANULARITY_FORMATS_COMPACT : GRANULARITY_FORMATS)[gran];
169
192
  if (useUtc) {
170
193
  return utcFormat(formatStr)(date);
171
194
  }