From d0cf9c8e5d5595ae220707a8e354034bccd01cd7 Mon Sep 17 00:00:00 2001 From: Evgenii Elkin <7499112+EugeneElkin@users.noreply.github.com> Date: Wed, 10 Feb 2021 12:13:02 +0300 Subject: [PATCH] ISO 8601 weeks numbering option (#81) * ISO 8601 weeks numbering option * Extended comments + some refactoring after review Co-authored-by: Nikita Grachev --- CHANGELOG.md | 3 + capabilities.json | 81 ++++-- package-lock.json | 230 +++++++----------- package.json | 2 +- pbiviz.json | 4 +- src/{ => calendars}/calendar.ts | 82 +++++-- src/calendars/calendarFactory.ts | 26 ++ src/calendars/calendarISO8061.ts | 111 +++++++++ src/calendars/weekStandards.ts | 4 + src/granularity/dayGranularity.ts | 4 +- src/granularity/granularity.ts | 1 - src/granularity/granularityBase.ts | 57 +---- src/granularity/granularityData.ts | 2 +- src/granularity/monthGranularity.ts | 6 +- src/granularity/quarterGranularity.ts | 4 +- src/granularity/weekGranularity.ts | 6 +- src/granularity/yearGranularity.ts | 4 +- src/settings/settings.ts | 2 + .../weeksDetermintaionStandardsSettings.ts | 31 +++ src/timeLine.ts | 21 +- test/visual.test.ts | 143 ++++++++++- 21 files changed, 566 insertions(+), 258 deletions(-) rename src/{ => calendars}/calendar.ts (66%) create mode 100644 src/calendars/calendarFactory.ts create mode 100644 src/calendars/calendarISO8061.ts create mode 100644 src/calendars/weekStandards.ts create mode 100644 src/settings/weeksDetermintaionStandardsSettings.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 659c35c..c8cb7b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## 2.3.0 +* A new option group "Weeks Determination Standards" containing a list of two items where the first one enables US weeks numbering (default) and the second one enables ISO 8601 weeks numbering + ## 2.2.0 * API was updated to 3.2.0 * Packages update diff --git a/capabilities.json b/capabilities.json index c6cff2e..3236f3d 100644 --- a/capabilities.json +++ b/capabilities.json @@ -1,27 +1,33 @@ { - "dataRoles": [{ - "name": "Time", - "kind": "Grouping", - "displayName": "Time", - "displayNameKey": "Role_Time" - }], - "dataViewMappings": [{ - "conditions": [{ - "Time": { - "max": 1 - } - }], - "categorical": { - "categories": { - "for": { - "in": "Time" - }, - "dataReductionAlgorithm": { - "sample": {} + "dataRoles": [ + { + "name": "Time", + "kind": "Grouping", + "displayName": "Time", + "displayNameKey": "Role_Time" + } + ], + "dataViewMappings": [ + { + "conditions": [ + { + "Time": { + "max": 1 + } + } + ], + "categorical": { + "categories": { + "for": { + "in": "Time" + }, + "dataReductionAlgorithm": { + "sample": {} + } } } } - }], + ], "objects": { "general": { "displayName": "General", @@ -59,6 +65,30 @@ } } }, + "weeksDetermintaionStandards": { + "displayName": "Weeks Determination Standards", + "displayNameKey": "Visual_Weeks_Determination_Standards", + "properties": { + "weekStandard": { + "displayName": "Standard", + "displayNameKey": "Visual_Week_Standard", + "type": { + "enumeration": [ + { + "value": "0", + "displayName": "-- none --", + "displayNameKey": "Visual_Week_Standard_None" + }, + { + "value": "1", + "displayName": "ISO 8601", + "displayNameKey": "Visual_Week_Standard_ISO8601" + } + ] + } + } + } + }, "calendar": { "displayName": "Fiscal Year", "displayNameKey": "Visual_FiscalYear", @@ -67,7 +97,8 @@ "displayName": "Month", "displayNameKey": "Visual_Month", "type": { - "enumeration": [{ + "enumeration": [ + { "value": "0", "displayName": "January", "displayNameKey": "Visual_Month_January" @@ -154,7 +185,8 @@ "displayName": "Day", "displayNameKey": "Visual_Day", "type": { - "enumeration": [{ + "enumeration": [ + { "value": "0", "displayName": "Sunday", "displayNameKey": "Visual_Day_Sunday" @@ -292,7 +324,8 @@ "displayName": "Granularity", "displayNameKey": "Visual_Granularity", "type": { - "enumeration": [{ + "enumeration": [ + { "value": "0", "displayName": "Year", "displayNameKey": "Visual_Granularity_Year" @@ -412,4 +445,4 @@ }, "supportsHighlight": true, "supportsSynchronizingFilterState": true -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5b7dc8b..cee5cdc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "powerbi-visuals-timeline", - "version": "2.2.0", + "version": "2.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2698,6 +2698,16 @@ "unset-value": "^1.0.0" } }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, "call-me-maybe": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", @@ -4109,36 +4119,6 @@ "stackframe": "^1.1.1" } }, - "es-abstract": { - "version": "1.17.6", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", - "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.0", - "is-regex": "^1.1.0", - "object-inspect": "^1.7.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.0", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, "escalade": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.0.2.tgz", @@ -4735,9 +4715,9 @@ } }, "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "version": "0.11.3", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", + "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", "dev": true, "requires": { "websocket-driver": ">=0.5.1" @@ -5106,6 +5086,17 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, + "get-intrinsic": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", + "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1" + } + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -5389,9 +5380,9 @@ } }, "html-entities": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz", - "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", + "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", "dev": true }, "html-escaper": { @@ -5427,6 +5418,12 @@ } } }, + "http-parser-js": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", + "dev": true + }, "http-proxy": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", @@ -5748,10 +5745,13 @@ } }, "is-arguments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", - "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==", - "dev": true + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", + "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "dev": true, + "requires": { + "call-bind": "^1.0.0" + } }, "is-binary-path": { "version": "2.1.0", @@ -5768,12 +5768,6 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, - "is-callable": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", - "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", - "dev": true - }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -5908,15 +5902,6 @@ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", "dev": true }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.1" - } - }, "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", @@ -6840,9 +6825,9 @@ } }, "loglevel": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.0.tgz", - "integrity": "sha512-i2sY04nal5jDcagM3FMfG++T69GEEM8CYuOfeOIvmXzOIcwE9a/CJPR0MFM97pYMj/u10lzz7/zd7+qwhrBTqQ==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", + "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", "dev": true }, "loose-envify": { @@ -7198,9 +7183,9 @@ "dev": true }, "node-forge": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.9.0.tgz", - "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", + "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", "dev": true }, "node-libs-browser": { @@ -7331,20 +7316,14 @@ } } }, - "object-inspect": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", - "dev": true - }, "object-is": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.2.tgz", - "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz", + "integrity": "sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==", "dev": true, "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" } }, "object-keys": { @@ -7735,9 +7714,9 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { "ms": "^2.1.1" @@ -8604,13 +8583,13 @@ } }, "regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", + "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", "dev": true, "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" } }, "regexpp": { @@ -8894,12 +8873,12 @@ "dev": true }, "selfsigned": { - "version": "1.10.7", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.7.tgz", - "integrity": "sha512-8M3wBCzeWIJnQfl43IKwOmC4H/RAp50S8DF60znzjW5GVqTcSe2vWclt7hmYVPkKPlHWOu5EaWOMZ2Y6W8ZXTA==", + "version": "1.10.8", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz", + "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==", "dev": true, "requires": { - "node-forge": "0.9.0" + "node-forge": "^0.10.0" } }, "semver": { @@ -9368,47 +9347,38 @@ } }, "sockjs": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", - "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", + "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", "dev": true, "requires": { - "faye-websocket": "^0.10.0", + "faye-websocket": "^0.11.3", "uuid": "^3.4.0", - "websocket-driver": "0.6.5" + "websocket-driver": "^0.7.4" } }, "sockjs-client": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.4.0.tgz", - "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.0.tgz", + "integrity": "sha512-8Dt3BDi4FYNrCFGTL/HtwVzkARrENdwOUf1ZoW/9p3M8lZdFT35jVdrHza+qgxuG9H3/shR4cuX/X9umUrjP8Q==", "dev": true, "requires": { - "debug": "^3.2.5", + "debug": "^3.2.6", "eventsource": "^1.0.7", - "faye-websocket": "~0.11.1", - "inherits": "^2.0.3", - "json3": "^3.3.2", - "url-parse": "^1.4.3" + "faye-websocket": "^0.11.3", + "inherits": "^2.0.4", + "json3": "^3.3.3", + "url-parse": "^1.4.7" }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "dev": true, "requires": { "ms": "^2.1.1" } - }, - "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } } } }, @@ -9710,26 +9680,6 @@ } } }, - "string.prototype.trimend": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", - "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, - "string.prototype.trimstart": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", - "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.5" - } - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -11085,9 +11035,9 @@ } }, "webpack-dev-server": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", - "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz", + "integrity": "sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ==", "dev": true, "requires": { "ansi-html": "0.0.7", @@ -11110,11 +11060,11 @@ "p-retry": "^3.0.1", "portfinder": "^1.0.26", "schema-utils": "^1.0.0", - "selfsigned": "^1.10.7", + "selfsigned": "^1.10.8", "semver": "^6.3.0", "serve-index": "^1.9.1", - "sockjs": "0.3.20", - "sockjs-client": "1.4.0", + "sockjs": "^0.3.21", + "sockjs-client": "^1.5.0", "spdy": "^4.0.2", "strip-ansi": "^3.0.1", "supports-color": "^6.1.0", @@ -11530,11 +11480,13 @@ } }, "websocket-driver": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", - "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" } }, diff --git a/package.json b/package.json index c5d3664..d3e8fc6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "powerbi-visuals-timeline", - "version": "2.2.0", + "version": "2.3.0", "description": "Timeline slicer is a graphical date range selector used as a filtering component in the report canvas", "repository": { "type": "git", diff --git a/pbiviz.json b/pbiviz.json index 95995b1..4de24f3 100644 --- a/pbiviz.json +++ b/pbiviz.json @@ -1,10 +1,10 @@ { "visual": { "name": "Timeline", - "displayName": "Timeline 2.2.0", + "displayName": "Timeline 2.3.0", "guid": "Timeline1447991079100", "visualClassName": "Timeline", - "version": "2.2.0", + "version": "2.3.0", "description": "Timeline slicer is a graphical date range selector used as a filtering component in the report canvas", "supportUrl": "https://community.powerbi.com", "gitHubUrl": "https://github.com/Microsoft/powerbi-visuals-timeline" diff --git a/src/calendar.ts b/src/calendars/calendar.ts similarity index 66% rename from src/calendar.ts rename to src/calendars/calendar.ts index 0b10bfa..7f74836 100644 --- a/src/calendar.ts +++ b/src/calendars/calendar.ts @@ -24,10 +24,12 @@ * THE SOFTWARE. */ -import { GranularityData } from "./granularity/granularityData"; - -import { CalendarSettings } from "./settings/calendarSettings"; -import { WeekDaySettings } from "./settings/weekDaySettings"; +import { GranularityData } from "../granularity/granularityData"; +import { CalendarSettings } from "../settings/calendarSettings"; +import { WeekDaySettings } from "../settings/weekDaySettings"; +import { WeeksDetermintaionStandardsSettings } from "../settings/weeksDetermintaionStandardsSettings"; +import { Utils } from "../utils"; +import { WeekStandards } from "./weekStandards"; interface IDateDictionary { [year: number]: Date; @@ -41,18 +43,17 @@ export interface IPeriodDates { export class Calendar { private static QuarterFirstMonths: number[] = [0, 3, 6, 9]; - private firstDayOfWeek: number; - private firstMonthOfYear: number; - private firstDayOfYear: number; - private dateOfFirstWeek: IDateDictionary; - private dateOfFirstFullWeek: IDateDictionary; - private quarterFirstMonths: number[]; - private isDaySelection: boolean; - - constructor( - calendarFormat: CalendarSettings, - weekDaySettings: WeekDaySettings) { - + protected firstDayOfWeek: number; + protected firstMonthOfYear: number; + protected firstDayOfYear: number; + protected dateOfFirstWeek: IDateDictionary; + protected dateOfFirstFullWeek: IDateDictionary; + protected quarterFirstMonths: number[]; + protected isDaySelection: boolean; + protected EmptyYearOffset: number = 0; + protected YearOffset: number = 1; + + constructor(calendarFormat: CalendarSettings, weekDaySettings: WeekDaySettings) { this.isDaySelection = weekDaySettings.daySelection; this.firstDayOfWeek = weekDaySettings.day; this.firstMonthOfYear = calendarFormat.month; @@ -66,6 +67,47 @@ export class Calendar { }); } + public getFiscalYearAjustment(): number { + const firstMonthOfYear = this.getFirstMonthOfYear(); + const firstDayOfYear = this.getFirstDayOfYear(); + + return ((firstMonthOfYear === 0 && firstDayOfYear === 1) ? 0 : 1); + } + + public determineYear(date: Date): number { + const firstMonthOfYear = this.getFirstMonthOfYear(); + const firstDayOfYear = this.getFirstDayOfYear(); + + const firstDate: Date = new Date( + date.getFullYear(), + firstMonthOfYear, + firstDayOfYear, + ); + + return date.getFullYear() + this.getFiscalYearAjustment() - ((firstDate <= date) + ? this.EmptyYearOffset + : this.YearOffset); + } + + public determineWeek(date: Date): number[] { + // For fiscal calendar case that started not from the 1st January a year may be greater on 1. + // It's Ok until this year is used to calculate date of first week. + // So, here is some adjustment was applied. + const year: number = this.determineYear(date); + const fiscalYearAdjustment = this.getFiscalYearAjustment(); + + const dateOfFirstWeek: Date = this.getDateOfFirstWeek(year - fiscalYearAdjustment); + const dateOfFirstFullWeek: Date = this.getDateOfFirstFullWeek(year - fiscalYearAdjustment); + // But number of weeks must be calculated using original date. + const weeks: number = Utils.GET_NUMBER_OF_WEEKS_BETWEEN_DATES(dateOfFirstFullWeek, date); + + if (date >= dateOfFirstFullWeek && dateOfFirstWeek < dateOfFirstFullWeek) { + return [weeks + 1, year]; + } + + return [weeks, year]; + } + public getFirstDayOfWeek(): number { return this.firstDayOfWeek; } @@ -149,11 +191,13 @@ export class Calendar { public isChanged( calendarSettings: CalendarSettings, - weekDaySettings: WeekDaySettings): boolean { - + weekDaySettings: WeekDaySettings, + weeksDetermintaionStandardsSettings: WeeksDetermintaionStandardsSettings + ): boolean { return this.firstMonthOfYear !== calendarSettings.month || this.firstDayOfYear !== calendarSettings.day - || this.firstDayOfWeek !== weekDaySettings.day; + || this.firstDayOfWeek !== weekDaySettings.day + || weeksDetermintaionStandardsSettings.weekStandard !== WeekStandards.NotSet; } public getDateOfFirstWeek(year: number): Date { diff --git a/src/calendars/calendarFactory.ts b/src/calendars/calendarFactory.ts new file mode 100644 index 0000000..31c7597 --- /dev/null +++ b/src/calendars/calendarFactory.ts @@ -0,0 +1,26 @@ +import { CalendarSettings } from "../settings/calendarSettings"; +import { WeekDaySettings } from "../settings/weekDaySettings"; +import { WeeksDetermintaionStandardsSettings } from "../settings/weeksDetermintaionStandardsSettings"; +import { Calendar } from "./calendar"; +import { WeekStandards } from "./weekStandards"; +import { CalendarISO8061 } from "./calendarISO8061"; + +export class CalendarFactory { + public create( + weeksDetermintaionStandardsSettings: WeeksDetermintaionStandardsSettings, + calendarSettings: CalendarSettings, + weekDaySettings: WeekDaySettings) : Calendar { + + let calendar: Calendar = null; + + switch (weeksDetermintaionStandardsSettings.weekStandard) { + case WeekStandards.ISO8061: + calendar = new CalendarISO8061(); + break; + default: + calendar = new Calendar(calendarSettings, weekDaySettings) + } + + return calendar; + } +} \ No newline at end of file diff --git a/src/calendars/calendarISO8061.ts b/src/calendars/calendarISO8061.ts new file mode 100644 index 0000000..8d07068 --- /dev/null +++ b/src/calendars/calendarISO8061.ts @@ -0,0 +1,111 @@ +import { Calendar } from "./calendar"; +import { CalendarSettings } from "../settings/calendarSettings"; +import { WeekDaySettings } from "../settings/weekDaySettings"; +import { WeeksDetermintaionStandardsSettings } from "../settings/weeksDetermintaionStandardsSettings"; +import { WeekStandards } from "./weekStandards"; +import { Utils } from "../utils"; + +export class CalendarISO8061 extends Calendar { + + constructor() { + const isoCalendarSettings = new CalendarSettings(); + isoCalendarSettings.month = 0; + isoCalendarSettings.day = 1; + const isoWeekDaySettings = new WeekDaySettings(); + isoWeekDaySettings.daySelection = true; + isoWeekDaySettings.day = 1; + + super(isoCalendarSettings, isoWeekDaySettings); + + //this.firstDayOfYear = calendarFormat.day; + } + + public determineWeek(date: Date): number[] { + const year: number = this.determineWeekYear(date); + const dateOfFirstFullWeek: Date = this.getDateOfFirstFullWeek(year); + const weeks: number = Utils.GET_NUMBER_OF_WEEKS_BETWEEN_DATES(dateOfFirstFullWeek, date); + + return [weeks, year]; + } + + /* + Returns a correct year for passed as a parameter date. + Regarding ISO, first week can start from 29th December to 4th January so, + If we pass date as 2021-01-03 for US calendar it belongs to the first week and should return 2021 + However, for ISO it still belongs to the latest week of 2020 year and here we have to return 2020 + */ + private determineWeekYear(date: Date): number { + let dateYear = date.getFullYear(); + const dateOfFirstWeek: Date = this.getDateOfFirstWeek(dateYear); + const dateOfFirstWeekNext: Date = this.getDateOfFirstWeek(dateYear + 1); + + if (date < dateOfFirstWeek) { + // This scenario works when the first date of ISO week year starts from the beginning of January: 1th, 2nd, 3rd, 4th + // + // Input date: January 3, 2021 [left condition expression] + // The first date of 2021 regarding ISO weeks: January 4, 2021 [right condition expression] + // Returning ISO week year for January 3, 2021: 2020 + // + // To get the ISO week year correctly, the algorythm just deduct 1 from actual year that is 2021 to get 2020 + dateYear -= 1 + } else if (date >= dateOfFirstWeekNext) { + // This scenario works when the first date of ISO week year starts from the last days of actual previous year: December, 29th | 30th | 31st + // + // Input date: December 31, 2019 [left condition expression] + // The first date of 2020 regarding ISO weeks: December 30, 2019 [right condition expression] + // Returning ISO week year: 2020 + // + // To get the ISO week year correctly, the algorythm just add 1 to actual year that is 2019 to get 2020 + dateYear += 1; + } + + return dateYear; + } + + public getDateOfFirstWeek(year: number): Date { + const dateOfFirstJan = new Date(year, 0, 1); + const dayOfFirstJanWeek = dateOfFirstJan.getDay(); + const firstJanDig = 1; + + let dateOfFirstWeek = dateOfFirstJan; + // The first week regarding ISO has to contain Thursday (4th day in the week) + if (dayOfFirstJanWeek <= 4) { + // If 1st January is Monday, Tuesday, Wednesday or Thursday => the first week date should be adjusted to left up to Monday + // 1st Jan is Tuesday setDate(1 - 2 + 1) = setDate(0) => 31st December (last day of previous month) + // 1st Jan is Monday setDate(1 - 1 + 1) = setDate(1) => 1st January (nothing has changed) + // 1st Jan is Wednesday setDate(1 - 3 + 1) = setDate(-1) => 30th December + // 1st Jan is Thursday setDate(1 - 4 + 1) = setDate(-2) => 29th December + // Digit 1 here is just constant to correct calculation + dateOfFirstWeek.setDate(firstJanDig - dayOfFirstJanWeek + 1); + } else { + // If 1st January is Friday, Saturday or Sunday => the first week date should be adjusted to right up to Monday + // 1st Jan is Friday setDate(1 + 8 - 5) = setDate(4) => 4th January + // 1st Jan is Saturday setDate(1 + 8 - 6) = setDate(3) => 3rd January + // 1st Jan is Sunday setDate(1 + 8 - 7) = setDate(2) => 2nd January + // Digit 8 here is just constant to correct calculation that represents a week + 1 + dateOfFirstWeek.setDate(firstJanDig - dayOfFirstJanWeek + 8); + } + + if (!this.dateOfFirstWeek[year]) { + this.dateOfFirstWeek[year] = dateOfFirstWeek; + } + + return this.dateOfFirstWeek[year]; + } + + public getDateOfFirstFullWeek(year: number): Date { + if (!this.dateOfFirstFullWeek[year]) { + this.dateOfFirstFullWeek[year] = this.getDateOfFirstWeek(year); + } + + return this.dateOfFirstFullWeek[year]; + } + + public isChanged( + calendarSettings: CalendarSettings, + weekDaySettings: WeekDaySettings, + weeksDetermintaionStandardsSettings: WeeksDetermintaionStandardsSettings + ): boolean { + return weeksDetermintaionStandardsSettings.weekStandard !== WeekStandards.ISO8061 + } +} \ No newline at end of file diff --git a/src/calendars/weekStandards.ts b/src/calendars/weekStandards.ts new file mode 100644 index 0000000..90e43e1 --- /dev/null +++ b/src/calendars/weekStandards.ts @@ -0,0 +1,4 @@ +export enum WeekStandards { + NotSet = 0, + ISO8061 = 1 +} \ No newline at end of file diff --git a/src/granularity/dayGranularity.ts b/src/granularity/dayGranularity.ts index 54959ec..73e0222 100644 --- a/src/granularity/dayGranularity.ts +++ b/src/granularity/dayGranularity.ts @@ -26,7 +26,7 @@ import { Selection } from "d3-selection"; -import { Calendar } from "../calendar"; +import { Calendar } from "../calendars/calendar"; import { ITimelineLabel } from "../dataInterfaces"; import { ITimelineDatePeriod } from "../datePeriod/datePeriod"; import { Utils } from "../utils"; @@ -55,7 +55,7 @@ export class DayGranularity extends GranularityBase { return [ this.shortMonthName(date), date.getDate(), - this.determineYear(date), + this.calendar.determineYear(date), ]; } diff --git a/src/granularity/granularity.ts b/src/granularity/granularity.ts index e081c2e..90f4787 100644 --- a/src/granularity/granularity.ts +++ b/src/granularity/granularity.ts @@ -36,7 +36,6 @@ import { } from "../dataInterfaces"; export interface IGranularity { - determineWeek?(date: Date): number[]; getType?(): GranularityType; splitDate(date: Date): (string | number)[]; getDatePeriods(): ITimelineDatePeriod[]; diff --git a/src/granularity/granularityBase.ts b/src/granularity/granularityBase.ts index e8f551a..1232b29 100644 --- a/src/granularity/granularityBase.ts +++ b/src/granularity/granularityBase.ts @@ -33,7 +33,7 @@ import { valueFormatter } from "powerbi-visuals-utils-formattingutils"; import { manipulation as svgManipulation } from "powerbi-visuals-utils-svgutils"; import { pixelConverter } from "powerbi-visuals-utils-typeutils"; -import { Calendar } from "../calendar"; +import { Calendar } from "../calendars/calendar"; import { ITimelineDatePeriod } from "../datePeriod/datePeriod"; import { GranularitySettings } from "../settings/granularitySettings"; import { Utils } from "../utils"; @@ -47,16 +47,7 @@ import { } from "../dataInterfaces"; export class GranularityBase implements IGranularity { - public static GET_FISCAL_YEAR_ADJUSTMENT(calendar: Calendar): number { - const firstMonthOfYear = calendar.getFirstMonthOfYear(); - const firstDayOfYear = calendar.getFirstDayOfYear(); - - return ((firstMonthOfYear === 0 && firstDayOfYear === 1) ? 0 : 1); - } - private static DefaultFraction: number = 1; - private static EmptyYearOffset: number = 0; - private static YearOffset: number = 1; protected calendar: Calendar; @@ -170,7 +161,7 @@ export class GranularityBase implements IGranularity { return [ this.shortMonthName(date), date.getDate(), - this.determineYear(date), + this.calendar.determineYear(date), ]; } @@ -236,8 +227,8 @@ export class GranularityBase implements IGranularity { identifierArray, index: datePeriods.length, startDate: date, - week: this.determineWeek(date), - year: this.determineYear(date), + week: this.calendar.determineWeek(date), + year: this.calendar.determineYear(date), }); } else { @@ -267,8 +258,8 @@ export class GranularityBase implements IGranularity { identifierArray: oldDatePeriod.identifierArray, index: oldDatePeriod.index + oldDatePeriod.fraction, startDate: newDate, - week: this.determineWeek(newDate), - year: this.determineYear(newDate), + week: this.calendar.determineWeek(newDate), + year: this.calendar.determineYear(newDate), }; oldDatePeriod.endDate = newDate; @@ -276,47 +267,13 @@ export class GranularityBase implements IGranularity { this.datePeriods.splice(index + 1, 0, newDateObject); } - public determineWeek(date: Date): number[] { - // For fiscal calendar case that started not from the 1st January a year may be greater on 1. - // It's Ok until this year is used to calculate date of first week. - // So, here is some adjustment was applied. - const year: number = this.determineYear(date); - const fiscalYearAdjustment = GranularityBase.GET_FISCAL_YEAR_ADJUSTMENT(this.calendar); - - const dateOfFirstWeek: Date = this.calendar.getDateOfFirstWeek(year - fiscalYearAdjustment); - const dateOfFirstFullWeek: Date = this.calendar.getDateOfFirstFullWeek(year - fiscalYearAdjustment); - // But number of weeks must be calculated using original date. - const weeks: number = Utils.GET_NUMBER_OF_WEEKS_BETWEEN_DATES(dateOfFirstFullWeek, date); - - if (date >= dateOfFirstFullWeek && dateOfFirstWeek < dateOfFirstFullWeek) { - return [weeks + 1, year]; - } - - return [weeks, year]; - } - - public determineYear(date: Date): number { - const firstMonthOfYear = this.calendar.getFirstMonthOfYear(); - const firstDayOfYear = this.calendar.getFirstDayOfYear(); - - const firstDate: Date = new Date( - date.getFullYear(), - firstMonthOfYear, - firstDayOfYear, - ); - - return date.getFullYear() + GranularityBase.GET_FISCAL_YEAR_ADJUSTMENT(this.calendar) - ((firstDate <= date) - ? GranularityBase.EmptyYearOffset - : GranularityBase.YearOffset); - } - /** * Returns the date's quarter name (e.g. Q1, Q2, Q3, Q4) * @param date A date */ protected quarterText(date: Date): string { let quarter: number = this.DefaultQuarter; - let year: number = this.determineYear(date); + let year: number = this.calendar.determineYear(date); while (date < this.calendar.getQuarterStartDate(year, quarter)) { if (quarter > 0) { diff --git a/src/granularity/granularityData.ts b/src/granularity/granularityData.ts index 4c5c508..bbdf7df 100644 --- a/src/granularity/granularityData.ts +++ b/src/granularity/granularityData.ts @@ -36,7 +36,7 @@ import { QuarterGranularity } from "./quarterGranularity"; import { WeekGranularity } from "./weekGranularity"; import { YearGranularity } from "./yearGranularity"; -import { Calendar } from "../calendar"; +import { Calendar } from "../calendars/calendar"; import { Utils } from "../utils"; export class GranularityData { diff --git a/src/granularity/monthGranularity.ts b/src/granularity/monthGranularity.ts index e9cab78..110ad8e 100644 --- a/src/granularity/monthGranularity.ts +++ b/src/granularity/monthGranularity.ts @@ -26,7 +26,7 @@ import { Selection } from "d3-selection"; -import { Calendar } from "../calendar"; +import { Calendar } from "../calendars/calendar"; import { ITimelineLabel } from "../dataInterfaces"; import { ITimelineDatePeriod } from "../datePeriod/datePeriod"; import { Utils } from "../utils"; @@ -54,13 +54,13 @@ export class MonthGranularity extends GranularityBase { public splitDate(date: Date): (string | number)[] { return [ this.shortMonthName(date), - this.determineYear(date), + this.calendar.determineYear(date), ]; } public sameLabel(firstDatePeriod: ITimelineDatePeriod, secondDatePeriod: ITimelineDatePeriod): boolean { return this.shortMonthName(firstDatePeriod.startDate) === this.shortMonthName(secondDatePeriod.startDate) - && this.determineYear(firstDatePeriod.startDate) === this.determineYear(secondDatePeriod.startDate); + && this.calendar.determineYear(firstDatePeriod.startDate) === this.calendar.determineYear(secondDatePeriod.startDate); } public generateLabel(datePeriod: ITimelineDatePeriod): ITimelineLabel { diff --git a/src/granularity/quarterGranularity.ts b/src/granularity/quarterGranularity.ts index 7176fdd..f7aa1c7 100644 --- a/src/granularity/quarterGranularity.ts +++ b/src/granularity/quarterGranularity.ts @@ -26,7 +26,7 @@ import { Selection } from "d3-selection"; -import { Calendar } from "../calendar"; +import { Calendar } from "../calendars/calendar"; import { ITimelineLabel } from "../dataInterfaces"; import { ITimelineDatePeriod } from "../datePeriod/datePeriod"; import { Utils } from "../utils"; @@ -54,7 +54,7 @@ export class QuarterGranularity extends GranularityBase { public splitDate(date: Date): (string | number)[] { return [ this.quarterText(date), - this.determineYear(date), + this.calendar.determineYear(date), ]; } diff --git a/src/granularity/weekGranularity.ts b/src/granularity/weekGranularity.ts index 00ed679..3496172 100644 --- a/src/granularity/weekGranularity.ts +++ b/src/granularity/weekGranularity.ts @@ -27,7 +27,7 @@ import { Selection } from "d3-selection"; import powerbiVisualsApi from "powerbi-visuals-api"; -import { Calendar } from "../calendar"; +import { Calendar } from "../calendars/calendar"; import { ITimelineLabel } from "../dataInterfaces"; import { ITimelineDatePeriod } from "../datePeriod/datePeriod"; import { Utils } from "../utils"; @@ -59,11 +59,11 @@ export class WeekGranularity extends GranularityBase { } public splitDate(date: Date): (string | number)[] { - return this.determineWeek(date); + return this.calendar.determineWeek(date); } public splitDateForTitle(date: Date): (string | number)[] { - const weekData = this.determineWeek(date); + const weekData = this.calendar.determineWeek(date); return [ `W${weekData[0]}`, diff --git a/src/granularity/yearGranularity.ts b/src/granularity/yearGranularity.ts index 65ada11..d6bc8aa 100644 --- a/src/granularity/yearGranularity.ts +++ b/src/granularity/yearGranularity.ts @@ -27,7 +27,7 @@ import { Selection } from "d3-selection"; import powerbiVisualsApi from "powerbi-visuals-api"; -import { Calendar } from "../calendar"; +import { Calendar } from "../calendars/calendar"; import { ITimelineLabel } from "../dataInterfaces"; import { ITimelineDatePeriod } from "../datePeriod/datePeriod"; import { Utils } from "../utils"; @@ -59,7 +59,7 @@ export class YearGranularity extends GranularityBase { } public splitDate(date: Date): (string | number)[] { - return [this.determineYear(date)]; + return [this.calendar.determineYear(date)]; } public sameLabel(firstDatePeriod: ITimelineDatePeriod, secondDatePeriod: ITimelineDatePeriod): boolean { diff --git a/src/settings/settings.ts b/src/settings/settings.ts index f3560c8..ee17b9f 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -35,6 +35,7 @@ import { GranularitySettings } from "./granularitySettings"; import { LabelsSettings } from "./labelsSettings"; import { ScrollAutoAdjustment } from "./scrollAutoAdjustment"; import { WeekDaySettings } from "./weekDaySettings"; +import { WeeksDetermintaionStandardsSettings } from "./weeksDetermintaionStandardsSettings"; export class Settings extends dataViewObjectsParser.DataViewObjectsParser { public general: GeneralSettings = new GeneralSettings(); @@ -47,4 +48,5 @@ export class Settings extends dataViewObjectsParser.DataViewObjectsParser { public labels: LabelsSettings = new LabelsSettings(); public scrollAutoAdjustment: ScrollAutoAdjustment = new ScrollAutoAdjustment(); public cursor: CursorSettings = new CursorSettings(); + public weeksDetermintaionStandards: WeeksDetermintaionStandardsSettings = new WeeksDetermintaionStandardsSettings(); } diff --git a/src/settings/weeksDetermintaionStandardsSettings.ts b/src/settings/weeksDetermintaionStandardsSettings.ts new file mode 100644 index 0000000..3fb97d6 --- /dev/null +++ b/src/settings/weeksDetermintaionStandardsSettings.ts @@ -0,0 +1,31 @@ +/* + * Power BI Visualizations + * + * Copyright (c) Microsoft Corporation + * All rights reserved. + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the ""Software""), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import { WeekStandards } from "../calendars/weekStandards"; + +export class WeeksDetermintaionStandardsSettings { + public weekStandard: number = WeekStandards.NotSet; +} diff --git a/src/timeLine.ts b/src/timeLine.ts index dd344f7..a4bb9b6 100644 --- a/src/timeLine.ts +++ b/src/timeLine.ts @@ -95,8 +95,11 @@ import { import { DatePeriodBase } from "./datePeriod/datePeriodBase"; -import { Calendar } from "./calendar"; +import { Calendar } from "./calendars/calendar"; import { Utils } from "./utils"; +import { CalendarISO8061 } from "./calendars/calendarISO8061"; +import { WeekStandards } from "./calendars/weekStandards"; +import { CalendarFactory } from "./calendars/calendarFactory"; interface IAdjustedFilterDatePeriod { period: DatePeriodBase; @@ -157,7 +160,7 @@ export class Timeline implements powerbiVisualsApi.extensibility.visual.IVisual } const isCalendarChanged: boolean = previousCalendar - && previousCalendar.isChanged(timelineSettings.calendar, timelineSettings.weekDay); + && previousCalendar.isChanged(timelineSettings.calendar, timelineSettings.weekDay, timelineSettings.weeksDetermintaionStandards); if (timelineData && timelineData.currentGranularity) { startDate = Utils.GET_START_SELECTION_DATE(timelineData); @@ -165,7 +168,7 @@ export class Timeline implements powerbiVisualsApi.extensibility.visual.IVisual } if (!initialized || isCalendarChanged) { - calendar = new Calendar(timelineSettings.calendar, timelineSettings.weekDay); + calendar = new CalendarFactory().create(timelineSettings.weeksDetermintaionStandards, timelineSettings.calendar, timelineSettings.weekDay); timelineData.currentGranularity = timelineGranularityData.getGranularity( timelineSettings.granularity.granularity); } else { @@ -576,11 +579,15 @@ export class Timeline implements powerbiVisualsApi.extensibility.visual.IVisual .on("drag", this.onCursorDrag.bind(this)) .on("end", this.onCursorDragEnd.bind(this)); + private calendarFactory: CalendarFactory = null; + constructor(options: powerbiVisualsApi.extensibility.visual.VisualConstructorOptions) { const element: HTMLElement = options.element; this.host = options.host; + this.calendarFactory = new CalendarFactory(); + this.selectionManager = this.host.createSelectionManager(); this.initialized = false; @@ -1018,6 +1025,12 @@ export class Timeline implements powerbiVisualsApi.extensibility.visual.IVisual delete instances[0].properties.day; } + // This options have no sense if ISO standard was picked + if ((options.objectName === "weekDay" || options.objectName === "calendar") + && settings.weeksDetermintaionStandards.weekStandard !== WeekStandards.NotSet) { + return null; + } + return instances; } @@ -1252,7 +1265,7 @@ export class Timeline implements powerbiVisualsApi.extensibility.visual.IVisual locale: string, localizationManager: powerbiVisualsApi.extensibility.ILocalizationManager, ) { - const calendar = new Calendar(timelineSettings.calendar, timelineSettings.weekDay); + const calendar: Calendar = this.calendarFactory.create(timelineSettings.weeksDetermintaionStandards, timelineSettings.calendar, timelineSettings.weekDay); timelineGranularityData.createGranularities(calendar, locale, localizationManager); timelineGranularityData.createLabels(); diff --git a/test/visual.test.ts b/test/visual.test.ts index c66c4c3..d906d05 100644 --- a/test/visual.test.ts +++ b/test/visual.test.ts @@ -32,7 +32,7 @@ import { assertColorsMatch, clickElement, d3Click, renderTimeout, } from "powerbi-visuals-utils-testutils"; -import { Calendar } from "../src/calendar"; +import { Calendar } from "../src/calendars/calendar"; import { ITimelineCursorOverElement, ITimelineData } from "../src/dataInterfaces"; import { ITimelineDatePeriod, ITimelineDatePeriodBase } from "../src/datePeriod/datePeriod"; import { DatePeriodBase } from "../src/datePeriod/datePeriodBase"; @@ -52,6 +52,7 @@ import { GranularityMock } from "./granularityMock"; import { areColorsEqual, getSolidColorStructuralObject } from "./helpers"; import { VisualBuilder } from "./visualBuilder"; import { VisualData } from "./visualData"; +import { CalendarISO8061 } from "../src/calendars/calendarISO8061"; describe("Timeline", () => { let visualBuilder: VisualBuilder; @@ -1130,7 +1131,7 @@ describe("Timeline - Granularity - 1 Jan (Regular Calendar)", () => { }); it("should return zero adjustment for a year", () => { - const yearAdjustment = GranularityBase.GET_FISCAL_YEAR_ADJUSTMENT(calendar); + const yearAdjustment = calendar.getFiscalYearAjustment(); expect(yearAdjustment).toEqual(0); }); }); @@ -1206,15 +1207,15 @@ describe("Timeline - Granularity - 1 Apr (Fiscal Calendar)", () => { }); it("should return [1] adjustment for a year", () => { - const yearAdjustment = GranularityBase.GET_FISCAL_YEAR_ADJUSTMENT(calendar); + const yearAdjustment = calendar.getFiscalYearAjustment(); expect(yearAdjustment).toEqual(1); }); }); describe("weeks order", () => { it("order ascending", () => { - const week1: number[] = granularities[0].determineWeek(new Date(2016, 3, 1)); - const week2: number[] = granularities[0].determineWeek(new Date(2016, 3, 8)); + const week1: number[] = calendar.determineWeek(new Date(2016, 3, 1)); + const week2: number[] = calendar.determineWeek(new Date(2016, 3, 8)); expect(week1[0]).toEqual(1); expect(week2[0]).toEqual(2); @@ -1222,6 +1223,138 @@ describe("Timeline - Granularity - 1 Apr (Fiscal Calendar)", () => { }); }); +describe("Timeline - Granularity - ISO 8601 Week numbering", () => { + let calendar: Calendar; + + beforeEach(() => { + calendar = new CalendarISO8061(); + }); + + describe("ISO Calendar Methods", () => { + it("first date of 1999 is January 4, 1999", () => { + const dateOfFirstWeek = calendar.getDateOfFirstWeek(1999); + const expectedDate = new Date(1999, 0, 4); + expect(dateOfFirstWeek).toEqual(expectedDate); + }); + + it("first date of 2000 is January 3, 2000", () => { + const dateOfFirstWeek = calendar.getDateOfFirstWeek(2000); + const expectedDate = new Date(2000, 0, 3); + expect(dateOfFirstWeek).toEqual(expectedDate); + }); + + it("first date of 2001 is January 1, 2001", () => { + const dateOfFirstWeek = calendar.getDateOfFirstWeek(2001); + const expectedDate = new Date(2001, 0, 1); + expect(dateOfFirstWeek).toEqual(expectedDate); + }); + + it("first date of 2002 is December 31, 2001", () => { + const dateOfFirstWeek = calendar.getDateOfFirstWeek(2002); + const expectedDate = new Date(2001, 11, 31); + expect(dateOfFirstWeek).toEqual(expectedDate); + }); + + it("first date of 2003 is December 30, 2002", () => { + const dateOfFirstWeek = calendar.getDateOfFirstWeek(2003); + const expectedDate = new Date(2002, 11, 30); + expect(dateOfFirstWeek).toEqual(expectedDate); + }); + + it("first date of 2009 is December 29, 2008", () => { + const dateOfFirstWeek = calendar.getDateOfFirstWeek(2009); + const expectedDate = new Date(2008, 11, 29); + expect(dateOfFirstWeek).toEqual(expectedDate); + }); + + it("first date of 2017 is January 2, 2017", () => { + const dateOfFirstWeek = calendar.getDateOfFirstWeek(2017); + const expectedDate = new Date(2017, 0, 2); + expect(dateOfFirstWeek).toEqual(expectedDate); + }); + + it("first date of 2019 is December 31, 2018", () => { + const dateOfFirstWeek = calendar.getDateOfFirstWeek(2019); + const expectedDate = new Date(2018, 11, 31); + expect(dateOfFirstWeek).toEqual(expectedDate); + }); + + it("first date of 2020 is December 30, 2019", () => { + const dateOfFirstWeek = calendar.getDateOfFirstWeek(2020); + const expectedDate = new Date(2019, 11, 30); + expect(dateOfFirstWeek).toEqual(expectedDate); + }); + + it("first date of 2021 is January 4, 2021", () => { + const dateOfFirstWeek = calendar.getDateOfFirstWeek(2021); + const expectedDate = new Date(2021, 0, 4); + expect(dateOfFirstWeek).toEqual(expectedDate); + }); + + it("week of December 25, 2017 to Decamber 31 is 52", () => { + let week = calendar.determineWeek(new Date(2017, 11, 25))[0]; + expect(week).toEqual(52); + week = calendar.determineWeek(new Date(2017, 11, 31))[0]; + expect(week).toEqual(52); + }); + + it("week of May 1, 2017 to May 7, 2017 is 18", () => { + let week = calendar.determineWeek(new Date(2017, 4, 1))[0]; + expect(week).toEqual(18); + week = calendar.determineWeek(new Date(2017, 4, 7))[0]; + expect(week).toEqual(18); + }); + + it("week of December 28, 2020 to January 3, 2021 is 53", () => { + let week = calendar.determineWeek(new Date(2020, 11, 28))[0]; + expect(week).toEqual(53); + week = calendar.determineWeek(new Date(2021, 0, 3))[0]; + expect(week).toEqual(53); + }); + + it("week of January 4, 2021 to January 10, 2021 is 1", () => { + let week = calendar.determineWeek(new Date(2021, 0, 4))[0]; + expect(week).toEqual(1); + week = calendar.determineWeek(new Date(2021, 0, 10))[0]; + expect(week).toEqual(1); + }); + + it("first week and first full week must bethe same", () => { + expect(calendar.getDateOfFirstWeek(2007)).toEqual(calendar.getDateOfFirstFullWeek(2007)); + expect(calendar.getDateOfFirstWeek(2019)).toEqual(calendar.getDateOfFirstFullWeek(2019)); + expect(calendar.getDateOfFirstWeek(2020)).toEqual(calendar.getDateOfFirstFullWeek(2020)); + }); + + it("fiscal year adjustment is 0", () => { + expect(calendar.getFiscalYearAjustment()).toEqual(0); + }); + + it("a year must be determine without relation to week numbers", () => { + expect(calendar.determineYear(new Date(2020, 11, 28))).toEqual(2020); + expect(calendar.determineYear(new Date(2021, 0, 2))).toEqual(2021); + expect(calendar.getYearPeriod(new Date(2021, 0, 2)).startDate).toEqual(new Date(2021, 0, 1)); + expect(calendar.getYearPeriod(new Date(2021, 0, 2)).endDate).toEqual(new Date(2022, 0, 1)); + }); + + it("a quarter must be determine without relation to week numbers", () => { + expect(calendar.getQuarterPeriod(new Date(2021, 0, 2)).startDate).toEqual(new Date(2021, 0, 1)); + expect(calendar.getQuarterPeriod(new Date(2021, 0, 2)).endDate).toEqual(new Date(2021, 3, 1)); + expect(calendar.getQuarterPeriod(new Date(2021, 3, 22)).startDate).toEqual(new Date(2021, 3, 1)); + expect(calendar.getQuarterPeriod(new Date(2021, 3, 22)).endDate).toEqual(new Date(2021, 6, 1)); + expect(calendar.getQuarterPeriod(new Date(2021, 7, 13)).startDate).toEqual(new Date(2021, 6, 1)); + expect(calendar.getQuarterPeriod(new Date(2021, 7, 13)).endDate).toEqual(new Date(2021, 9, 1)); + expect(calendar.getQuarterPeriod(new Date(2021, 10, 35)).startDate).toEqual(new Date(2021, 9, 1)); + expect(calendar.getQuarterPeriod(new Date(2021, 10, 35)).endDate).toEqual(new Date(2022, 0, 1)); + }) + + it("a month must be determine without relation to week numbers", () => { + expect(calendar.getMonthPeriod(new Date(2021, 0, 2)).startDate).toEqual(new Date(2021, 0, 1)); + expect(calendar.getMonthPeriod(new Date(2021, 0, 2)).endDate).toEqual(new Date(2021, 1, 1)); + }) + }); +}); + + describe("Timeline - TimelineUtils", () => { describe("getIndexByPosition", () => { const indexes: number[] = [0, 1, 2, 3, 3.14, 4, 4.15, 5];