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`,