diff --git a/package-lock.json b/package-lock.json index 53cf38f..400719c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,13 @@ "name": "@geneontology/web-components", "version": "0.0.1", "license": "MIT", + "dependencies": { + "@geneontology/dbxrefs": "^1.0.16" + }, "devDependencies": { "@eslint/js": "^9.15.0", "@stencil/core": "^4.22.3", + "@stencil/sass": "^3.0.12", "@types/jest": "^29.5.14", "@types/node": "^22.9.1", "eslint": "^9.15.0", @@ -691,6 +695,34 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@geneontology/dbxrefs": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/@geneontology/dbxrefs/-/dbxrefs-1.0.16.tgz", + "integrity": "sha512-zKvbcfs+LU4ZnUo00A5ae0V+FVMIM4ftxcfKtJWmtnLKwWy1sOn2y36HGcz/rEmLwd2zBApKJ0ojmgBH416Q5Q==", + "license": "BSD-3", + "dependencies": { + "axios": "^0.21.1", + "js-yaml": "^4.0.0" + } + }, + "node_modules/@geneontology/dbxrefs/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/@geneontology/dbxrefs/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1244,6 +1276,20 @@ "npm": ">=7.10.0" } }, + "node_modules/@stencil/sass": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@stencil/sass/-/sass-3.0.12.tgz", + "integrity": "sha512-aXMgpG13ftxLYo2dDauapvE9gKzSxTAqCMOfTqbPhKUCZ43JsknkLx+PArRaFtfYeVGSQ8eTS4ck7/Nlec+PNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0", + "npm": ">=6.0.0" + }, + "peerDependencies": { + "@stencil/core": ">=2.0.0 || >=3.0.0-beta.0 || >= 4.0.0-beta.0 || >= 4.0.0" + } + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -1772,6 +1818,15 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, "node_modules/b4a": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz", @@ -3030,6 +3085,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", diff --git a/package.json b/package.json index ba77031..a93a05f 100644 --- a/package.json +++ b/package.json @@ -37,9 +37,13 @@ "test.watch": "stencil test --spec --e2e --watchAll", "generate": "stencil generate" }, + "dependencies": { + "@geneontology/dbxrefs": "^1.0.16" + }, "devDependencies": { "@eslint/js": "^9.15.0", "@stencil/core": "^4.22.3", + "@stencil/sass": "^3.0.12", "@types/jest": "^29.5.14", "@types/node": "^22.9.1", "eslint": "^9.15.0", diff --git a/src/components.d.ts b/src/components.d.ts index 0717c0e..0478284 100644 --- a/src/components.d.ts +++ b/src/components.d.ts @@ -5,16 +5,805 @@ * It contains typing information for all components that exist in this project. */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; -export namespace Components {} +import { IRibbonGroup, IRibbonModel, IRibbonSubject } from "./globals/models"; +export { IRibbonGroup, IRibbonModel, IRibbonSubject } from "./globals/models"; +export namespace Components { + interface WcGoAutocomplete { + /** + * Category to constrain the search; by default search "gene" Other values accepted: `undefined` : search both terms and genes `gene` : will only search genes used in GO `biological%20process` : will search for GO BP terms `molecular%20function` : will search for GO MF terms `cellular%20component` : will search for GO CC terms `cellular%20component,molecular%20function,biological%20process` : will search any GO term + */ + category: string; + /** + * Maximum number of results to show + */ + maxResults: number; + /** + * Default placeholder for the autocomplete + */ + placeholder: string; + value: string; + } + interface WcGoRibbon { + /** + * add a cell at the beginning of each row/subject to show all annotations + */ + addCellAll: boolean; + annotationLabels: string; + baseApiUrl: string; + /** + * false = show a gradient of colors to indicate the value of a cell true = show only two colors (minColor; maxColor) to indicate the values of a cell + */ + binaryColor: boolean; + /** + * 0 = Normal 1 = Bold + */ + categoryAllStyle: number; + /** + * Override of the category case 0 (default) = unchanged 1 = to lower case 2 = to upper case + */ + categoryCase: number; + /** + * 0 = Normal 1 = Bold + */ + categoryOtherStyle: number; + classLabels: string; + /** + * Which value to base the cell color on 0 = class count 1 = annotation count + */ + colorBy: number; + /** + * if provided, will override any value provided in subjects and subset + */ + data: string; + excludePB: boolean; + /** + * Filter rows based on the presence of one or more values in a given column The filtering will be based on cell label or id Example: filter-by="evidence:ISS,ISO or multi-step filters: filter-by:evidence:ISS,ISO;term:xxx" Note: if value is "", remove any filtering + */ + filterBy: string; + filterCrossAspect: boolean; + filterReference: string; + /** + * If true, the ribbon will fire an event if a user click an empty cell If false, the ribbon will not fire the event on an empty cell Note: if selectionMode == SELECTION.COLUMN, then the event will trigger if at least one of the selected cells has annotations + */ + fireEventOnEmptyCells: boolean; + groupBaseUrl: string; + /** + * Using this parameter, the table rows can bee grouped based on column ids A multiple step grouping is possible by using a ";" between groups The grouping applies before the ordering Example: hid-1,hid-3 OR hid-1,hid-3;hid-2 Note: if value is "", remove any grouping + */ + groupBy: string; + groupClickable: boolean; + groupMaxLabelSize: number; + groupNewTab: boolean; + /** + * Used to hide specific column of the table + */ + hideColumns: string; + maxColor: string; + maxHeatLevel: number; + minColor: string; + /** + * This is used to sort the table depending of a column The column cells must be single values The ordering applies after the grouping Note: if value is "", remove any ordering + */ + orderBy: string; + /** + * If no value is provided, the ribbon will load without any group selected. If a value is provided, the ribbon will show the requested group as selected The value should be the id of the group to be selected + */ + selected: any; + /** + * Click handling of a cell. 0 = select only the cell (1 subject, 1 group) 1 = select the whole column (all subjects, 1 group) + */ + selectionMode: number; + /** + * add a cell at the end of each row/subject to represent all annotations not mapped to a specific term + */ + showOtherGroup: boolean; + subjectBaseUrl: string; + subjectOpenNewTab: boolean; + /** + * Position the subject label of each row 0 = None 1 = Left 2 = Right 3 = Bottom + */ + subjectPosition: number; + subjectUseTaxonIcon: boolean; + /** + * provide gene ids (e.g. RGD:620474,RGD:3889 or as a list ["RGD:620474", "RGD:3889"]) + */ + subjects: string; + subset: string; + } + interface WcLightModal { + close: () => Promise; + modalAnchor: string; + modalContent: string; + modalTitle: string; + open: () => Promise; + toggle: () => Promise; + x: number; + y: number; + } + interface WcRibbonCell { + annotationLabels: string; + /** + * If set to true, won't show any color and can not be hovered or selected This is used for group that can not have annotation for a given subject + */ + available: boolean; + binaryColor: boolean; + classLabels: string; + colorBy: number; + group: IRibbonGroup; + hovered: boolean; + maxColor: string; + maxHeatLevel: number; + minColor: string; + selected: boolean; + subject: IRibbonSubject; + } + interface WcRibbonStrips { + /** + * add a cell at the beginning of each row/subject to show all annotations + */ + addCellAll: boolean; + annotationLabels: string; + baseApiUrl: string; + /** + * false = show a gradient of colors to indicate the value of a cell true = show only two colors (minColor; maxColor) to indicate the values of a cell + */ + binaryColor: boolean; + /** + * 0 = Normal 1 = Bold + */ + categoryAllStyle: number; + /** + * Override of the category case 0 (default) = unchanged 1 = to lower case 2 = to upper case + */ + categoryCase: number; + /** + * 0 = Normal 1 = Bold + */ + categoryOtherStyle: number; + classLabels: string; + /** + * Which value to base the cell color on 0 = class count 1 = annotation count + */ + colorBy: number; + /** + * if provided, will override any value provided in subjects and subset + */ + data: string; + /** + * If true, the ribbon will fire an event if a user click an empty cell If false, the ribbon will not fire the event on an empty cell Note: if selectionMode == SELECTION.COLUMN, then the event will trigger if at least one of the selected cells has annotations + */ + fireEventOnEmptyCells: boolean; + groupBaseUrl: string; + groupClickable: boolean; + groupMaxLabelSize: number; + groupNewTab: boolean; + maxColor: string; + maxHeatLevel: number; + minColor: string; + ribbonSummary: IRibbonModel; + selectGroup: (group_id: any) => Promise; + /** + * If no value is provided, the ribbon will load without any group selected. If a value is provided, the ribbon will show the requested group as selected The value should be the id of the group to be selected + */ + selected: any; + /** + * Click handling of a cell. 0 = select only the cell (1 subject, 1 group) 1 = select the whole column (all subjects, 1 group) + */ + selectionMode: number; + showOtherGroup: boolean; + subjectBaseUrl: string; + subjectOpenNewTab: boolean; + /** + * Position the subject label of each row 0 = None 1 = Left 2 = Right 3 = Bottom + */ + subjectPosition: number; + subjectUseTaxonIcon: boolean; + /** + * provide gene ids (e.g. RGD:620474,RGD:3889 or as a list ["RGD:620474", "RGD:3889"]) + */ + subjects: string; + subset: string; + /** + * When this is set to false, changing the subjects Prop won't trigger the reload of the ribbon This is necessary when the ribbon is showing data other than GO or not using the internal fetchData mechanism + */ + updateOnSubjectChange: boolean; + } + interface WcRibbonSubject { + newTab: boolean; + subject: IRibbonSubject; + subjectBaseURL: string; + } + interface WcRibbonTable { + baseApiUrl: string; + /** + * Reading biolink data. This will trigger a render of the table as would changing data + */ + bioLinkData: string; + /** + * Must follow the appropriate JSON data model Can be given as either JSON or stringified JSON + */ + data: string; + /** + * Filter rows based on the presence of one or more values in a given column The filtering will be based on cell label or id Example: filter-by="evidence:ISS,ISO or multi-step filters: filter-by:evidence:ISS,ISO;term:xxx" Note: if value is "", remove any filtering + */ + filterBy: string; + groupBaseUrl: string; + /** + * Using this parameter, the table rows can bee grouped based on column ids A multiple step grouping is possible by using a ";" between groups The grouping applies before the ordering Example: hid-1,hid-3 OR hid-1,hid-3;hid-2 Note: if value is "", remove any grouping + */ + groupBy: string; + /** + * Used to hide specific column of the table + */ + hideColumns: string; + /** + * This is used to sort the table depending of a column The column cells must be single values The ordering applies after the grouping Note: if value is "", remove any ordering + */ + orderBy: string; + showCurie: () => Promise; + showDBXrefs: () => Promise; + showOriginalTable: () => Promise; + showTable: () => Promise; + subjectBaseUrl: string; + } + interface WcSpinner { + /** + * Define the color of the spinner. This parameter is optional and will override any declared CSS variable + */ + spinnerColor: string; + /** + * Define the size of the spinner (TO DO). + */ + spinnerSize: number; + /** + * Define the style of the spinner. Accepted values: default, spinner, circle, ring, dual-ring, roller, ellipsis, grid, hourglass, ripple, facebook, heart + */ + spinnerStyle: string; + } +} +export interface WcGoAutocompleteCustomEvent extends CustomEvent { + detail: T; + target: HTMLWcGoAutocompleteElement; +} +export interface WcRibbonStripsCustomEvent extends CustomEvent { + detail: T; + target: HTMLWcRibbonStripsElement; +} +export interface WcRibbonSubjectCustomEvent extends CustomEvent { + detail: T; + target: HTMLWcRibbonSubjectElement; +} declare global { - interface HTMLElementTagNameMap {} + interface HTMLWcGoAutocompleteElementEventMap { + itemSelected: any; + } + interface HTMLWcGoAutocompleteElement + extends Components.WcGoAutocomplete, + HTMLStencilElement { + addEventListener( + type: K, + listener: ( + this: HTMLWcGoAutocompleteElement, + ev: WcGoAutocompleteCustomEvent, + ) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: K, + listener: (this: Document, ev: DocumentEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: ( + this: HTMLWcGoAutocompleteElement, + ev: WcGoAutocompleteCustomEvent, + ) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: Document, ev: DocumentEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; + } + var HTMLWcGoAutocompleteElement: { + prototype: HTMLWcGoAutocompleteElement; + new (): HTMLWcGoAutocompleteElement; + }; + interface HTMLWcGoRibbonElement + extends Components.WcGoRibbon, + HTMLStencilElement {} + var HTMLWcGoRibbonElement: { + prototype: HTMLWcGoRibbonElement; + new (): HTMLWcGoRibbonElement; + }; + interface HTMLWcLightModalElement + extends Components.WcLightModal, + HTMLStencilElement {} + var HTMLWcLightModalElement: { + prototype: HTMLWcLightModalElement; + new (): HTMLWcLightModalElement; + }; + interface HTMLWcRibbonCellElement + extends Components.WcRibbonCell, + HTMLStencilElement {} + var HTMLWcRibbonCellElement: { + prototype: HTMLWcRibbonCellElement; + new (): HTMLWcRibbonCellElement; + }; + interface HTMLWcRibbonStripsElementEventMap { + cellClick: any; + cellEnter: any; + cellLeave: any; + groupClick: any; + groupEnter: any; + groupLeave: any; + } + interface HTMLWcRibbonStripsElement + extends Components.WcRibbonStrips, + HTMLStencilElement { + addEventListener( + type: K, + listener: ( + this: HTMLWcRibbonStripsElement, + ev: WcRibbonStripsCustomEvent, + ) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: K, + listener: (this: Document, ev: DocumentEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: ( + this: HTMLWcRibbonStripsElement, + ev: WcRibbonStripsCustomEvent, + ) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: Document, ev: DocumentEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; + } + var HTMLWcRibbonStripsElement: { + prototype: HTMLWcRibbonStripsElement; + new (): HTMLWcRibbonStripsElement; + }; + interface HTMLWcRibbonSubjectElementEventMap { + subjectClick: any; + } + interface HTMLWcRibbonSubjectElement + extends Components.WcRibbonSubject, + HTMLStencilElement { + addEventListener( + type: K, + listener: ( + this: HTMLWcRibbonSubjectElement, + ev: WcRibbonSubjectCustomEvent, + ) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: K, + listener: (this: Document, ev: DocumentEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | AddEventListenerOptions, + ): void; + addEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | AddEventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: ( + this: HTMLWcRibbonSubjectElement, + ev: WcRibbonSubjectCustomEvent, + ) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: Document, ev: DocumentEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: K, + listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, + options?: boolean | EventListenerOptions, + ): void; + removeEventListener( + type: string, + listener: EventListenerOrEventListenerObject, + options?: boolean | EventListenerOptions, + ): void; + } + var HTMLWcRibbonSubjectElement: { + prototype: HTMLWcRibbonSubjectElement; + new (): HTMLWcRibbonSubjectElement; + }; + interface HTMLWcRibbonTableElement + extends Components.WcRibbonTable, + HTMLStencilElement {} + var HTMLWcRibbonTableElement: { + prototype: HTMLWcRibbonTableElement; + new (): HTMLWcRibbonTableElement; + }; + interface HTMLWcSpinnerElement + extends Components.WcSpinner, + HTMLStencilElement {} + var HTMLWcSpinnerElement: { + prototype: HTMLWcSpinnerElement; + new (): HTMLWcSpinnerElement; + }; + interface HTMLElementTagNameMap { + "wc-go-autocomplete": HTMLWcGoAutocompleteElement; + "wc-go-ribbon": HTMLWcGoRibbonElement; + "wc-light-modal": HTMLWcLightModalElement; + "wc-ribbon-cell": HTMLWcRibbonCellElement; + "wc-ribbon-strips": HTMLWcRibbonStripsElement; + "wc-ribbon-subject": HTMLWcRibbonSubjectElement; + "wc-ribbon-table": HTMLWcRibbonTableElement; + "wc-spinner": HTMLWcSpinnerElement; + } } declare namespace LocalJSX { - interface IntrinsicElements {} + interface WcGoAutocomplete { + /** + * Category to constrain the search; by default search "gene" Other values accepted: `undefined` : search both terms and genes `gene` : will only search genes used in GO `biological%20process` : will search for GO BP terms `molecular%20function` : will search for GO MF terms `cellular%20component` : will search for GO CC terms `cellular%20component,molecular%20function,biological%20process` : will search any GO term + */ + category?: string; + /** + * Maximum number of results to show + */ + maxResults?: number; + /** + * Event triggered whenever an item is selected from the autocomplete + */ + onItemSelected?: (event: WcGoAutocompleteCustomEvent) => void; + /** + * Default placeholder for the autocomplete + */ + placeholder?: string; + value?: string; + } + interface WcGoRibbon { + /** + * add a cell at the beginning of each row/subject to show all annotations + */ + addCellAll?: boolean; + annotationLabels?: string; + baseApiUrl?: string; + /** + * false = show a gradient of colors to indicate the value of a cell true = show only two colors (minColor; maxColor) to indicate the values of a cell + */ + binaryColor?: boolean; + /** + * 0 = Normal 1 = Bold + */ + categoryAllStyle?: number; + /** + * Override of the category case 0 (default) = unchanged 1 = to lower case 2 = to upper case + */ + categoryCase?: number; + /** + * 0 = Normal 1 = Bold + */ + categoryOtherStyle?: number; + classLabels?: string; + /** + * Which value to base the cell color on 0 = class count 1 = annotation count + */ + colorBy?: number; + /** + * if provided, will override any value provided in subjects and subset + */ + data?: string; + excludePB?: boolean; + /** + * Filter rows based on the presence of one or more values in a given column The filtering will be based on cell label or id Example: filter-by="evidence:ISS,ISO or multi-step filters: filter-by:evidence:ISS,ISO;term:xxx" Note: if value is "", remove any filtering + */ + filterBy?: string; + filterCrossAspect?: boolean; + filterReference?: string; + /** + * If true, the ribbon will fire an event if a user click an empty cell If false, the ribbon will not fire the event on an empty cell Note: if selectionMode == SELECTION.COLUMN, then the event will trigger if at least one of the selected cells has annotations + */ + fireEventOnEmptyCells?: boolean; + groupBaseUrl?: string; + /** + * Using this parameter, the table rows can bee grouped based on column ids A multiple step grouping is possible by using a ";" between groups The grouping applies before the ordering Example: hid-1,hid-3 OR hid-1,hid-3;hid-2 Note: if value is "", remove any grouping + */ + groupBy?: string; + groupClickable?: boolean; + groupMaxLabelSize?: number; + groupNewTab?: boolean; + /** + * Used to hide specific column of the table + */ + hideColumns?: string; + maxColor?: string; + maxHeatLevel?: number; + minColor?: string; + /** + * This is used to sort the table depending of a column The column cells must be single values The ordering applies after the grouping Note: if value is "", remove any ordering + */ + orderBy?: string; + /** + * If no value is provided, the ribbon will load without any group selected. If a value is provided, the ribbon will show the requested group as selected The value should be the id of the group to be selected + */ + selected?: any; + /** + * Click handling of a cell. 0 = select only the cell (1 subject, 1 group) 1 = select the whole column (all subjects, 1 group) + */ + selectionMode?: number; + /** + * add a cell at the end of each row/subject to represent all annotations not mapped to a specific term + */ + showOtherGroup?: boolean; + subjectBaseUrl?: string; + subjectOpenNewTab?: boolean; + /** + * Position the subject label of each row 0 = None 1 = Left 2 = Right 3 = Bottom + */ + subjectPosition?: number; + subjectUseTaxonIcon?: boolean; + /** + * provide gene ids (e.g. RGD:620474,RGD:3889 or as a list ["RGD:620474", "RGD:3889"]) + */ + subjects?: string; + subset?: string; + } + interface WcLightModal { + modalAnchor?: string; + modalContent?: string; + modalTitle?: string; + x?: number; + y?: number; + } + interface WcRibbonCell { + annotationLabels?: string; + /** + * If set to true, won't show any color and can not be hovered or selected This is used for group that can not have annotation for a given subject + */ + available?: boolean; + binaryColor?: boolean; + classLabels?: string; + colorBy?: number; + group?: IRibbonGroup; + hovered?: boolean; + maxColor?: string; + maxHeatLevel?: number; + minColor?: string; + selected?: boolean; + subject?: IRibbonSubject; + } + interface WcRibbonStrips { + /** + * add a cell at the beginning of each row/subject to show all annotations + */ + addCellAll?: boolean; + annotationLabels?: string; + baseApiUrl?: string; + /** + * false = show a gradient of colors to indicate the value of a cell true = show only two colors (minColor; maxColor) to indicate the values of a cell + */ + binaryColor?: boolean; + /** + * 0 = Normal 1 = Bold + */ + categoryAllStyle?: number; + /** + * Override of the category case 0 (default) = unchanged 1 = to lower case 2 = to upper case + */ + categoryCase?: number; + /** + * 0 = Normal 1 = Bold + */ + categoryOtherStyle?: number; + classLabels?: string; + /** + * Which value to base the cell color on 0 = class count 1 = annotation count + */ + colorBy?: number; + /** + * if provided, will override any value provided in subjects and subset + */ + data?: string; + /** + * If true, the ribbon will fire an event if a user click an empty cell If false, the ribbon will not fire the event on an empty cell Note: if selectionMode == SELECTION.COLUMN, then the event will trigger if at least one of the selected cells has annotations + */ + fireEventOnEmptyCells?: boolean; + groupBaseUrl?: string; + groupClickable?: boolean; + groupMaxLabelSize?: number; + groupNewTab?: boolean; + maxColor?: string; + maxHeatLevel?: number; + minColor?: string; + /** + * This event is triggered whenever a ribbon cell is clicked + */ + onCellClick?: (event: WcRibbonStripsCustomEvent) => void; + /** + * This event is triggered whenever the mouse enters a cell area + */ + onCellEnter?: (event: WcRibbonStripsCustomEvent) => void; + /** + * This event is triggered whenever the mouse leaves a cell area + */ + onCellLeave?: (event: WcRibbonStripsCustomEvent) => void; + /** + * This event is triggered whenever a group cell is clicked + */ + onGroupClick?: (event: WcRibbonStripsCustomEvent) => void; + /** + * This event is triggered whenever the mouse enters a group cell area + */ + onGroupEnter?: (event: WcRibbonStripsCustomEvent) => void; + /** + * This event is triggered whenever the mouse leaves a group cell area + */ + onGroupLeave?: (event: WcRibbonStripsCustomEvent) => void; + ribbonSummary?: IRibbonModel; + /** + * If no value is provided, the ribbon will load without any group selected. If a value is provided, the ribbon will show the requested group as selected The value should be the id of the group to be selected + */ + selected?: any; + /** + * Click handling of a cell. 0 = select only the cell (1 subject, 1 group) 1 = select the whole column (all subjects, 1 group) + */ + selectionMode?: number; + showOtherGroup?: boolean; + subjectBaseUrl?: string; + subjectOpenNewTab?: boolean; + /** + * Position the subject label of each row 0 = None 1 = Left 2 = Right 3 = Bottom + */ + subjectPosition?: number; + subjectUseTaxonIcon?: boolean; + /** + * provide gene ids (e.g. RGD:620474,RGD:3889 or as a list ["RGD:620474", "RGD:3889"]) + */ + subjects?: string; + subset?: string; + /** + * When this is set to false, changing the subjects Prop won't trigger the reload of the ribbon This is necessary when the ribbon is showing data other than GO or not using the internal fetchData mechanism + */ + updateOnSubjectChange?: boolean; + } + interface WcRibbonSubject { + newTab?: boolean; + /** + * This event is triggered whenever a subject label is clicked Can call preventDefault() to avoid the default behavior (opening the linked subject page) + */ + onSubjectClick?: (event: WcRibbonSubjectCustomEvent) => void; + subject?: IRibbonSubject; + subjectBaseURL?: string; + } + interface WcRibbonTable { + baseApiUrl?: string; + /** + * Reading biolink data. This will trigger a render of the table as would changing data + */ + bioLinkData?: string; + /** + * Must follow the appropriate JSON data model Can be given as either JSON or stringified JSON + */ + data?: string; + /** + * Filter rows based on the presence of one or more values in a given column The filtering will be based on cell label or id Example: filter-by="evidence:ISS,ISO or multi-step filters: filter-by:evidence:ISS,ISO;term:xxx" Note: if value is "", remove any filtering + */ + filterBy?: string; + groupBaseUrl?: string; + /** + * Using this parameter, the table rows can bee grouped based on column ids A multiple step grouping is possible by using a ";" between groups The grouping applies before the ordering Example: hid-1,hid-3 OR hid-1,hid-3;hid-2 Note: if value is "", remove any grouping + */ + groupBy?: string; + /** + * Used to hide specific column of the table + */ + hideColumns?: string; + /** + * This is used to sort the table depending of a column The column cells must be single values The ordering applies after the grouping Note: if value is "", remove any ordering + */ + orderBy?: string; + subjectBaseUrl?: string; + } + interface WcSpinner { + /** + * Define the color of the spinner. This parameter is optional and will override any declared CSS variable + */ + spinnerColor?: string; + /** + * Define the size of the spinner (TO DO). + */ + spinnerSize?: number; + /** + * Define the style of the spinner. Accepted values: default, spinner, circle, ring, dual-ring, roller, ellipsis, grid, hourglass, ripple, facebook, heart + */ + spinnerStyle?: string; + } + interface IntrinsicElements { + "wc-go-autocomplete": WcGoAutocomplete; + "wc-go-ribbon": WcGoRibbon; + "wc-light-modal": WcLightModal; + "wc-ribbon-cell": WcRibbonCell; + "wc-ribbon-strips": WcRibbonStrips; + "wc-ribbon-subject": WcRibbonSubject; + "wc-ribbon-table": WcRibbonTable; + "wc-spinner": WcSpinner; + } } export { LocalJSX as JSX }; declare module "@stencil/core" { export namespace JSX { - interface IntrinsicElements {} + interface IntrinsicElements { + "wc-go-autocomplete": LocalJSX.WcGoAutocomplete & + JSXBase.HTMLAttributes; + "wc-go-ribbon": LocalJSX.WcGoRibbon & + JSXBase.HTMLAttributes; + "wc-light-modal": LocalJSX.WcLightModal & + JSXBase.HTMLAttributes; + "wc-ribbon-cell": LocalJSX.WcRibbonCell & + JSXBase.HTMLAttributes; + "wc-ribbon-strips": LocalJSX.WcRibbonStrips & + JSXBase.HTMLAttributes; + "wc-ribbon-subject": LocalJSX.WcRibbonSubject & + JSXBase.HTMLAttributes; + "wc-ribbon-table": LocalJSX.WcRibbonTable & + JSXBase.HTMLAttributes; + "wc-spinner": LocalJSX.WcSpinner & + JSXBase.HTMLAttributes; + } } } diff --git a/src/components/go-autocomplete/go-autocomplete.css b/src/components/go-autocomplete/go-autocomplete.css new file mode 100644 index 0000000..eb9e4ed --- /dev/null +++ b/src/components/go-autocomplete/go-autocomplete.css @@ -0,0 +1,132 @@ +/* Based on https://www.w3schools.com/howto/howto_js_autocomplete.asp */ + +@import url("https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css"); + +:root { + --font-family: Lato, sans-serif; + --font-size: 12px; + --autocomplete-width: 310px; + --autocomplete-item-font-size: 10px; + --autocomplete-item-height: 300px; +} + +* { + box-sizing: border-box; +} +body { + font-size: var(--font-size); + font-family: var(--font-family); +} +.autocomplete { + /*the container must be positioned relative:*/ + position: relative; + display: inline-block; + width: var(--autocomplete-width); +} +input { + border: 1px solid transparent; + background-color: #f1f1f1; + padding: 10px; + font-size: var(--font-size); +} +input[type="text"] { + background-color: #f1f1f1; + border-radius: 10px; + width: 100%; +} +input[type="submit"] { + background-color: DodgerBlue; + color: #fff; +} +.autocomplete-items { + position: absolute; + border: 1px solid #d4d4d4; + border-bottom: none; + border-top: none; + z-index: 99; + /*position the autocomplete items to be the same width as the container:*/ + top: 100%; + left: 0; + right: 0; + position: absolute; + cursor: default; + z-index: 1001 !important; + font-size: var(--font-size); + line-height: 1.2rem; + overflow-y: scroll; + height: var(--autocomplete-item-height); + border: 1px solid black; + border-radius: 0px 0px 0px 15px; + padding: 0.5rem 0rem 0rem 0.5rem; +} +.autocomplete-items div { + padding: 10px; + cursor: pointer; + background-color: #fff; + border-bottom: 1px solid #d4d4d4; + display: table-row; +} +.autocomplete-items div:hover { + /*when hovering an item:*/ + background-color: #e9e9e9; +} +.autocomplete-active { + /*when navigating through the items using the arrow keys:*/ + background-color: DodgerBlue !important; + color: #ffffff; +} + +.autocomplete__item__label { + display: table-cell; + width: 50%; + padding: 0.2rem 0.5rem; +} + +.autocomplete__item__taxon { + display: table-cell; + width: 50%; + padding: 0.2rem 0.5rem; + margin: 1rem; + text-align: right; +} + +.autocomplete__item__id { + display: table-cell; + width: 0%; + padding: 0.2rem 0.5rem; +} + +a { + text-decoration: none; +} + +.autocomplete i { + position: absolute; +} + +.icon { + padding: 10px; + min-width: 40px; +} + +.icon-left { + padding: 10px; + left: 0px; +} + +.icon-right { + padding: 10px; + right: 0px; + cursor: pointer; +} + +.input-field { + width: 100%; + padding: 10px 35px; + text-align: left; +} + +.active { + background: greenyellow; + background-color: greenyellow; +} diff --git a/src/components/go-autocomplete/go-autocomplete.tsx b/src/components/go-autocomplete/go-autocomplete.tsx new file mode 100644 index 0000000..c987fae --- /dev/null +++ b/src/components/go-autocomplete/go-autocomplete.tsx @@ -0,0 +1,192 @@ +import { Component, Prop, State, Event, EventEmitter, h } from "@stencil/core"; + +import * as dbxrefs from "@geneontology/dbxrefs"; + +@Component({ + tag: "wc-go-autocomplete", + styleUrl: "go-autocomplete.css", + shadow: false, +}) +export class GOAutocomplete { + searchBox: HTMLInputElement; + + @Prop() value: string; + + /** + * Category to constrain the search; by default search "gene" + * Other values accepted: + * `undefined` : search both terms and genes + * `gene` : will only search genes used in GO + * `biological%20process` : will search for GO BP terms + * `molecular%20function` : will search for GO MF terms + * `cellular%20component` : will search for GO CC terms + * `cellular%20component,molecular%20function,biological%20process` : will search any GO term + */ + @Prop() category: string = "gene"; + + /** + * Maximum number of results to show + */ + @Prop() maxResults = 100; + + /** + * Default placeholder for the autocomplete + */ + @Prop() placeholder = ""; + + /** + * Event triggered whenever an item is selected from the autocomplete + */ + @Event({ eventName: "itemSelected", cancelable: true, bubbles: true }) + itemSelected: EventEmitter; + + @State() docs; // will contain the results of the search (e.g. from the GO API) + + @State() ready = false; + + goApiUrl = "https://api.geneontology.org/api/search/entity/autocomplete/"; + ncbiTaxonUrl = "https://www.ncbi.nlm.nih.gov/Taxonomy/Browser/wwwtax.cgi?id="; + + /** + * Executed once during component initialization + * Here, just ask for dbxrefs to load + */ + componentWillLoad() { + dbxrefs.init().then(() => { + this.ready = true; + }); + } + + autocomplete() { + let url = ""; + if (!this.category) { + url = this.goApiUrl + this.value + "&rows=" + this.maxResults; + } else if (this.category && this.category.includes(",")) { + const tmp = "?category=" + this.category.split(",").join("&category="); + url = this.goApiUrl + this.value + tmp + "&rows=" + this.maxResults; + console.log(url); + } else { + url = + this.goApiUrl + + this.value + + "?category=" + + this.category + + "&rows=" + + this.maxResults; + } + fetch(url) + .then((response) => { + if (response.status != 200) { + console.error("Not a 200 response: ", response); + } else { + return response.json(); + } + }) + .then((data) => { + this.docs = data["docs"]; + // console.log(this.docs); + }); + } + + timer = undefined; + newSearch(evt) { + this.value = evt.target.value; + if (this.timer) { + clearTimeout(this.timer); + } + if (this.value.length < 2) { + this.docs = undefined; + return; + } + this.timer = setTimeout(() => { + this.autocomplete(); + }, 800); + } + + select(target, doc) { + if (!target.href) { + // console.log("doc selected: ", doc); + this.docs = undefined; + this.value = ""; + this.itemSelected.emit({ selected: doc }); + } + } + + render() { + return !this.ready ? ( + "" + ) : ( +
+ + (this.searchBox = el as HTMLInputElement)} + onInput={(evt) => this.newSearch(evt)} + > + { + this.docs = undefined; + this.value = ""; + }} + > + + {!this.docs ? ( + "" + ) : ( +
+ {this.docs.map((doc) => { + return this.renderDoc(doc); + })} +
+ )} +
+ ); + } + + renderDoc(doc) { + // TODO: url taxon should also be fetch from dbxrefs.yaml; have to look in synonyms + const url_taxon = this.ncbiTaxonUrl + doc.taxon.replace("NCBITaxon:", ""); + const db = doc.id.substring(0, doc.id.indexOf(":")); + const id = doc.id.substring(doc.id.indexOf(":") + 1); + // console.log("DOC: " ,doc); + let url_id = "#"; + try { + url_id = dbxrefs.getURL(db, undefined, id); + } catch { + // console.error(err); + } + + // TODO: temporary fix while the db-xrefs.yaml gets updated + if (url_id.includes("https://www.ncbi.nlm.nih.gov/gene/")) { + // then this was fixed + } else if (url_id.includes("https://www.ncbi.nlm.nih.gov/gene")) { + url_id = url_id.replace( + "https://www.ncbi.nlm.nih.gov/gene", + "https://www.ncbi.nlm.nih.gov/gene/", + ); + } + + return ( +
this.select(evt.target, doc)}> + + { + + {doc.label} + + } + + + { + + {doc.taxon_label} + + } + +
+ ); + } +} diff --git a/src/components/go-autocomplete/index.html b/src/components/go-autocomplete/index.html new file mode 100644 index 0000000..bc84e9d --- /dev/null +++ b/src/components/go-autocomplete/index.html @@ -0,0 +1,93 @@ + + + + + + GO Autocomplete Web Component + + + + + + + + +
+ + +
+ + + + + diff --git a/src/components/go-autocomplete/readme.md b/src/components/go-autocomplete/readme.md new file mode 100644 index 0000000..a7be41e --- /dev/null +++ b/src/components/go-autocomplete/readme.md @@ -0,0 +1,22 @@ +# my-component + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------- | +| `category` | `category` | Category to constrain the search; by default search "gene" Other values accepted: `undefined` : search both terms and genes `gene` : will only search genes used in GO `biological%20process` : will search for GO BP terms `molecular%20function` : will search for GO MF terms `cellular%20component` : will search for GO CC terms `cellular%20component,molecular%20function,biological%20process` : will search any GO term | `string` | `"gene"` | +| `maxResults` | `max-results` | Maximum number of results to show | `number` | `100` | +| `placeholder` | `placeholder` | Default placeholder for the autocomplete | `string` | `""` | +| `value` | `value` | | `string` | `undefined` | + +## Events + +| Event | Description | Type | +| -------------- | ------------------------------------------------------------------ | ------------------ | +| `itemSelected` | Event triggered whenever an item is selected from the autocomplete | `CustomEvent` | + +--- + +_Built with [StencilJS](https://stenciljs.com/)_ diff --git a/src/components/go-light-modal/index.html b/src/components/go-light-modal/index.html new file mode 100644 index 0000000..847b5e2 --- /dev/null +++ b/src/components/go-light-modal/index.html @@ -0,0 +1,28 @@ + + + + + + Lightweight Modal Component + + + + + +

