diff --git a/packages/graphic-walker/src/components/dataTable/pagination.tsx b/packages/graphic-walker/src/components/dataTable/pagination.tsx
index b944d6a5..3ce2a3ce 100644
--- a/packages/graphic-walker/src/components/dataTable/pagination.tsx
+++ b/packages/graphic-walker/src/components/dataTable/pagination.tsx
@@ -91,7 +91,7 @@ export default function Pagination(props: PaginationProps) {
)}
- {showIndices.slice(1, -1).map((page) => pageButton(page.index))}
+ {showIndices.slice(1, showIndices.length > 2 ? -1 : undefined).map((page) => pageButton(page.index))}
{showIndices.length > 2 && showIndices[showIndices.length - 1].index > showIndices[showIndices.length - 2].index + 1 && (
diff --git a/packages/graphic-walker/src/components/dropdownSelect/combobox.tsx b/packages/graphic-walker/src/components/dropdownSelect/combobox.tsx
index 125e6680..701f304e 100644
--- a/packages/graphic-walker/src/components/dropdownSelect/combobox.tsx
+++ b/packages/graphic-walker/src/components/dropdownSelect/combobox.tsx
@@ -40,11 +40,11 @@ function Combobox({
{
- if (currentValue === '_none') {
+ onSelect={() => {
+ if (opt.value === '_none') {
onSelect?.('');
} else {
- onSelect?.(currentValue === selectedKey ? '' : currentValue);
+ onSelect?.(opt.value === selectedKey ? '' : opt.value);
}
setOpen(false);
}}
diff --git a/packages/graphic-walker/src/constants.ts b/packages/graphic-walker/src/constants.ts
index cf83757d..d8f8ab31 100644
--- a/packages/graphic-walker/src/constants.ts
+++ b/packages/graphic-walker/src/constants.ts
@@ -1,11 +1,20 @@
export const COUNT_FIELD_ID = 'gw_count_fid';
-export const DATE_TIME_DRILL_LEVELS = [
- "year", "quarter", "month", "week", "day", "hour", "minute", "second"
-] as const;
+export const DATE_TIME_DRILL_LEVELS = ['year', 'quarter', 'month', 'week', 'day', 'hour', 'minute', 'second', 'iso_year', 'iso_week'] as const;
export const DATE_TIME_FEATURE_LEVELS = [
- "year", "quarter", "month", "week", "weekday", "day", "hour", "minute", "second"
+ 'year',
+ 'quarter',
+ 'month',
+ 'week',
+ 'weekday',
+ 'day',
+ 'hour',
+ 'minute',
+ 'second',
+ 'iso_year',
+ 'iso_week',
+ 'iso_weekday',
] as const;
export const MEA_KEY_ID = 'gw_mea_key_fid';
export const MEA_VAL_ID = 'gw_mea_val_fid';
-export const PAINT_FIELD_ID = 'gw_paint_fid';
\ No newline at end of file
+export const PAINT_FIELD_ID = 'gw_paint_fid';
diff --git a/packages/graphic-walker/src/lib/op/dateTime.test.ts b/packages/graphic-walker/src/lib/op/dateTime.test.ts
new file mode 100644
index 00000000..f5f68fac
--- /dev/null
+++ b/packages/graphic-walker/src/lib/op/dateTime.test.ts
@@ -0,0 +1,539 @@
+import dateTimeDrill from './dateTimeDrill';
+import dateTimeFeature from './dateTimeFeature';
+
+function bind(a: any[], b: any[]) {
+ return a.map((v, i) => [v, b[i]]);
+}
+
+const getParams = (value: T) => [
+ {
+ type: 'field',
+ value: 'date',
+ } as const,
+ {
+ type: 'value',
+ value,
+ } as const,
+ {
+ type: 'displayOffset',
+ value: 0,
+ } as const,
+];
+
+const date = [
+ '2010-01-01T07:41:08.000Z',
+ '2010-04-22T03:21:58.000Z',
+ '2010-12-31T15:38:24.000Z',
+ '2011-01-01T08:55:46.000Z',
+ '2011-08-15T14:23:12.000Z',
+ '2011-12-31T04:45:38.000Z',
+ '2012-01-01T21:53:07.000Z',
+ '2012-02-20T14:33:22.000Z',
+ '2012-12-31T01:49:05.000Z',
+ '2013-01-01T17:06:33.000Z',
+ '2013-10-05T02:11:42.000Z',
+ '2013-12-31T12:42:53.000Z',
+ '2014-01-01T13:45:49.000Z',
+ '2014-07-04T08:33:12.000Z',
+ '2014-12-30T16:39:56.000Z',
+ '2015-01-01T20:09:42.000Z',
+ '2015-11-11T18:25:08.000Z',
+ '2015-12-31T06:45:31.000Z',
+ '2016-01-01T11:33:45.000Z',
+ '2016-09-09T15:01:40.000Z',
+ '2016-12-31T04:03:11.000Z',
+ '2017-01-01T13:22:04.000Z',
+ '2017-06-03T09:38:09.000Z',
+ '2017-12-31T12:14:58.000Z',
+ '2018-01-01T22:54:44.000Z',
+ '2018-05-05T17:43:32.000Z',
+ '2018-12-31T04:45:25.000Z',
+];
+
+// export const DATE_TIME_DRILL_LEVELS = [
+// "year", "quarter", "month", "week", "day", "hour", "minute", "second", 'iso_year', 'iso_week',
+// ] as const;
+describe('drill', () => {
+ test('year', () => {
+ const { result } = dateTimeDrill('result', getParams('year'), { date });
+ expect(
+ bind(
+ date,
+ result.map((x) => new Date(x).toISOString())
+ )
+ ).toEqual(
+ bind(date, [
+ '2010-01-01T00:00:00.000Z',
+ '2010-01-01T00:00:00.000Z',
+ '2010-01-01T00:00:00.000Z',
+ '2011-01-01T00:00:00.000Z',
+ '2011-01-01T00:00:00.000Z',
+ '2011-01-01T00:00:00.000Z',
+ '2012-01-01T00:00:00.000Z',
+ '2012-01-01T00:00:00.000Z',
+ '2012-01-01T00:00:00.000Z',
+ '2013-01-01T00:00:00.000Z',
+ '2013-01-01T00:00:00.000Z',
+ '2013-01-01T00:00:00.000Z',
+ '2014-01-01T00:00:00.000Z',
+ '2014-01-01T00:00:00.000Z',
+ '2014-01-01T00:00:00.000Z',
+ '2015-01-01T00:00:00.000Z',
+ '2015-01-01T00:00:00.000Z',
+ '2015-01-01T00:00:00.000Z',
+ '2016-01-01T00:00:00.000Z',
+ '2016-01-01T00:00:00.000Z',
+ '2016-01-01T00:00:00.000Z',
+ '2017-01-01T00:00:00.000Z',
+ '2017-01-01T00:00:00.000Z',
+ '2017-01-01T00:00:00.000Z',
+ '2018-01-01T00:00:00.000Z',
+ '2018-01-01T00:00:00.000Z',
+ '2018-01-01T00:00:00.000Z',
+ ])
+ );
+ });
+ test('quarter', () => {
+ const { result } = dateTimeDrill(
+ 'result',
+ getParams('quarter'),
+
+ { date }
+ );
+ expect(
+ bind(
+ date,
+ result.map((x) => new Date(x).toISOString())
+ )
+ ).toEqual(
+ bind(date, [
+ '2010-01-01T00:00:00.000Z',
+ '2010-04-01T00:00:00.000Z',
+ '2010-10-01T00:00:00.000Z',
+ '2011-01-01T00:00:00.000Z',
+ '2011-07-01T00:00:00.000Z',
+ '2011-10-01T00:00:00.000Z',
+ '2012-01-01T00:00:00.000Z',
+ '2012-01-01T00:00:00.000Z',
+ '2012-10-01T00:00:00.000Z',
+ '2013-01-01T00:00:00.000Z',
+ '2013-10-01T00:00:00.000Z',
+ '2013-10-01T00:00:00.000Z',
+ '2014-01-01T00:00:00.000Z',
+ '2014-07-01T00:00:00.000Z',
+ '2014-10-01T00:00:00.000Z',
+ '2015-01-01T00:00:00.000Z',
+ '2015-10-01T00:00:00.000Z',
+ '2015-10-01T00:00:00.000Z',
+ '2016-01-01T00:00:00.000Z',
+ '2016-07-01T00:00:00.000Z',
+ '2016-10-01T00:00:00.000Z',
+ '2017-01-01T00:00:00.000Z',
+ '2017-04-01T00:00:00.000Z',
+ '2017-10-01T00:00:00.000Z',
+ '2018-01-01T00:00:00.000Z',
+ '2018-04-01T00:00:00.000Z',
+ '2018-10-01T00:00:00.000Z',
+ ])
+ );
+ });
+ test('month', () => {
+ const { result } = dateTimeDrill('result', getParams('month'), { date });
+ expect(
+ bind(
+ date,
+ result.map((x) => new Date(x).toISOString())
+ )
+ ).toEqual(
+ bind(date, [
+ '2010-01-01T00:00:00.000Z',
+ '2010-04-01T00:00:00.000Z',
+ '2010-12-01T00:00:00.000Z',
+ '2011-01-01T00:00:00.000Z',
+ '2011-08-01T00:00:00.000Z',
+ '2011-12-01T00:00:00.000Z',
+ '2012-01-01T00:00:00.000Z',
+ '2012-02-01T00:00:00.000Z',
+ '2012-12-01T00:00:00.000Z',
+ '2013-01-01T00:00:00.000Z',
+ '2013-10-01T00:00:00.000Z',
+ '2013-12-01T00:00:00.000Z',
+ '2014-01-01T00:00:00.000Z',
+ '2014-07-01T00:00:00.000Z',
+ '2014-12-01T00:00:00.000Z',
+ '2015-01-01T00:00:00.000Z',
+ '2015-11-01T00:00:00.000Z',
+ '2015-12-01T00:00:00.000Z',
+ '2016-01-01T00:00:00.000Z',
+ '2016-09-01T00:00:00.000Z',
+ '2016-12-01T00:00:00.000Z',
+ '2017-01-01T00:00:00.000Z',
+ '2017-06-01T00:00:00.000Z',
+ '2017-12-01T00:00:00.000Z',
+ '2018-01-01T00:00:00.000Z',
+ '2018-05-01T00:00:00.000Z',
+ '2018-12-01T00:00:00.000Z',
+ ])
+ );
+ });
+ test('week', () => {
+ const { result } = dateTimeDrill('result', getParams('week'), { date });
+ expect(
+ bind(
+ date,
+ result.map((x) => new Date(x).toISOString())
+ )
+ ).toEqual(
+ bind(date, [
+ '2009-12-27T00:00:00.000Z',
+ '2010-04-18T00:00:00.000Z',
+ '2010-12-26T00:00:00.000Z',
+ '2010-12-26T00:00:00.000Z',
+ '2011-08-14T00:00:00.000Z',
+ '2011-12-25T00:00:00.000Z',
+ '2012-01-01T00:00:00.000Z',
+ '2012-02-19T00:00:00.000Z',
+ '2012-12-30T00:00:00.000Z',
+ '2012-12-30T00:00:00.000Z',
+ '2013-09-29T00:00:00.000Z',
+ '2013-12-29T00:00:00.000Z',
+ '2013-12-29T00:00:00.000Z',
+ '2014-06-29T00:00:00.000Z',
+ '2014-12-28T00:00:00.000Z',
+ '2014-12-28T00:00:00.000Z',
+ '2015-11-08T00:00:00.000Z',
+ '2015-12-27T00:00:00.000Z',
+ '2015-12-27T00:00:00.000Z',
+ '2016-09-04T00:00:00.000Z',
+ '2016-12-25T00:00:00.000Z',
+ '2017-01-01T00:00:00.000Z',
+ '2017-05-28T00:00:00.000Z',
+ '2017-12-31T00:00:00.000Z',
+ '2017-12-31T00:00:00.000Z',
+ '2018-04-29T00:00:00.000Z',
+ '2018-12-30T00:00:00.000Z',
+ ])
+ );
+ });
+ test('day', () => {
+ const { result } = dateTimeDrill('result', getParams('day'), { date });
+ expect(
+ bind(
+ date,
+ result.map((x) => new Date(x).toISOString())
+ )
+ ).toEqual(
+ bind(date, [
+ '2010-01-01T00:00:00.000Z',
+ '2010-04-22T00:00:00.000Z',
+ '2010-12-31T00:00:00.000Z',
+ '2011-01-01T00:00:00.000Z',
+ '2011-08-15T00:00:00.000Z',
+ '2011-12-31T00:00:00.000Z',
+ '2012-01-01T00:00:00.000Z',
+ '2012-02-20T00:00:00.000Z',
+ '2012-12-31T00:00:00.000Z',
+ '2013-01-01T00:00:00.000Z',
+ '2013-10-05T00:00:00.000Z',
+ '2013-12-31T00:00:00.000Z',
+ '2014-01-01T00:00:00.000Z',
+ '2014-07-04T00:00:00.000Z',
+ '2014-12-30T00:00:00.000Z',
+ '2015-01-01T00:00:00.000Z',
+ '2015-11-11T00:00:00.000Z',
+ '2015-12-31T00:00:00.000Z',
+ '2016-01-01T00:00:00.000Z',
+ '2016-09-09T00:00:00.000Z',
+ '2016-12-31T00:00:00.000Z',
+ '2017-01-01T00:00:00.000Z',
+ '2017-06-03T00:00:00.000Z',
+ '2017-12-31T00:00:00.000Z',
+ '2018-01-01T00:00:00.000Z',
+ '2018-05-05T00:00:00.000Z',
+ '2018-12-31T00:00:00.000Z',
+ ])
+ );
+ });
+ test('hour', () => {
+ const { result } = dateTimeDrill('result', getParams('hour'), { date });
+ expect(
+ bind(
+ date,
+ result.map((x) => new Date(x).toISOString())
+ )
+ ).toEqual(
+ bind(date, [
+ '2010-01-01T07:00:00.000Z',
+ '2010-04-22T03:00:00.000Z',
+ '2010-12-31T15:00:00.000Z',
+ '2011-01-01T08:00:00.000Z',
+ '2011-08-15T14:00:00.000Z',
+ '2011-12-31T04:00:00.000Z',
+ '2012-01-01T21:00:00.000Z',
+ '2012-02-20T14:00:00.000Z',
+ '2012-12-31T01:00:00.000Z',
+ '2013-01-01T17:00:00.000Z',
+ '2013-10-05T02:00:00.000Z',
+ '2013-12-31T12:00:00.000Z',
+ '2014-01-01T13:00:00.000Z',
+ '2014-07-04T08:00:00.000Z',
+ '2014-12-30T16:00:00.000Z',
+ '2015-01-01T20:00:00.000Z',
+ '2015-11-11T18:00:00.000Z',
+ '2015-12-31T06:00:00.000Z',
+ '2016-01-01T11:00:00.000Z',
+ '2016-09-09T15:00:00.000Z',
+ '2016-12-31T04:00:00.000Z',
+ '2017-01-01T13:00:00.000Z',
+ '2017-06-03T09:00:00.000Z',
+ '2017-12-31T12:00:00.000Z',
+ '2018-01-01T22:00:00.000Z',
+ '2018-05-05T17:00:00.000Z',
+ '2018-12-31T04:00:00.000Z',
+ ])
+ );
+ });
+ test('minute', () => {
+ const { result } = dateTimeDrill('result', getParams('minute'), { date });
+ expect(
+ bind(
+ date,
+ result.map((x) => new Date(x).toISOString())
+ )
+ ).toEqual(
+ bind(date, [
+ '2010-01-01T07:41:00.000Z',
+ '2010-04-22T03:21:00.000Z',
+ '2010-12-31T15:38:00.000Z',
+ '2011-01-01T08:55:00.000Z',
+ '2011-08-15T14:23:00.000Z',
+ '2011-12-31T04:45:00.000Z',
+ '2012-01-01T21:53:00.000Z',
+ '2012-02-20T14:33:00.000Z',
+ '2012-12-31T01:49:00.000Z',
+ '2013-01-01T17:06:00.000Z',
+ '2013-10-05T02:11:00.000Z',
+ '2013-12-31T12:42:00.000Z',
+ '2014-01-01T13:45:00.000Z',
+ '2014-07-04T08:33:00.000Z',
+ '2014-12-30T16:39:00.000Z',
+ '2015-01-01T20:09:00.000Z',
+ '2015-11-11T18:25:00.000Z',
+ '2015-12-31T06:45:00.000Z',
+ '2016-01-01T11:33:00.000Z',
+ '2016-09-09T15:01:00.000Z',
+ '2016-12-31T04:03:00.000Z',
+ '2017-01-01T13:22:00.000Z',
+ '2017-06-03T09:38:00.000Z',
+ '2017-12-31T12:14:00.000Z',
+ '2018-01-01T22:54:00.000Z',
+ '2018-05-05T17:43:00.000Z',
+ '2018-12-31T04:45:00.000Z',
+ ])
+ );
+ });
+ test('second', () => {
+ const { result } = dateTimeDrill('result', getParams('second'), { date });
+ expect(
+ bind(
+ date,
+ result.map((x) => new Date(x).toISOString())
+ )
+ ).toEqual(
+ bind(date, [
+ '2010-01-01T07:41:08.000Z',
+ '2010-04-22T03:21:58.000Z',
+ '2010-12-31T15:38:24.000Z',
+ '2011-01-01T08:55:46.000Z',
+ '2011-08-15T14:23:12.000Z',
+ '2011-12-31T04:45:38.000Z',
+ '2012-01-01T21:53:07.000Z',
+ '2012-02-20T14:33:22.000Z',
+ '2012-12-31T01:49:05.000Z',
+ '2013-01-01T17:06:33.000Z',
+ '2013-10-05T02:11:42.000Z',
+ '2013-12-31T12:42:53.000Z',
+ '2014-01-01T13:45:49.000Z',
+ '2014-07-04T08:33:12.000Z',
+ '2014-12-30T16:39:56.000Z',
+ '2015-01-01T20:09:42.000Z',
+ '2015-11-11T18:25:08.000Z',
+ '2015-12-31T06:45:31.000Z',
+ '2016-01-01T11:33:45.000Z',
+ '2016-09-09T15:01:40.000Z',
+ '2016-12-31T04:03:11.000Z',
+ '2017-01-01T13:22:04.000Z',
+ '2017-06-03T09:38:09.000Z',
+ '2017-12-31T12:14:58.000Z',
+ '2018-01-01T22:54:44.000Z',
+ '2018-05-05T17:43:32.000Z',
+ '2018-12-31T04:45:25.000Z',
+ ])
+ );
+ });
+
+ test('iso_year', () => {
+ const { result } = dateTimeDrill('result', getParams('iso_year'), { date });
+ expect(
+ bind(
+ date,
+ result.map((x) => new Date(x).toISOString())
+ )
+ ).toEqual(
+ bind(date, [
+ '2009-01-01T00:00:00.000Z',
+ '2010-01-01T00:00:00.000Z',
+ '2010-01-01T00:00:00.000Z',
+ '2010-01-01T00:00:00.000Z',
+ '2011-01-01T00:00:00.000Z',
+ '2011-01-01T00:00:00.000Z',
+ '2011-01-01T00:00:00.000Z',
+ '2012-01-01T00:00:00.000Z',
+ '2013-01-01T00:00:00.000Z',
+ '2013-01-01T00:00:00.000Z',
+ '2013-01-01T00:00:00.000Z',
+ '2014-01-01T00:00:00.000Z',
+ '2014-01-01T00:00:00.000Z',
+ '2014-01-01T00:00:00.000Z',
+ '2015-01-01T00:00:00.000Z',
+ '2015-01-01T00:00:00.000Z',
+ '2015-01-01T00:00:00.000Z',
+ '2015-01-01T00:00:00.000Z',
+ '2015-01-01T00:00:00.000Z',
+ '2016-01-01T00:00:00.000Z',
+ '2016-01-01T00:00:00.000Z',
+ '2016-01-01T00:00:00.000Z',
+ '2017-01-01T00:00:00.000Z',
+ '2017-01-01T00:00:00.000Z',
+ '2018-01-01T00:00:00.000Z',
+ '2018-01-01T00:00:00.000Z',
+ '2019-01-01T00:00:00.000Z',
+ ])
+ );
+ });
+ test('iso_week', () => {
+ const { result } = dateTimeDrill('result', getParams('iso_week'), { date });
+ expect(
+ bind(
+ date,
+ result.map((x) => new Date(x).toISOString())
+ )
+ ).toEqual(
+ bind(date, [
+ '2009-12-28T00:00:00.000Z',
+ '2010-04-19T00:00:00.000Z',
+ '2010-12-27T00:00:00.000Z',
+ '2010-12-27T00:00:00.000Z',
+ '2011-08-15T00:00:00.000Z',
+ '2011-12-26T00:00:00.000Z',
+ '2011-12-26T00:00:00.000Z',
+ '2012-02-20T00:00:00.000Z',
+ '2012-12-31T00:00:00.000Z',
+ '2012-12-31T00:00:00.000Z',
+ '2013-09-30T00:00:00.000Z',
+ '2013-12-30T00:00:00.000Z',
+ '2013-12-30T00:00:00.000Z',
+ '2014-06-30T00:00:00.000Z',
+ '2014-12-29T00:00:00.000Z',
+ '2014-12-29T00:00:00.000Z',
+ '2015-11-09T00:00:00.000Z',
+ '2015-12-28T00:00:00.000Z',
+ '2015-12-28T00:00:00.000Z',
+ '2016-09-05T00:00:00.000Z',
+ '2016-12-26T00:00:00.000Z',
+ '2016-12-26T00:00:00.000Z',
+ '2017-05-29T00:00:00.000Z',
+ '2017-12-25T00:00:00.000Z',
+ '2018-01-01T00:00:00.000Z',
+ '2018-04-30T00:00:00.000Z',
+ '2018-12-31T00:00:00.000Z',
+ ])
+ );
+ });
+});
+
+describe('feature', () => {
+ test('year', () => {
+ const { result } = dateTimeFeature('result', getParams('year'), { date });
+ expect(result).toEqual([
+ 2010, 2010, 2010, 2011, 2011, 2011, 2012, 2012, 2012, 2013, 2013, 2013, 2014, 2014, 2014, 2015, 2015, 2015, 2016, 2016, 2016, 2017, 2017, 2017,
+ 2018, 2018, 2018,
+ ]);
+ });
+ test('quarter', () => {
+ const { result } = dateTimeFeature('result', getParams('quarter'), { date });
+ expect(bind(date, result)).toEqual(bind(date, [1, 2, 4, 1, 3, 4, 1, 1, 4, 1, 4, 4, 1, 3, 4, 1, 4, 4, 1, 3, 4, 1, 2, 4, 1, 2, 4]));
+ });
+ test('month', () => {
+ const { result } = dateTimeFeature('result', getParams('month'), { date });
+ expect(bind(date, result)).toEqual(bind(date, [1, 4, 12, 1, 8, 12, 1, 2, 12, 1, 10, 12, 1, 7, 12, 1, 11, 12, 1, 9, 12, 1, 6, 12, 1, 5, 12]));
+ });
+ test('week', () => {
+ const { result } = dateTimeFeature('result', getParams('week'), { date });
+ expect(bind(date, result)).toEqual(bind(date, [0, 16, 52, 0, 33, 52, 1, 8, 53, 0, 39, 52, 0, 26, 52, 0, 45, 52, 0, 36, 52, 1, 22, 53, 0, 17, 52]));
+ });
+ test('weekday', () => {
+ const { result } = dateTimeFeature('result', getParams('weekday'), { date });
+ expect(bind(date, result)).toEqual(bind(date, [5, 4, 5, 6, 1, 6, 0, 1, 1, 2, 6, 2, 3, 5, 2, 4, 3, 4, 5, 5, 6, 0, 6, 0, 1, 6, 1]));
+ });
+ test('day', () => {
+ const { result } = dateTimeFeature('result', getParams('day'), { date });
+ expect(bind(date, result)).toEqual(bind(date, [1, 22, 31, 1, 15, 31, 1, 20, 31, 1, 5, 31, 1, 4, 30, 1, 11, 31, 1, 9, 31, 1, 3, 31, 1, 5, 31]));
+ });
+ test('hour', () => {
+ const { result } = dateTimeFeature('result', getParams('hour'), { date });
+ expect(bind(date, result)).toEqual(bind(date, [7, 3, 15, 8, 14, 4, 21, 14, 1, 17, 2, 12, 13, 8, 16, 20, 18, 6, 11, 15, 4, 13, 9, 12, 22, 17, 4]));
+ });
+ test('minute', () => {
+ const { result } = dateTimeFeature('result', getParams('minute'), { date });
+ expect(bind(date, result)).toEqual(
+ bind(date, [41, 21, 38, 55, 23, 45, 53, 33, 49, 6, 11, 42, 45, 33, 39, 9, 25, 45, 33, 1, 3, 22, 38, 14, 54, 43, 45])
+ );
+ });
+ test('second', () => {
+ const { result } = dateTimeFeature('result', getParams('second'), { date });
+ expect(bind(date, result)).toEqual(bind(date, [8, 58, 24, 46, 12, 38, 7, 22, 5, 33, 42, 53, 49, 12, 56, 42, 8, 31, 45, 40, 11, 4, 9, 58, 44, 32, 25]));
+ });
+ test('iso_year', () => {
+ const { result } = dateTimeFeature('result', getParams('iso_year'), { date });
+ expect(bind(date, result)).toEqual(
+ bind(
+ date,
+ [
+ 2009, 2010, 2010, 2010, 2011, 2011, 2011, 2012, 2013, 2013, 2013, 2014, 2014, 2014, 2015, 2015, 2015, 2015, 2015, 2016, 2016, 2016, 2017,
+ 2017, 2018, 2018, 2019,
+ ]
+ )
+ );
+ });
+ test('iso_week', () => {
+ const { result } = dateTimeFeature('result', getParams('iso_week'), { date });
+ expect(bind(date, result)).toEqual(bind(date, [53, 16, 52, 52, 33, 52, 52, 8, 1, 1, 40, 1, 1, 27, 1, 1, 46, 53, 53, 36, 52, 52, 22, 52, 1, 18, 1]));
+ });
+ test('iso_weekday', () => {
+ const { result } = dateTimeFeature('result', getParams('iso_weekday'), { date });
+ expect(bind(date, result)).toEqual(bind(date, [5, 4, 5, 6, 1, 6, 7, 1, 1, 2, 6, 2, 3, 5, 2, 4, 3, 4, 5, 5, 6, 7, 6, 7, 1, 6, 1]));
+ });
+});
+
+describe('edge', () => {
+ const edgeDates = [
+ '2012-12-31T00:00:00.000Z',
+ '2016-01-03T00:00:00.000Z',
+ '2016-01-04T00:00:00.000Z',
+ '2017-01-01T00:00:00.000Z',
+ '2017-01-02T00:00:00.000Z',
+ '2017-01-03T00:00:00.000Z',
+ '2017-12-31T00:00:00.000Z',
+ '2018-01-01T00:00:00.000Z',
+ '2018-01-02T00:00:00.000Z',
+ ];
+ test('week', () => {
+ const { result } = dateTimeFeature('result', getParams('week'), { date: edgeDates });
+ expect(bind(edgeDates, result)).toEqual(bind(edgeDates, [53, 1, 1, 1, 1, 1, 53, 0, 0]));
+ });
+ test('iso_week', () => {
+ const { result } = dateTimeFeature('result', getParams('iso_week'), { date: edgeDates });
+ expect(bind(edgeDates, result)).toEqual(bind(edgeDates, [1, 53, 1, 52, 1, 1, 52, 1, 1]));
+ });
+});
diff --git a/packages/graphic-walker/src/lib/op/dateTimeDrill.ts b/packages/graphic-walker/src/lib/op/dateTimeDrill.ts
index 0c630ae3..a2f86c7b 100644
--- a/packages/graphic-walker/src/lib/op/dateTimeDrill.ts
+++ b/packages/graphic-walker/src/lib/op/dateTimeDrill.ts
@@ -1,10 +1,16 @@
import { DATE_TIME_DRILL_LEVELS } from '../../constants';
import type { IExpParameter } from '../../interfaces';
import type { IDataFrame } from '../execExp';
-import { newOffsetDate } from './offset';
+import { OffsetDate, newOffsetDate } from './offset';
const formatDate = (date: Date) => date.getTime();
+const isoLargeYears = [
+ 4, 9, 15, 20, 26, 32, 37, 43, 48, 54, 60, 65, 71, 76, 82, 88, 93, 99, 105, 111, 116, 122, 128, 133, 139, 144, 150, 156, 161, 167, 172, 178, 184, 189, 195,
+ 201, 207, 212, 218, 224, 229, 235, 240, 246, 252, 257, 263, 268, 274, 280, 285, 291, 296, 303, 308, 314, 320, 325, 331, 336, 342, 348, 353, 359, 364, 370,
+ 376, 381, 387, 392, 398,
+];
+
function dateTimeDrill(resKey: string, params: IExpParameter[], data: IDataFrame): IDataFrame {
const fieldKey = params.find((p) => p.type === 'field')?.value;
const drillLevel = params.find((p) => p.type === 'value')?.value as (typeof DATE_TIME_DRILL_LEVELS)[number] | undefined;
@@ -80,6 +86,40 @@ function dateTimeDrill(resKey: string, params: IExpParameter[], data: IDataFrame
[resKey]: newValues,
};
}
+ case 'iso_year': {
+ const newValues = fieldValues.map((v) => {
+ const date = newDate(v);
+ const _Y = date.getFullYear();
+ const dayInFirstWeek = toOffsetDate(_Y, 0, 4);
+ const firstMondayOfYear = newDate(newDate(dayInFirstWeek).setDate(dayInFirstWeek.getDate() - (dayInFirstWeek.getDay() || 7) + 1));
+ if (date.getTime() < firstMondayOfYear.getTime()) {
+ return formatDate(toOffsetDate(_Y - 1, 0, 1));
+ }
+ const nextDayInFirstWeek = toOffsetDate(_Y + 1, 0, 4);
+ const nextFirstMondayOfYear = newDate(
+ newDate(nextDayInFirstWeek).setDate(nextDayInFirstWeek.getDate() - (nextDayInFirstWeek.getDay() || 7) + 1)
+ );
+ return formatDate(toOffsetDate(date.getTime() < nextFirstMondayOfYear.getTime() ? _Y : _Y + 1, 0, 1));
+ });
+ return {
+ ...data,
+ [resKey]: newValues,
+ };
+ }
+ case 'iso_week': {
+ const newValues = fieldValues.map((v) => {
+ const today = newDate(v);
+ const date = newDate(today.setDate(today.getDate() - (today.getDay() || 7) + 1));
+ const Y = date.getFullYear();
+ const M = date.getMonth();
+ const D = date.getDate();
+ return formatDate(toOffsetDate(Y, M, D));
+ });
+ return {
+ ...data,
+ [resKey]: newValues,
+ };
+ }
case 'hour': {
const newValues = fieldValues.map((v) => {
const date = newDate(v);
diff --git a/packages/graphic-walker/src/lib/op/dateTimeFeature.ts b/packages/graphic-walker/src/lib/op/dateTimeFeature.ts
index 0f7110a1..ffde6b85 100644
--- a/packages/graphic-walker/src/lib/op/dateTimeFeature.ts
+++ b/packages/graphic-walker/src/lib/op/dateTimeFeature.ts
@@ -15,6 +15,19 @@ function dateTimeDrill(resKey: string, params: IExpParameter[], data: IDataFrame
const prepareDate = newOffsetDate(offset);
const toOffsetDate = newOffsetDate(displayOffset);
const newDate = ((...x: []) => toOffsetDate(prepareDate(...x))) as typeof prepareDate;
+ function getISOYear(v: any) {
+ const date = newDate(v);
+ const y = date.getFullYear();
+ const dayInFirstWeek = toOffsetDate(y, 0, 4);
+ const firstMondayOfYear = newDate(newDate(dayInFirstWeek).setDate(dayInFirstWeek.getDate() - (dayInFirstWeek.getDay() || 7) + 1));
+ if (date.getTime() < firstMondayOfYear.getTime()) {
+ return y - 1;
+ }
+ const nextY = y + 1;
+ const nextDayInFirstWeek = toOffsetDate(nextY, 0, 4);
+ const nextFirstMondayOfYear = newDate(newDate(nextDayInFirstWeek).setDate(nextDayInFirstWeek.getDate() - (nextDayInFirstWeek.getDay() || 7) + 1));
+ return date.getTime() < nextFirstMondayOfYear.getTime() ? y : nextY;
+ }
switch (drillLevel) {
case 'year': {
const newValues = fieldValues.map((v) => {
@@ -50,13 +63,12 @@ function dateTimeDrill(resKey: string, params: IExpParameter[], data: IDataFrame
case 'week': {
const newValues = fieldValues.map((v) => {
const date = newDate(v);
- const _Y = date.getFullYear();
- const _firstDayOfYear = newDate(_Y, 0, 1);
- const _SundayOfFirstWeek = newDate(newDate(_firstDayOfYear).setDate(_firstDayOfYear.getDate() - _firstDayOfYear.getDay()));
- const Y = date.getTime() - _SundayOfFirstWeek.getTime() > 1_000 * 60 * 60 * 24 * 7 ? _Y : _SundayOfFirstWeek.getFullYear();
- const firstDayOfYear = newDate(Y, 0, 1);
- const SundayOfFirstWeek = newDate(newDate(firstDayOfYear).setDate(firstDayOfYear.getDate() - firstDayOfYear.getDay()));
- const W = Math.floor((date.getTime() - SundayOfFirstWeek.getTime()) / (7 * 24 * 60 * 60 * 1_000)) + 1;
+ const Y = date.getFullYear();
+ const firstDayOfYear = toOffsetDate(Y, 0, 1);
+ const SundayOfFirstWeek = newDate(firstDayOfYear.setDate(firstDayOfYear.getDate() - firstDayOfYear.getDay()));
+ const FirstSundayOfYear =
+ SundayOfFirstWeek.getFullYear() === Y ? SundayOfFirstWeek : newDate(SundayOfFirstWeek.setDate(SundayOfFirstWeek.getDate() + 7));
+ const W = Math.floor((date.getTime() - FirstSundayOfYear.getTime()) / (7 * 24 * 60 * 60 * 1_000)) + 1;
return W;
});
return {
@@ -74,6 +86,37 @@ function dateTimeDrill(resKey: string, params: IExpParameter[], data: IDataFrame
[resKey]: newValues,
};
}
+ case 'iso_year': {
+ const newValues = fieldValues.map(getISOYear);
+ return {
+ ...data,
+ [resKey]: newValues,
+ };
+ }
+ case 'iso_week': {
+ const newValues = fieldValues.map((v) => {
+ const date = newDate(v);
+ const y = getISOYear(v);
+ const dayInFirstWeek = toOffsetDate(y, 0, 4);
+ const firstMondayOfYear = newDate(newDate(dayInFirstWeek).setDate(dayInFirstWeek.getDate() - (dayInFirstWeek.getDay() || 7) + 1));
+ const W = Math.floor((date.getTime() - firstMondayOfYear.getTime()) / (7 * 24 * 60 * 60 * 1_000)) + 1;
+ return W;
+ });
+ return {
+ ...data,
+ [resKey]: newValues,
+ };
+ }
+ case 'iso_weekday': {
+ const newValues = fieldValues.map((v) => {
+ const date = newDate(v);
+ return date.getDay() || 7;
+ });
+ return {
+ ...data,
+ [resKey]: newValues,
+ };
+ }
case 'day': {
const newValues = fieldValues.map((v) => {
const date = newDate(v);
diff --git a/packages/graphic-walker/src/lib/vega.ts b/packages/graphic-walker/src/lib/vega.ts
index 897a0f9b..ecc6887d 100644
--- a/packages/graphic-walker/src/lib/vega.ts
+++ b/packages/graphic-walker/src/lib/vega.ts
@@ -70,7 +70,7 @@ export function toVegaSpec({
const rowFacetField = rowLeftFacetFields.length > 0 ? rowLeftFacetFields[rowLeftFacetFields.length - 1] : NULL_FIELD;
const colFacetField = colLeftFacetFields.length > 0 ? colLeftFacetFields[colLeftFacetFields.length - 1] : NULL_FIELD;
- const geomFieldIds = [...rows, ...columns, color, opacity, size]
+ const geomFieldIds = [...rows, ...columns, color, opacity, size, ...details]
.filter((f) => Boolean(f))
.filter((f) => f!.aggName !== 'expr')
.map((f) => (f as IViewField).fid);
@@ -123,7 +123,7 @@ export function toVegaSpec({
column: colFacetField,
xOffset: NULL_FIELD,
yOffset: NULL_FIELD,
- details,
+ details: details.map(guard).filter((x) => x !== NULL_FIELD),
defaultAggregated,
stack,
geomType,
diff --git a/packages/graphic-walker/src/locales/en-US.json b/packages/graphic-walker/src/locales/en-US.json
index b6bbb115..0c8b2bac 100644
--- a/packages/graphic-walker/src/locales/en-US.json
+++ b/packages/graphic-walker/src/locales/en-US.json
@@ -328,7 +328,10 @@
"day": "Day",
"hour": "Hour",
"minute": "Minute",
- "second": "Second"
+ "second": "Second",
+ "iso_year": "ISO Year",
+ "iso_week": "ISO Week",
+ "iso_weekday": "ISO Weekday"
}
}
},
diff --git a/packages/graphic-walker/src/locales/ja-JP.json b/packages/graphic-walker/src/locales/ja-JP.json
index 9909ea86..e60742b6 100644
--- a/packages/graphic-walker/src/locales/ja-JP.json
+++ b/packages/graphic-walker/src/locales/ja-JP.json
@@ -322,7 +322,10 @@
"day": "日",
"hour": "時",
"minute": "分",
- "second": "秒"
+ "second": "秒",
+ "iso_year": "ISO年",
+ "iso_week": "ISO週",
+ "iso_weekday": "ISO曜日"
}
}
},
diff --git a/packages/graphic-walker/src/locales/zh-CN.json b/packages/graphic-walker/src/locales/zh-CN.json
index 5f588882..b49ae418 100644
--- a/packages/graphic-walker/src/locales/zh-CN.json
+++ b/packages/graphic-walker/src/locales/zh-CN.json
@@ -328,7 +328,10 @@
"day": "日",
"hour": "时",
"minute": "分",
- "second": "秒"
+ "second": "秒",
+ "iso_year": "ISO年",
+ "iso_week": "ISO周",
+ "iso_weekday": "ISO星期几"
}
}
},
diff --git a/packages/graphic-walker/src/renderer/index.tsx b/packages/graphic-walker/src/renderer/index.tsx
index fd005542..3a908d30 100644
--- a/packages/graphic-walker/src/renderer/index.tsx
+++ b/packages/graphic-walker/src/renderer/index.tsx
@@ -29,6 +29,8 @@ import { GLOBAL_CONFIG } from '../config';
import { Item } from 'vega';
import { viewEncodingKeys } from '@/models/visSpec';
import LoadingLayer from '@/components/loadingLayer';
+import { getTimeFormat } from '@/lib/inferMeta';
+import { unexceptedUTCParsedPatternFormats } from '@/lib/op/offset';
interface RendererProps {
vizThemeConfig: IThemeKey | GWGlobalConfig;
@@ -149,16 +151,31 @@ const Renderer = forwardRef(function (props, r
vizStore.showEmbededMenu([e.clientX, e.clientY]);
vizStore.setFilters(values);
});
- const selectedMarkObject = values.vlPoint.or[0];
+ const { vlPoint, ...datums } = values;
+ const selectedMarkObject = Object.fromEntries(Object.entries(datums).map(([k, vs]) => [k, vs instanceof Array ? vs[0] : undefined]));
// check selected fields include temporal, and return temporal timestamp to original data
const allFields = viewEncodingKeys(visualConfig.geoms[0]).flatMap((k) => encodings[k] as IViewField[]);
const selectedTemporalFields = Object.keys(selectedMarkObject)
.map((k) => allFields.find((x) => x.fid === k))
.filter((x): x is IViewField => !!x && x.semanticType === 'temporal');
if (selectedTemporalFields.length > 0) {
+ const displayOffset = visualConfig.timezoneDisplayOffset ?? new Date().getTimezoneOffset();
selectedTemporalFields.forEach((f) => {
+ const offset = f.offset ?? new Date().getTimezoneOffset();
const set = new Set(viewData.map((x) => x[f.fid] as string | number));
- selectedMarkObject[f.fid] = [...set.values()].find((x) => new Date(x).getTime() === selectedMarkObject[f.fid]);
+ selectedMarkObject[f.fid] = Array.from(set).find((x) => {
+ const format = getTimeFormat(x);
+ let offsetTime = displayOffset * -60000;
+ if (format !== 'timestamp') {
+ offsetTime += offset * 60000;
+ if (!unexceptedUTCParsedPatternFormats.includes(format)) {
+ // the raw data will be parsed as local timezone, so reduce the offset with the local time zone.
+ offsetTime -= new Date().getTimezoneOffset() * 60000;
+ }
+ }
+ const time = new Date(x).getTime() + offsetTime;
+ return time === selectedMarkObject[f.fid];
+ });
});
}
if (e.item.mark.marktype === 'line') {
diff --git a/packages/graphic-walker/src/vis/spec/encode.ts b/packages/graphic-walker/src/vis/spec/encode.ts
index a222b41a..d718c07d 100644
--- a/packages/graphic-walker/src/vis/spec/encode.ts
+++ b/packages/graphic-walker/src/vis/spec/encode.ts
@@ -32,24 +32,37 @@ export function availableChannels(geomType: string): Set {
}
function encodeTimeunit(unit: (typeof DATE_TIME_DRILL_LEVELS)[number]) {
switch (unit) {
+ case 'iso_year':
+ case 'year':
+ return 'utcyear';
case 'quarter':
- return 'yearquarter';
+ return 'utcyearquarter';
case 'month':
- return 'yearmonth';
+ return 'utcyearmonth';
+ case 'iso_week':
case 'week':
- return 'yearweek';
+ return 'utcyearweek';
case 'day':
- return 'yearmonthdate';
+ return 'utcyearmonthdate';
case 'hour':
- return 'yearmonthdatehours';
+ return 'utcyearmonthdatehours';
case 'minute':
- return 'yearmonthdatehoursminutes';
+ return 'utcyearmonthdatehoursminutes';
case 'second':
- return 'yearmonthdatehoursminutesseconds';
+ return 'utcyearmonthdatehoursminutesseconds';
}
return unit;
}
+function isoTimeformat(unit: string) {
+ switch (unit) {
+ case 'iso_year':
+ return '%G';
+ case 'iso_week':
+ return '%G W%V';
+ }
+}
+
export function encodeFid(fid: string) {
return fid
.replace(/([\"\'\.\[\]\/\\])/g, '\\$1')
@@ -100,6 +113,9 @@ export function channelEncode(props: IEncodeProps) {
encoding[c].scale = { type: 'utc' };
}
if (field.semanticType === 'temporal' && field.timeUnit) {
+ if (field.timeUnit.startsWith('iso')) {
+ encoding[c].format = isoTimeformat(field.timeUnit);
+ }
encoding[c].timeUnit = encodeTimeunit(field.timeUnit);
}
if (c === 'color' && field.expression?.op === 'paint') {
diff --git a/packages/graphic-walker/src/vis/spec/tooltip.ts b/packages/graphic-walker/src/vis/spec/tooltip.ts
index 16640ad3..36319d03 100644
--- a/packages/graphic-walker/src/vis/spec/tooltip.ts
+++ b/packages/graphic-walker/src/vis/spec/tooltip.ts
@@ -13,7 +13,8 @@ export function addTooltipEncode(encoding: { [key: string]: any }, details: Read
title: encoding[ck].title,
} as Record,
(draft) => {
- if (encoding[ck].timeUnit) {
+ if (encoding[ck].timeUnit && !encoding[ck].format) {
+ // timeUnit overrides format
draft.timeUnit = encoding[ck].timeUnit;
}
if (encoding[ck].scale) {
@@ -30,8 +31,8 @@ export function addTooltipEncode(encoding: { [key: string]: any }, details: Read
})
.concat(
details.map((f) => ({
- field: defaultAggregated ? getMeaAggKey(f.fid, f.aggName) : f.fid,
- title: defaultAggregated ? getMeaAggName(f.name, f.aggName) : f.name,
+ field: defaultAggregated && f.analyticType === 'measure' ? getMeaAggKey(f.fid, f.aggName) : f.fid,
+ title: defaultAggregated && f.analyticType === 'measure' ? getMeaAggName(f.name, f.aggName) : f.name,
type: f.semanticType,
}))
);