diff --git a/CHANGELOG.md b/CHANGELOG.md index a7f2392e0..ebba77916 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ [Semantic Versioning](https://semver.org/) +## [6.1.0] - 2024-05-16 +### New feature +- New track builder `UnmodeledTrackBuilder` that merges unmodeled tracks from instances + ## [6.0.14] - 2024-04-02 ### Dependency update - rcsb-charts v0.2.23 diff --git a/package.json b/package.json index eb1af4008..d4fa343a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rcsb/rcsb-saguaro-app", - "version": "6.0.14", + "version": "6.1.0", "description": "RCSB 1D Saguaro Web App", "main": "build/app.js", "files": [ diff --git a/src/RcsbAnnotationConfig/RcsbAnnotationConfig.ac.json b/src/RcsbAnnotationConfig/RcsbAnnotationConfig.ac.json index 8a12967c5..052831c35 100644 --- a/src/RcsbAnnotationConfig/RcsbAnnotationConfig.ac.json +++ b/src/RcsbAnnotationConfig/RcsbAnnotationConfig.ac.json @@ -88,6 +88,7 @@ ], "instance_order": [ "MA_QA_METRIC_LOCAL_TYPE_PLDDT", + "UNMODELED", "SECONDARY_STRUCTURE", "UNOBSERVED", "ZERO_OCCUPANCY_RESIDUE_XYZ", @@ -122,6 +123,14 @@ "DISORDER_BINDING" ], "config": [{ + "type": "UNMODELED", + "display": "block-area", + "color": { + "colors": ["#BBB", "#999"], + "thresholds": [0.999] + }, + "title": "UNMODELED" + },{ "type": "TRANSIT_PEPTIDE", "color": "#189f3e", "display": "block", diff --git a/src/RcsbFvExamples/EntitySummaryFv.ts b/src/RcsbFvExamples/EntitySummaryFv.ts index ae9633a65..c386a51a9 100644 --- a/src/RcsbFvExamples/EntitySummaryFv.ts +++ b/src/RcsbFvExamples/EntitySummaryFv.ts @@ -1,5 +1,9 @@ import {buildEntitySummaryFv} from "../RcsbFvWeb/RcsbFvBuilder"; +import {getJsonFromUrl, onLoad} from "./utils/events"; -buildEntitySummaryFv("pfv", "select", "AF_AFQ8WZ42F1_1").then((module)=>{ - console.log(module) -}); \ No newline at end of file +onLoad(()=>{ + const args: {entityId:string} = getJsonFromUrl().entityId ? getJsonFromUrl() : {entityId:"3HBX_1"}; + buildEntitySummaryFv("pfv", "select", args.entityId).then((module)=>{ + console.log(module) + }); +}); diff --git a/src/RcsbFvExamples/index.html b/src/RcsbFvExamples/index.html index f5d226860..392479209 100644 --- a/src/RcsbFvExamples/index.html +++ b/src/RcsbFvExamples/index.html @@ -10,7 +10,7 @@
-
+
diff --git a/src/RcsbFvExamples/utils/events.ts b/src/RcsbFvExamples/utils/events.ts new file mode 100644 index 000000000..36ce45164 --- /dev/null +++ b/src/RcsbFvExamples/utils/events.ts @@ -0,0 +1,17 @@ + +export function getJsonFromUrl() { + const url = location.search; + var query = url.substring(1); + var result: any = {}; + query.split("&").forEach(function(part) { + var item = part.split("="); + result[item[0]] = decodeURIComponent(item[1]); + }); + return result; +} + +export function onLoad(f:()=>void){ + document.addEventListener("DOMContentLoaded", function(event) { + f(); + }); +} \ No newline at end of file diff --git a/src/RcsbFvWeb/RcsbFvModule/RcsbFvEntity.ts b/src/RcsbFvWeb/RcsbFvModule/RcsbFvEntity.ts index 710b5feee..78a3d4988 100644 --- a/src/RcsbFvWeb/RcsbFvModule/RcsbFvEntity.ts +++ b/src/RcsbFvWeb/RcsbFvModule/RcsbFvEntity.ts @@ -13,9 +13,13 @@ import {CollectAlignmentInterface} from "../../RcsbCollectTools/AlignmentCollect import {Assertions} from "../../RcsbUtils/Helpers/Assertions"; import assertDefined = Assertions.assertDefined; import {TagDelimiter} from "@rcsb/rcsb-api-tools/build/RcsbUtils/TagDelimiter"; +import {RcsbFvBoardConfigInterface} from "@rcsb/rcsb-saguaro/lib/RcsbFv/RcsbFvConfig/RcsbFvConfigInterface"; +import {addUnmodeledTrackBuilder, UnmodeledTrackBuilder} from "../../RcsbUtils/TrackGenerators/UnmodeledTrackBuilder"; export class RcsbFvEntity extends RcsbFvAbstractModule { + private readonly unmodeledTrackBuilder = new UnmodeledTrackBuilder(); + protected async protectedBuild(): Promise { const buildConfig: RcsbFvModuleBuildInterface = this.buildConfig; assertDefined(buildConfig.entityId); @@ -35,7 +39,13 @@ export class RcsbFvEntity extends RcsbFvAbstractModule { titleSuffix: this.titleSuffix.bind(this), filters: buildConfig.additionalConfig?.filters, annotationProcessing:buildConfig.additionalConfig?.annotationProcessing, - externalTrackBuilder: buildConfig.additionalConfig?.externalTrackBuilder + rcsbContext:{ + entityId: buildConfig.entityId + }, + externalTrackBuilder: addUnmodeledTrackBuilder( + this.unmodeledTrackBuilder, + buildConfig.additionalConfig?.externalTrackBuilder + ) }; const annotationsFeatures: AnnotationFeatures[] = await this.annotationCollector.collect(annotationsRequestContext); await this.buildAnnotationsTrack(annotationsRequestContext,annotationsFeatures); @@ -45,6 +55,13 @@ export class RcsbFvEntity extends RcsbFvAbstractModule { return void 0; } + protected async getBoardConfig(): Promise { + return { + ... this.boardConfigData, + tooltipGenerator: this.unmodeledTrackBuilder.getTooltip() + }; + } + protected concatAlignmentAndAnnotationTracks(): void { const buildConfig: RcsbFvModuleBuildInterface = this.buildConfig; this.rowConfigData = !buildConfig.additionalConfig?.hideAlignments ? diff --git a/src/RcsbFvWeb/RcsbFvModule/RcsbFvUniprotEntity.ts b/src/RcsbFvWeb/RcsbFvModule/RcsbFvUniprotEntity.ts index 338303243..053e4333a 100644 --- a/src/RcsbFvWeb/RcsbFvModule/RcsbFvUniprotEntity.ts +++ b/src/RcsbFvWeb/RcsbFvModule/RcsbFvUniprotEntity.ts @@ -15,9 +15,13 @@ import {CollectAlignmentInterface} from "../../RcsbCollectTools/AlignmentCollect import {Assertions} from "../../RcsbUtils/Helpers/Assertions"; import assertDefined = Assertions.assertDefined; import {TagDelimiter} from "@rcsb/rcsb-api-tools/build/RcsbUtils/TagDelimiter"; +import {addUnmodeledTrackBuilder, UnmodeledTrackBuilder} from "../../RcsbUtils/TrackGenerators/UnmodeledTrackBuilder"; +import {RcsbFvBoardConfigInterface} from "@rcsb/rcsb-saguaro/lib/RcsbFv/RcsbFvConfig/RcsbFvConfigInterface"; export class RcsbFvUniprotEntity extends RcsbFvAbstractModule { + private readonly unmodeledTrackBuilder = new UnmodeledTrackBuilder(); + protected async protectedBuild(): Promise { const buildConfig: RcsbFvModuleBuildInterface = this.buildConfig; const upAcc: string | undefined = buildConfig.upAcc; @@ -48,7 +52,13 @@ export class RcsbFvUniprotEntity extends RcsbFvAbstractModule { filters:additionalConfig?.filters instanceof Array ? additionalConfig.filters.concat(filters) : filters, titleSuffix: this.titleSuffix.bind(this), annotationProcessing:buildConfig.additionalConfig?.annotationProcessing, - externalTrackBuilder: buildConfig.additionalConfig?.externalTrackBuilder + rcsbContext:{ + entityId: buildConfig.entityId + }, + externalTrackBuilder: addUnmodeledTrackBuilder( + this.unmodeledTrackBuilder, + buildConfig.additionalConfig?.externalTrackBuilder + ) }; const annotationsFeatures: AnnotationFeatures[] = await this.annotationCollector.collect(annotationsRequestContext); await this.buildAnnotationsTrack(annotationsRequestContext,annotationsFeatures); @@ -59,6 +69,13 @@ export class RcsbFvUniprotEntity extends RcsbFvAbstractModule { } + protected async getBoardConfig(): Promise { + return { + ... this.boardConfigData, + tooltipGenerator: this.unmodeledTrackBuilder.getTooltip() + }; + } + protected concatAlignmentAndAnnotationTracks(): void { this.rowConfigData = [this.referenceTrack].concat(this.alignmentTracks).concat(this.annotationTracks); } diff --git a/src/RcsbUtils/TrackGenerators/UnmodeledTrackBuilder.ts b/src/RcsbUtils/TrackGenerators/UnmodeledTrackBuilder.ts new file mode 100644 index 000000000..87d4ce3a7 --- /dev/null +++ b/src/RcsbUtils/TrackGenerators/UnmodeledTrackBuilder.ts @@ -0,0 +1,141 @@ +import { + AnnotationFeatures, + Feature, + FeaturePosition, + Source, + Type +} from "@rcsb/rcsb-api-tools/build/RcsbGraphQL/Types/Borrego/GqlTypes"; +import {PolymerEntityInstanceInterface} from "../../RcsbCollectTools/DataCollectors/PolymerEntityInstancesCollector"; +import {range} from "lodash"; +import {rcsbRequestCtxManager} from "../../RcsbRequest/RcsbRequestContextManager"; +import {RcsbFvTooltipInterface} from "@rcsb/rcsb-saguaro/lib/RcsbFv/RcsbFvTooltip/RcsbFvTooltipInterface"; +import {RcsbFvTooltip} from "../../RcsbFvWeb/RcsbFvTooltip/RcsbFvTooltip"; +import { + RcsbFvTrackDataAnnotationInterface +} from "../../RcsbFvWeb/RcsbFvFactories/RcsbFvTrackFactory/RcsbFvTrackDataAnnotationInterface"; +import {ExternalTrackBuilderInterface} from "../../RcsbCollectTools/FeatureTools/ExternalTrackBuilderInterface"; + +export function addUnmodeledTrackBuilder( + unmodeledTrackBuilder: UnmodeledTrackBuilder, + externalTrackBuilder?: ExternalTrackBuilderInterface +): ExternalTrackBuilderInterface { + const trackBuilder = async (data: { + annotations: Array; + rcsbContext?: Partial + }): Promise> => { + return [ + ... await externalTrackBuilder?.filterFeatures?.(data) ?? [], + ... await unmodeledTrackBuilder.filterFeatures(data) + ]; + } + return { + ... externalTrackBuilder, + filterFeatures: async (data) => { + externalTrackBuilder?.filterFeatures?.(data); + return trackBuilder({ + annotations: await externalTrackBuilder?.filterFeatures?.(data) ?? data.annotations, + rcsbContext: data.rcsbContext + }); + } + } +} + +export class UnmodeledTrackBuilder { + + private readonly unmodeledDescription: Map = new Map(); + public async filterFeatures(data: { + annotations: Array; + rcsbContext?: Partial; + }): Promise> { + if (!data.rcsbContext) + return data.annotations; + return [ + ...await this.buildUnobserved(data.annotations, data.rcsbContext), + ...filterUnobserved(data.annotations) + ]; + } + + public getTooltip(): RcsbFvTooltipInterface{ + return new UnobservedToolTip(this.unmodeledDescription); + } + + private async buildUnobserved(annotations: Array, rcsbContext: Partial): Promise> { + const nInstances = (await rcsbRequestCtxManager.getEntityProperties(rcsbContext.entityId ?? "none"))[0].instances.length; + const featurePositions = Array.from(annotations.filter( + ann => ann.features?.filter( + f => f?.type == Type.UnobservedResidueXyz + )?.length ?? 0 > 0 + ).map( + ann => ann.features?.filter( + (f): f is Feature => f?.type == Type.UnobservedResidueXyz + ) ?? [] + ).flat().map( + feature => feature.feature_positions?.filter( + (p): p is FeaturePosition => typeof p != "undefined" + ) ?? [] + ).flat().map( + p => p.beg_seq_id && p.end_seq_id ? range(p.beg_seq_id,p.end_seq_id+1) : [] + ).flat().reduce( + (map,idx) => map.set(idx, (map.get(idx) ?? 0) + 1), + new Map + ).entries()).map(([idx,unobserved])=>{ + this.unmodeledDescription.set(`${rcsbContext.entityId}:${idx}`, [unobserved, nInstances]); + return { + beg_seq_id: idx, + values: [Math.round(unobserved / nInstances * 100)/100] + } + }); + return [{ + source: Source.PdbEntity, + target_id: rcsbContext.entityId, + features: [{ + type: UNMODELED as Type, + feature_positions: featurePositions + }] + }] + } + +} + +class UnobservedToolTip implements RcsbFvTooltipInterface { + + private readonly regularTooltip: RcsbFvTooltip = new RcsbFvTooltip(); + private readonly trackDescription: Map; + + constructor(trackDescription: Map) { + this.trackDescription = trackDescription; + } + + showTooltip(d: RcsbFvTrackDataAnnotationInterface): HTMLElement | undefined { + return this.regularTooltip.showTooltip(d); + } + + showTooltipDescription(d: RcsbFvTrackDataAnnotationInterface): HTMLElement | undefined { + if(d.title == UNMODELED as Type) + return this.overloadTooltipDescription(d); + return this.regularTooltip.showTooltipDescription(d); + } + + overloadTooltipDescription(d: RcsbFvTrackDataAnnotationInterface): HTMLElement | undefined { + if(!this.trackDescription.has(`${d.sourceId}:${d.begin}`)) + return undefined; + const tooltipDescriptionDiv = document.createElement<"div">("div"); + const [unobserved, total] = this.trackDescription.get(`${d.sourceId}:${d.begin}`) ?? [0,0]; + tooltipDescriptionDiv.append(`Unmodeled in ${unobserved} of ${total} chains`); + return tooltipDescriptionDiv; + } + +} + +function filterUnobserved(annotations: Array): Array { + return annotations.map(ann=>{ + return { + ...ann, + features: ann.features?.filter( + f=> f?.type != Type.UnobservedResidueXyz && f?.type != Type.UnobservedAtomXyz + ) + } + }); +} + +const UNMODELED = "UNMODELED"; diff --git a/webpack.server.dev.config.js b/webpack.server.dev.config.js index 71e18468e..47dbe3c0a 100644 --- a/webpack.server.dev.config.js +++ b/webpack.server.dev.config.js @@ -86,7 +86,8 @@ const server = { }, devServer: { compress: true, - port: 9000, + allowedHosts: "all", + port: 9000 }, plugins: Object.keys(entries).map(key=>new HtmlWebpackPlugin({ filename:`${key}.html`,