Lightweight Modal Web Component example

+ + + + + + + diff --git a/src/components/go-light-modal/light-modal.css b/src/components/go-light-modal/light-modal.css new file mode 100644 index 0000000..1503d30 --- /dev/null +++ b/src/components/go-light-modal/light-modal.css @@ -0,0 +1,74 @@ +/* Inspired by: https://codepen.io/timothylong/pen/HhAer */ + +.modal-card { + position: relative; + background-color: rgba(255, 255, 255, 0.25); + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 999; + visibility: hidden; + opacity: 0; + pointer-events: none; + transition: all 0.3s; +} + +.modal-card > div { + width: 500px; + position: absolute; + top: 0px; + left: 100px; + transform: translate(0%, -50%); + padding: 2em; + background: #ffffff; + border-radius: 15px; + -webkit-box-shadow: 4px 6px 42px 0px rgba(194, 194, 194, 1); + -moz-box-shadow: 4px 6px 42px 0px rgba(194, 194, 194, 1); + box-shadow: 4px 6px 42px 0px rgba(194, 194, 194, 1); +} + +.modal-card-visible { + visibility: visible; + opacity: 1; + pointer-events: auto; +} + +header { + font-weight: bold; +} + +h1 { + font-size: 150%; + margin: 0 0 15px; +} + +a { + color: inherit; +} + +.container { + display: grid; + justify-content: center; + align-items: center; + height: 100vh; +} + +.modal-window div:not(:last-of-type) { + margin-bottom: 15px; +} + +small { + color: #aaa; +} + +.btn { + background-color: #fff; + padding: 1em 1.5em; + border-radius: 3px; + text-decoration: none; +} + +.absolute-position { + position: absolute !important; +} diff --git a/src/components/go-light-modal/light-modal.tsx b/src/components/go-light-modal/light-modal.tsx new file mode 100644 index 0000000..ea4d854 --- /dev/null +++ b/src/components/go-light-modal/light-modal.tsx @@ -0,0 +1,129 @@ +// Inspired by https://codepen.io/timothylong/pen/HhAer + +import { Component, Prop, Element, Method, Watch, h } from "@stencil/core"; +// import { State } from '@stencil/core'; + +@Component({ + tag: "wc-light-modal", + styleUrl: "light-modal.css", + shadow: false, +}) +export class LightModal { + mid = "modal-card"; + mclass = "modal-card-visible"; + + @Element() modal; + modalCard!: HTMLDivElement; + modalContentDiv!: HTMLDivElement; + + @Prop() modalAnchor: string = ""; + + @Prop() modalTitle: string = "TITLE"; + + @Prop() modalContent: string = "HTML CONTENT"; + + // If provided, the tooltip will display at the designated position + @Prop() x = -1; + @Prop() y = -1; + + // @Watch('modalTitle') + // modalTitleChanged(newValue, oldValue) { + + // } + + @Watch("modalContent") + modalTitleChanged(newValue, oldValue) { + if (this.modalContentDiv && newValue != oldValue) { + // console.log("content div: ", this.modalContentDiv); + this.modalContentDiv.innerHTML = newValue; + } + } + + componentDidLoad() { + if (this.modalContentDiv) { + // console.log("content div: ", this.modalContentDiv); + this.modalContentDiv.innerHTML = this.modalContent; + } + } + + @Method() + async open() { + // console.log("open" , this.modalCard); + if ( + this.modalCard.classList && + !this.modalCard.classList.contains(this.mclass) + ) { + this.modalCard.classList.add(this.mclass); + } + } + + @Method() + async close() { + // console.log("close" , this.modalCard); + if ( + this.modalCard.classList && + this.modalCard.classList.contains(this.mclass) + ) { + this.modalCard.classList.remove(this.mclass); + } + } + + @Method() + async toggle() { + // console.log("close" , this.modalCard); + if (this.modalCard.classList) { + if (this.modalCard.classList.contains(this.mclass)) { + this.modalCard.classList.remove(this.mclass); + } else { + this.modalCard.classList.add(this.mclass); + } + } + } + + // componentDidRender() { + // console.log("***** component did update: ", this.x, this.y); + // if(this.x != -1 && this.y != -1) { + // this.modalCard.style.top = "" + this.y; + // this.modalCard.style.left = "" + this.y; + // console.log("changing modal card pos: " , this.x , this.y); + // } + // } + + render() { + const classes = "modal-card"; + let styles = {}; + if (this.x != -1 && this.y != -1) { + styles = { top: this.y + "px", left: this.x + "px" }; + } + + return ( +
+ {/* {this.trick} */} + {/* */} + +
(this.modalCard = el as HTMLDivElement)} + > +
+

