diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug-report.md similarity index 100% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to .github/ISSUE_TEMPLATE/bug-report.md diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature-request.md similarity index 88% rename from .github/ISSUE_TEMPLATE/feature_request.md rename to .github/ISSUE_TEMPLATE/feature-request.md index eec33edebb..5e31d25b69 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -21,7 +21,10 @@ Search open/closed issues before submitting. Someone may have requested the same - + ## 📋 Tasks -- [ ] This is a subtask of the feature. (It can be converted to an issue.) + + diff --git a/.github/ISSUE_TEMPLATE/interaction_design_request.md b/.github/ISSUE_TEMPLATE/interaction-design-request.md similarity index 90% rename from .github/ISSUE_TEMPLATE/interaction_design_request.md rename to .github/ISSUE_TEMPLATE/interaction-design-request.md index 1583489eb7..ea23537e94 100644 --- a/.github/ISSUE_TEMPLATE/interaction_design_request.md +++ b/.github/ISSUE_TEMPLATE/interaction-design-request.md @@ -1,6 +1,6 @@ --- name: 👈 Interaction Design Request -about: (PO ONLY) A small chunk of work to be done by an Interaction Designer +about: A small chunk of work to be done by an Interaction Designer title: 'nimble-{name} interaction design request' labels: 'interaction design,triage' --- @@ -16,20 +16,19 @@ labels: 'interaction design,triage' ## 🎯 Core Requirements - +--> ## 🍆 Non-requirements - + - +--> ## 🥅 Acceptance Criteria diff --git a/.github/ISSUE_TEMPLATE/new_component.md b/.github/ISSUE_TEMPLATE/new-component.md similarity index 93% rename from .github/ISSUE_TEMPLATE/new_component.md rename to .github/ISSUE_TEMPLATE/new-component.md index 302c431755..36315f1eab 100644 --- a/.github/ISSUE_TEMPLATE/new_component.md +++ b/.github/ISSUE_TEMPLATE/new-component.md @@ -1,8 +1,8 @@ --- name: 💡 New Component -about: (DEV TEAM ONLY) New Nimble component +about: New Nimble component title: 'nimble-{name} Component' -labels: 'new component,enhancement' +labels: 'new component,enhancement,triage' --- # 💡 New Component diff --git a/.github/ISSUE_TEMPLATE/tech_debt.md b/.github/ISSUE_TEMPLATE/tech-debt.md similarity index 50% rename from .github/ISSUE_TEMPLATE/tech_debt.md rename to .github/ISSUE_TEMPLATE/tech-debt.md index 7a91d2645b..3f2ed678db 100644 --- a/.github/ISSUE_TEMPLATE/tech_debt.md +++ b/.github/ISSUE_TEMPLATE/tech-debt.md @@ -1,6 +1,6 @@ --- name: 🧹 Tech Debt -about: (DEV TEAM ONLY) Non-user-visible improvement to code or development process +about: Non-user-visible improvement to code or development process title: '' labels: 'tech debt,triage' --- diff --git a/.github/ISSUE_TEMPLATE/user_story.md b/.github/ISSUE_TEMPLATE/user_story.md deleted file mode 100644 index 8325c8a436..0000000000 --- a/.github/ISSUE_TEMPLATE/user_story.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -name: 📌 User story -about: (DEV TEAM ONLY) A small chunk of work to be done -title: '(Fully descriptive title)' ---- - - - -# 📌 User Story diff --git a/.github/ISSUE_TEMPLATE/visual_design_request.md b/.github/ISSUE_TEMPLATE/visual-design-request.md similarity index 88% rename from .github/ISSUE_TEMPLATE/visual_design_request.md rename to .github/ISSUE_TEMPLATE/visual-design-request.md index 8954610f02..95f30b9df1 100644 --- a/.github/ISSUE_TEMPLATE/visual_design_request.md +++ b/.github/ISSUE_TEMPLATE/visual-design-request.md @@ -1,6 +1,6 @@ --- name: 🎨 Visual Design Request -about: (DEV TEAM ONLY) A small chunk of work to be done by Visual Designer +about: A small chunk of work to be done by Visual Designer title: 'nimble-{name} visual design request' labels: 'visual design,triage' --- @@ -16,20 +16,19 @@ labels: 'visual design,triage' ## 🎯 Core Requirements - + +--> ## 🍆 Non-requirements - + - +--> ## 🥅 Acceptance Criteria diff --git a/.github/renovate.json b/.github/renovate.json index bd347e65d9..e4f99e043d 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -17,14 +17,14 @@ "groupName": "npm dependencies lock update only", "matchManagers": ["npm"], "rangeStrategy": "in-range-only", - "matchDepTypes": ["dependencies", "devDependencies"], + "matchDepTypes": ["dependencies", "devDependencies", "peerDependencies"], "enabled": true }, { "groupName": "npm dependencies update to latest", "matchManagers": ["npm"], "rangeStrategy": "update-lockfile", - "matchDepTypes": ["dependencies", "devDependencies"], + "matchDepTypes": ["dependencies", "devDependencies", "peerDependencies"], "excludePackagePatterns":[ "^@angular", "@microsoft/fast-foundation", diff --git a/angular-workspace/projects/ni/nimble-angular/CHANGELOG.json b/angular-workspace/projects/ni/nimble-angular/CHANGELOG.json index f0e1d56f45..0a5bf2ef65 100644 --- a/angular-workspace/projects/ni/nimble-angular/CHANGELOG.json +++ b/angular-workspace/projects/ni/nimble-angular/CHANGELOG.json @@ -1,6 +1,21 @@ { "name": "@ni/nimble-angular", "entries": [ + { + "date": "Thu, 07 Mar 2024 21:20:52 GMT", + "version": "20.2.20", + "tag": "@ni/nimble-angular_v20.2.20", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@ni/nimble-angular", + "comment": "Bump @ni/nimble-components to v21.10.0", + "commit": "not available" + } + ] + } + }, { "date": "Wed, 06 Mar 2024 17:56:10 GMT", "version": "20.2.19", diff --git a/angular-workspace/projects/ni/nimble-angular/CHANGELOG.md b/angular-workspace/projects/ni/nimble-angular/CHANGELOG.md index 4cf49e4f6d..343cd1ed90 100644 --- a/angular-workspace/projects/ni/nimble-angular/CHANGELOG.md +++ b/angular-workspace/projects/ni/nimble-angular/CHANGELOG.md @@ -1,9 +1,17 @@ # Change Log - @ni/nimble-angular -This log was last generated on Wed, 06 Mar 2024 17:56:10 GMT and should not be manually modified. +This log was last generated on Thu, 07 Mar 2024 21:20:52 GMT and should not be manually modified. +## 20.2.20 + +Thu, 07 Mar 2024 21:20:52 GMT + +### Patches + +- Bump @ni/nimble-components to v21.10.0 + ## 20.2.19 Wed, 06 Mar 2024 17:56:10 GMT diff --git a/angular-workspace/projects/ni/nimble-angular/package.json b/angular-workspace/projects/ni/nimble-angular/package.json index 5d2f6d001a..c784c92f79 100644 --- a/angular-workspace/projects/ni/nimble-angular/package.json +++ b/angular-workspace/projects/ni/nimble-angular/package.json @@ -1,6 +1,6 @@ { "name": "@ni/nimble-angular", - "version": "20.2.19", + "version": "20.2.20", "description": "Angular components for the NI Nimble Design System", "scripts": { "invoke-publish": "cd ../../../ && npm run build:library && cd dist/ni/nimble-angular && npm publish" @@ -31,7 +31,7 @@ "@angular/forms": "^15.2.10", "@angular/localize": "^15.2.10", "@angular/router": "^15.2.10", - "@ni/nimble-components": "^21.9.1" + "@ni/nimble-components": "^21.10.0" }, "dependencies": { "tslib": "^2.2.0" diff --git a/package-lock.json b/package-lock.json index 0f43a287c8..9824d0ca80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -74,7 +74,7 @@ }, "angular-workspace/projects/ni/nimble-angular": { "name": "@ni/nimble-angular", - "version": "20.2.19", + "version": "20.2.20", "license": "MIT", "dependencies": { "tslib": "^2.2.0" @@ -85,7 +85,7 @@ "@angular/forms": "^15.2.10", "@angular/localize": "^15.2.10", "@angular/router": "^15.2.10", - "@ni/nimble-components": "^21.9.1" + "@ni/nimble-components": "^21.10.0" } }, "node_modules/@11ty/dependency-tree": { @@ -11442,6 +11442,15 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", "dev": true }, + "node_modules/@swc/helpers": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.6.tgz", + "integrity": "sha512-aYX01Ke9hunpoCexYAgQucEpARGQ5w/cqHFrIR+e9gdKb1QWTsVJuTJ2ozQzIAxLyRQe/m+2RqzkyOOGiMKRQA==", + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@swc/types": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", @@ -11819,6 +11828,18 @@ "@types/node": "*" } }, + "node_modules/@types/command-line-args": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", + "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", + "peer": true + }, + "node_modules/@types/command-line-usage": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/command-line-usage/-/command-line-usage-5.0.4.tgz", + "integrity": "sha512-BwR5KP3Es/CSht0xqBcUXS3qCAUVXwpRKsV2+arxeb65atasuXG9LykC9Ab10Cw3s2raH92ZqOeILaQbsB2ACg==", + "peer": true + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -12133,7 +12154,6 @@ "version": "20.11.24", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz", "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } @@ -13311,6 +13331,26 @@ "node": ">= 8" } }, + "node_modules/apache-arrow": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/apache-arrow/-/apache-arrow-15.0.0.tgz", + "integrity": "sha512-e6aunxNKM+woQf137ny3tp/xbLjFJS2oGQxQhYGqW6dGeIwNV1jOeEAeR6sS2jwAI2qLO83gYIP2MBz02Gw5Xw==", + "peer": true, + "dependencies": { + "@swc/helpers": "^0.5.2", + "@types/command-line-args": "^5.2.1", + "@types/command-line-usage": "^5.0.2", + "@types/node": "^20.6.0", + "command-line-args": "^5.2.1", + "command-line-usage": "^7.0.1", + "flatbuffers": "^23.5.26", + "json-bignum": "^0.0.3", + "tslib": "^2.6.2" + }, + "bin": { + "arrow2csv": "bin/arrow2csv.cjs" + } + }, "node_modules/app-root-dir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz", @@ -18663,6 +18703,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/flatbuffers": { + "version": "23.5.26", + "resolved": "https://registry.npmjs.org/flatbuffers/-/flatbuffers-23.5.26.tgz", + "integrity": "sha512-vE+SI9vrJDwi1oETtTIFldC/o9GsVKRM+s6EL0nQgxXlYV1Vc4Tk30hj4xGICftInKQKj1F3up2n8UbIVobISQ==", + "peer": true + }, "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", @@ -22064,6 +22110,15 @@ "node": ">=4" } }, + "node_modules/json-bignum": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/json-bignum/-/json-bignum-0.0.3.tgz", + "integrity": "sha512-2WHyXj3OfHSgNyuzDbSxI1w2jgw5gkWSWhS7Qg4bWXx1nLk3jnbwfUeS0PSba3IzpTUWdHxBieELUzXRjQB2zg==", + "peer": true, + "engines": { + "node": ">=0.8" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -31755,8 +31810,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.0", @@ -33765,7 +33819,7 @@ }, "packages/nimble-blazor": { "name": "@ni/nimble-blazor", - "version": "14.3.17", + "version": "14.3.18", "hasInstallScript": true, "license": "MIT", "devDependencies": { @@ -33801,7 +33855,7 @@ }, "packages/nimble-components": { "name": "@ni/nimble-components", - "version": "21.9.1", + "version": "21.10.0", "license": "MIT", "dependencies": { "@microsoft/fast-colors": "^5.3.1", @@ -33902,6 +33956,9 @@ "webpack": "^5.75.0", "webpack-cli": "^5.0.1", "webpack-dev-middleware": "^7.0.0" + }, + "peerDependencies": { + "apache-arrow": "^15.0.0" } }, "packages/nimble-components/node_modules/@types/mdast": { diff --git a/packages/nimble-blazor/package.json b/packages/nimble-blazor/package.json index c9bd8fa20c..4668f5b82b 100644 --- a/packages/nimble-blazor/package.json +++ b/packages/nimble-blazor/package.json @@ -1,6 +1,6 @@ { "name": "@ni/nimble-blazor", - "version": "14.3.17", + "version": "14.3.18", "description": "Blazor components for the NI Nimble Design System", "scripts": { "postinstall": "node build/generate-playwright-version-properties/source/index.js", diff --git a/packages/nimble-components/CHANGELOG.json b/packages/nimble-components/CHANGELOG.json index 2f8fef1ce7..ca97d42e76 100644 --- a/packages/nimble-components/CHANGELOG.json +++ b/packages/nimble-components/CHANGELOG.json @@ -1,6 +1,36 @@ { "name": "@ni/nimble-components", "entries": [ + { + "date": "Thu, 07 Mar 2024 22:17:07 GMT", + "version": "21.10.0", + "tag": "@ni/nimble-components_v21.10.0", + "comments": { + "none": [ + { + "author": "20542556+mollykreis@users.noreply.github.com", + "package": "@ni/nimble-components", + "commit": "ece50add37226bd2c2c7d9df9283cdb1853941ce", + "comment": "Create HLD for placeholders in the table" + } + ] + } + }, + { + "date": "Thu, 07 Mar 2024 21:20:52 GMT", + "version": "21.10.0", + "tag": "@ni/nimble-components_v21.10.0", + "comments": { + "minor": [ + { + "author": "33986780+munteannatan@users.noreply.github.com", + "package": "@ni/nimble-components", + "commit": "a9b5cede5ebbb79127bf36e91b31d17d4bf241b9", + "comment": "New Wafer Map Component API. Introduced `diesTable` and two rendering strategies switched by this input" + } + ] + } + }, { "date": "Wed, 06 Mar 2024 17:56:10 GMT", "version": "21.9.1", diff --git a/packages/nimble-components/CHANGELOG.md b/packages/nimble-components/CHANGELOG.md index e4d09e4936..7909968930 100644 --- a/packages/nimble-components/CHANGELOG.md +++ b/packages/nimble-components/CHANGELOG.md @@ -1,9 +1,17 @@ # Change Log - @ni/nimble-components -This log was last generated on Wed, 06 Mar 2024 17:56:10 GMT and should not be manually modified. +This log was last generated on Thu, 07 Mar 2024 21:20:52 GMT and should not be manually modified. +## 21.10.0 + +Thu, 07 Mar 2024 21:20:52 GMT + +### Minor changes + +- New Wafer Map Component API. Introduced `diesTable` and two rendering strategies switched by this input ([ni/nimble@a9b5ced](https://github.com/ni/nimble/commit/a9b5cede5ebbb79127bf36e91b31d17d4bf241b9)) + ## 21.9.1 Wed, 06 Mar 2024 17:56:10 GMT diff --git a/packages/nimble-components/package.json b/packages/nimble-components/package.json index 47aaf391d9..381faa6616 100644 --- a/packages/nimble-components/package.json +++ b/packages/nimble-components/package.json @@ -1,6 +1,6 @@ { "name": "@ni/nimble-components", - "version": "21.9.1", + "version": "21.10.0", "description": "Styled web components for the NI Nimble Design System", "scripts": { "build": "npm run generate-icons && npm run generate-workers && npm run build-components && npm run bundle-components && npm run generate-scss && npm run build-storybook", @@ -75,6 +75,7 @@ "@tiptap/extension-bold": "^2.2.2", "@tiptap/extension-bullet-list": "^2.2.2", "@tiptap/extension-document": "^2.2.2", + "@tiptap/extension-hard-break": "^2.2.2", "@tiptap/extension-history": "^2.2.2", "@tiptap/extension-italic": "^2.2.2", "@tiptap/extension-link": "^2.2.2", @@ -84,7 +85,6 @@ "@tiptap/extension-paragraph": "^2.2.2", "@tiptap/extension-placeholder": "^2.2.2", "@tiptap/extension-text": "^2.2.2", - "@tiptap/extension-hard-break": "^2.2.2", "@types/d3-array": "^3.0.4", "@types/d3-random": "^3.0.1", "@types/d3-scale": "^4.0.2", @@ -102,6 +102,9 @@ "prosemirror-state": "^1.4.3", "tslib": "^2.2.0" }, + "peerDependencies": { + "apache-arrow": "^15.0.0" + }, "devDependencies": { "@babel/cli": "^7.13.16", "@babel/core": "^7.20.12", diff --git a/packages/nimble-components/src/table/specs/spec-images/PlacholderText.png b/packages/nimble-components/src/table/specs/spec-images/PlacholderText.png new file mode 100644 index 0000000000..8b124f440d Binary files /dev/null and b/packages/nimble-components/src/table/specs/spec-images/PlacholderText.png differ diff --git a/packages/nimble-components/src/table/specs/table-column-placeholder-hld.md b/packages/nimble-components/src/table/specs/table-column-placeholder-hld.md new file mode 100644 index 0000000000..39f93b9dc8 --- /dev/null +++ b/packages/nimble-components/src/table/specs/table-column-placeholder-hld.md @@ -0,0 +1,205 @@ +# Placeholders for table columns HLD + +## Problem Statement + +In some cases, an application may want to display a placeholder value in the table when a record does not have a value for a given field. The application should be able to customize that placeholder based on their needs. + +## Links To Relevant Work Items and Reference Material + +- [Nimble issue 1538](https://github.com/ni/nimble/issues/1538) +- [Nimble issue 1511](https://github.com/ni/nimble/issues/1511) + +## Implementation / Design + +### High-level behavior + +There are two places where a value is displayed in the table that placeholders need to be considered: (1) in a cell and (2) in a group row. The cell placeholder will be configurable on each column that supports having a placeholder, and it will default to an empty string. The group row placeholder will come from the table's localization provider, and it will not be configurable through a column's API. + +Placeholders within a cell will be rendered with nimble's placeholder font, which is currently 60% opacity of nimble's body font. + +Placeholders within a group row will have no special visual treatment. + +Placeholder values will behave consistently with other strings rendered in the table in that they will truncate with an ellipsis and title if they are longer than the available space. They will be sorted based on the record value rather than the placeholder string. + +Below is an example of what placeholders will look like in the table. In this example, the table is grouped by the "Quote" column, which has been configured to have a placeholder value of "None". + +![Placeholder text example](./spec-images/PlacholderText.png) + +### Column-specific decisions + +The exact behavior of placeholders in each existing table column is described below. + +#### Text column + +| Special-cased field values | Cell display | Group row display | +| -------------------------- | ------------------------------------------------------------ | ----------------- | +| `undefined` | column placeholder, or empty if no placeholder is configured | `"No value"` | +| `null` | column placeholder, or empty if no placeholder is configured | `"No value"` | +| `''`\* | \ | `"Empty"` | + +\*Only empty string (`''`) is treated as a special case for group row placeholders. Other whitespace values will be rendered as-is and should be pre-processed as appropriate by the application. + +Column best practices: + +- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`. +- Avoid mixing empty string with `undefined`/`null`. The distinction when grouping between `"No value"` and `"Empty"` is not likely meaningful to a user. +- Avoid displaying whitespace values that are not empty string (`''`) as these values will be rendered as-is in group rows. + +#### Anchor column + +| Special-cased field values | Cell display | Group row display | +| --------------------------------------------------- | ------------------------------------------------------------------ | ----------------- | +| Both label and href are `undefined` or `null` | column placeholder, or empty if no placeholder is configured | `"No value"` | +| Label is `undefined` or `null` with defined href | href value is used as the link's href and the link's display value | `"No value"` | +| Label is defined with href of `undefined` or `null` | label as a plain string with no link | The label | +| Label is `''` with any href\* | \ | `"Empty"` | + +\*Only empty string (`''`) is treated as a special case for group row placeholders. Other whitespace values will be rendered as-is and should be pre-processed as appropriate by the application. + +Column best practices: + +- Provide useful labels for well known urls. While an absent label will show the full URL for accessibility, it is useful to instead provide a clear and unique label to improve grouping. + - For example, a column of links to notebooks where a notebook may no longer exist, and thus a label is not available, could pre-process the notebook urls and create the label `Missing Notebook (UNIQUE_NOTEBOOK_ID)`. This allows multiple rows referencing the same missing notebook to be grouped together. + - Alternatively if the urls are not well-known structures, the application should explicitly provide the href as the label to keep unique labels and preserve grouping as opposed to using `null` / `undefined` labels. +- Applications should avoid having duplicate labels to different hrefs as those are inaccessible to screen readers (and sighted users). See [high-level discussion](https://fae.disability.illinois.edu/rulesets/LINK_2/) of [aria SC 2.4.4](https://www.w3.org/TR/WCAG22/#link-purpose-in-context). + - For example, applications should avoid having `undefined` / `null` as the label as that causes multiple unrelated URLs to be grouped together under the group label "No value". Accessibility is okay as the full url will be shown but the value of grouping is limited. + - For example, if a label is missing, an application should avoid generating a non-unique label for multiple URLs (i.e. `Missing Notebook`) as that harms accessibility and limits the value of grouping. +- Avoid using empty string or other whitespace-only labels with defined hrefs. This will cause the rendered anchor to have no text associated with it, and it will be difficult for a user to see that the anchor exists. +- Applications may leave the href as `null` / `undefined` to have the anchor column behave effectively like a string column +- Avoid mixing `undefined` and `null` as values for the label field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`. + - As explained above, it is not recommended to use `undefined` or `null` labels when the data has defined hrefs. +- Avoid mixing empty string with `undefined`/`null` as values for the label field. The distinction when grouping between `"No value"` and `"Empty"` is not likely meaningful to a user. + - As explained above, it is not recommended to use empty string, `undefined`, or `null` labels when the data has defined hrefs. + +#### Number column + +| Special-cased field values | Cell display | Group row display | +| -------------------------- | ------------------------------------------------------------ | ----------------- | +| `undefined` | column placeholder, or empty if no placeholder is configured | `"No value"` | +| `null` | column placeholder, or empty if no placeholder is configured | `"No value"` | + +The alignment of the placeholder in the cell will match the alignment of the number in the column. + +Column best practices: + +- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`. +- If relevant to your data source, make sure to consider the IEEE 754 special cases of `-Inf`, `+Inf`, `-0`, `+0`, and `NaN`. + +#### Date column + +| Special-cased field values | Cell display | Group row display | +| ----------------------------------- | ------------------------------------------------------------ | ------------------ | +| `undefined` | column placeholder, or empty if no placeholder is configured | `"No value"` | +| `null` | column placeholder, or empty if no placeholder is configured | `"No value"` | +| Invalid value (e.g. `Number.NaN`)\* | \ | \ | + +\*This is considered invalid data from the table's perspective and should be fixed within the client application. + +Column best practices: + +- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`. + +#### Duration column + +| Special-cased field values | Cell display | Group row display | +| ----------------------------------- | ------------------------------------------------------------ | ------------------ | +| `undefined` | column placeholder, or empty if no placeholder is configured | `"No value"` | +| `null` | column placeholder, or empty if no placeholder is configured | `"No value"` | +| Invalid value (e.g. `Number.NaN`)\* | \ | \ | + +\*This is considered invalid data from the table's perspective and should be fixed within the client application. + +Column best practices: + +- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`. + +#### Icon mapping column + +The icon mapping column will not have a configuration for a placeholder. + +| Special-cased field values | Cell display | Group row display | +| -------------------------- | ------------- | ------------------ | +| `undefined` | \ | `"No value"` | +| `null` | \ | `"No value"` | +| Non-mapped value\* | \ | \ | + +\*This is considered invalid data from the table's perspective and should be fixed within the client application. + +Column best practices: + +- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`. +- Avoid using values that do not correspond to a mapping for the column. +- To display an empty cell but have a non-blank group row, create a mapping of the record value to an `undefined` icon. + +#### Text mapping column + +The text mapping column will not have a configuration for a placeholder. + +| Special-cased field values | Cell display | Group row display | +| -------------------------- | ------------- | ------------------ | +| `undefined` | \ | `"No value"` | +| `null` | \ | `"No value"` | +| Non-mapped value\* | \ | \ | + +\*This is considered invalid data from the table's perspective and should be fixed within the client application. + +Column best practices: + +- Avoid mixing `undefined` and `null` as values for the same field. When grouping this will lead to two groups (one for `null` values and one for `undefined` values) that both have the text `"No value"`. +- Avoid using values that do not correspond to a mapping for the column. + +### Implementation plan + +A column's placeholder will be stored as part of that column's `columnConfig` object. There will be no changes to the `TableColumn`, `ColumnInternals`, or `ColumnInternalsOptions` classes. The placeholder from the `columnConfig` object will be used by the cell views when rendering the cell. + +We will create a placeholder mixin that adds the following to columns that chose to use it: + +- `placeholder` string property +- `placeholder` attribute +- abstract `placeholderChanged` function that will force columns using the mixin to implement `placeholderChanged` to update their column configuration + +The columns that will be updated to use this mixin are: + +- TableColumnText +- TableColumnAnchor +- TableColumnNumberText +- TableColumnDateText +- TableColumnDurationText + +### Localization + +All group row placeholder strings will be localized through the table's localization provider. Those strings are: + +- No value +- Empty + +If an application is localized, it can set a column's `placeholder` to a localized value. + +## Alternative Implementations / Designs + +### Configurable placeholders in group rows + +In addition to extending column APIs to have a `placeholder` property, they could also be extended to have a `group-placeholder` property. However, this level of configuration is not required by any client applications today. This feature is also likely to introduce inconsistency throughout our applications. + +If an application wants to modify the value of a group row placeholder without this feature, they can do so through the localization provider. + +### Allow different placeholders to be configured for each cell + +We could add the ability to have different placeholders configured for each cell, but this poses a few different problems: + +1. This would likely need to be a drastically different API, such as having the placeholder specified in the record. That would lead to quite a bit of duplicate information being set on the table, particularly when there are no use cases for this right now. +1. This would lead to a confusing state for the user because the placholder would be different for various rows, but all those rows would be in a single group. + +A use case for different information being presented to the user for each cell will likely be solved by [a different feature to show cell-specific state](https://github.com/ni/nimble/issues/1776). + +## Future Work + +Future columns should consider adding a placeholder as part of their API. The general guidance for placeholders is: + +- Group rows should always have a non-blank value to display, assuming the data provided to the table is valid. +- A column's API should support an application being able to render placeholder text in cell when the record value is `undefined` or `null`. + - There are exceptions to this, such as the enum text column because the client application is expected to provide only known values that correspond to specified mappings. + +## Open Issues + +_None_ diff --git a/packages/nimble-components/src/wafer-map/index.ts b/packages/nimble-components/src/wafer-map/index.ts index a37ef562cd..aed309664d 100644 --- a/packages/nimble-components/src/wafer-map/index.ts +++ b/packages/nimble-components/src/wafer-map/index.ts @@ -5,6 +5,7 @@ import { } from '@microsoft/fast-element'; import { DesignSystem, FoundationElement } from '@microsoft/fast-foundation'; import { zoomIdentity, ZoomTransform } from 'd3-zoom'; +import type { Table } from 'apache-arrow'; import { template } from './template'; import { styles } from './styles'; import { DataManager } from './modules/data-manager'; @@ -21,6 +22,7 @@ import { } from './types'; import { WaferMapUpdateTracker } from './modules/wafer-map-update-tracker'; import { WaferMapValidator } from './modules/wafer-map-validator'; +import { WorkerRenderer } from './modules/worker-renderer'; declare global { interface HTMLElementTagNameMap { @@ -90,7 +92,14 @@ export class WaferMap extends FoundationElement { /** * @internal */ - public readonly renderer = new RenderingModule(this); + public readonly mainRenderer = new RenderingModule(this); + /** + * @internal + */ + public readonly workerRenderer = new WorkerRenderer(this); + + @observable + public renderer: RenderingModule | WorkerRenderer = this.mainRenderer; /** * @internal @@ -139,6 +148,8 @@ export class WaferMap extends FoundationElement { @observable public highlightedTags: string[] = []; @observable public dies: WaferMapDie[] = []; + @observable public diesTable: Table | undefined; + @observable public colorScale: WaferMapColorScale = { colors: [], values: [] @@ -175,9 +186,12 @@ export class WaferMap extends FoundationElement { * The hover does not require an event update, but it's also the last update in the sequence. */ public update(): void { + this.validate(); + if (this.validity.invalidDiesTableSchema) { + return; + } if (this.waferMapUpdateTracker.requiresEventsUpdate) { this.eventCoordinator.detachEvents(); - this.waferMapValidator.validateGridDimensions(); if (this.waferMapUpdateTracker.requiresContainerDimensionsUpdate) { this.dataManager.updateContainerDimensions(); this.renderer.updateSortedDiesAndDrawWafer(); @@ -203,6 +217,11 @@ export class WaferMap extends FoundationElement { } } + private validate(): void { + this.waferMapValidator.validateGridDimensions(); + this.waferMapValidator.validateDiesTableSchema(); + } + private createResizeObserver(): ResizeObserver { const resizeObserver = new ResizeObserver(entries => { const entry = entries[0]; @@ -272,6 +291,17 @@ export class WaferMap extends FoundationElement { private diesChanged(): void { this.waferMapUpdateTracker.track('dies'); + this.renderer = this.diesTable === undefined + ? this.mainRenderer + : this.workerRenderer; + this.waferMapUpdateTracker.queueUpdate(); + } + + private diesTableChanged(): void { + this.waferMapUpdateTracker.track('dies'); + this.renderer = this.diesTable === undefined + ? this.mainRenderer + : this.workerRenderer; this.waferMapUpdateTracker.queueUpdate(); } diff --git a/packages/nimble-components/src/wafer-map/modules/prerendering.ts b/packages/nimble-components/src/wafer-map/modules/prerendering.ts index eb3bdab107..9d4db977d0 100644 --- a/packages/nimble-components/src/wafer-map/modules/prerendering.ts +++ b/packages/nimble-components/src/wafer-map/modules/prerendering.ts @@ -52,9 +52,12 @@ export class Prerendering { this.wafermap.colorScale, this.wafermap.colorScaleMode ); + const isDieRenderInfo = ( + info: DieRenderInfo | null + ): info is DieRenderInfo => info !== null; this._diesRenderInfo = this.wafermap.dies .map(die => this.computeDieRenderInfo(die)) - .filter(info => info !== null) as DieRenderInfo[]; + .filter(isDieRenderInfo); } private computeDieRenderInfo(die: WaferMapDie): DieRenderInfo | null { diff --git a/packages/nimble-components/src/wafer-map/modules/wafer-map-validator.ts b/packages/nimble-components/src/wafer-map/modules/wafer-map-validator.ts index 06aa8fb2ad..9ef38a00a0 100644 --- a/packages/nimble-components/src/wafer-map/modules/wafer-map-validator.ts +++ b/packages/nimble-components/src/wafer-map/modules/wafer-map-validator.ts @@ -1,3 +1,4 @@ +import { DataType, Precision } from 'apache-arrow'; import type { WaferMap } from '..'; import type { WaferMapValidity } from '../types'; @@ -7,11 +8,13 @@ import type { WaferMapValidity } from '../types'; */ export class WaferMapValidator { private invalidGridDimensions = false; + private invalidDiesTableSchema = false; public constructor(private readonly wafermap: WaferMap) {} public getValidity(): WaferMapValidity { return { - invalidGridDimensions: this.invalidGridDimensions + invalidGridDimensions: this.invalidGridDimensions, + invalidDiesTableSchema: this.invalidDiesTableSchema }; } @@ -40,4 +43,48 @@ export class WaferMapValidator { } return !this.invalidGridDimensions; } + + public validateDiesTableSchema(): boolean { + this.invalidDiesTableSchema = false; + if (this.wafermap.diesTable === undefined) { + this.invalidDiesTableSchema = false; + } else { + const colIndexField = this.wafermap.diesTable.schema.fields.findIndex( + f => f.name === 'colIndex' + ); + const rowIndexField = this.wafermap.diesTable.schema.fields.findIndex( + f => f.name === 'rowIndex' + ); + const valueField = this.wafermap.diesTable.schema.fields.findIndex( + f => f.name === 'value' + ); + if ( + this.wafermap.diesTable.numCols < 3 + || colIndexField === -1 + || rowIndexField === -1 + || valueField === -1 + || !DataType.isInt( + this.wafermap.diesTable.schema.fields[colIndexField]!.type + ) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + || this.wafermap.diesTable.schema.fields[colIndexField]!.type + .bitWidth !== 32 + || !DataType.isInt( + this.wafermap.diesTable.schema.fields[rowIndexField]!.type + ) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + || this.wafermap.diesTable.schema.fields[rowIndexField]!.type + .bitWidth !== 32 + || !DataType.isFloat( + this.wafermap.diesTable.schema.fields[valueField]!.type + ) + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + || this.wafermap.diesTable.schema.fields[valueField]!.type + .precision !== Precision.DOUBLE + ) { + this.invalidDiesTableSchema = true; + } + } + return !this.invalidDiesTableSchema; + } } diff --git a/packages/nimble-components/src/wafer-map/modules/worker-renderer.ts b/packages/nimble-components/src/wafer-map/modules/worker-renderer.ts new file mode 100644 index 0000000000..24dbcc8a73 --- /dev/null +++ b/packages/nimble-components/src/wafer-map/modules/worker-renderer.ts @@ -0,0 +1,53 @@ +import type { WaferMap } from '..'; +import { HoverDieOpacity } from '../types'; + +/** + * Responsible for drawing the dies inside the wafer map, adding dieText and scaling the canvas + */ +export class WorkerRenderer { + public constructor(private readonly wafermap: WaferMap) {} + + public updateSortedDiesAndDrawWafer(): void { + // redundant function for backwards compatibility + this.drawWafer(); + } + + public drawWafer(): void { + // rendering will be implemented in a future PR + this.renderHover(); + } + + public renderHover(): void { + this.wafermap.hoverWidth = this.wafermap.dataManager.dieDimensions.width + * this.wafermap.transform.k; + this.wafermap.hoverHeight = this.wafermap.dataManager.dieDimensions.height + * this.wafermap.transform.k; + this.wafermap.hoverOpacity = this.wafermap.hoverDie === undefined + ? HoverDieOpacity.hide + : HoverDieOpacity.show; + this.wafermap.hoverTransform = this.calculateHoverTransform(); + } + + private calculateHoverTransform(): string { + if (this.wafermap.hoverDie !== undefined) { + const scaledX = this.wafermap.dataManager.horizontalScale( + this.wafermap.hoverDie.x + ); + if (scaledX === undefined) { + return ''; + } + const scaledY = this.wafermap.dataManager.verticalScale( + this.wafermap.hoverDie.y + ); + if (scaledY === undefined) { + return ''; + } + const transformedPoint = this.wafermap.transform.apply([ + scaledX + this.wafermap.dataManager.margin.left, + scaledY + this.wafermap.dataManager.margin.top + ]); + return `translate(${transformedPoint[0]}, ${transformedPoint[1]})`; + } + return ''; + } +} diff --git a/packages/nimble-components/src/wafer-map/tests/data-generator.ts b/packages/nimble-components/src/wafer-map/tests/data-generator.ts index e89fc9cbc8..fff32e67e5 100644 --- a/packages/nimble-components/src/wafer-map/tests/data-generator.ts +++ b/packages/nimble-components/src/wafer-map/tests/data-generator.ts @@ -1,3 +1,4 @@ +import { Table, tableFromArrays } from 'apache-arrow'; import type { WaferMapDie } from '../types'; import type { IValueGenerator } from './value-generator'; @@ -19,6 +20,20 @@ const generateStringValue = ( return valueToString(value); }; +const generateFloatValue = ( + x: number, + y: number, + valueGenerator: IValueGenerator +): number => { + let value: number; + if (valueGenerator !== undefined) { + value = valueGenerator(x, y); + } else { + value = Math.random() * 100; + } + return value; +}; + const generateTagValue = (valueGenerator: IValueGenerator): string => { let value: string; if (valueGenerator !== undefined) { @@ -91,3 +106,54 @@ export const generateWaferData = ( } return diesSet; }; + +export const generateWaferTableData = ( + numDies: number, + valueGenerator: IValueGenerator +): Table => { + const colIndex = []; + const rowIndex = []; + const value = []; + + if (numDies > 0) { + // calculate the equivalent radius of a circle that would contain the <<<>>> number of dies + const radius = Math.ceil(Math.sqrt(numDies / Math.PI)); + const centerX = radius; + const centerY = radius; + + // Generate dies values - start from the bottom and go up + for (let i = centerY - radius; i <= centerY + radius; i++) { + let stringValue: number; + + // generate points left of centerX + for ( + let j = centerX; + (j - centerX) * (j - centerX) + (i - centerY) * (i - centerY) + <= radius * radius; + j-- + ) { + stringValue = generateFloatValue(i, j, valueGenerator); + colIndex.push(j); + rowIndex.push(i); + value.push(stringValue); + } + // generate points right of centerX + for ( + let j = centerX + 1; + (j - centerX) * (j - centerX) + (i - centerY) * (i - centerY) + <= radius * radius; + j++ + ) { + stringValue = generateFloatValue(i, j, valueGenerator); + colIndex.push(j); + rowIndex.push(i); + value.push(stringValue); + } + } + } + return tableFromArrays({ + colIndex: Int32Array.from(colIndex), + rowIndex: Int32Array.from(rowIndex), + value: Float32Array.from(value) + }); +}; diff --git a/packages/nimble-components/src/wafer-map/tests/sets.ts b/packages/nimble-components/src/wafer-map/tests/sets.ts index 96045d371d..3b26320e4c 100644 --- a/packages/nimble-components/src/wafer-map/tests/sets.ts +++ b/packages/nimble-components/src/wafer-map/tests/sets.ts @@ -1,3 +1,4 @@ +import { Table, tableFromArrays } from 'apache-arrow'; import type { WaferMapDie, WaferMapColorScale } from '../types'; export const highlightedTagsSets: string[][] = [ @@ -100,6 +101,62 @@ export const wafermapDieSets: WaferMapDie[][] = [ ] ]; +export const wafermapDiesTableSets: Table[] = [ + tableFromArrays({ + colIndex: Int32Array.from([0, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4]), + rowIndex: Int32Array.from([2, 2, 1, 3, 2, 1, 0, 3, 4, 2, 1, 3, 2]), + value: Float32Array.from([ + 14.24, 76.43, 44.63, 67.93, 72.71, 79.04, 26.49, 37.79, 59.82, 52.9, + 98.5, 20.83, 62.8 + ]), + firstTag: [ + 'a', + 'b', + 'g', + 'a', + 'h', + 'b', + 'c', + null, + null, + null, + 'g', + 'c', + 'g' + ], + secondTag: [ + 'b', + 'c', + null, + null, + 'e', + null, + null, + null, + null, + null, + null, + null, + null + ], + metadata: [ + 'metadata02', + 'metadata12', + 'metadata11', + 'metadata13', + 'metadata22', + 'metadata21', + 'metadata20', + 'metadata23', + 'metadata24', + 'metadata32', + 'metadata31', + 'metadata33', + 'metadata42' + ] + }) +]; + export const waferMapColorScaleSets: WaferMapColorScale[] = [ { colors: ['red', 'orange', 'green'], diff --git a/packages/nimble-components/src/wafer-map/tests/utilities.ts b/packages/nimble-components/src/wafer-map/tests/utilities.ts index f99e96a02e..f654e3b9bf 100644 --- a/packages/nimble-components/src/wafer-map/tests/utilities.ts +++ b/packages/nimble-components/src/wafer-map/tests/utilities.ts @@ -1,4 +1,5 @@ import { ScaleBand, scaleBand } from 'd3-scale'; +import type { Table } from 'apache-arrow'; import { Dimensions, Margin, @@ -122,7 +123,10 @@ export function getWaferMapMockComputations( originLocation: WaferMapOriginLocation, canvasWidth: number, canvasHeight: number, - validity: WaferMapValidity = { invalidGridDimensions: false } + validity: WaferMapValidity = { + invalidGridDimensions: false, + invalidDiesTableSchema: false + } ): Pick< WaferMap, 'dies' | 'originLocation' | 'canvasWidth' | 'canvasHeight' | 'validity' @@ -140,12 +144,17 @@ export function getWaferMapMockValidator( gridMinX: number | undefined, gridMaxX: number | undefined, gridMinY: number | undefined, - gridMaxY: number | undefined -): Pick { + gridMaxY: number | undefined, + diesTable: Table | undefined = undefined +): Pick< + WaferMap, + 'gridMinX' | 'gridMaxX' | 'gridMinY' | 'gridMaxY' | 'diesTable' + > { return { gridMinX, gridMaxX, gridMinY, - gridMaxY + gridMaxY, + diesTable }; } diff --git a/packages/nimble-components/src/wafer-map/tests/wafer-map-validator.spec.ts b/packages/nimble-components/src/wafer-map/tests/wafer-map-validator.spec.ts index 7325e432ce..732833fa9a 100644 --- a/packages/nimble-components/src/wafer-map/tests/wafer-map-validator.spec.ts +++ b/packages/nimble-components/src/wafer-map/tests/wafer-map-validator.spec.ts @@ -1,3 +1,4 @@ +import { Table, tableFromArrays } from 'apache-arrow'; import type { WaferMap } from '..'; import { WaferMapValidator } from '../modules/wafer-map-validator'; import { getWaferMapMockValidator } from './utilities'; @@ -5,80 +6,175 @@ import { getWaferMapMockValidator } from './utilities'; describe('Wafermap Validator module', () => { let waferMapValidator: WaferMapValidator; - describe('with undefined grid dimensions', () => { - beforeEach(() => { - const waferMock = getWaferMapMockValidator( - undefined, - undefined, - undefined, - undefined - ); - waferMapValidator = new WaferMapValidator(waferMock as WaferMap); - waferMapValidator.validateGridDimensions(); - }); + it('with undefined grid dimensions should be valid', () => { + const waferMock = getWaferMapMockValidator( + undefined, + undefined, + undefined, + undefined + ); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateGridDimensions(); - it('should be valid', () => { - expect(waferMapValidator.isValid()).toBeTrue(); - }); + expect(waferMapValidator.isValid()).toBeTrue(); }); - describe('with equal grid dimensions', () => { - beforeEach(() => { - const waferMock = getWaferMapMockValidator(0, 0, 0, 0); - waferMapValidator = new WaferMapValidator(waferMock as WaferMap); - waferMapValidator.validateGridDimensions(); - }); + it('with equal grid dimensions should be valid', () => { + const waferMock = getWaferMapMockValidator(0, 0, 0, 0); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateGridDimensions(); - it('should be valid', () => { - expect(waferMapValidator.isValid()).toBeTrue(); - }); + expect(waferMapValidator.isValid()).toBeTrue(); }); - describe('with positive grid dimensions', () => { - beforeEach(() => { - const waferMock = getWaferMapMockValidator(1, 2, 1, 2); - waferMapValidator = new WaferMapValidator(waferMock as WaferMap); - waferMapValidator.validateGridDimensions(); - }); + it('with positive grid dimensions should be valid', () => { + const waferMock = getWaferMapMockValidator(1, 2, 1, 2); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateGridDimensions(); - it('should be valid', () => { - expect(waferMapValidator.isValid()).toBeTrue(); - }); + expect(waferMapValidator.isValid()).toBeTrue(); }); - describe('with negative grid dimensions', () => { - beforeEach(() => { - const waferMock = getWaferMapMockValidator(-2, -1, -2, -1); - waferMapValidator = new WaferMapValidator(waferMock as WaferMap); - waferMapValidator.validateGridDimensions(); - }); + it('with negative grid dimensions should be valid', () => { + const waferMock = getWaferMapMockValidator(-2, -1, -2, -1); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateGridDimensions(); - it('should be valid', () => { - expect(waferMapValidator.isValid()).toBeTrue(); - }); + expect(waferMapValidator.isValid()).toBeTrue(); }); - describe('with one undefined grid dimension', () => { - beforeEach(() => { - const waferMock = getWaferMapMockValidator(0, 0, 0, undefined); - waferMapValidator = new WaferMapValidator(waferMock as WaferMap); - waferMapValidator.validateGridDimensions(); - }); + it('with one undefined grid dimension should not be valid', () => { + const waferMock = getWaferMapMockValidator(0, 0, 0, undefined); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateGridDimensions(); - it('should not be valid', () => { - expect(waferMapValidator.isValid()).toBeFalse(); - }); + expect(waferMapValidator.isValid()).toBeFalse(); }); - describe('with impossible grid dimension', () => { - beforeEach(() => { - const waferMock = getWaferMapMockValidator(1, -1, 1, -1); - waferMapValidator = new WaferMapValidator(waferMock as WaferMap); - waferMapValidator.validateGridDimensions(); - }); + it('with impossible grid dimension should not be valid', () => { + const waferMock = getWaferMapMockValidator(1, -1, 1, -1); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateGridDimensions(); - it('should not be valid', () => { - expect(waferMapValidator.isValid()).toBeFalse(); + expect(waferMapValidator.getValidity()).toEqual({ + invalidGridDimensions: true, + invalidDiesTableSchema: false }); + expect(waferMapValidator.isValid()).toBeFalse(); + }); + + it('with undefined dies table should be valid', () => { + const waferMock = getWaferMapMockValidator( + undefined, + undefined, + undefined, + undefined, + undefined + ); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateDiesTableSchema(); + expect(waferMapValidator.isValid()).toBeTrue(); + }); + + it('with colIndex, rowIndex and value column as Int32, Int32 and Float64 dies table should be valid', () => { + const waferMock = getWaferMapMockValidator( + undefined, + undefined, + undefined, + undefined, + tableFromArrays({ + colIndex: Int32Array.from([]), + rowIndex: Int32Array.from([]), + value: Float64Array.from([]) + }) + ); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateDiesTableSchema(); + + expect(waferMapValidator.isValid()).toBeTrue(); + }); + + it('with colIndex, rowIndex and value column as Int32, Int32 and Float32 dies table should be invalid', () => { + const waferMock = getWaferMapMockValidator( + undefined, + undefined, + undefined, + undefined, + tableFromArrays({ + colIndex: Int32Array.from([]), + rowIndex: Int32Array.from([]), + value: Float32Array.from([]) + }) + ); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateDiesTableSchema(); + + expect(waferMapValidator.isValid()).toBeFalse(); + }); + + it('with colIndex, rowIndex and value column as Int8, Int32 and Float64 dies table should be invalid', () => { + const waferMock = getWaferMapMockValidator( + undefined, + undefined, + undefined, + undefined, + tableFromArrays({ + colIndex: Int8Array.from([]), + rowIndex: Int32Array.from([]), + value: Float64Array.from([]) + }) + ); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateDiesTableSchema(); + + expect(waferMapValidator.isValid()).toBeFalse(); + }); + + it('with colIndex, rowIndex and value column as Int32 dies table should be invalid', () => { + const waferMock = getWaferMapMockValidator( + undefined, + undefined, + undefined, + undefined, + tableFromArrays({ + colIndex: Int32Array.from([]), + rowIndex: Int32Array.from([]), + value: Int32Array.from([]) + }) + ); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateDiesTableSchema(); + + expect(waferMapValidator.isValid()).toBeFalse(); + }); + + it('with no column dies table should be invalid', () => { + const waferMock = getWaferMapMockValidator( + undefined, + undefined, + undefined, + undefined, + new Table() + ); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateDiesTableSchema(); + + expect(waferMapValidator.isValid()).toBeFalse(); + }); + it('with just colIndex and rowIndex column dies table should be invalid', () => { + const waferMock = getWaferMapMockValidator( + undefined, + undefined, + undefined, + undefined, + tableFromArrays({ + colIndex: Int32Array.from([]), + rowIndex: Int32Array.from([]) + }) + ); + waferMapValidator = new WaferMapValidator(waferMock as WaferMap); + waferMapValidator.validateDiesTableSchema(); + + expect(waferMapValidator.isValid()).toBeFalse(); }); }); diff --git a/packages/nimble-components/src/wafer-map/tests/wafer-map.spec.ts b/packages/nimble-components/src/wafer-map/tests/wafer-map.spec.ts index 82326e07db..f811df6fce 100644 --- a/packages/nimble-components/src/wafer-map/tests/wafer-map.spec.ts +++ b/packages/nimble-components/src/wafer-map/tests/wafer-map.spec.ts @@ -1,4 +1,5 @@ import { html } from '@microsoft/fast-element'; +import { Table, tableFromArrays } from 'apache-arrow'; import { WaferMap } from '..'; import { processUpdates } from '../../testing/async-helpers'; import { type Fixture, fixture } from '../../utilities/tests/fixture'; @@ -7,6 +8,8 @@ import { WaferMapOrientation, WaferMapOriginLocation } from '../types'; +import { RenderingModule } from '../modules/rendering'; +import { WorkerRenderer } from '../modules/worker-renderer'; async function setup(): Promise> { return fixture(html``); @@ -85,6 +88,24 @@ describe('WaferMap', () => { expect(spy).toHaveBeenCalledTimes(1); }); + it('will use RenderingModule after dies change', () => { + element.dies = [{ x: 1, y: 1, value: '1' }]; + processUpdates(); + expect(element.renderer instanceof RenderingModule).toBeTrue(); + }); + + it('will update once after diesTable change', () => { + element.diesTable = new Table(); + processUpdates(); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('will use WorkerRenderer after diesTable change', () => { + element.diesTable = new Table(); + processUpdates(); + expect(element.renderer instanceof WorkerRenderer).toBeTrue(); + }); + it('will update once after colorScale changes', () => { element.colorScale = { colors: ['red', 'red'], values: ['1', '1'] }; processUpdates(); @@ -106,6 +127,56 @@ describe('WaferMap', () => { }); }); + describe('worker renderer draw flow', () => { + let drawWaferSpy: jasmine.Spy; + beforeEach(() => { + drawWaferSpy = spyOn(element.workerRenderer, 'drawWafer'); + }); + + it('will call drawWafer after supported diesTable change', () => { + element.diesTable = tableFromArrays({ + colIndex: Int32Array.from([]), + rowIndex: Int32Array.from([]), + value: Float64Array.from([]) + }); + processUpdates(); + expect(element.validity.invalidDiesTableSchema).toBeFalse(); + expect(drawWaferSpy).toHaveBeenCalledTimes(1); + }); + + it('will not call drawWafer after unsupported diesTable change', () => { + element.diesTable = new Table(); + processUpdates(); + expect(element.validity.invalidDiesTableSchema).toBeTrue(); + expect(drawWaferSpy).toHaveBeenCalledTimes(0); + }); + }); + + describe('worker renderer flow', () => { + let renderHoverSpy: jasmine.Spy; + beforeEach(() => { + renderHoverSpy = spyOn(element.workerRenderer, 'renderHover'); + }); + + it('will call renderHover after supported diesTable change', () => { + element.diesTable = tableFromArrays({ + colIndex: Int32Array.from([]), + rowIndex: Int32Array.from([]), + value: Float64Array.from([]) + }); + processUpdates(); + expect(element.validity.invalidDiesTableSchema).toBeFalse(); + expect(renderHoverSpy).toHaveBeenCalledTimes(1); + }); + + it('will not call renderHover after unsupported diesTable change', () => { + element.diesTable = new Table(); + processUpdates(); + expect(element.validity.invalidDiesTableSchema).toBeTrue(); + expect(renderHoverSpy).toHaveBeenCalledTimes(0); + }); + }); + describe('zoom flow', () => { let initialValue: string | undefined; diff --git a/packages/nimble-components/src/wafer-map/tests/wafer-map.stories.ts b/packages/nimble-components/src/wafer-map/tests/wafer-map.stories.ts index 2d4a35ac5a..2cd7f02974 100644 --- a/packages/nimble-components/src/wafer-map/tests/wafer-map.stories.ts +++ b/packages/nimble-components/src/wafer-map/tests/wafer-map.stories.ts @@ -1,10 +1,11 @@ import { html } from '@microsoft/fast-element'; import type { Meta, StoryObj } from '@storybook/html'; +import type { Table } from 'apache-arrow'; import { createUserSelectedThemeStory, incubatingWarning } from '../../utilities/tests/storybook'; -import { generateWaferData } from './data-generator'; +import { generateWaferData, generateWaferTableData } from './data-generator'; import { goodValueGenerator, badValueGenerator, @@ -23,7 +24,8 @@ import { import { highlightedTagsSets, wafermapDieSets, - waferMapColorScaleSets + waferMapColorScaleSets, + wafermapDiesTableSets } from './sets'; import { waferMapTag } from '..'; @@ -32,7 +34,10 @@ interface WaferMapArgs { colorScaleMode: WaferMapColorScaleMode; dieLabelsHidden: boolean; dieLabelsSuffix: string; + apiVersion: 'stable' | 'experimental'; dies: string; + highlightedTags: string; + diesTable: string; maxCharacters: number; orientation: WaferMapOrientation; originLocation: WaferMapOriginLocation; @@ -42,7 +47,6 @@ interface WaferMapArgs { gridMaxY: number | undefined; dieHover: unknown; validity: WaferMapValidity; - highlightedTags: string; } const getDiesSet = ( @@ -82,6 +86,37 @@ const getDiesSet = ( return returnedValue; }; +const getDiesTableSet = (setName: string, sets: Table[]): Table | undefined => { + const seed = 0.5; + let returnedValue: Table | undefined; + switch (setName) { + case 'fixedDies10': + returnedValue = sets[0]!; + break; + case 'goodDies100': + returnedValue = generateWaferTableData( + 100, + goodValueGenerator(seed) + ); + break; + case 'goodDies1000': + returnedValue = generateWaferTableData( + 1000, + goodValueGenerator(seed) + )!; + break; + case 'badDies10000': + returnedValue = generateWaferTableData( + 10000, + badValueGenerator(seed) + )!; + break; + default: + returnedValue = undefined; + } + return returnedValue; +}; + const getHighlightedTags = (setName: string, sets: string[][]): string[] => { let returnedValue: string[]; switch (setName) { @@ -107,6 +142,7 @@ const getHighlightedTags = (setName: string, sets: string[][]): string[] => { const metadata: Meta = { title: 'Incubating/Wafer Map', parameters: { + viewMode: 'docs', actions: { handles: ['click', 'die-hover'] } @@ -126,6 +162,7 @@ const metadata: Meta = { :colorScale="${x => x.colorScale}" :dies="${x => getDiesSet(x.dies, wafermapDieSets)}" :highlightedTags="${x => getHighlightedTags(x.highlightedTags, highlightedTagsSets)}" + :diesTable="${x => getDiesTableSet(x.diesTable, wafermapDiesTableSets)}" > `), args: { + apiVersion: 'stable', colorScale: waferMapColorScaleSets[0], colorScaleMode: WaferMapColorScaleMode.linear, dies: 'fixedDies10', + diesTable: undefined, + highlightedTags: 'set1', dieLabelsHidden: false, dieLabelsSuffix: '', - highlightedTags: 'set1', maxCharacters: 4, orientation: WaferMapOrientation.left, originLocation: WaferMapOriginLocation.bottomLeft, @@ -151,6 +190,20 @@ const metadata: Meta = { gridMaxY: undefined }, argTypes: { + apiVersion: { + name: 'API Version', + description: + 'Select the API version of the component. The stable version is the one that is recommended for production use, while the experimental version is the one that is still under development and is not recommended for production use. The default value is `stable`. To enable the Experimental API in code, the `diesTable` should be used in place of the `dies`.', + options: ['stable', 'experimental'], + control: { + type: 'inline-radio', + labels: { + stable: 'Stable', + experimental: 'Experimental' + } + }, + defaultValue: 'stable' + }, colorScale: { description: `Represents the color spectrum which shows the status of the dies on the wafer. @@ -185,7 +238,7 @@ const metadata: Meta = { } }, dies: { - description: `Represents the input data, an array of \`WaferMapDie\`, which will be rendered by the wafer map + description: `Represents the input data, an array of \`WaferMapDie\`, which will be rendered by the wafer map. Part of the Stable API.
Usage details @@ -207,19 +260,34 @@ const metadata: Meta = { badDies10000: 'Very large dies set of mostly bad values' } }, - defaultValue: 'set1' - }, - dieLabelsHidden: { - name: 'die-labels-hidden', - description: - 'Boolean value that determines if the dies labels in the wafer map view are shown or not. Default value is false.', - control: { type: 'boolean' } + defaultValue: 'fixedDies10', + if: { arg: 'apiVersion', eq: 'stable' } }, - dieLabelsSuffix: { - name: 'die-labels-suffix', - description: - 'String that can be added as a label at the end of each wafer map die value', - control: { type: 'text' } + diesTable: { + description: `Represents the input data, an apache-arrow \`Table\`, which will be rendered by the wafer map. Part of the Experimental API. + +
+ Usage details + The \`diesTable\` element is a public property. As such, it is not available as an attribute, however it can be read or set on the corresponding \`WaferMap\` DOM element. +
+ `, + options: [ + 'fixedDies10', + 'goodDies100', + 'goodDies1000', + 'badDies10000' + ], + control: { + type: 'radio', + labels: { + fixedDies10: 'Small dies set of fixed values', + goodDies100: 'Medium dies set of mostly good values', + goodDies1000: 'Large dies set of mostly good values', + badDies10000: 'Very large dies set of mostly bad values' + } + }, + defaultValue: 'fixedDies10', + if: { arg: 'apiVersion', eq: 'experimental' } }, highlightedTags: { description: `Represent a list of strings that will be highlighted in the wafer map view. Each die has a tags?: string[] property, if at least one element of highlightedTags equals at least one element of die.tags the die will be highlighted. @@ -241,6 +309,18 @@ const metadata: Meta = { }, defaultValue: 'set1' }, + dieLabelsHidden: { + name: 'die-labels-hidden', + description: + 'Boolean value that determines if the dies labels in the wafer map view are shown or not. Default value is false.', + control: { type: 'boolean' } + }, + dieLabelsSuffix: { + name: 'die-labels-suffix', + description: + 'String that can be added as a label at the end of each wafer map die value', + control: { type: 'text' } + }, maxCharacters: { name: 'max-characters', description: @@ -308,7 +388,9 @@ const metadata: Meta = { description: `Readonly object of boolean values that represents the validity states that the wafer map's configuration can be in. The object's type is \`WaferMapValidity\`, and it contains the following boolean properties: -- \`invalidGridDimensions \`: \`true\` when some of the \`gridMinX\`, \`gridMinY\`, \`gridMaxX\` or \`gridMaxY\` are \`undefined\`, but \`false\` when all of them are provided or all of them are \`undefined\``, +- \`invalidGridDimensions \`: \`true\` when some of the \`gridMinX\`, \`gridMinY\`, \`gridMaxX\` or \`gridMaxY\` are \`undefined\`, but \`false\` when all of them are provided or all of them are \`undefined\` + +- \`invalidDiesTableSchema \`: \`true\` when the \`diesTable\` does not have all of the three expected columns: \`colIndex\`, \`rowIndex\` and \`value\`, but \`false\` when all of them are provided or the \`diesTable\` is \`undefined\``, control: false } } diff --git a/packages/nimble-components/src/wafer-map/types.ts b/packages/nimble-components/src/wafer-map/types.ts index c8af44ad91..cac887b0a0 100644 --- a/packages/nimble-components/src/wafer-map/types.ts +++ b/packages/nimble-components/src/wafer-map/types.ts @@ -1,5 +1,3 @@ -import type { DataManager } from './modules/data-manager'; - export const WaferMapOriginLocation = { bottomLeft: 'bottom-left', bottomRight: 'bottom-right', @@ -50,13 +48,6 @@ export interface WaferMapColorScale { values: string[]; } -export interface HoverHandlerData { - canvas: HTMLCanvasElement; - rect: HTMLElement; - dataManager: DataManager; - originLocation: WaferMapOriginLocation; -} - export interface Dimensions { readonly width: number; readonly height: number; @@ -86,4 +77,5 @@ export interface ValidityObject { } export interface WaferMapValidity extends ValidityObject { readonly invalidGridDimensions: boolean; + readonly invalidDiesTableSchema: boolean; }