+ {this.modalTitle} + +

+ + +
+
+
+ ); + } +} diff --git a/src/components/go-light-modal/readme.md b/src/components/go-light-modal/readme.md new file mode 100644 index 0000000..64c2914 --- /dev/null +++ b/src/components/go-light-modal/readme.md @@ -0,0 +1,37 @@ +# wc-light-modal + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------------- | --------------- | ----------- | -------- | ---------------- | +| `modalAnchor` | `modal-anchor` | | `string` | `""` | +| `modalContent` | `modal-content` | | `string` | `"HTML CONTENT"` | +| `modalTitle` | `modal-title` | | `string` | `"TITLE"` | +| `x` | `x` | | `number` | `-1` | +| `y` | `y` | | `number` | `-1` | + +## Methods + +### `close() => Promise` + +#### Returns + +Type: `Promise` + +### `open() => Promise` + +#### Returns + +Type: `Promise` + +### `toggle() => Promise` + +#### Returns + +Type: `Promise` + +--- + +_Built with [StencilJS](https://stenciljs.com/)_ diff --git a/src/components/go-ribbon-cell/readme.md b/src/components/go-ribbon-cell/readme.md new file mode 100644 index 0000000..8335dc4 --- /dev/null +++ b/src/components/go-ribbon-cell/readme.md @@ -0,0 +1,38 @@ +# wc-ribbon-cell + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------- | +| `annotationLabels` | `annotation-labels` | | `string` | `"annotation,annotations"` | +| `available` | `available` | If set to true, won't show any color and can not be hovered or selected This is used for group that can not have annotation for a given subject | `boolean` | `true` | +| `binaryColor` | `binary-color` | | `boolean` | `false` | +| `classLabels` | `class-labels` | | `string` | `"term,terms"` | +| `colorBy` | `color-by` | | `number` | `COLOR_BY.CLASS_COUNT` | +| `group` | -- | | `IRibbonGroup` | `undefined` | +| `hovered` | `hovered` | | `boolean` | `false` | +| `maxColor` | `max-color` | | `string` | `"24,73,180"` | +| `maxHeatLevel` | `max-heat-level` | | `number` | `48` | +| `minColor` | `min-color` | | `string` | `"255,255,255"` | +| `selected` | `selected` | | `boolean` | `false` | +| `subject` | -- | | `IRibbonSubject` | `undefined` | + +## Dependencies + +### Used by + +- [wc-ribbon-strips](../go-ribbon-strips) + +### Graph + +```mermaid +graph TD; + wc-ribbon-strips --> wc-ribbon-cell + style wc-ribbon-cell fill:#f9f,stroke:#333,stroke-width:4px +``` + +--- + +_Built with [StencilJS](https://stenciljs.com/)_ diff --git a/src/components/go-ribbon-cell/ribbon-cell.scss b/src/components/go-ribbon-cell/ribbon-cell.scss new file mode 100644 index 0000000..d974492 --- /dev/null +++ b/src/components/go-ribbon-cell/ribbon-cell.scss @@ -0,0 +1,26 @@ +.clicked { + border: 1px solid #002eff; + box-shadow: 0px 0px 8px #002eff; +} + +.hovered { + // border: 1px solid black; +} + +.unavailable { + cursor: not-allowed; + background: linear-gradient( + to top left, + rgba(0, 0, 0, 0) 0%, + rgba(0, 0, 0, 0) calc(50% - 0.8px), + rgba(240, 32, 32, 0.5) 50%, + rgba(0, 0, 0, 0) calc(50% + 0.8px), + rgba(0, 0, 0, 0) 100% + ); + // linear-gradient(to top right, + // rgba(0,0,0,0) 0%, + // rgba(0,0,0,0) calc(50% - 0.8px), + // rgba(0,0,0,1) 50%, + // rgba(0,0,0,0) calc(50% + 0.8px), + // rgba(0,0,0,0) 100%); +} diff --git a/src/components/go-ribbon-cell/ribbon-cell.tsx b/src/components/go-ribbon-cell/ribbon-cell.tsx new file mode 100644 index 0000000..ff46a54 --- /dev/null +++ b/src/components/go-ribbon-cell/ribbon-cell.tsx @@ -0,0 +1,208 @@ +import { h } from "@stencil/core"; + +import { Component, Prop, Element } from "@stencil/core"; + +import { heatColor, darken } from "./utils"; +import { CELL_TYPES, COLOR_BY } from "../../globals/enums"; + +import { IRibbonGroup, IRibbonSubject } from "../../globals/models"; +import { Watch } from "@stencil/core"; + +@Component({ + tag: "wc-ribbon-cell", + styleUrl: "./ribbon-cell.scss", + shadow: false, +}) +export class RibbonCell { + @Element() el: HTMLElement; + + @Prop() subject: IRibbonSubject; + @Prop() group: IRibbonGroup; + + @Prop() classLabels = "term,terms"; + @Prop() annotationLabels = "annotation,annotations"; + @Prop() colorBy = COLOR_BY.CLASS_COUNT; + @Prop() binaryColor = false; + @Prop() minColor = "255,255,255"; + @Prop() maxColor = "24,73,180"; + @Prop() maxHeatLevel = 48; + + arrayMinColor; + arrayMaxColor; + + @Watch("minColor") + minColorChanged(newValue, oldValue) { + if (newValue != oldValue) this.updateMinColor(newValue); + } + updateMinColor(newValue) { + if (newValue instanceof Array) { + this.arrayMinColor = newValue; + } else if (newValue.includes(",")) { + this.arrayMinColor = newValue.split(","); + } + this.arrayMinColor = this.arrayMinColor.map((elt) => +elt); + } + + @Watch("maxColor") + maxColorChanged(newValue, oldValue) { + if (newValue != oldValue) this.updateMaxColor(newValue); + } + updateMaxColor(newValue) { + if (newValue instanceof Array) { + this.arrayMaxColor = newValue; + } else if (newValue.includes(",")) { + this.arrayMaxColor = newValue.split(","); + } + this.arrayMaxColor = this.arrayMaxColor.map((elt) => +elt); + } + + arrayAnnotationLabels; + arrayClassLabels; + + @Watch("annotationLabels") + annotationLabelsChanged(newValue, oldValue) { + if (newValue != oldValue) this.updateAnnotationLabels(newValue); + } + updateAnnotationLabels(newValue) { + if (newValue instanceof Array) { + this.arrayAnnotationLabels = newValue; + } else if (newValue.includes(",")) { + this.arrayAnnotationLabels = newValue.split(","); + } + this.arrayAnnotationLabels = this.arrayAnnotationLabels.map((elt) => + elt.trim(), + ); + } + + @Watch("classLabels") + classLabelsChanged(newValue, oldValue) { + if (newValue != oldValue) this.updateClassLabels(newValue); + } + updateClassLabels(newValue) { + if (newValue instanceof Array) { + this.arrayClassLabels = newValue; + } else if (newValue.includes(",")) { + this.arrayClassLabels = newValue.split(","); + } + this.arrayClassLabels = this.arrayClassLabels.map((elt) => elt.trim()); + } + + /** + * If set to true, won't show any color and can not be hovered or selected + * This is used for group that can not have annotation for a given subject + */ + @Prop() available = true; + @Prop() selected = false; + @Prop() hovered = false; + + cellColor(nbClasses, nbAnnotations) { + const levels = + this.colorBy == COLOR_BY.CLASS_COUNT ? nbClasses : nbAnnotations; + let newColor = heatColor( + levels, + this.maxHeatLevel, + this.arrayMinColor, + this.arrayMaxColor, + this.binaryColor, + ); + if (this.hovered) { + const tmp = newColor.replace(/[^\d,]/g, "").split(","); + const val = darken(tmp, 0.4); + newColor = "rgb(" + val.join(",") + ")"; + } + return newColor; + } + + getNbClasses() { + if (this.group.type == "GlobalAll") { + return this.subject.nb_classes; + } + const cellid = + this.group.id + (this.group.type == CELL_TYPES.OTHER ? "-other" : ""); + const cell = + cellid in this.subject.groups ? this.subject.groups[cellid] : undefined; + return cell ? cell["ALL"]["nb_classes"] : 0; + } + + getNbAnnotations() { + if (this.group.type == "GlobalAll") { + return this.subject.nb_annotations; + } + const cellid = + this.group.id + (this.group.type == CELL_TYPES.OTHER ? "-other" : ""); + const cell = + cellid in this.subject.groups ? this.subject.groups[cellid] : undefined; + return cell ? cell["ALL"]["nb_annotations"] : 0; + } + + hasAnnotations() { + return this.getNbAnnotations() > 0; + } + + /** + * This is executed once when the component gets loaded + */ + componentWillLoad() { + this.updateMinColor(this.minColor); + this.updateMaxColor(this.maxColor); + this.updateAnnotationLabels(this.annotationLabels); + this.updateClassLabels(this.classLabels); + } + + render() { + if (!this.available) { + const title = + this.subject.label + " can not have data for " + this.group.label; + const classes = "ribbon__subject--cell unavailable"; + return ( + + {" "} + + ); + } + + const nbClasses = this.getNbClasses(); + const nbAnnotations = this.getNbAnnotations(); + + let title = + "Subject: " + + this.subject.id + + ":" + + this.subject.label + + "\n\nGroup: " + + this.group.id + + ": " + + this.group.label; + + if (nbAnnotations > 0) { + title += + "\n\n" + + nbClasses + + " " + + (nbClasses > 1 ? this.arrayClassLabels[1] : this.arrayClassLabels[0]) + + ", " + + nbAnnotations + + " " + + (nbAnnotations > 1 + ? this.arrayAnnotationLabels[1] + : this.arrayAnnotationLabels[0]); + } else { + title += "\n\nNo data available"; + } + this.el.style.setProperty( + "background", + this.cellColor(nbClasses, nbAnnotations), + ); + + let classes = + this.selected && nbAnnotations > 0 + ? "ribbon__subject--cell clicked" + : "ribbon__subject--cell"; + classes += this.hovered && nbAnnotations > 0 ? " hovered" : ""; + return ( + + {" "} + + ); + } +} diff --git a/src/components/go-ribbon-cell/utils.ts b/src/components/go-ribbon-cell/utils.ts new file mode 100644 index 0000000..2213e38 --- /dev/null +++ b/src/components/go-ribbon-cell/utils.ts @@ -0,0 +1,57 @@ +export function darken(color, factor) { + const newColor = [...color]; + for (let i = 0; i < newColor.length; i++) { + if (newColor[i] < 255) { + newColor[i] = Math.round(Math.max(0, newColor[i] * (1 - factor))); + } + } + // newColor[0] = Math.min(255, Math.round(newColor[0] * (1 + factor))); + return newColor; +} + +/** + * Return a color based on interpolation (count, minColor, maxColor) and normalized by maxHeatLevel + * @param {*} count + * @param {*} maxHeatLevel + */ +export function heatColor( + level, + maxHeatLevel, + minColor, + maxColor, + binaryColor = false, +) { + if (level === 0) { + return toRGB(minColor); + } + + if (binaryColor) { + return toRGB(maxColor); + } + + // this is a linear version for interpolation + // let fraction = Math.min(level, maxHeatLevel) / maxHeatLevel; + + // this is the log version for interpolation (better highlight the most annotated classes) + // note: safari needs integer and not float for rgb function + const fraction = + Math.min(10 * Math.log(level + 1), maxHeatLevel) / maxHeatLevel; + + // there are some annotations and we want a continuous color (r, g, b) + const itemColor = []; // [r,g,b] + itemColor[0] = Math.round( + minColor[0] + fraction * (maxColor[0] - minColor[0]), + ); + itemColor[1] = Math.round( + minColor[1] + fraction * (maxColor[1] - minColor[1]), + ); + itemColor[2] = Math.round( + minColor[2] + fraction * (maxColor[2] - minColor[2]), + ); + + return toRGB(itemColor); +} + +function toRGB(array) { + return "rgb(" + array[0] + "," + array[1] + "," + array[2] + ")"; +} diff --git a/src/components/go-ribbon-strips/index.html b/src/components/go-ribbon-strips/index.html new file mode 100644 index 0000000..a770b3e --- /dev/null +++ b/src/components/go-ribbon-strips/index.html @@ -0,0 +1,111 @@ + + + + + + Generic Ribbon - WC + + + + + + + + + + diff --git a/src/components/go-ribbon-strips/readme.md b/src/components/go-ribbon-strips/readme.md new file mode 100644 index 0000000..cb4003d --- /dev/null +++ b/src/components/go-ribbon-strips/readme.md @@ -0,0 +1,89 @@ +# wc-ribbon-strips + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ----------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------------- | +| `addCellAll` | `add-cell-all` | add a cell at the beginning of each row/subject to show all annotations | `boolean` | `true` | +| `annotationLabels` | `annotation-labels` | | `string` | `"annotation,annotations"` | +| `baseApiUrl` | `base-api-url` | | `string` | `"https://api.geneontology.org/api/ontology/ribbon/"` | +| `binaryColor` | `binary-color` | false = show a gradient of colors to indicate the value of a cell true = show only two colors (minColor; maxColor) to indicate the values of a cell | `boolean` | `false` | +| `categoryAllStyle` | `category-all-style` | 0 = Normal 1 = Bold | `number` | `FONT_STYLE.NORMAL` | +| `categoryCase` | `category-case` | Override of the category case 0 (default) = unchanged 1 = to lower case 2 = to upper case | `number` | `FONT_CASE.LOWER_CASE` | +| `categoryOtherStyle` | `category-other-style` | 0 = Normal 1 = Bold | `number` | `FONT_STYLE.NORMAL` | +| `classLabels` | `class-labels` | | `string` | `"term,terms"` | +| `colorBy` | `color-by` | Which value to base the cell color on 0 = class count 1 = annotation count | `number` | `COLOR_BY.ANNOTATION_COUNT` | +| `data` | `data` | if provided, will override any value provided in subjects and subset | `string` | `undefined` | +| `fireEventOnEmptyCells` | `fire-event-on-empty-cells` | If true, the ribbon will fire an event if a user click an empty cell If false, the ribbon will not fire the event on an empty cell Note: if selectionMode == SELECTION.COLUMN, then the event will trigger if at least one of the selected cells has annotations | `boolean` | `false` | +| `groupBaseUrl` | `group-base-url` | | `string` | `"http://amigo.geneontology.org/amigo/term/"` | +| `groupClickable` | `group-clickable` | | `boolean` | `true` | +| `groupMaxLabelSize` | `group-max-label-size` | | `number` | `60` | +| `groupNewTab` | `group-new-tab` | | `boolean` | `true` | +| `maxColor` | `max-color` | | `string` | `"24,73,180"` | +| `maxHeatLevel` | `max-heat-level` | | `number` | `48` | +| `minColor` | `min-color` | | `string` | `"255,255,255"` | +| `ribbonSummary` | -- | | `IRibbonModel` | `undefined` | +| `selected` | `selected` | If no value is provided, the ribbon will load without any group selected. If a value is provided, the ribbon will show the requested group as selected The value should be the id of the group to be selected | `any` | `undefined` | +| `selectionMode` | `selection-mode` | Click handling of a cell. 0 = select only the cell (1 subject, 1 group) 1 = select the whole column (all subjects, 1 group) | `number` | `SELECTION.CELL` | +| `showOtherGroup` | `show-other-group` | | `boolean` | `false` | +| `subjectBaseUrl` | `subject-base-url` | | `string` | `"http://amigo.geneontology.org/amigo/gene_product/"` | +| `subjectOpenNewTab` | `subject-open-new-tab` | | `boolean` | `true` | +| `subjectPosition` | `subject-position` | Position the subject label of each row 0 = None 1 = Left 2 = Right 3 = Bottom | `number` | `POSITION.LEFT` | +| `subjectUseTaxonIcon` | `subject-use-taxon-icon` | | `boolean` | `undefined` | +| `subjects` | `subjects` | provide gene ids (e.g. RGD:620474,RGD:3889 or as a list ["RGD:620474", "RGD:3889"]) | `string` | `undefined` | +| `subset` | `subset` | | `string` | `"goslim_agr"` | +| `updateOnSubjectChange` | `update-on-subject-change` | When this is set to false, changing the subjects Prop won't trigger the reload of the ribbon This is necessary when the ribbon is showing data other than GO or not using the internal fetchData mechanism | `boolean` | `true` | + +## Events + +| Event | Description | Type | +| ------------ | ------------------------------------------------------------------- | ------------------ | +| `cellClick` | This event is triggered whenever a ribbon cell is clicked | `CustomEvent` | +| `cellEnter` | This event is triggered whenever the mouse enters a cell area | `CustomEvent` | +| `cellLeave` | This event is triggered whenever the mouse leaves a cell area | `CustomEvent` | +| `groupClick` | This event is triggered whenever a group cell is clicked | `CustomEvent` | +| `groupEnter` | This event is triggered whenever the mouse enters a group cell area | `CustomEvent` | +| `groupLeave` | This event is triggered whenever the mouse leaves a group cell area | `CustomEvent` | + +## Methods + +### `selectGroup(group_id: any) => Promise` + +#### Parameters + +| Name | Type | Description | +| ---------- | ----- | ----------- | +| `group_id` | `any` | | + +#### Returns + +Type: `Promise` + +## Dependencies + +### Used by + +- [wc-go-ribbon](../go-ribbon) + +### Depends on + +- [wc-spinner](../go-spinner) +- [wc-ribbon-subject](../go-ribbon-subject) +- [wc-ribbon-cell](../go-ribbon-cell) + +### Graph + +```mermaid +graph TD; + wc-ribbon-strips --> wc-spinner + wc-ribbon-strips --> wc-ribbon-subject + wc-ribbon-strips --> wc-ribbon-cell + wc-go-ribbon --> wc-ribbon-strips + style wc-ribbon-strips fill:#f9f,stroke:#333,stroke-width:4px +``` + +--- + +_Built with [StencilJS](https://stenciljs.com/)_ diff --git a/src/components/go-ribbon-strips/ribbon-strips.scss b/src/components/go-ribbon-strips/ribbon-strips.scss new file mode 100644 index 0000000..d4bfc76 --- /dev/null +++ b/src/components/go-ribbon-strips/ribbon-strips.scss @@ -0,0 +1,126 @@ +$ribbon__cell--width: 18px !default; +$ribbon__cell--height: 18px !default; +$ribbon__cell--separator: 15px !default; + +$ribbon--width: 100% !default; + +$ribbon__subject__label-left--width: 140px !default; +$ribbon__subject__label-right--margin-left: 1rem !default; +$ribbon__subject__cell--box-shadow: 0 1px 4px rgba(0, 0, 0, 0.26); + +.ribbon { + &, + & * { + margin: 0; + padding: 0; + box-sizing: border-box; + font-weight: normal; + } + + b { + font-weight: 800; + } + + display: table; + width: $ribbon--width; + + &__category { + display: block; + margin-bottom: 0.5rem; + margin-top: 12.4rem; + + &--separator { + display: inline-block; + padding: 0px $ribbon__cell--separator; + } + + &--cell { + display: inline-block; + width: $ribbon__cell--width; + outline: none; + text-align: left; + white-space: nowrap; + transform: translateY(-2px) rotate(-45deg); + + &:hover { + cursor: help; + font-weight: bold; + } + } + } + + &__subject { + display: block; + padding-bottom: 0.05rem; + white-space: nowrap; + + &__label { + &--left { + display: inline-block; + width: $ribbon__subject__label-left--width; + } + + &--right { + display: inline-block; + margin-left: $ribbon__subject__label-right--margin-left; + } + } + + &--separator { + display: inline-block; + padding: 0px $ribbon__cell--separator; + } + + &--cell { + display: inline-block; + width: $ribbon__cell--width; + height: $ribbon__cell--height; + box-shadow: $ribbon__subject__cell--box-shadow; + outline: none; + text-align: center; + + &--hover { + cursor: pointer; + border: 1px solid black; + } + + &--no-annotation { + display: inline-block; + width: $ribbon__cell--width; + height: $ribbon__cell--height; + box-shadow: $ribbon__subject__cell--box-shadow; + outline: none; + text-align: center; + + &:hover { + cursor: not-allowed !important; + } + } + + &--disabled { + display: inline-block; + width: $ribbon__cell--width; + height: $ribbon__cell--height; + box-shadow: $ribbon__subject__cell--box-shadow; + outline: none; + text-align: center; + + background: repeating-linear-gradient( + 45deg, + #ffffff, + rgba(0, 0, 0, 0.1) 1px, + #ffffff 2px, + #ffffff 12px + ); + } + } + } +} + +.selected { + font-weight: bold !important; +} + +.clickable { + cursor: pointer !important; +} diff --git a/src/components/go-ribbon-strips/ribbon-strips.tsx b/src/components/go-ribbon-strips/ribbon-strips.tsx new file mode 100644 index 0000000..a5aa0e9 --- /dev/null +++ b/src/components/go-ribbon-strips/ribbon-strips.tsx @@ -0,0 +1,833 @@ +import { + h, + Component, + Prop, + Element, + Event, + EventEmitter, + State, + Watch, +} from "@stencil/core"; + +import { truncate, groupKey, subjectGroupKey } from "./utils"; +import { sameArray } from "../../globals/utils"; +import { + IRibbonModel, + IRibbonCategory, + IRibbonGroup, + IRibbonSubject, + IRibbonCellEvent, + IRibbonCellClick, + IRibbonGroupEvent, +} from "../../globals/models"; + +import { + COLOR_BY, + POSITION, + SELECTION, + EXP_CODES, + CELL_TYPES, + FONT_CASE, + FONT_STYLE, +} from "../../globals/enums"; +import { Method } from "@stencil/core"; + +@Component({ + tag: "wc-ribbon-strips", + styleUrl: "./ribbon-strips.scss", + shadow: false, +}) +export class RibbonStrips { + @Element() ribbonElement; + + @Prop() baseApiUrl = "https://api.geneontology.org/api/ontology/ribbon/"; + + @Prop() subjectBaseUrl: string = + "http://amigo.geneontology.org/amigo/gene_product/"; + @Prop() groupBaseUrl: string = "http://amigo.geneontology.org/amigo/term/"; + + @Prop() subset: string = "goslim_agr"; + + /** + * provide gene ids (e.g. RGD:620474,RGD:3889 or as a list ["RGD:620474", "RGD:3889"]) + */ + @Prop() subjects: string = undefined; + + @Prop() classLabels = "term,terms"; + @Prop() annotationLabels = "annotation,annotations"; + + /** + * Which value to base the cell color on + * 0 = class count + * 1 = annotation count + */ + @Prop() colorBy = COLOR_BY.ANNOTATION_COUNT; + + /** + * false = show a gradient of colors to indicate the value of a cell + * true = show only two colors (minColor; maxColor) to indicate the values of a cell + */ + @Prop() binaryColor = false; + @Prop() minColor = "255,255,255"; + @Prop() maxColor = "24,73,180"; + @Prop() maxHeatLevel = 48; + @Prop() groupMaxLabelSize = 60; + + /** + * Override of the category case + * 0 (default) = unchanged + * 1 = to lower case + * 2 = to upper case + */ + @Prop() categoryCase = FONT_CASE.LOWER_CASE; + + /** + * 0 = Normal + * 1 = Bold + */ + @Prop() categoryAllStyle = FONT_STYLE.NORMAL; + /** + * 0 = Normal + * 1 = Bold + */ + @Prop() categoryOtherStyle = FONT_STYLE.NORMAL; + + @Prop() showOtherGroup = false; + + /** + * Position the subject label of each row + * 0 = None + * 1 = Left + * 2 = Right + * 3 = Bottom + */ + @Prop() subjectPosition = POSITION.LEFT; + @Prop() subjectUseTaxonIcon: boolean; + @Prop() subjectOpenNewTab: boolean = true; + @Prop() groupNewTab: boolean = true; + @Prop() groupClickable: boolean = true; + + /** + * Click handling of a cell. + * 0 = select only the cell (1 subject, 1 group) + * 1 = select the whole column (all subjects, 1 group) + */ + @Prop() selectionMode = SELECTION.CELL; + + /** + * If no value is provided, the ribbon will load without any group selected. + * If a value is provided, the ribbon will show the requested group as selected + * The value should be the id of the group to be selected + */ + @Prop() selected; + + /** + * If true, the ribbon will fire an event if a user click an empty cell + * If false, the ribbon will not fire the event on an empty cell + * Note: if selectionMode == SELECTION.COLUMN, then the event will trigger if at least one of the selected cells has annotations + */ + @Prop() fireEventOnEmptyCells = false; + + // @Watch('selected') + // selectedChanged(newValue, oldValue) { + // console.log("selectedChanged(", newValue , oldValue , ")"); + // if(newValue != oldValue) { + // let gp = this.getGroup(newValue); + // this.selectCells(this.ribbonSummary.subjects, gp); + // } + // } + + /** + * add a cell at the beginning of each row/subject to show all annotations + */ + @Prop() addCellAll: boolean = true; + + /** + * if provided, will override any value provided in subjects and subset + */ + @Prop() data: string; + + @Watch("data") + dataChanged(newValue, oldValue) { + if (newValue != oldValue) { + this.loadData(newValue); + } + } + + /** + * When this is set to false, changing the subjects Prop won't trigger the reload of the ribbon + * This is necessary when the ribbon is showing data other than GO or not using the internal fetchData mechanism + */ + @Prop() updateOnSubjectChange = true; + + /** + * This method is automatically called whenever the value of "subjects" changes + * Note this method can be (and should be) deactivated (use updateOnSubjectChange) + * when the ribbon is not loading from GO and not using the internal fetchData mechanism + * @param newValue a new subject is submitted (e.g. gene) + * @param oldValue old value of the subject (e.g. gene or genes) + */ + @Watch("subjects") + subjectsChanged(newValue, oldValue) { + // if we don't want to update the ribbon on subject changes + if (!this.updateOnSubjectChange) { + this.loading = false; + return; + } + + if (newValue != oldValue) { + // Fetch data based on subjects and subset + this.fetchData(this.subjects).then( + (data) => { + this.ribbonSummary = data; + this.loading = false; + }, + (error) => { + console.error(error); + this.loading = false; + }, + ); + } + } + + @State() selectedGroup: IRibbonGroup; + + /** + * This event is triggered whenever a ribbon cell is clicked + */ + @Event({ eventName: "cellClick", cancelable: true, bubbles: true }) + cellClick: EventEmitter; + + /** + * This event is triggered whenever the mouse enters a cell area + */ + @Event({ eventName: "cellEnter", cancelable: true, bubbles: true }) + cellEnter: EventEmitter; + + /** + * This event is triggered whenever the mouse leaves a cell area + */ + @Event({ eventName: "cellLeave", cancelable: true, bubbles: true }) + cellLeave: EventEmitter; + + /** + * This event is triggered whenever a group cell is clicked + */ + @Event({ eventName: "groupClick", cancelable: true, bubbles: true }) + groupClick: EventEmitter; + + /** + * This event is triggered whenever the mouse enters a group cell area + */ + @Event({ eventName: "groupEnter", cancelable: true, bubbles: true }) + groupEnter: EventEmitter; + + /** + * This event is triggered whenever the mouse leaves a group cell area + */ + @Event({ eventName: "groupLeave", cancelable: true, bubbles: true }) + groupLeave: EventEmitter; + + @Prop() ribbonSummary: IRibbonModel; + + loading = true; + onlyExperimental = false; + + groupAll: IRibbonGroup = { + id: "all", + label: "all annotations", + description: "Show all annotations for all categories", + type: "GlobalAll", + }; + + loadData(data) { + if (data) { + // If was injected as string, transform to json + if (typeof data == "string") { + this.ribbonSummary = JSON.parse(data); + } else { + this.ribbonSummary = data; + } + this.loading = false; + this.subjects = this.ribbonSummary.subjects + .map((elt) => elt.id) + .join(","); + return true; + } + return false; + } + + getGroup(group_id) { + if (!this.ribbonSummary) return null; + if (group_id == "all") return this.groupAll; + for (const cat of this.ribbonSummary.categories) { + for (const gp of cat.groups) { + if (gp.id == group_id) return gp; + } + } + return null; + } + + /** + * Once the component is loaded, fetch the data + */ + componentWillLoad() { + // Prioritize data if provided + if (this.loadData(this.data)) return; + + // If no subjects were provided, don't try to fetch data + if (!this.subjects) { + this.loading = false; + return; + } + + // Fetch data based on subjects and subset + this.fetchData(this.subjects).then( + (data) => { + this.ribbonSummary = data; + this.loading = false; + }, + (error) => { + console.error(error); + this.loading = false; + }, + ); + } + + componentDidLoad() { + this.selectGroup(this.selected); + this.selected = null; + } + + fetchData(subjects) { + if (subjects.includes(",")) { + subjects = subjects.split(","); + } + if (subjects instanceof Array) { + subjects = subjects.join("&subject="); + } + + let query = + this.baseApiUrl + "?subset=" + this.subset + "&subject=" + subjects; + if (this.onlyExperimental) { + query += EXP_CODES.map((exp) => "&ecodes=" + exp).join(""); + } + console.log("API query is " + query); + + return fetch(query) + .then((response: Response) => { + return response.json(); + }) + .catch((error) => { + return error; + }); + } + + filterExperiment(checkbox) { + this.onlyExperimental = checkbox.target.checked; + + // Fetch data based on subjects and subset + this.fetchData(this.subjects).then( + (data) => { + this.ribbonSummary = data; + this.loading = false; + }, + (error) => { + console.error(error); + this.loading = false; + }, + ); + } + + formerColor; + formerColors; + onCellEnter(subjects, group) { + if (subjects instanceof Array) { + this.formerColors = new Map(); + + // change header style + const el = this.ribbonElement.querySelector("#" + groupKey(group)); + el.classList.add("selected"); + + for (const subject of subjects) { + const el = this.ribbonElement.querySelector( + "#" + subjectGroupKey(subject, group), + ); + el.hovered = true; + el.style.cursor = "pointer"; + } + } else { + // change cell style + let nbAnnotations = + group.id in subjects.groups + ? subjects.groups[group.id]["ALL"]["nb_annotations"] + : 0; + if (group.type == "GlobalAll") { + nbAnnotations = subjects.nb_annotations; + } else { + nbAnnotations = + group.id in subjects.groups + ? subjects.groups[group.id]["ALL"]["nb_annotations"] + : 0; + } + + // let el = this.ribbonElement.shadowRoot.querySelector("#" + this.transform(subjects.id) + "-" + this.transform(group.id)); + let el = this.ribbonElement.querySelector( + "#" + subjectGroupKey(subjects, group), + ); + + el.style.cursor = nbAnnotations == 0 ? "not-allowed" : "pointer"; + + if (nbAnnotations > 0) { + el.hovered = true; + + // change header style + el = this.ribbonElement.querySelector("#" + groupKey(group)); + el.classList.add("selected"); + } + } + const event: IRibbonCellEvent = { subjects: subjects, group: group }; + this.cellEnter.emit(event); + } + + onCellLeave(subjects, group) { + if (subjects instanceof Array) { + // change the header style + const el = this.ribbonElement.querySelector("#" + groupKey(group)); + el.classList.remove("selected"); + + for (const subject of subjects) { + const el = this.ribbonElement.querySelector( + "#" + subjectGroupKey(subject, group), + ); + el.hovered = false; + } + this.formerColors = undefined; + } else { + // change the cell style + let el = this.ribbonElement.querySelector( + "#" + subjectGroupKey(subjects, group), + ); + el.hovered = false; + + // change the header style + el = this.ribbonElement.querySelector("#" + groupKey(group)); + el.classList.remove("selected"); + } + const event: IRibbonCellEvent = { subjects: subjects, group: group }; + this.cellLeave.emit(event); + } + + previouslyHovered = []; + overCells(subjects, group) { + if (!(subjects instanceof Array)) { + subjects = [subjects]; + } + + const subs = subjects.map( + (elt) => elt.id + "@" + group.id + "@" + group.type, + ); + const prevSubs = this.previouslyHovered.map( + (elt) => elt.subject.id + "@" + elt.group.id + "@" + elt.group.type, + ); + const same = sameArray(subs, prevSubs); + + if (!same) { + for (const cell of this.previouslyHovered) { + cell.hovered = false; + } + } + this.previouslyHovered = []; + + const hovered: boolean[] = []; + for (const subject of subjects) { + const cell = this.ribbonElement.querySelector( + "#" + subjectGroupKey(subject, group), + ); + cell.hovered = !cell.hovered; + hovered.push(cell.hovered); + this.previouslyHovered.push(cell); + } + return hovered; + } + + previouslySelected = []; + selectCells(subjects, group, toggle = true) { + if (!(subjects instanceof Array)) { + subjects = [subjects]; + } + + const subs = subjects.map( + (elt) => elt.id + "@" + group.id + "@" + group.type, + ); + const prevSubs = this.previouslySelected.map( + (elt) => elt.subject.id + "@" + elt.group.id + "@" + elt.group.type, + ); + const same = sameArray(subs, prevSubs); + + if (!same) { + for (const cell of this.previouslySelected) { + cell.selected = false; + } + } + this.previouslySelected = []; + + const selected: boolean[] = []; + let lastCell; + for (const subject of subjects) { + const cell = this.ribbonElement.querySelector( + "#" + subjectGroupKey(subject, group), + ); + cell.selected = toggle ? !cell.selected : true; + selected.push(cell.selected); + this.previouslySelected.push(cell); + lastCell = cell; + } + if (lastCell.selected) { + this.selectedGroup = group; + } else { + this.selectedGroup = undefined; + } + return selected; + } + + onCellClick(subjects, group) { + if (!(subjects instanceof Array)) { + subjects = [subjects]; + } + + // if don't fire events on empty cells + if (!this.fireEventOnEmptyCells) { + let hasAnnotations = false; + // if single cell selection, check if it has annotations + if (this.selectionMode == SELECTION.CELL) { + const keys = Object.keys(subjects[0].groups); + for (const key of keys) { + if (subjects[0].groups[key]["ALL"].nb_annotations > 0) + hasAnnotations = true; + } + + // if multiple cells selection, check if at least one has annotations + } else { + for (const sub of subjects) { + if (group.id == "all") { + hasAnnotations = hasAnnotations || sub.nb_annotations > 0; + } else { + hasAnnotations = + hasAnnotations || + (group.id in sub.groups && + sub.groups[group.id]["ALL"]["nb_annotations"] > 0); + } + } + } + if (!hasAnnotations) { + return; + } + } + + const selected = this.selectCells(subjects, group); + + const event: IRibbonCellClick = { + subjects: subjects, + group: group, + selected: selected, + }; + this.cellClick.emit(event); + } + + onGroupClick(category, group) { + this.selectCells(this.ribbonSummary.subjects, group); + const event = { category: category, group: group }; + this.groupClick.emit(event); + } + + onGroupEnter(category, group) { + this.overCells(this.ribbonSummary.subjects, group); + const event: IRibbonGroupEvent = { + subjects: this.ribbonSummary.subjects, + category: category, + group: group, + }; + this.groupEnter.emit(event); + } + + onGroupLeave(category, group) { + this.overCells(this.ribbonSummary.subjects, group); + const event: IRibbonGroupEvent = { + subjects: this.ribbonSummary.subjects, + category: category, + group: group, + }; + this.groupLeave.emit(event); + } + + applyCategoryStyling(category) { + const cc0 = truncate(category, this.groupMaxLabelSize, "..."); + const cc1 = this.applyCategoryCase(cc0); + const cc2 = this.applyCategoryBold(cc1); + return cc2; + } + + applyCategoryBold(category) { + const lc = category.toLowerCase(); + if (lc.startsWith("all") && this.categoryAllStyle == FONT_STYLE.BOLD) { + return {category}; + } + if (lc.startsWith("other") && this.categoryOtherStyle == FONT_STYLE.BOLD) { + return {category}; + } + return category; + } + + applyCategoryCase(category) { + if (this.categoryCase == FONT_CASE.LOWER_CASE) { + return category.toLowerCase(); + } else if (this.categoryCase == FONT_CASE.UPPER_CASE) { + return category.toUpperCase(); + } + return category; + } + + @Method() + async selectGroup(group_id) { + setTimeout(() => { + if (group_id && this.ribbonSummary) { + const gp = this.getGroup(group_id); + if (gp) { + this.selectCells(this.ribbonSummary.subjects, gp, false); + } else { + console.warn("Could not find group <", group_id, ">"); + } + } + }, 750); + } + + render() { + // return [ "hello", ] + + // Still loading (executing fetch) + if (this.loading) { + // return ( "Loading Ribbon..." ); + return ( + + ); + } + + if (!this.subjects && !this.ribbonSummary) { + return
Must provide at least one subject
; + } + + // API request undefined + if (!this.ribbonSummary) { + return
No data available
; + } + + // API request done but not subject retrieved + const nbSubjects: number = this.ribbonSummary.subjects.length; + if (nbSubjects == 0) { + return
Must provide at least one subject
; + } + + // Data is present, show the ribbon + return ( +
+ + {this.renderCategory()} + {this.renderSubjects()} +
+ {/*
+ Show only experimental annotations */} +
+ ); + } + + renderCategory() { + return ( + + {this.subjectPosition == POSITION.LEFT ? ( + + ) : ( + "" + )} + + {this.addCellAll ? ( + this.onGroupEnter(null, this.groupAll)} + onMouseLeave={() => this.onGroupLeave(null, this.groupAll)} + onClick={ + this.groupClickable + ? () => this.onGroupClick(undefined, this.groupAll) + : undefined + } + > + {this.applyCategoryStyling(this.groupAll.label)} + + ) : ( + "" + )} + + {this.ribbonSummary.categories.map((category: IRibbonCategory) => { + return [ + , + category.groups.map((group: IRibbonGroup) => { + if (group.type == CELL_TYPES.OTHER && !this.showOtherGroup) { + return; + } + + const classes = this.groupClickable + ? "ribbon__category--cell clickable" + : "ribbon__category--cell"; + return ( + this.onGroupEnter(category, group)} + onMouseLeave={() => this.onGroupLeave(category, group)} + onClick={ + this.groupClickable + ? () => this.onGroupClick(category, group) + : undefined + } + > + {this.selectedGroup == group ? ( + + {this.applyCategoryStyling(group.label)} + + ) : ( + this.applyCategoryStyling(group.label) + )} + + ); + }), + ]; + })} + + {this.subjectPosition == POSITION.RIGHT ? ( + + ) : ( + "" + )} + + ); + } + + renderSubjects() { + return this.ribbonSummary.subjects.map((subject: IRibbonSubject) => { + const subjects = + this.selectionMode == SELECTION.CELL + ? subject + : this.ribbonSummary.subjects; + return ( + + {this.subjectPosition == POSITION.LEFT ? ( + + ) : ( + "" + )} + + {this.addCellAll ? ( + this.onCellClick(subjects, this.groupAll)} + onMouseEnter={() => this.onCellEnter(subjects, this.groupAll)} + onMouseLeave={() => this.onCellLeave(subjects, this.groupAll)} + /> + ) : ( + "" + )} + + {this.ribbonSummary.categories.map((category: IRibbonCategory) => { + return [ + , + category.groups.map((group: IRibbonGroup) => { + const cellid = + group.id + (group.type == CELL_TYPES.OTHER ? "-other" : ""); + const cell = + cellid in subject.groups ? subject.groups[cellid] : undefined; + + const nbAnnotations = cell ? cell["ALL"]["nb_annotations"] : 0; + + // by default the group should be available + let available = true; + + // if a value was given, then override the default value + if ( + cell && + Object.prototype.hasOwnProperty.call(cell, "available") + ) { + available = cell.available; + } + + if (group.type == CELL_TYPES.OTHER && !this.showOtherGroup) { + return; + } + + return ( + this.onCellClick(subjects, group)} + onMouseEnter={() => this.onCellEnter(subjects, group)} + onMouseLeave={() => this.onCellLeave(subjects, group)} + /> + ); + }), + ]; + })} + + {this.subjectPosition == POSITION.RIGHT ? ( + + ) : ( + "" + )} + + ); + }); + } +} diff --git a/src/components/go-ribbon-strips/utils.ts b/src/components/go-ribbon-strips/utils.ts new file mode 100644 index 0000000..7f839c7 --- /dev/null +++ b/src/components/go-ribbon-strips/utils.ts @@ -0,0 +1,32 @@ +export function truncate(text, size, ending) { + if (size == null) { + size = 100; + } + if (ending == null) { + ending = "..."; + } + if (text.length > size) { + return text.substring(0, size - ending.length) + ending; + } else { + return text; + } +} + +export function transformID(txt) { + return txt.replace(":", "_"); +} + +export function groupKey(group) { + return "category-" + transformID(group.id) + "-" + group.type; +} + +export function subjectGroupKey(subject, group) { + return ( + "subject-" + + transformID(subject.id) + + "-category-" + + transformID(group.id) + + "-" + + group.type + ); +} diff --git a/src/components/go-ribbon-subject/readme.md b/src/components/go-ribbon-subject/readme.md new file mode 100644 index 0000000..db3b80f --- /dev/null +++ b/src/components/go-ribbon-subject/readme.md @@ -0,0 +1,35 @@ +# wc-ribbon-subject + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ---------------- | -------------------- | ----------- | ---------------- | ----------- | +| `newTab` | `new-tab` | | `boolean` | `undefined` | +| `subject` | -- | | `IRibbonSubject` | `undefined` | +| `subjectBaseURL` | `subject-base-u-r-l` | | `string` | `undefined` | + +## Events + +| Event | Description | Type | +| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | +| `subjectClick` | This event is triggered whenever a subject label is clicked Can call preventDefault() to avoid the default behavior (opening the linked subject page) | `CustomEvent` | + +## Dependencies + +### Used by + +- [wc-ribbon-strips](../go-ribbon-strips) + +### Graph + +```mermaid +graph TD; + wc-ribbon-strips --> wc-ribbon-subject + style wc-ribbon-subject fill:#f9f,stroke:#333,stroke-width:4px +``` + +--- + +_Built with [StencilJS](https://stenciljs.com/)_ diff --git a/src/components/go-ribbon-subject/ribbon-subject.scss b/src/components/go-ribbon-subject/ribbon-subject.scss new file mode 100644 index 0000000..3e7f42b --- /dev/null +++ b/src/components/go-ribbon-subject/ribbon-subject.scss @@ -0,0 +1,15 @@ +.ribbon { + &__subject { + &__label { + &--link { + text-decoration: none; + color: black; + font-weight: normal; + + &:hover { + font-weight: bold; + } + } + } + } +} diff --git a/src/components/go-ribbon-subject/ribbon-subject.tsx b/src/components/go-ribbon-subject/ribbon-subject.tsx new file mode 100644 index 0000000..5d01a6f --- /dev/null +++ b/src/components/go-ribbon-subject/ribbon-subject.tsx @@ -0,0 +1,66 @@ +import { h } from "@stencil/core"; + +import { Component, Prop, State, Element } from "@stencil/core"; + +import { formatTaxonLabel } from "./utils"; +import { EventEmitter, Event } from "@stencil/core"; +import { IRibbonSubject } from "../../globals/models"; + +@Component({ + tag: "wc-ribbon-subject", + styleUrl: "./ribbon-subject.scss", + shadow: false, +}) +export class RibbonSubject { + @Element() el: HTMLElement; + + @Prop() subject: IRibbonSubject; + + @Prop() subjectBaseURL: string; + @Prop() newTab: boolean; + + @State() id: string; + + constructor() { + if (!this.subjectBaseURL.endsWith("/")) { + this.subjectBaseURL += "/"; + } + // fix due to doubling MGI:MGI: in GO + this.id = this.subject.id; + if (this.subject.id.startsWith("MGI:")) { + this.id = "MGI:" + this.subject.id; + } + } + + /** + * This event is triggered whenever a subject label is clicked + * Can call preventDefault() to avoid the default behavior (opening the linked subject page) + */ + @Event({ eventName: "subjectClick", cancelable: true, bubbles: true }) + subjectClick: EventEmitter; + + onSubjectClick(event, subject) { + const ev = { originalEvent: event, subject: subject }; + this.subjectClick.emit(ev); + } + + render() { + return ( + + { + this.onSubjectClick(e, this.subject); + }} + target={this.newTab ? "_blank" : "_self"} + > + {this.subject.label + + " (" + + formatTaxonLabel(this.subject.taxon_label) + + ")"} + + + ); + } +} diff --git a/src/components/go-ribbon-subject/utils.ts b/src/components/go-ribbon-subject/utils.ts new file mode 100644 index 0000000..f838042 --- /dev/null +++ b/src/components/go-ribbon-subject/utils.ts @@ -0,0 +1,4 @@ +export function formatTaxonLabel(species) { + const split = species.split(" "); + return split[0].substring(0, 1) + split[1].substring(0, 2); +} diff --git a/src/components/go-ribbon-table/index.html b/src/components/go-ribbon-table/index.html new file mode 100644 index 0000000..b5f856d --- /dev/null +++ b/src/components/go-ribbon-table/index.html @@ -0,0 +1,61 @@ + + + + + + Generic Table - WC + + + + + + + + + + diff --git a/src/components/go-ribbon-table/readme.md b/src/components/go-ribbon-table/readme.md new file mode 100644 index 0000000..1dd854f --- /dev/null +++ b/src/components/go-ribbon-table/readme.md @@ -0,0 +1,61 @@ +# wc-ribbon-table + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ----------------------------------------------------- | +| `baseApiUrl` | `base-api-url` | | `string` | `"https://api.geneontology.org/api/ontology/ribbon/"` | +| `bioLinkData` | `bio-link-data` | Reading biolink data. This will trigger a render of the table as would changing data | `string` | `undefined` | +| `data` | `data` | Must follow the appropriate JSON data model Can be given as either JSON or stringified JSON | `string` | `undefined` | +| `filterBy` | `filter-by` | Filter rows based on the presence of one or more values in a given column The filtering will be based on cell label or id Example: filter-by="evidence:ISS,ISO or multi-step filters: filter-by:evidence:ISS,ISO;term:xxx" Note: if value is "", remove any filtering | `string` | `undefined` | +| `groupBaseUrl` | `group-base-url` | | `string` | `"http://amigo.geneontology.org/amigo/term/"` | +| `groupBy` | `group-by` | Using this parameter, the table rows can bee grouped based on column ids A multiple step grouping is possible by using a ";" between groups The grouping applies before the ordering Example: hid-1,hid-3 OR hid-1,hid-3;hid-2 Note: if value is "", remove any grouping | `string` | `undefined` | +| `hideColumns` | `hide-columns` | Used to hide specific column of the table | `string` | `undefined` | +| `orderBy` | `order-by` | This is used to sort the table depending of a column The column cells must be single values The ordering applies after the grouping Note: if value is "", remove any ordering | `string` | `undefined` | +| `subjectBaseUrl` | `subject-base-url` | | `string` | `"http://amigo.geneontology.org/amigo/gene_product/"` | + +## Methods + +### `showCurie() => Promise` + +#### Returns + +Type: `Promise` + +### `showDBXrefs() => Promise` + +#### Returns + +Type: `Promise` + +### `showOriginalTable() => Promise` + +#### Returns + +Type: `Promise` + +### `showTable() => Promise` + +#### Returns + +Type: `Promise` + +## Dependencies + +### Used by + +- [wc-go-ribbon](../go-ribbon) + +### Graph + +```mermaid +graph TD; + wc-go-ribbon --> wc-ribbon-table + style wc-ribbon-table fill:#f9f,stroke:#333,stroke-width:4px +``` + +--- + +_Built with [StencilJS](https://stenciljs.com/)_ diff --git a/src/components/go-ribbon-table/ribbon-table.scss b/src/components/go-ribbon-table/ribbon-table.scss new file mode 100644 index 0000000..a613808 --- /dev/null +++ b/src/components/go-ribbon-table/ribbon-table.scss @@ -0,0 +1,84 @@ +$table__border: 0px solid black !default; +$table__border--radius: 5px !default; + +$table__header--bgcolor: rgb(255, 255, 255) !default; +$table__header--color: rgb(0, 0, 0) !default; +$table__header__cell--padding: 2px !default; +$table__header__cell--font-weight: 800 !default; + +$table__row--bgcolor: white !default; +$table__row__cell--padding: 2px !default; +$table__row__cell--height: 20px !default; + +$table__header__divider: 2px solid #ddd !default; +$table__divider: 1px solid #ddd !default; + +$table__row__supercell__cell__link--decoration: none !default; +$table__row__supercell__cell__link--color: rgb(73, 101, 194) !default; + +.table { + &, + & * { + margin: 0; + padding: 0; + box-sizing: border-box; + font-weight: normal; + } + + width: 100%; + border: $table__border; + border-radius: $table__border--radius; + + &__header { + background-color: $table__header--bgcolor; + border-bottom: 1px solid black; + color: $table__header--color; + // text-shadow: 3px 3px 2px #474747; + // box-shadow: 1px 1px 3px 0px black; + text-align: left; + border-top: $table__header__divider; + border-bottom: $table__header__divider; + cursor: help; + + &__cell { + padding: $table__header__cell--padding; + font-weight: $table__header__cell--font-weight; + border-top: $table__header__divider; + border-bottom: $table__header__divider; + } + } + + &__row { + background-color: $table__row--bgcolor; + border-bottom: $table__divider; + + &__supercell { + vertical-align: top !important; + border-bottom: $table__divider; + + &__list { + list-style: none; + } + + &__cell { + height: $table__row__cell--height; + padding: $table__row__cell--padding; + white-space: nowrap; + + &__link { + text-decoration: $table__row__supercell__cell__link--decoration; + color: $table__row__supercell__cell__link--color; + } + + &--not { + color: red; + text-transform: uppercase; + font-weight: 700; + border: 1px solid red; + padding: 0px 2px; + margin-right: 10px; + } + } + } + } +} diff --git a/src/components/go-ribbon-table/ribbon-table.tsx b/src/components/go-ribbon-table/ribbon-table.tsx new file mode 100644 index 0000000..53f505f --- /dev/null +++ b/src/components/go-ribbon-table/ribbon-table.tsx @@ -0,0 +1,519 @@ +import { Component, h, Prop, Watch, State, Method } from "@stencil/core"; + +import { ITable, ISuperCell } from "../../globals/models"; +import { bioLinkToTable, addEmptyCells } from "./utils"; + +import * as dbxrefs from "@geneontology/dbxrefs"; + +@Component({ + tag: "wc-ribbon-table", + styleUrl: "./ribbon-table.scss", + shadow: false, +}) +export class RibbonTable { + @Prop() baseApiUrl = "https://api.geneontology.org/api/ontology/ribbon/"; + + @Prop() subjectBaseUrl: string = + "http://amigo.geneontology.org/amigo/gene_product/"; + @Prop() groupBaseUrl: string = "http://amigo.geneontology.org/amigo/term/"; + + /** + * Using this parameter, the table rows can bee grouped based on column ids + * A multiple step grouping is possible by using a ";" between groups + * The grouping applies before the ordering + * Example: hid-1,hid-3 OR hid-1,hid-3;hid-2 + * Note: if value is "", remove any grouping + */ + @Prop() groupBy: string; + + @Watch("groupBy") + groupByChanged(newValue, oldValue) { + // console.log("groupByChanged(" , newValue , "; " , oldValue , ")"); + if (newValue != oldValue) { + this.updateTable(); + } + } + + /** + * This is used to sort the table depending of a column + * The column cells must be single values + * The ordering applies after the grouping + * Note: if value is "", remove any ordering + */ + @Prop() orderBy: string; + + @Watch("orderBy") + orderByChanged(newValue, oldValue) { + // console.log("orderByChanged(" , newValue , "; " , oldValue , ")"); + if (newValue != oldValue) { + this.updateTable(); + } + } + + /** + * Filter rows based on the presence of one or more values in a given column + * The filtering will be based on cell label or id + * Example: filter-by="evidence:ISS,ISO or multi-step filters: filter-by:evidence:ISS,ISO;term:xxx" + * Note: if value is "", remove any filtering + */ + @Prop() filterBy: string; + + @Watch("filterBy") + filterByChanged(newValue, oldValue) { + // console.log("filterByChanged(" , newValue , "; " , oldValue , ")"); + if (newValue != oldValue) { + this.updateTable(); + } + } + + /** + * Used to hide specific column of the table + */ + @Prop() hideColumns: string; + + @Watch("hideColumns") + hideColumnsChanged(newValue, oldValue) { + if (newValue != oldValue) { + this.updateTable(); + } + } + + /** + * Must follow the appropriate JSON data model + * Can be given as either JSON or stringified JSON + */ + @Prop() data: string; + + /** + * Reading biolink data. This will trigger a render of the table as would changing data + */ + @Prop() bioLinkData: string; + + /** + * This contains the original table, converted from either data or bioLinkData + * Its value only changes when data or bioLinkData changes + */ + originalTable: ITable; + + /** + * Contains the current representation from originalTable, including any grouping or sorting + * Any change to this state will trigger a render + */ + @State() + table: ITable; + + /** + * Contains (header_id ; header) + * Used to speed up some cell processing (eg access baseURL) + */ + headerMap; + + @Watch("data") + dataChanged(newValue, oldValue) { + if (newValue != oldValue) { + // console.log("DATA CHANGED: ", newValue); + this.loadFromData(); + } + } + + loadFromData() { + if (typeof this.data == "string") { + this.originalTable = JSON.parse(this.data); + } else { + this.originalTable = this.data; + } + this.updateTable(); + } + + @Watch("bioLinkData") + bioLinkDataChanged(newValue, oldValue) { + if (newValue != oldValue) { + // console.log("BIOLINK DATA CHANGED: ", newValue); + this.loadFromBioLinkData(); + } + } + + loadFromBioLinkData() { + // if no data, empty the current table + if (this.bioLinkData == undefined || this.bioLinkData == "") { + this.originalTable = undefined; + this.table = undefined; + return; + } + + if (dbxrefs.isReady()) { + if (typeof this.bioLinkData == "string") { + this.originalTable = bioLinkToTable( + JSON.parse(this.bioLinkData), + dbxrefs.getURL, + ); + } else { + this.originalTable = bioLinkToTable(this.bioLinkData, dbxrefs.getURL); + } + this.updateTable(); + } else { + dbxrefs.init().then(() => { + console.log("dbx: ", dbxrefs); + console.log(dbxrefs.getURL("WB", undefined, "WBGene00006575")); + if (typeof this.bioLinkData == "string") { + this.originalTable = bioLinkToTable( + JSON.parse(this.bioLinkData), + dbxrefs.getURL, + ); + } else { + this.originalTable = bioLinkToTable(this.bioLinkData, dbxrefs.getURL); + } + this.updateTable(); + }); + } + } + + updateTable() { + if (this.originalTable) { + // deep copy required to keep the original table safe + let tempTable = JSON.parse(JSON.stringify(this.originalTable)); + tempTable = addEmptyCells(tempTable); + + // step-1: is grouping the table rows based on provided columns (if any) + if (this.groupBy && this.groupBy != "") { + // multiple steps grouping + if (this.groupBy.includes(";")) { + const split = this.groupBy.split(";"); + for (const groups of split) { + tempTable = this.groupByColumns( + tempTable, + groups.split(","), + false, + ); + } + // single step grouping + } else { + tempTable = this.groupByColumns( + tempTable, + this.groupBy.split(","), + false, + ); + } + } + + // step-2: order the table rows based on provided columns (if any) + if (this.orderBy && this.orderBy != "") { + tempTable.rows.sort((a, b) => { + // console.log("sort(", a, b, ")"); + const eqa = a.cells.filter((elt) => elt.headerId == this.orderBy)[0]; + const eqb = b.cells.filter((elt) => elt.headerId == this.orderBy)[0]; + return eqa.values[0].label.localeCompare(eqb.values[0].label); + }); + } + + // step-3: filter the table based on provided {col, values} (if any) + if (this.filterBy && this.filterBy != "") { + // multiple steps grouping + if (this.filterBy.includes(";")) { + const split = this.filterBy.split(";"); + for (const filters of split) { + tempTable = this.filterByColumns(tempTable, filters); + } + // single step grouping + } else { + tempTable = this.filterByColumns(tempTable, this.filterBy); + } + } + + // step-4: hide columns based on provided hideColumns parameter + if (this.hideColumns && this.hideColumns != "") { + const cols = this.hideColumns.includes(",") + ? this.hideColumns.split(",") + : [this.hideColumns]; + for (const header of tempTable.header) { + header.hide = cols.includes(header.id); + } + } + + // assigning this to the state - will trigger a render + this.table = tempTable; + this.createHeaderMap(); + } + } + + goContextURL = + "https://raw.githubusercontent.com/prefixcommons/biocontext/master/registry/go_context.jsonld"; + curie; + + componentWillLoad() { + // enter with the regular data format + if (this.data) { + this.loadFromData(); + + // enter with biolink data format + } else if (this.bioLinkData) { + this.loadFromBioLinkData(); + } + } + + createHeaderMap() { + this.headerMap = new Map(); + for (const header of this.table.header) { + this.headerMap.set(header.id, header); + } + } + + mergeCells(cells) { + return cells; + } + + /** + * Will group the table rows based on unique values in specified columns + * @param table the table to be grouped + * @param keyColumns ids of the columns to create unique rows - will only work with cells containing single value, not array + * @param filterRedudancy if true, the values of merged columns will be filtered + */ + groupByColumns(table, keyColumns, filterRedudancy = true) { + // console.log("groupByColumns(", table , keyColumns, filterRedudancy , ")"); + + const firstRow = table.rows[0].cells; + const otherCells = firstRow.filter( + (elt) => !keyColumns.includes(elt.headerId), + ); + const otherColumns = otherCells.map((elt) => elt.headerId); + // console.log("other cols: ", otherColumns); + + // building the list of unique rows + const uRows = new Map(); + for (let i = 0; i < table.rows.length; i++) { + const row = table.rows[i]; + const keyCells = row.cells.filter((elt) => + keyColumns.includes(elt.headerId), + ); + const key = keyCells.map((elt) => elt.values[0].label).join("-"); + + let rows = []; + if (uRows.has(key)) { + rows = uRows.get(key); + } else { + uRows.set(key, rows); + } + rows.push(row); + } + // console.log("unique rows: ", uRows); + + const newTable = { newTab: table.newTab, header: table.header, rows: [] }; + + // this was required either by stenciljs or web component + // for an integration in alliance REACT project + // somehow more recent iterators on Map were not working ! + const akeys = Array.from(uRows.keys()); + + // going through each set of unique rows + // for(let rrows of uRows.values()) { + for (let i = 0; i < akeys.length; i++) { + const key = akeys[i]; + const rrows = uRows.get(key); + // console.log("Uniq.Row (" , key , "): ", rrows); + const row = { cells: [] }; + for (const header of table.header) { + // console.log(" --- header: ", header); + let eqcell: ISuperCell = undefined; + if (keyColumns.includes(header.id)) { + eqcell = rrows[0].cells.filter((elt) => elt.headerId == header.id)[0]; + } else if (otherColumns.includes(header.id)) { + eqcell = { + headerId: header.id, + values: [], + }; + + for (const eqrow of rrows) { + const otherCell = eqrow.cells.filter( + (elt) => elt.headerId == header.id, + )[0]; + eqcell.headerId = otherCell.headerId; + eqcell.id = otherCell.id; + eqcell.clickable = otherCell.clickable; + eqcell.foldable = otherCell.foldable; + eqcell.selectable = otherCell.selectable; + + // TODO: can include test here for filder redudancy + for (const val of otherCell.values) { + if (filterRedudancy) { + /* empty */ + } + eqcell.values.push(val); + } + } + } + + // console.log(" --- H: ", header , "E: ", eqcell); + if (eqcell) { + row.cells.push(eqcell); + } + } + newTable.rows.push(row); + } + return newTable; + } + + filterByColumns(table, filters) { + const split = filters.split(":"); + const key = split[0]; + let values = split[1]; + if (values.includes(",")) { + values = values.split(","); + } else { + values = [values]; + } + + table.rows = table.rows.filter((row) => { + const eqcell = row.cells.filter((elt) => { + return elt.headerId == key; + })[0]; + const hasValue = eqcell.values.some((elt) => { + // return (elt.label && values.includes(elt.label)) || (elt.id && values.includes(elt.id)); + return ( + (elt.label && values.some((val) => elt.label.includes(val))) || + (elt.id && values.some((val) => elt.id.includes(val))) + ); + }); + return hasValue; + }); + return table; + } + + render() { + if (!this.table) { + return ""; + } + + // console.log("TABLE:", table); + return ( +
+ + {this.renderHeader(this.table)} + {this.renderRows(this.table)} +
+
+ ); + } + + renderHeader(table) { + return ( + + {table.header.map((cell) => { + return [ + cell.hide ? ( + "" + ) : ( + + {cell.label} + + ), + ]; + })} + + ); + } + + renderRows(table) { + // console.log("Render table: ", table); + return table.rows.map((row) => { + return [ + + {row.cells.map((superCell) => { + if (!this.headerMap) { + return ""; + } + const header = this.headerMap.get(superCell.headerId); + if (header.hide) { + return ""; + } + + let baseURL = header.baseURL; + // adding automatically the ending slash cause too many problem (eg base URL that are example.com/tototo?uri=) + // baseURL = baseURL ? addEndingSlash(baseURL) : ""; + baseURL = baseURL ? baseURL : ""; + + return ( + +
    + { + // Todo: this is where we can have a strategy for folding cells + superCell.values.map((cell) => { + let url = cell.url; + if (url && baseURL.length > 0) { + url = baseURL + url.replace(baseURL, ""); + } + + // create tags if any + let tag_span = ""; + if (cell.tags) { + tag_span = ( + + {cell.tags.join(", ")} + + ); + } + + return [ +
  • + {cell.url ? ( + + {tag_span} {cell.label} + + ) : ( +
    this.onCellClick(cell) + : () => "" + } + > + {tag_span} {cell.label} +
    + )} +
  • , + ]; + }) + } +
+ + ); + })} + , + ]; + }); + } + + onCellClick(cell) { + console.log("Cell clicked: ", cell); + } + + @Method() + async showOriginalTable() { + console.log(this.originalTable); + } + + @Method() + async showTable() { + console.log(this.table); + } + + @Method() + async showCurie() { + console.log(this.curie); + } + + @Method() + async showDBXrefs() { + console.log(dbxrefs.getDBXrefs()); + } +} diff --git a/src/components/go-ribbon-table/utils.tsx b/src/components/go-ribbon-table/utils.tsx new file mode 100644 index 0000000..0b95c5a --- /dev/null +++ b/src/components/go-ribbon-table/utils.tsx @@ -0,0 +1,182 @@ +/** + * For table that have cells with multiple values + * When merging rows based on such cells with multiple values, we have to fill with empty cells the columns that + * don't contain as many element, so we'll keep the ordering and link between merged rows + * Note: Should only be launched once on a table + * @param table + */ +export function addEmptyCells(table) { + for (const row of table.rows) { + let nbMax = 0; + for (const header of table.header) { + const eqcell = row.cells.filter((elt) => elt.headerId == header.id)[0]; + // console.log("R: ", row , "H: ", header , "E:", eqcell); + nbMax = Math.max(nbMax, eqcell.values.length); + } + for (const header of table.header) { + const eqcell = row.cells.filter((elt) => elt.headerId == header.id)[0]; + while (eqcell.values.length < nbMax) { + eqcell.values.push({ label: "" }); + } + } + } + return table; +} + +export function aspectShortLabel(txt) { + if (txt == "biological_process") { + return "P"; + } else if (txt == "molecular_activity" || txt == "molecular_function") { + return "F"; + } else if (txt == "cellular_component") { + return "C"; + } + return "U"; +} + +export function bioLinkToTable(data, getURL) { + const table = { + newTab: true, + header: [ + { + label: "Aspect", + id: "aspect", + description: + "High level category that gather multiple groups (eg ontology terms)", + }, + { + label: "Gene", + id: "gene", + description: "Gene or gene product", + baseURL: "http://amigo.geneontology.org/amigo/gene_product/", + }, + { + label: "Qualifier", + id: "qualifier", + description: + "Most often, describe if an entity (eg gene) has or has NOT a given feature (ontology term)", + }, + { + label: "Term", + id: "term", + description: + "Ontology term used to describe a feature of the gene product", + baseURL: "http://amigo.geneontology.org/amigo/term/", + }, + { + label: "Evidence", + id: "evidence", + description: + "Type of evidence supporting that a given gene product has a certain feature (ontology term)", + baseURL: + "http://www.ontobee.org/ontology/ECO?iri=http://purl.obolibrary.org/obo/", + }, + { + label: "With/From", + description: + "In case of sequence similarity, is used to state from which gene product the feature (ontology term) was inferred", + id: "with_from", + }, + { + label: "Reference", + description: + "Reference that support that a given gene product has a certain feature (ontology term)", + id: "reference", + }, + ], + + rows: [], + }; + + for (const subject of data) { + // console.log("S:", subject); + for (const assoc of subject.assocs) { + table.rows.push({ + cells: [ + { + headerId: "aspect", + values: [ + { + label: aspectShortLabel(assoc.object.category[0]), + }, + ], + }, + + { + headerId: "gene", + values: [ + { + label: assoc.subject.label, + url: assoc.subject.id, + }, + ], + }, + + { + headerId: "qualifier", + values: assoc.qualifiers + ? assoc.qualifiers.map((elt) => { + return { + label: elt, + }; + }) + : [{ label: "" }], + }, + + { + headerId: "term", + values: [ + { + label: assoc.object.label, + url: assoc.object.id, + tags: assoc.qualifiers ? assoc.qualifiers : undefined, + }, + ], + }, + + { + headerId: "evidence", + values: [ + { + label: assoc.evidence_type, + description: assoc.evidence_label, + url: assoc.evidence.replace(":", "_"), + }, + ], + }, + + { + headerId: "with_from", + values: assoc.evidence_with + ? assoc.evidence_with.map((elt) => { + return { + label: elt, + url: getURL( + elt.substring(0, elt.indexOf(":")), + undefined, + elt.substring(elt.indexOf(":") + 1), + ), + }; + }) + : [{ label: "" }], + }, + + { + headerId: "reference", + values: assoc.reference.map((elt) => { + return { + label: elt, + url: getURL( + elt.substring(0, elt.indexOf(":")), + undefined, + elt.substring(elt.indexOf(":") + 1), + ), + }; + }), + }, + ], + }); + } + } + return table; +} diff --git a/src/components/go-ribbon/go-ribbon.css b/src/components/go-ribbon/go-ribbon.css new file mode 100644 index 0000000..5d4e87f --- /dev/null +++ b/src/components/go-ribbon/go-ribbon.css @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/src/components/go-ribbon/go-ribbon.tsx b/src/components/go-ribbon/go-ribbon.tsx new file mode 100644 index 0000000..2153e84 --- /dev/null +++ b/src/components/go-ribbon/go-ribbon.tsx @@ -0,0 +1,525 @@ +import { Component, Element, h, Prop, Watch, State } from "@stencil/core"; + +import { IRibbonGroup } from "../../globals/models"; + +import { + COLOR_BY, + POSITION, + SELECTION, + FONT_CASE, + FONT_STYLE, +} from "../../globals/enums"; + +import { getCategory, getCategoryIdLabel, diffAssociations } from "./utils"; + +import { sameArray } from "../../globals/utils"; + +@Component({ + tag: "wc-go-ribbon", + styleUrl: "./go-ribbon.css", + shadow: false, +}) +export class GORibbon { + @Element() GORibbon; + + ribbonStrips: HTMLWcRibbonStripsElement; + ribbonTable: HTMLWcRibbonTableElement; + + @State() loadingTable = false; + + mockup = [ + { + subject: "UniProtKB:P04637", + slim: "GO:0003723", + assocs: [ + { + id: "556e6950726f744b420950303436333709545035330909474f3a3030303337333009504d49443a3136323133323132094944410909460943656c6c756c61722074756d6f7220616e746967656e20703533095035330970726f7465696e097461786f6e3a393630360932303137303132350943414641090909", + subject: { + id: "HGNC:11998", + iri: "http://identifiers.org/uniprot/P04637", + label: "TP53", + taxon: { + id: "NCBITaxon:9606", + iri: "http://purl.obolibrary.org/obo/NCBITaxon_9606", + label: "Homo sapiens", + }, + }, + object: { + id: "GO:0003730", + iri: "http://purl.obolibrary.org/obo/GO_0003730", + label: "mRNA 3'-UTR binding", + category: ["molecular_activity"], + }, + negated: false, + relation: null, + publications: [{ id: "UniProtKB" }], + provided_by: ["CAFA"], + reference: ["PMID:16213212"], + type: "protein", + evidence: "ECO:0000314", + evidence_label: "direct assay evidence used in manual assertion", + evidence_type: "IDA", + evidence_closure: [ + "ECO:0000352", + "ECO:0000000", + "ECO:0000002", + "ECO:0000006", + "ECO:0000314", + "ECO:0000269", + ], + evidence_closure_label: [ + "evidence used in manual assertion", + "evidence", + "direct assay evidence", + "experimental evidence", + "direct assay evidence used in manual assertion", + "experimental evidence used in manual assertion", + ], + evidence_subset_closure: ["ECO:0000006", "ECO:0000314"], + evidence_subset_closure_label: [ + "experimental evidence", + "direct assay evidence used in manual assertion", + ], + evidence_type_closure: [ + "evidence used in manual assertion", + "evidence", + "direct assay evidence", + "experimental evidence", + "direct assay evidence used in manual assertion", + "experimental evidence used in manual assertion", + ], + slim: ["GO:0003723"], + }, + ], + }, + ]; + + @Prop() filterReference = "PMID:,DOI:,GO_REF:,Reactome:"; + + @Prop() excludePB = true; + + @Prop() filterCrossAspect = true; + + @Prop() baseApiUrl = "https://api.geneontology.org/api/ontology/ribbon/"; + + @Prop() subjectBaseUrl: string = + "http://amigo.geneontology.org/amigo/gene_product/"; + @Prop() groupBaseUrl: string = "http://amigo.geneontology.org/amigo/term/"; + + @Prop() subset: string = "goslim_agr"; + + /** + * provide gene ids (e.g. RGD:620474,RGD:3889 or as a list ["RGD:620474", "RGD:3889"]) + */ + @Prop() subjects: string = undefined; + + @Prop() classLabels = "term,terms"; + @Prop() annotationLabels = "annotation,annotations"; + + /** + * Which value to base the cell color on + * 0 = class count + * 1 = annotation count + */ + @Prop() colorBy = COLOR_BY.ANNOTATION_COUNT; + + /** + * false = show a gradient of colors to indicate the value of a cell + * true = show only two colors (minColor; maxColor) to indicate the values of a cell + */ + @Prop() binaryColor = false; + @Prop() minColor = "255,255,255"; + @Prop() maxColor = "24,73,180"; + @Prop() maxHeatLevel = 48; + @Prop() groupMaxLabelSize = 60; + + /** + * Override of the category case + * 0 (default) = unchanged + * 1 = to lower case + * 2 = to upper case + */ + @Prop() categoryCase = FONT_CASE.LOWER_CASE; + + /** + * 0 = Normal + * 1 = Bold + */ + @Prop() categoryAllStyle = FONT_STYLE.NORMAL; + /** + * 0 = Normal + * 1 = Bold + */ + @Prop() categoryOtherStyle = FONT_STYLE.NORMAL; + + /** + * add a cell at the end of each row/subject to represent all annotations not mapped to a specific term + */ + @Prop() showOtherGroup = true; + + /** + * add a cell at the beginning of each row/subject to show all annotations + */ + @Prop() addCellAll: boolean = true; + + /** + * Position the subject label of each row + * 0 = None + * 1 = Left + * 2 = Right + * 3 = Bottom + */ + @Prop() subjectPosition = POSITION.LEFT; + @Prop() subjectUseTaxonIcon: boolean; + @Prop() subjectOpenNewTab: boolean = true; + @Prop() groupNewTab: boolean = true; + @Prop() groupClickable: boolean = true; + + /** + * Click handling of a cell. + * 0 = select only the cell (1 subject, 1 group) + * 1 = select the whole column (all subjects, 1 group) + */ + @Prop() selectionMode = SELECTION.CELL; + + /** + * If no value is provided, the ribbon will load without any group selected. + * If a value is provided, the ribbon will show the requested group as selected + * The value should be the id of the group to be selected + */ + @Prop() selected; + + /** + * If true, the ribbon will fire an event if a user click an empty cell + * If false, the ribbon will not fire the event on an empty cell + * Note: if selectionMode == SELECTION.COLUMN, then the event will trigger if at least one of the selected cells has annotations + */ + @Prop() fireEventOnEmptyCells = false; + + /** + * if provided, will override any value provided in subjects and subset + */ + @Prop() data: string; + + @State() selectedGroup: IRibbonGroup; + + onlyExperimental = false; + + /** + * Using this parameter, the table rows can bee grouped based on column ids + * A multiple step grouping is possible by using a ";" between groups + * The grouping applies before the ordering + * Example: hid-1,hid-3 OR hid-1,hid-3;hid-2 + * Note: if value is "", remove any grouping + */ + @Prop() groupBy: string = "term,qualifier"; + + /** + * This is used to sort the table depending of a column + * The column cells must be single values + * The ordering applies after the grouping + * Note: if value is "", remove any ordering + */ + @Prop() orderBy: string = "term"; + + /** + * Filter rows based on the presence of one or more values in a given column + * The filtering will be based on cell label or id + * Example: filter-by="evidence:ISS,ISO or multi-step filters: filter-by:evidence:ISS,ISO;term:xxx" + * Note: if value is "", remove any filtering + */ + @Prop() filterBy: string; + + /** + * Used to hide specific column of the table + */ + @Prop() hideColumns: string = "qualifier"; + + /** + * Must follow the appropriate JSON data model + * Can be given as either JSON or stringified JSON + */ + @State() tableData: string; + + /** + * Reading biolink data. This will trigger a render of the table as would changing data + */ + @State() bioLinkData: string; + + /** + * This method is automatically called whenever the value of "subjects" changes + * @param newValue a new subject is submitted (e.g. gene) + * @param oldValue old value of the subject (e.g. gene or genes) + */ + @Watch("subjects") + subjectsChanged(newValue, oldValue) { + if (newValue != oldValue) { + this.bioLinkData = undefined; + this.tableData = undefined; + } + } + + /** + * Check if a HTML element has a parent with provided id + * @param {} elt HTML element to check + * @param {*} id id to look in the parents of provided element + */ + hasParentElementId(elt, id) { + if (elt.id == id) { + return true; + } + if (!elt.parentElement) { + return false; + } + return this.hasParentElementId(elt.parentElement, id); + } + + /** + * Add listeners to the Ribbon strips + */ + componentWillLoad() { + // if(this.hasParentElementId()) + document.addEventListener("cellClick", this.onCellClick.bind(this)); + document.addEventListener("groupClick", this.onGroupClick.bind(this)); + } + + /** + * Remove listeners to the Ribbon strips + */ + disconnectedCallback() { + document.removeEventListener("cellClick", this.onCellClick); + document.removeEventListener("groupClick", this.onGroupClick); + } + + applyTableFilters(data, group) { + if (this.filterReference != "") { + data = this.applyFilterReference(data); + } + if (this.excludePB) { + data = this.applyFilterPB(data); + } + if (this.filterCrossAspect) { + data = this.applyFilterCrossAspect(data, group); + } + return data; + } + + applyFilterReference(data) { + const filters = this.filterReference.includes(",") + ? this.filterReference.split(",") + : [this.filterReference.trim()]; + + for (let i = 0; i < data.length; i++) { + data[i].assocs = data[i].assocs.filter((assoc) => { + assoc.reference = assoc.reference.filter((ref) => + filters.some((filter) => ref.includes(filter)), + ); + return assoc; + }); + } + return data; + } + + applyFilterPB(data) { + for (let i = 0; i < data.length; i++) { + data[i].assocs = data[i].assocs.filter( + (assoc) => assoc.object.id != "GO:0005515", + ); + } + return data; + } + + applyFilterCrossAspect(data, group) { + const aspect = getCategoryIdLabel( + group, + this.ribbonStrips.ribbonSummary.categories, + ); + for (let i = 0; i < data.length; i++) { + data[i].assocs = data[i].assocs.filter((assoc) => { + const cat = + assoc.object.category[0] == "molecular_activity" + ? "molecular_function" + : assoc.object.category[0]; + return aspect == undefined || cat == aspect[1]; + }); + } + return data; + } + + sameSelection(selection) { + if (!this.previousSelection) { + return false; + } + const sameGroupID = selection.group.id == this.previousSelection.group.id; + const sameGroupType = + selection.group.type == this.previousSelection.group.type; + const sameSubject = sameArray( + selection.subjects, + this.previousSelection.subjects, + ); + + return sameGroupID && sameGroupType && sameSubject; + } + + previousSelection = null; + onCellClick(e) { + console.log("Cell Clicked", e.detail); + this.loadingTable = true; + + const selection = e.detail; + const group = selection.group; + let group_ids = group.id; + let subject_ids = selection.subjects.map((elt) => elt.id); + + if (this.sameSelection(selection)) { + this.bioLinkData = undefined; + this.previousSelection = null; + this.loadingTable = false; + console.log("yep that's the same"); + return; + } + + this.previousSelection = selection; + + if (group.id == "all") { + group_ids = this.ribbonStrips.ribbonSummary.categories.map((elt) => { + return elt.id; + }); + group_ids = group_ids.join("&slim="); + } + + const goApiUrl = "https://api.geneontology.org/api/"; + subject_ids = subject_ids.join("&subject="); + const query = + goApiUrl + + "bioentityset/slimmer/function?slim=" + + group_ids + + "&subject=" + + subject_ids + + "&rows=-1"; + console.log("query: ", query); + + // fetch the json data + fetch(query) + .then((response) => { + return response.json(); + }) + .then((data) => { + data = this.applyTableFilters(data, group); + + if (group.type == "Other") { + const aspect = getCategory( + group, + this.ribbonStrips.ribbonSummary.categories, + ); + let terms = aspect.groups.filter((elt) => { + return elt.type == "Term"; + }); + terms = terms.map((elt) => { + return elt.id; + }); + terms = terms.join("&slim="); + + const query_terms = + goApiUrl + + "bioentityset/slimmer/function?slim=" + + terms + + "&subject=" + + subject_ids + + "&rows=-1"; + console.log("query_terms: ", query_terms); + + // fetch the json data + fetch(query_terms) + .then((response_terms) => { + return response_terms.json(); + }) + .then((data_terms) => { + data_terms = this.applyTableFilters(data_terms, group); + + let concat_assocs = []; + for (const array of data_terms) { + concat_assocs = concat_assocs.concat(array.assocs); + } + + const other_assocs = diffAssociations( + data[0].assocs, + concat_assocs, + ); + data[0].assocs = other_assocs; + this.loadingTable = false; + this.bioLinkData = JSON.stringify(data); + }); + } else { + this.loadingTable = false; + this.bioLinkData = JSON.stringify(data); + } + }); + } + + onGroupClick(e) { + console.log("Group Clicked", e.detail); + } + + render() { + return [ + (this.ribbonStrips = el)} + base-api-url={this.baseApiUrl} + subject-base-url={this.subjectBaseUrl} + group-base-url={this.groupBaseUrl} + add-cell-all={this.addCellAll} + binary-color={this.binaryColor} + color-by={this.colorBy} + min-color={this.minColor} + max-color={this.maxColor} + max-heat-level={this.maxHeatLevel} + annotation-labels={this.annotationLabels} + class-labels={this.classLabels} + data={this.data} + category-case={this.categoryCase} + category-all-style={this.categoryAllStyle} + categoryOtherStyle={this.categoryOtherStyle} + group-max-label-size={this.groupMaxLabelSize} + group-new-tab={this.groupNewTab} + group-clickable={this.groupClickable} + fire-event-on-empty-cells={this.fireEventOnEmptyCells} + subjects={this.subjects} + subject-open-new-tab={this.subjectOpenNewTab} + subject-position={this.subjectPosition} + subject-use-taxon-icon={this.subjectUseTaxonIcon} + selection-mode={this.selectionMode} + selected={this.selected} + subset={this.subset} + show-other-group={this.showOtherGroup} + />, + + (this.subjects && this.subjects.length > 0) || this.data ? ( +
+ Cell color indicative of annotation volume +
+ ) : ( + "" + ), + + this.loadingTable ? ( + + ) : ( + (this.ribbonTable = el)} + base-api-url={this.baseApiUrl} + subject-base-url={this.subjectBaseUrl} + group-base-url={this.groupBaseUrl} + data={this.tableData} + bio-link-data={this.bioLinkData} + group-by={this.groupBy} + order-by={this.orderBy} + filter-by={this.filterBy} + hide-columns={this.hideColumns} + /> + ), + ]; + } +} diff --git a/src/components/go-ribbon/index.html b/src/components/go-ribbon/index.html new file mode 100644 index 0000000..f835480 --- /dev/null +++ b/src/components/go-ribbon/index.html @@ -0,0 +1,46 @@ + + + + + + Web Component GO Ribbon + + + + + + +
+ Add gene to the GO ribbon:
(can use gene symbol or model organism ID such as RGD:3889)
+ + + +
+ + + + + + + + diff --git a/src/components/go-ribbon/readme.md b/src/components/go-ribbon/readme.md new file mode 100644 index 0000000..0105faa --- /dev/null +++ b/src/components/go-ribbon/readme.md @@ -0,0 +1,67 @@ +# wc-go-ribbon + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ----------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------- | ----------------------------------------------------- | +| `addCellAll` | `add-cell-all` | add a cell at the beginning of each row/subject to show all annotations | `boolean` | `true` | +| `annotationLabels` | `annotation-labels` | | `string` | `"annotation,annotations"` | +| `baseApiUrl` | `base-api-url` | | `string` | `"https://api.geneontology.org/api/ontology/ribbon/"` | +| `binaryColor` | `binary-color` | false = show a gradient of colors to indicate the value of a cell true = show only two colors (minColor; maxColor) to indicate the values of a cell | `boolean` | `false` | +| `categoryAllStyle` | `category-all-style` | 0 = Normal 1 = Bold | `number` | `FONT_STYLE.NORMAL` | +| `categoryCase` | `category-case` | Override of the category case 0 (default) = unchanged 1 = to lower case 2 = to upper case | `number` | `FONT_CASE.LOWER_CASE` | +| `categoryOtherStyle` | `category-other-style` | 0 = Normal 1 = Bold | `number` | `FONT_STYLE.NORMAL` | +| `classLabels` | `class-labels` | | `string` | `"term,terms"` | +| `colorBy` | `color-by` | Which value to base the cell color on 0 = class count 1 = annotation count | `number` | `COLOR_BY.ANNOTATION_COUNT` | +| `data` | `data` | if provided, will override any value provided in subjects and subset | `string` | `undefined` | +| `excludePB` | `exclude-p-b` | | `boolean` | `true` | +| `filterBy` | `filter-by` | Filter rows based on the presence of one or more values in a given column The filtering will be based on cell label or id Example: filter-by="evidence:ISS,ISO or multi-step filters: filter-by:evidence:ISS,ISO;term:xxx" Note: if value is "", remove any filtering | `string` | `undefined` | +| `filterCrossAspect` | `filter-cross-aspect` | | `boolean` | `true` | +| `filterReference` | `filter-reference` | | `string` | `"PMID:,DOI:,GO_REF:,Reactome:"` | +| `fireEventOnEmptyCells` | `fire-event-on-empty-cells` | If true, the ribbon will fire an event if a user click an empty cell If false, the ribbon will not fire the event on an empty cell Note: if selectionMode == SELECTION.COLUMN, then the event will trigger if at least one of the selected cells has annotations | `boolean` | `false` | +| `groupBaseUrl` | `group-base-url` | | `string` | `"http://amigo.geneontology.org/amigo/term/"` | +| `groupBy` | `group-by` | Using this parameter, the table rows can bee grouped based on column ids A multiple step grouping is possible by using a ";" between groups The grouping applies before the ordering Example: hid-1,hid-3 OR hid-1,hid-3;hid-2 Note: if value is "", remove any grouping | `string` | `"term,qualifier"` | +| `groupClickable` | `group-clickable` | | `boolean` | `true` | +| `groupMaxLabelSize` | `group-max-label-size` | | `number` | `60` | +| `groupNewTab` | `group-new-tab` | | `boolean` | `true` | +| `hideColumns` | `hide-columns` | Used to hide specific column of the table | `string` | `"qualifier"` | +| `maxColor` | `max-color` | | `string` | `"24,73,180"` | +| `maxHeatLevel` | `max-heat-level` | | `number` | `48` | +| `minColor` | `min-color` | | `string` | `"255,255,255"` | +| `orderBy` | `order-by` | This is used to sort the table depending of a column The column cells must be single values The ordering applies after the grouping Note: if value is "", remove any ordering | `string` | `"term"` | +| `selected` | `selected` | If no value is provided, the ribbon will load without any group selected. If a value is provided, the ribbon will show the requested group as selected The value should be the id of the group to be selected | `any` | `undefined` | +| `selectionMode` | `selection-mode` | Click handling of a cell. 0 = select only the cell (1 subject, 1 group) 1 = select the whole column (all subjects, 1 group) | `number` | `SELECTION.CELL` | +| `showOtherGroup` | `show-other-group` | add a cell at the end of each row/subject to represent all annotations not mapped to a specific term | `boolean` | `true` | +| `subjectBaseUrl` | `subject-base-url` | | `string` | `"http://amigo.geneontology.org/amigo/gene_product/"` | +| `subjectOpenNewTab` | `subject-open-new-tab` | | `boolean` | `true` | +| `subjectPosition` | `subject-position` | Position the subject label of each row 0 = None 1 = Left 2 = Right 3 = Bottom | `number` | `POSITION.LEFT` | +| `subjectUseTaxonIcon` | `subject-use-taxon-icon` | | `boolean` | `undefined` | +| `subjects` | `subjects` | provide gene ids (e.g. RGD:620474,RGD:3889 or as a list ["RGD:620474", "RGD:3889"]) | `string` | `undefined` | +| `subset` | `subset` | | `string` | `"goslim_agr"` | + +## Dependencies + +### Depends on + +- [wc-ribbon-strips](../go-ribbon-strips) +- [wc-spinner](../go-spinner) +- [wc-ribbon-table](../go-ribbon-table) + +### Graph + +```mermaid +graph TD; + wc-go-ribbon --> wc-ribbon-strips + wc-go-ribbon --> wc-spinner + wc-go-ribbon --> wc-ribbon-table + wc-ribbon-strips --> wc-spinner + wc-ribbon-strips --> wc-ribbon-subject + wc-ribbon-strips --> wc-ribbon-cell + style wc-go-ribbon fill:#f9f,stroke:#333,stroke-width:4px +``` + +--- + +_Built with [StencilJS](https://stenciljs.com/)_ diff --git a/src/components/go-ribbon/utils.ts b/src/components/go-ribbon/utils.ts new file mode 100644 index 0000000..413aee4 --- /dev/null +++ b/src/components/go-ribbon/utils.ts @@ -0,0 +1,67 @@ +/** + * Return the category object for a given group + * @param {*} group group object (eg ontology term) + */ +export function getCategory(group, categories) { + const cat = categories.filter((cat) => { + return cat.groups.some((gp) => gp.id == group.id); + }); + return cat.length > 0 ? cat[0] : undefined; +} + +/** + * Return the category [id, label] for a given group + * @param {*} group group object (eg ontology term) + */ +export function getCategoryIdLabel(group, categories) { + const cat = categories.filter((cat) => { + return cat.groups.some((gp) => gp.id == group.id); + }); + return cat.length > 0 ? [cat[0].id, cat[0].label] : undefined; +} + +export function associationKey(assoc) { + if (assoc.qualifier) { + return ( + assoc.subject.id + + "@" + + assoc.object.id + + "@" + + assoc.negated + + "@" + + assoc.qualifier.join("-") + ); + } + return assoc.subject.id + "@" + assoc.object.id + "@" + assoc.negated; +} + +export function fullAssociationKey(assoc) { + const key = + associationKey(assoc) + + "@" + + assoc.evidence_type + + "@" + + assoc.provided_by + + "@" + + assoc.reference.join("#"); + return key; +} + +export function diffAssociations(assocs_all, assocs_exclude) { + const list = []; + for (const assoc of assocs_all) { + let found = false; + const key_all = fullAssociationKey(assoc); + for (const exclude of assocs_exclude) { + const key_exclude = fullAssociationKey(exclude); + if (key_all == key_exclude) { + found = true; + break; + } + } + if (!found) { + list.push(assoc); + } + } + return list; +} diff --git a/src/components/go-spinner/go-spinner.css b/src/components/go-spinner/go-spinner.css new file mode 100644 index 0000000..696d80c --- /dev/null +++ b/src/components/go-spinner/go-spinner.css @@ -0,0 +1,629 @@ +:host { + display: block; +} + +.lds-circle { + display: inline-block; + transform: translateZ(1px); +} +.lds-circle > div { + display: inline-block; + width: var(--spinner-width, 64px); + height: var(--spinner-height, 64px); + margin: 8px; + border-radius: 50%; + background: var(--color-primary, #fff); + animation: lds-circle 2.4s cubic-bezier(0, 0.2, 0.8, 1) infinite; +} +@keyframes lds-circle { + 0%, + 100% { + animation-timing-function: cubic-bezier(0.5, 0, 1, 0.5); + } + 0% { + transform: rotateY(0deg); + } + 50% { + transform: rotateY(1800deg); + animation-timing-function: cubic-bezier(0, 0.5, 0.5, 1); + } + 100% { + transform: rotateY(3600deg); + } +} + +.lds-dual-ring { + display: inline-block; + width: 80px; + height: 80px; +} +.lds-dual-ring:after { + content: " "; + display: block; + width: 64px; + height: 64px; + margin: 8px; + border-radius: 50%; + border: 6px solid var(--color-primary, #fff); + border-color: var(--color-primary, #fff) transparent + var(--color-primary, #fff) transparent; + animation: lds-dual-ring 1.2s linear infinite; +} +@keyframes lds-dual-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.lds-facebook { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-facebook div { + display: inline-block; + position: absolute; + left: 8px; + width: 16px; + background: var(--color-primary, #fff); + animation: lds-facebook 1.2s cubic-bezier(0, 0.5, 0.5, 1) infinite; +} +.lds-facebook div:nth-child(1) { + left: 8px; + animation-delay: -0.24s; +} +.lds-facebook div:nth-child(2) { + left: 32px; + animation-delay: -0.12s; +} +.lds-facebook div:nth-child(3) { + left: 56px; + animation-delay: 0; +} +@keyframes lds-facebook { + 0% { + top: 8px; + height: 64px; + } + 50%, + 100% { + top: 24px; + height: 32px; + } +} + +.lds-heart { + display: inline-block; + position: relative; + width: 80px; + height: 80px; + transform: rotate(45deg); + transform-origin: 40px 40px; +} +.lds-heart div { + top: 32px; + left: 32px; + position: absolute; + width: 32px; + height: 32px; + background: var(--color-primary, #fff); + animation: lds-heart 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1); +} +.lds-heart div:after, +.lds-heart div:before { + content: " "; + position: absolute; + display: block; + width: 32px; + height: 32px; + background: var(--color-primary, #fff); +} +.lds-heart div:before { + left: -24px; + border-radius: 50% 0 0 50%; +} +.lds-heart div:after { + top: -24px; + border-radius: 50% 50% 0 0; +} +@keyframes lds-heart { + 0% { + transform: scale(0.95); + } + 5% { + transform: scale(1.1); + } + 39% { + transform: scale(0.85); + } + 45% { + transform: scale(1); + } + 60% { + transform: scale(0.95); + } + 100% { + transform: scale(0.9); + } +} + +.lds-ring { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-ring div { + box-sizing: border-box; + display: block; + position: absolute; + width: 64px; + height: 64px; + margin: 8px; + border: 8px solid var(--color-primary, #fff); + border-radius: 50%; + animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + border-color: var(--color-primary, #fff) transparent transparent transparent; +} +.lds-ring div:nth-child(1) { + animation-delay: -0.45s; +} +.lds-ring div:nth-child(2) { + animation-delay: -0.3s; +} +.lds-ring div:nth-child(3) { + animation-delay: -0.15s; +} +@keyframes lds-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.lds-roller { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-roller div { + animation: lds-roller 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + transform-origin: 40px 40px; +} +.lds-roller div:after { + content: " "; + display: block; + position: absolute; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--color-primary, #fff); + margin: -4px 0 0 -4px; +} +.lds-roller div:nth-child(1) { + animation-delay: -0.036s; +} +.lds-roller div:nth-child(1):after { + top: 63px; + left: 63px; +} +.lds-roller div:nth-child(2) { + animation-delay: -0.072s; +} +.lds-roller div:nth-child(2):after { + top: 68px; + left: 56px; +} +.lds-roller div:nth-child(3) { + animation-delay: -0.108s; +} +.lds-roller div:nth-child(3):after { + top: 71px; + left: 48px; +} +.lds-roller div:nth-child(4) { + animation-delay: -0.144s; +} +.lds-roller div:nth-child(4):after { + top: 72px; + left: 40px; +} +.lds-roller div:nth-child(5) { + animation-delay: -0.18s; +} +.lds-roller div:nth-child(5):after { + top: 71px; + left: 32px; +} +.lds-roller div:nth-child(6) { + animation-delay: -0.216s; +} +.lds-roller div:nth-child(6):after { + top: 68px; + left: 24px; +} +.lds-roller div:nth-child(7) { + animation-delay: -0.252s; +} +.lds-roller div:nth-child(7):after { + top: 63px; + left: 17px; +} +.lds-roller div:nth-child(8) { + animation-delay: -0.288s; +} +.lds-roller div:nth-child(8):after { + top: 56px; + left: 12px; +} +@keyframes lds-roller { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.lds-default { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-default div { + position: absolute; + width: 6px; + height: 6px; + background: var(--color-primary, #fff); + border-radius: 50%; + animation: lds-default 1.2s linear infinite; +} +.lds-default div:nth-child(1) { + animation-delay: 0s; + top: 37px; + left: 66px; +} +.lds-default div:nth-child(2) { + animation-delay: -0.1s; + top: 22px; + left: 62px; +} +.lds-default div:nth-child(3) { + animation-delay: -0.2s; + top: 11px; + left: 52px; +} +.lds-default div:nth-child(4) { + animation-delay: -0.3s; + top: 7px; + left: 37px; +} +.lds-default div:nth-child(5) { + animation-delay: -0.4s; + top: 11px; + left: 22px; +} +.lds-default div:nth-child(6) { + animation-delay: -0.5s; + top: 22px; + left: 11px; +} +.lds-default div:nth-child(7) { + animation-delay: -0.6s; + top: 37px; + left: 7px; +} +.lds-default div:nth-child(8) { + animation-delay: -0.7s; + top: 52px; + left: 11px; +} +.lds-default div:nth-child(9) { + animation-delay: -0.8s; + top: 62px; + left: 22px; +} +.lds-default div:nth-child(10) { + animation-delay: -0.9s; + top: 66px; + left: 37px; +} +.lds-default div:nth-child(11) { + animation-delay: -1s; + top: 62px; + left: 52px; +} +.lds-default div:nth-child(12) { + animation-delay: -1.1s; + top: 52px; + left: 62px; +} +@keyframes lds-default { + 0%, + 20%, + 80%, + 100% { + transform: scale(1); + } + 50% { + transform: scale(1.5); + } +} + +.lds-ellipsis { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-ellipsis div { + position: absolute; + top: 33px; + width: 13px; + height: 13px; + border-radius: 50%; + background: var(--color-primary, #fff); + animation-timing-function: cubic-bezier(0, 1, 1, 0); +} +.lds-ellipsis div:nth-child(1) { + left: 8px; + animation: lds-ellipsis1 0.6s infinite; +} +.lds-ellipsis div:nth-child(2) { + left: 8px; + animation: lds-ellipsis2 0.6s infinite; +} +.lds-ellipsis div:nth-child(3) { + left: 32px; + animation: lds-ellipsis2 0.6s infinite; +} +.lds-ellipsis div:nth-child(4) { + left: 56px; + animation: lds-ellipsis3 0.6s infinite; +} +@keyframes lds-ellipsis1 { + 0% { + transform: scale(0); + } + 100% { + transform: scale(1); + } +} +@keyframes lds-ellipsis3 { + 0% { + transform: scale(1); + } + 100% { + transform: scale(0); + } +} +@keyframes lds-ellipsis2 { + 0% { + transform: translate(0, 0); + } + 100% { + transform: translate(24px, 0); + } +} + +.lds-grid { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-grid div { + position: absolute; + width: 16px; + height: 16px; + border-radius: 50%; + background: var(--color-primary, #fff); + animation: lds-grid 1.2s linear infinite; +} +.lds-grid div:nth-child(1) { + top: 8px; + left: 8px; + animation-delay: 0s; +} +.lds-grid div:nth-child(2) { + top: 8px; + left: 32px; + animation-delay: -0.4s; +} +.lds-grid div:nth-child(3) { + top: 8px; + left: 56px; + animation-delay: -0.8s; +} +.lds-grid div:nth-child(4) { + top: 32px; + left: 8px; + animation-delay: -0.4s; +} +.lds-grid div:nth-child(5) { + top: 32px; + left: 32px; + animation-delay: -0.8s; +} +.lds-grid div:nth-child(6) { + top: 32px; + left: 56px; + animation-delay: -1.2s; +} +.lds-grid div:nth-child(7) { + top: 56px; + left: 8px; + animation-delay: -0.8s; +} +.lds-grid div:nth-child(8) { + top: 56px; + left: 32px; + animation-delay: -1.2s; +} +.lds-grid div:nth-child(9) { + top: 56px; + left: 56px; + animation-delay: -1.6s; +} +@keyframes lds-grid { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} + +.lds-hourglass { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-hourglass:after { + content: " "; + display: block; + border-radius: 50%; + width: 0; + height: 0; + margin: 8px; + box-sizing: border-box; + border: 32px solid var(--color-primary, #fff); + border-color: var(--color-primary, #fff) transparent + var(--color-primary, #fff) transparent; + animation: lds-hourglass 1.2s infinite; +} +@keyframes lds-hourglass { + 0% { + transform: rotate(0); + animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19); + } + 50% { + transform: rotate(900deg); + animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + } + 100% { + transform: rotate(1800deg); + } +} + +.lds-ripple { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-ripple div { + position: absolute; + border: 4px solid var(--color-primary, #fff); + opacity: 1; + border-radius: 50%; + animation: lds-ripple 1s cubic-bezier(0, 0.2, 0.8, 1) infinite; +} +.lds-ripple div:nth-child(2) { + animation-delay: -0.5s; +} +@keyframes lds-ripple { + 0% { + top: 36px; + left: 36px; + width: 0; + height: 0; + opacity: 1; + } + 100% { + top: 0px; + left: 0px; + width: 72px; + height: 72px; + opacity: 0; + } +} + +.lds-spinner { + color: official; + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-spinner div { + transform-origin: 40px 40px; + animation: lds-spinner 1.2s linear infinite; +} +.lds-spinner div:after { + content: " "; + display: block; + position: absolute; + top: 3px; + left: 37px; + width: 6px; + height: 18px; + border-radius: 20%; + background: var(--color-primary, #fff); +} +.lds-spinner div:nth-child(1) { + transform: rotate(0deg); + animation-delay: -1.1s; +} +.lds-spinner div:nth-child(2) { + transform: rotate(30deg); + animation-delay: -1s; +} +.lds-spinner div:nth-child(3) { + transform: rotate(60deg); + animation-delay: -0.9s; +} +.lds-spinner div:nth-child(4) { + transform: rotate(90deg); + animation-delay: -0.8s; +} +.lds-spinner div:nth-child(5) { + transform: rotate(120deg); + animation-delay: -0.7s; +} +.lds-spinner div:nth-child(6) { + transform: rotate(150deg); + animation-delay: -0.6s; +} +.lds-spinner div:nth-child(7) { + transform: rotate(180deg); + animation-delay: -0.5s; +} +.lds-spinner div:nth-child(8) { + transform: rotate(210deg); + animation-delay: -0.4s; +} +.lds-spinner div:nth-child(9) { + transform: rotate(240deg); + animation-delay: -0.3s; +} +.lds-spinner div:nth-child(10) { + transform: rotate(270deg); + animation-delay: -0.2s; +} +.lds-spinner div:nth-child(11) { + transform: rotate(300deg); + animation-delay: -0.1s; +} +.lds-spinner div:nth-child(12) { + transform: rotate(330deg); + animation-delay: 0s; +} +@keyframes lds-spinner { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} diff --git a/src/components/go-spinner/go-spinner.tsx b/src/components/go-spinner/go-spinner.tsx new file mode 100644 index 0000000..a5c849e --- /dev/null +++ b/src/components/go-spinner/go-spinner.tsx @@ -0,0 +1,190 @@ +import { Component, Prop, h, Element, Watch } from "@stencil/core"; + +import { SPINNER_STYLE } from "../../globals/enums"; + +@Component({ + tag: "wc-spinner", + styleUrl: "go-spinner.css", + shadow: false, +}) +export class Spinner { + @Element() elt; + + // export const SPINNER_STYLE = { "CIRCLE": "circle", "DUAL_RING" : "dual-ring", "FACEBOOK": "facebook", "HEART" : "heart", "RING" : "ring", "ROLLER" : "roller", "DEFAULT" : "default", "ELLIPSIS": "ellipsis", "GRID": "grid", "HOURGLASS": "hourglass", "RIPPLE": "ripple", "SPINNER": "spinner" }; + + /** + * Define the style of the spinner. + * Accepted values: default, spinner, circle, ring, dual-ring, roller, ellipsis, grid, hourglass, ripple, facebook, heart + */ + @Prop() spinnerStyle: string; + + /** + * Define the size of the spinner (TO DO). + */ + @Prop() spinnerSize: number; + + @Watch("spinnerSize") + spinnerSizeChanged(newSize, oldSize) { + if (newSize != oldSize) { + this.elt.style.setProperty("--spinner-width", newSize); + this.elt.style.setProperty("--spinner-height", newSize); + } + } + + /** + * Define the color of the spinner. + * This parameter is optional and will override any declared CSS variable + */ + @Prop() spinnerColor: string; + + @Watch("spinnerColor") + spinnerColorChanged(newColor, oldColor) { + if (newColor != oldColor) { + this.elt.style.setProperty("--color-primary", newColor); + } + } + + getSpinnerColor() { + return this.elt.style.getProperty("--color-primary"); + } + + componentDidLoad() { + if (this.spinnerColor) { + this.spinnerColorChanged(this.spinnerColor, null); + } + // if(this.spinnerSize) { + // this.spinnerSizeChanged(this.spinnerSize, null); + // } + } + + render() { + switch (this.spinnerStyle) { + case SPINNER_STYLE.CIRCLE: + return ( +
+
+
+ ); + case SPINNER_STYLE.DUAL_RING: + return
; + case SPINNER_STYLE.FACEBOOK: + return ( +
+
+
+
+
+ ); + case SPINNER_STYLE.HEART: + return ( +
+
+
+ ); + case SPINNER_STYLE.RING: + return ( +
+
+
+
+
+
+ ); + case SPINNER_STYLE.ROLLER: + return ( +
+
+
+
+
+
+
+
+
+
+ ); + case SPINNER_STYLE.DEFAULT: + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); + case SPINNER_STYLE.ELLIPSIS: + return ( +
+
+
+
+
+
+ ); + case SPINNER_STYLE.GRID: + return ( +
+
+
+
+
+
+
+
+
+
+
+ ); + case SPINNER_STYLE.HOURGLASS: + return
; + case SPINNER_STYLE.RIPPLE: + return ( +
+
+
+
+ ); + case SPINNER_STYLE.SPINNER: + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); + } + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); + } +} diff --git a/src/components/go-spinner/index.html b/src/components/go-spinner/index.html new file mode 100644 index 0000000..6ea131a --- /dev/null +++ b/src/components/go-spinner/index.html @@ -0,0 +1,27 @@ + + + + + + Web Component Spinner + + + + + + + + + + diff --git a/src/components/go-spinner/readme.md b/src/components/go-spinner/readme.md new file mode 100644 index 0000000..984d701 --- /dev/null +++ b/src/components/go-spinner/readme.md @@ -0,0 +1,31 @@ +# my-component + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| -------------- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ----------- | +| `spinnerColor` | `spinner-color` | Define the color of the spinner. This parameter is optional and will override any declared CSS variable | `string` | `undefined` | +| `spinnerSize` | `spinner-size` | Define the size of the spinner (TO DO). | `number` | `undefined` | +| `spinnerStyle` | `spinner-style` | Define the style of the spinner. Accepted values: default, spinner, circle, ring, dual-ring, roller, ellipsis, grid, hourglass, ripple, facebook, heart | `string` | `undefined` | + +## Dependencies + +### Used by + +- [wc-go-ribbon](../go-ribbon) +- [wc-ribbon-strips](../go-ribbon-strips) + +### Graph + +```mermaid +graph TD; + wc-go-ribbon --> wc-spinner + wc-ribbon-strips --> wc-spinner + style wc-spinner fill:#f9f,stroke:#333,stroke-width:4px +``` + +--- + +_Built with [StencilJS](https://stenciljs.com/)_ diff --git a/src/globals/enums.ts b/src/globals/enums.ts new file mode 100644 index 0000000..5da4adb --- /dev/null +++ b/src/globals/enums.ts @@ -0,0 +1,43 @@ +// Enum to control what is the input count value for the color +export const COLOR_BY = { CLASS_COUNT: 0, ANNOTATION_COUNT: 1 }; + +// Enum to control the label position of each entity +export const POSITION = { NONE: 0, LEFT: 1, RIGHT: 2, BOTTOM: 3 }; + +// Enum to control the selection mode of a ribbon: selecting the cell or the column +export const SELECTION = { CELL: 0, COLUMN: 1 }; + +export const EXP_CODES = [ + "EXP", + "IDA", + "IPI", + "IMP", + "IGI", + "IEP", + "HTP", + "HDA", + "HMP", + "HGI", + "HEP", +]; + +export const CELL_TYPES = { ALL: "All", TERM: "Term", OTHER: "Other" }; + +export const FONT_CASE = { UNCHANGED: 0, LOWER_CASE: 1, UPPER_CASE: 2 }; + +export const FONT_STYLE = { NORMAL: 0, BOLD: 1 }; + +export const SPINNER_STYLE = { + CIRCLE: "circle", + DUAL_RING: "dual-ring", + FACEBOOK: "facebook", + HEART: "heart", + RING: "ring", + ROLLER: "roller", + DEFAULT: "default", + ELLIPSIS: "ellipsis", + GRID: "grid", + HOURGLASS: "hourglass", + RIPPLE: "ripple", + SPINNER: "spinner", +}; diff --git a/src/globals/models.ts b/src/globals/models.ts new file mode 100644 index 0000000..31a74b9 --- /dev/null +++ b/src/globals/models.ts @@ -0,0 +1,83 @@ +export interface IRibbonGroup { + id: string; + label: string; + description: string; + type: string; +} + +export interface IRibbonCategory { + id: string; + label: string; + description: string; + groups: IRibbonGroup[]; +} + +export interface IRibbonSubject { + id: string; + label: string; + taxon_id: string; + taxon_label: string; + nb_classes: number; + nb_annotations: number; + groups: Record; +} + +export interface IRibbonModel { + categories: IRibbonCategory[]; + subjects: IRibbonSubject[]; +} + +export interface IRibbonCellEvent { + subjects: IRibbonSubject[]; + group: IRibbonGroup; +} + +export interface IRibbonGroupEvent { + subjects: IRibbonSubject[]; + category: IRibbonCategory; + group: IRibbonGroup; +} + +export interface IRibbonCellClick extends IRibbonCellEvent { + selected: boolean[]; +} + +export interface ISuperCell { + id?: string; + headerId: string; + clickable?: boolean; + selectable?: boolean; + foldable?: boolean; + values: ICell[]; +} + +export interface ICell { + id?: string; + label: string; + description?: string; + url?: string; + icon?: string; + tags?: string[]; + clickable?: boolean; + selectable?: boolean; +} + +export interface IHeaderCell extends ICell { + sortable?: boolean; + searchable?: boolean; + baseURL?: string; // if defined, convert cell URL to use this baseURL + foldListThr?: number; // if defined, fold the cells that have more than X items + hide?: boolean; // if true, won't show the column that would be considered only for treatment (eg grouping) +} + +export interface IRow { + foldable?: boolean; + // id?: string; + cells: ISuperCell[]; +} + +export interface ITable { + header: IHeaderCell[]; + rows: IRow[]; + newTab?: boolean; +} diff --git a/src/globals/utils.ts b/src/globals/utils.ts new file mode 100644 index 0000000..929f698 --- /dev/null +++ b/src/globals/utils.ts @@ -0,0 +1,12 @@ +export function sameArray(array1, array2) { + if (array1.length !== array2.length) { + return false; + } + + for (let i = 0; i < array1.length; i++) { + if (array1[i] !== array2[i]) { + return false; + } + } + return true; +} diff --git a/src/index.html b/src/index.html index 16463a5..3f1fefc 100644 --- a/src/index.html +++ b/src/index.html @@ -11,5 +11,26 @@ - + + + diff --git a/stencil.config.ts b/stencil.config.ts index da483f8..233aaf0 100644 --- a/stencil.config.ts +++ b/stencil.config.ts @@ -1,7 +1,9 @@ import { Config } from "@stencil/core"; +import { sass } from "@stencil/sass"; export const config: Config = { namespace: "web-components", + plugins: [sass()], outputTargets: [ { type: "dist", @@ -18,6 +20,7 @@ export const config: Config = { { type: "www", serviceWorker: null, // disable service workers + copy: [{ src: "**/*.html" }, { src: "*.css" }], }, ], testing: {