Browse Source

label improvements for various properties

- asa
- rci
- geometry quality
- desnity fit
- interactions
Alexander Rose 5 years ago
parent
commit
0e135c4645

+ 16 - 2
src/mol-model-props/computed/interactions/interactions.ts

@@ -5,7 +5,7 @@
  */
 
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
-import { Structure, Unit } from '../../../mol-model/structure';
+import { Structure, Unit, Bond } from '../../../mol-model/structure';
 import { Features, FeaturesBuilder } from './features';
 import { ValenceModelProvider } from '../valence-model';
 import { InteractionsIntraContacts, InteractionsInterContacts, FeatureType, interactionTypeLabel } from './common';
@@ -24,6 +24,7 @@ import { DataLocation } from '../../../mol-model/location';
 import { CentroidHelper } from '../../../mol-math/geometry/centroid-helper';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { DataLoci } from '../../../mol-model/loci';
+import { bondLabel, LabelGranularity } from '../../../mol-theme/label';
 
 export { Interactions }
 
@@ -103,7 +104,20 @@ namespace Interactions {
     }
 
     export function getLabel(interactions: Interactions, elements: ReadonlyArray<Element>) {
-        return elements.length > 0 ? _label(interactions, elements[0]) : ''
+        const element = elements[0]
+        if (element === undefined) return ''
+        const { unitA, indexA, unitB, indexB } = element
+        const { unitsFeatures } = interactions
+        const { members: mA, offsets: oA } = unitsFeatures.get(unitA.id)
+        const { members: mB, offsets: oB } = unitsFeatures.get(unitB.id)
+        const options = { granularity: 'element' as LabelGranularity }
+        if (oA[indexA + 1] - oA[indexA] > 1 || oB[indexB + 1] - oB[indexB] > 1) {
+            options.granularity = 'residue'
+        }
+        return [
+            _label(interactions, element),
+            bondLabel(Bond.Location(unitA, mA[oA[indexA]], unitB, mB[oB[indexB]]), options)
+        ].join('</br>')
     }
 }
 

+ 2 - 2
src/mol-model-props/rcsb/representations/validation-report-clashes.ts

@@ -99,7 +99,7 @@ function getIntraClashLabel(unit: Unit.Atomic, clashes: IntraUnitClashes, elemen
     const dist = distance[idx].toFixed(2)
 
     return [
-        `RCSB Clash id: ${id[idx]} | Magnitude: ${mag} \u212B | Distance: ${dist} \u212B`,
+        `Clash id: ${id[idx]} | Magnitude: ${mag} \u212B | Distance: ${dist} \u212B`,
         bondLabel(Bond.Location(unit, clashes.a[idx], unit, clashes.b[idx]))
     ].join('</br>')
 }
@@ -211,7 +211,7 @@ function getInterClashLabel(clashes: InterUnitClashes, elements: number[]) {
     const dist = c.props.distance.toFixed(2)
 
     return [
-        `RCSB Clash id: ${c.props.id} | Magnitude: ${mag} \u212B | Distance: ${dist} \u212B`,
+        `Clash id: ${c.props.id} | Magnitude: ${mag} \u212B | Distance: ${dist} \u212B`,
         bondLabel(Bond.Location(c.unitA, c.indexA, c.unitB, c.indexB))
     ].join('</br>')
 }

+ 2 - 4
src/mol-model-props/rcsb/themes/density-fit.ts

@@ -32,12 +32,10 @@ export function DensityFitColorTheme(ctx: ThemeDataContext, props: {}): ColorThe
 
     const validationReport = ctx.structure && ValidationReportProvider.get(ctx.structure.models[0])
     const contextHash = validationReport?.version
-
-    const rsrz = validationReport?.value?.rsrz
-    const rscc = validationReport?.value?.rscc
     const model = ctx.structure?.models[0]
 
-    if (rsrz && rscc && model) {
+    if (validationReport?.value && model) {
+        const { rsrz, rscc } = validationReport.value
         const residueIndex = model.atomicHierarchy.residueAtomSegments.index
         color = (location: Location): Color => {
             if (StructureElement.Location.is(location) && location.unit.model === model) {

+ 73 - 33
src/mol-model-props/rcsb/validation-report.ts

@@ -47,22 +47,20 @@ interface ValidationReport {
      * Set of bond outliers
      */
     bondOutliers: {
-        index: Map<ElementIndex, number>
+        index: Map<ElementIndex, number[]>
         data: {
-            atomA: ElementIndex, atomB: ElementIndex
-            zScore: number, mean: number, mindiff: number,
-            numobs: number, obsval: number, stdev: number
+            tag: string, atomA: ElementIndex, atomB: ElementIndex
+            z: number, mean: number, obs: number, stdev: number
         }[]
     }
     /**
      * Set of angle outliers
      */
     angleOutliers: {
-        index: Map<ElementIndex, number>
+        index: Map<ElementIndex, number[]>
         data: {
-            atomA: ElementIndex, atomB: ElementIndex, atomC: ElementIndex,
-            zScore: number, mean: number, mindiff: number,
-            numobs: number, obsval: number, stdev: number
+            tag: string, atomA: ElementIndex, atomB: ElementIndex, atomC: ElementIndex,
+            z: number, mean: number, obs: number, stdev: number
         }[]
     }
 
@@ -309,15 +307,27 @@ function hasAttr(a: NamedNodeMap, name: string, value: string) {
 
 function getMogInfo(a: NamedNodeMap) {
     return {
-        zScore: parseFloat(getItem(a, 'Zscore')),
         mean: parseFloat(getItem(a, 'mean')),
-        mindiff: parseFloat(getItem(a, 'mindiff')),
-        numobs: parseInt(getItem(a, 'numobs')),
-        obsval: parseFloat(getItem(a, 'obsval')),
+        obs: parseFloat(getItem(a, 'obsval')),
         stdev: parseFloat(getItem(a, 'stdev')),
+        z: parseFloat(getItem(a, 'Zscore')),
     }
 }
 
+function getMolInfo(a: NamedNodeMap) {
+    return {
+        mean: parseFloat(getItem(a, 'mean')),
+        obs: parseFloat(getItem(a, 'obs')),
+        stdev: parseFloat(getItem(a, 'stdev')),
+        z: parseInt(getItem(a, 'z')),
+    }
+}
+
+function addIndex(index: number, element: ElementIndex, map: Map<ElementIndex, number[]>) {
+    if (map.has(element)) map.get(element)!.push(index)
+    else map.set(element, [index])
+}
+
 function ClashesBuilder(elementsCount: number) {
     const aIndices: ElementIndex[] = []
     const bIndices: ElementIndex[] = []
@@ -363,11 +373,11 @@ function parseValidationReportXml(xml: XMLDocument, model: Model): ValidationRep
     const geometryIssues = new Map<ResidueIndex, Set<string>>()
 
     const bondOutliers = {
-        index: new Map<ElementIndex, number>(),
+        index: new Map<ElementIndex, number[]>(),
         data: [] as ValidationReport['bondOutliers']['data']
     }
     const angleOutliers = {
-        index: new Map<ElementIndex, number>(),
+        index: new Map<ElementIndex, number[]>(),
         data: [] as ValidationReport['angleOutliers']['data']
     }
 
@@ -418,11 +428,37 @@ function parseValidationReportXml(xml: XMLDocument, model: Model): ValidationRep
         const issues = new Set<string>()
 
         if (isPolymer) {
-            const angleOutliers = g.getElementsByTagName('angle-outlier')
-            if (angleOutliers.length) issues.add('angle-outlier')
+            const molBondOutliers = g.getElementsByTagName('bond-outlier')
+            if (molBondOutliers.length) issues.add('bond-outlier')
+
+            for (let j = 0, jl = molBondOutliers.length; j < jl; ++j) {
+                const bo = molBondOutliers[j].attributes
+                const idx = bondOutliers.data.length
+                const atomA = index.findAtomOnResidue(rI, getItem(bo, 'atom0'))
+                const atomB = index.findAtomOnResidue(rI, getItem(bo, 'atom1'))
+                addIndex(idx, atomA, bondOutliers.index)
+                addIndex(idx, atomB, bondOutliers.index)
+                bondOutliers.data.push({
+                    tag: 'bond-outlier', atomA, atomB, ...getMolInfo(bo)
+                })
+            }
+
+            const molAngleOutliers = g.getElementsByTagName('angle-outlier')
+            if (molAngleOutliers.length) issues.add('angle-outlier')
 
-            const bondOutliers = g.getElementsByTagName('bond-outlier')
-            if (bondOutliers.length) issues.add('bond-outlier')
+            for (let j = 0, jl = molAngleOutliers.length; j < jl; ++j) {
+                const ao = molAngleOutliers[j].attributes
+                const idx = bondOutliers.data.length
+                const atomA = index.findAtomOnResidue(rI, getItem(ao, 'atom0'))
+                const atomB = index.findAtomOnResidue(rI, getItem(ao, 'atom1'))
+                const atomC = index.findAtomOnResidue(rI, getItem(ao, 'atom2'))
+                addIndex(idx, atomA, angleOutliers.index)
+                addIndex(idx, atomB, angleOutliers.index)
+                addIndex(idx, atomC, angleOutliers.index)
+                angleOutliers.data.push({
+                    tag: 'angle-outlier', atomA, atomB, atomC, ...getMolInfo(ao)
+                })
+            }
 
             const planeOutliers = g.getElementsByTagName('plane-outlier')
             if (planeOutliers.length) issues.add('plane-outlier')
@@ -432,33 +468,37 @@ function parseValidationReportXml(xml: XMLDocument, model: Model): ValidationRep
             if (hasAttr(ga, 'RNApucker', 'outlier')) issues.add('RNApucker-outlier')
         } else {
             const mogBondOutliers = g.getElementsByTagName('mog-bond-outlier')
-            if (mogBondOutliers.length) issues.add('mogul-bond-outlier')
-
-            const mogAngleOutliers = g.getElementsByTagName('mog-angle-outlier')
-            if (mogAngleOutliers.length) issues.add('mogul-angle-outlier')
+            if (mogBondOutliers.length) issues.add('mog-bond-outlier')
 
             for (let j = 0, jl = mogBondOutliers.length; j < jl; ++j) {
-                const mbo = mogBondOutliers[ j ].attributes
+                const mbo = mogBondOutliers[j].attributes
                 const atoms = getItem(mbo, 'atoms').split(',')
                 const idx = bondOutliers.data.length
                 const atomA = index.findAtomOnResidue(rI, atoms[0])
                 const atomB = index.findAtomOnResidue(rI, atoms[1])
-                bondOutliers.index.set(atomA, idx)
-                bondOutliers.index.set(atomB, idx)
-                bondOutliers.data.push({ atomA, atomB, ...getMogInfo(mbo) })
+                addIndex(idx, atomA, bondOutliers.index)
+                addIndex(idx, atomB, bondOutliers.index)
+                bondOutliers.data.push({
+                    tag: 'mog-bond-outlier', atomA, atomB, ...getMogInfo(mbo)
+                })
             }
 
+            const mogAngleOutliers = g.getElementsByTagName('mog-angle-outlier')
+            if (mogAngleOutliers.length) issues.add('mog-angle-outlier')
+
             for (let j = 0, jl = mogAngleOutliers.length; j < jl; ++j) {
-                const mao = mogAngleOutliers[ j ].attributes
+                const mao = mogAngleOutliers[j].attributes
                 const atoms = getItem(mao, 'atoms').split(',')
                 const idx = angleOutliers.data.length
                 const atomA = index.findAtomOnResidue(rI, atoms[0])
                 const atomB = index.findAtomOnResidue(rI, atoms[1])
-                const atomC = index.findAtomOnResidue(rI, atoms[1])
-                angleOutliers.index.set(atomA, idx)
-                angleOutliers.index.set(atomB, idx)
-                angleOutliers.index.set(atomC, idx)
-                angleOutliers.data.push({ atomA, atomB, atomC, ...getMogInfo(mao) })
+                const atomC = index.findAtomOnResidue(rI, atoms[2])
+                addIndex(idx, atomA, angleOutliers.index)
+                addIndex(idx, atomB, angleOutliers.index)
+                addIndex(idx, atomC, angleOutliers.index)
+                angleOutliers.data.push({
+                    tag: 'mog-angle-outlier', atomA, atomB, atomC, ...getMogInfo(mao)
+                })
             }
         }
 
@@ -466,7 +506,7 @@ function parseValidationReportXml(xml: XMLDocument, model: Model): ValidationRep
         if (clashes.length) issues.add('clash')
 
         for (let j = 0, jl = clashes.length; j < jl; ++j) {
-            const ca = clashes[ j ].attributes
+            const ca = clashes[j].attributes
             const id = parseInt(getItem(ca, 'cid'))
             const magnitude = parseFloat(getItem(ca, 'clashmag'))
             const distance = parseFloat(getItem(ca, 'dist'))

+ 37 - 35
src/mol-plugin/behavior/dynamic/custom-props/computed/accessible-surface-area.ts

@@ -21,40 +21,7 @@ export const AccessibleSurfaceArea = PluginBehavior.create<{ autoAttach: boolean
 
         private label = (loci: Loci): string | undefined => {
             if (!this.params.showTooltip) return
-
-            const { granularity } = this.ctx.interactivity.props
-            if (granularity === 'element' || granularity === 'elementInstances') return
-
-            if(loci.kind === 'element-loci') {
-                if (loci.elements.length === 0) return;
-
-                const accessibleSurfaceArea = AccessibleSurfaceAreaProvider.get(loci.structure).value
-                if (!accessibleSurfaceArea) return;
-
-                const { getSerialIndex } = loci.structure.root.serialMapping
-                const { area, serialResidueIndex } = accessibleSurfaceArea
-                const seen = new Set<number>()
-                let cummulativeArea = 0
-
-                for (const { indices, unit } of loci.elements) {
-                    OrderedSet.forEach(indices, idx => {
-                        const rSI = serialResidueIndex[getSerialIndex(unit, unit.elements[idx])]
-                        if (rSI !== -1 && !seen.has(rSI)) {
-                            cummulativeArea += area[rSI]
-                            seen.add(rSI)
-                        }
-                    })
-                }
-                if (seen.size === 0) return
-
-                return `Accessible Surface Area: ${cummulativeArea.toFixed(2)} \u212B<sup>3</sup>`;
-
-            } else if(loci.kind === 'structure-loci') {
-                const accessibleSurfaceArea = AccessibleSurfaceAreaProvider.get(loci.structure).value
-                if (!accessibleSurfaceArea) return;
-
-                return `Accessible Surface Area: ${arraySum(accessibleSurfaceArea.area).toFixed(2)} \u212B<sup>3</sup>`;
-            }
+            return accessibleSurfaceAreaLabel(loci)
         }
 
         update(p: { autoAttach: boolean, showTooltip: boolean }) {
@@ -84,4 +51,39 @@ export const AccessibleSurfaceArea = PluginBehavior.create<{ autoAttach: boolean
         autoAttach: PD.Boolean(false),
         showTooltip: PD.Boolean(true)
     })
-});
+});
+
+function accessibleSurfaceAreaLabel(loci: Loci): string | undefined {
+    if(loci.kind === 'element-loci') {
+        if (loci.elements.length === 0) return;
+
+        const accessibleSurfaceArea = AccessibleSurfaceAreaProvider.get(loci.structure).value
+        if (!accessibleSurfaceArea) return;
+
+        const { getSerialIndex } = loci.structure.root.serialMapping
+        const { area, serialResidueIndex } = accessibleSurfaceArea
+        const seen = new Set<number>()
+        let cummulativeArea = 0
+
+        for (const { indices, unit } of loci.elements) {
+            const { elements } = unit
+            OrderedSet.forEach(indices, idx => {
+                const rSI = serialResidueIndex[getSerialIndex(unit, elements[idx])]
+                if (rSI !== -1 && !seen.has(rSI)) {
+                    cummulativeArea += area[rSI]
+                    seen.add(rSI)
+                }
+            })
+        }
+        if (seen.size === 0) return
+        const residueCount = `<small>(${seen.size} ${seen.size > 1 ? 'Residues' : 'Residue'})</small>`
+
+        return `Accessible Surface Area ${residueCount}: ${cummulativeArea.toFixed(2)} \u212B<sup>3</sup>`;
+
+    } else if(loci.kind === 'structure-loci') {
+        const accessibleSurfaceArea = AccessibleSurfaceAreaProvider.get(loci.structure).value
+        if (!accessibleSurfaceArea) return;
+
+        return `Accessible Surface Area <small>(Whole Structure)</small>: ${arraySum(accessibleSurfaceArea.area).toFixed(2)} \u212B<sup>3</sup>`;
+    }
+}

+ 180 - 16
src/mol-plugin/behavior/dynamic/custom-props/rcsb/validation-report.ts

@@ -13,6 +13,7 @@ import { Loci } from '../../../../../mol-model/loci';
 import { OrderedSet } from '../../../../../mol-data/int';
 import { ClashesRepresentationProvider } from '../../../../../mol-model-props/rcsb/representations/validation-report-clashes';
 import { DensityFitColorThemeProvider } from '../../../../../mol-model-props/rcsb/themes/density-fit';
+import { cantorPairing } from '../../../../../mol-data/util';
 
 const Tag = ValidationReport.Tag
 
@@ -23,10 +24,19 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean,
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
         private provider = ValidationReportProvider
 
+        private label = (loci: Loci): string | undefined => {
+            if (!this.params.showTooltip) return
+            return [
+                geometryQualityLabel(loci),
+                densityFitLabel(loci),
+                randomCoilIndexLabel(loci)
+            ].filter(l => !!l).join('</br>')
+        }
+
         register(): void {
             this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
 
-            this.ctx.lociLabels.addProvider(geometryQualityLabelProvider);
+            this.ctx.lociLabels.addProvider(this.label);
 
             this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add(Tag.DensityFit, DensityFitColorThemeProvider)
             this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.add(Tag.GeometryQuality, GeometryQualityColorThemeProvider)
@@ -46,7 +56,7 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean,
         unregister() {
             this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
 
-            this.ctx.lociLabels.removeProvider(geometryQualityLabelProvider);
+            this.ctx.lociLabels.removeProvider(this.label);
 
             this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove(Tag.DensityFit)
             this.ctx.structureRepresentation.themeCtx.colorThemeRegistry.remove(Tag.GeometryQuality)
@@ -62,22 +72,176 @@ export const RCSBValidationReport = PluginBehavior.create<{ autoAttach: boolean,
     })
 });
 
-function geometryQualityLabelProvider(loci: Loci): string | undefined {
-    switch (loci.kind) {
-        case 'element-loci':
-            if (loci.elements.length === 0) return;
-            const e = loci.elements[0];
-            const geometryIssues = ValidationReportProvider.get(e.unit.model).value?.geometryIssues
-            if (!geometryIssues) return
+function geometryQualityLabel(loci: Loci): string | undefined {
+    if (loci.kind === 'element-loci') {
+        if (loci.elements.length === 0) return
+
+        if (loci.elements.length === 1 && OrderedSet.size(loci.elements[0].indices) === 1) {
+            const { unit, indices } = loci.elements[0]
+
+            const validationReport = ValidationReportProvider.get(unit.model).value
+            if (!validationReport) return
+
+            const { bondOutliers, angleOutliers } = validationReport
+            const eI = unit.elements[OrderedSet.start(indices)]
+            const issues = new Set<string>()
+
+            const bonds = bondOutliers.index.get(eI)
+            if (bonds) bonds.forEach(b => issues.add(bondOutliers.data[b].tag))
+
+            const angles = angleOutliers.index.get(eI)
+            if (angles) angles.forEach(a => issues.add(angleOutliers.data[a].tag))
+
+            if (issues.size === 0) {
+                return `RCSB Geometry Quality <small>(1 Atom)</small>: no issues`;
+            }
+
+            const summary: string[] = []
+            issues.forEach(name => summary.push(name))
+            return `Geometry Quality <small>(1 Atom)</small>: ${summary.join(', ')}`;
+        }
+
+        let hasValidationReport = false
+        const seen = new Set<number>()
+        const cummulativeIssues = new Map<string, number>()
+
+        for (const { indices, unit } of loci.elements) {
+            const validationReport = ValidationReportProvider.get(unit.model).value
+            if (!validationReport) continue
+            hasValidationReport = true
+
+            const { geometryIssues } = validationReport
+            const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index
+            const { elements } = unit
+
+            OrderedSet.forEach(indices, idx => {
+                const eI = elements[idx]
+
+                const rI = residueIndex[eI]
+                const residueKey = cantorPairing(rI, unit.id)
+                if (!seen.has(residueKey)) {
+                    const issues = geometryIssues.get(rI)
+                    if (issues) {
+                        issues.forEach(name => {
+                            const count = cummulativeIssues.get(name) || 0
+                            cummulativeIssues.set(name, count + 1)
+                        })
+                    }
+                    seen.add(residueKey)
+                }
+            })
+        }
+
+        if (!hasValidationReport) return
+
+        const residueCount = `<small>(${seen.size} ${seen.size > 1 ? 'Residues' : 'Residue'})</small>`
+
+        if (cummulativeIssues.size === 0) {
+            return `Geometry Quality ${residueCount}: no issues`;
+        }
+
+        const summary: string[] = []
+        cummulativeIssues.forEach((count, name) => {
+            summary.push(`${name}${count > 1 ? ` \u00D7 ${count}` : ''}`)
+        })
+        return `Geometry Quality ${residueCount}: ${summary.join(', ')}`;
+    }
+}
+
+function densityFitLabel(loci: Loci): string | undefined {
+    if (loci.kind === 'element-loci') {
+        if (loci.elements.length === 0) return;
+
+        const seen = new Set<number>()
+        const rsrzSeen = new Set<number>()
+        const rsccSeen = new Set<number>()
+        let rsrzSum = 0
+        let rsccSum = 0
+
+        for (const { indices, unit } of loci.elements) {
+            const validationReport = ValidationReportProvider.get(unit.model).value
+            if (!validationReport) continue
+
+            const { rsrz, rscc } = validationReport
+            const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index
+            const { elements } = unit
+
+            OrderedSet.forEach(indices, idx => {
+                const eI = elements[idx]
+                const rI = residueIndex[eI]
+
+                const residueKey = cantorPairing(rI, unit.id)
+                if (!seen.has(residueKey)) {
+                    const rsrzValue = rsrz.get(rI)
+                    const rsccValue = rscc.get(rI)
+                    if (rsrzValue !== undefined) {
+                        rsrzSum += rsrzValue
+                        rsrzSeen.add(residueKey)
+                    } else if (rsccValue !== undefined) {
+                        rsccSum += rsccValue
+                        rsccSeen.add(residueKey)
+                    }
+                    seen.add(residueKey)
+                }
+            })
+        }
+
+        if (seen.size === 0) return
+
+        const summary: string[] = []
+
+        if (rsrzSeen.size) {
+            const rsrzCount = `<small>(${rsrzSeen.size} ${rsrzSeen.size > 1 ? 'Residues' : 'Residue'})</small>`
+            const rsrzAvg = rsrzSum / rsrzSeen.size
+            summary.push(`Real Space R ${rsrzCount}: ${rsrzAvg.toFixed(2)}`)
+        }
+        if (rsccSeen.size) {
+            const rsccCount = `<small>(${rsccSeen.size} ${rsccSeen.size > 1 ? 'Residues' : 'Residue'})</small>`
+            const rsccAvg = rsccSum / rsccSeen.size
+            summary.push(`Real Space Correlation Coefficient ${rsccCount}: ${rsccAvg.toFixed(2)}`)
+        }
+
+        if (summary.length) {
+            return summary.join('</br>')
+        }
+    }
+}
+
+function randomCoilIndexLabel(loci: Loci): string | undefined {
+    if (loci.kind === 'element-loci') {
+        if (loci.elements.length === 0) return;
+
+        const seen = new Set<number>()
+        let sum = 0
+
+        for (const { indices, unit } of loci.elements) {
+            const validationReport = ValidationReportProvider.get(unit.model).value
+            if (!validationReport) continue
+
+            const { rci } = validationReport
+            const residueIndex = unit.model.atomicHierarchy.residueAtomSegments.index
+            const { elements } = unit
+
+            OrderedSet.forEach(indices, idx => {
+                const eI = elements[idx]
+                const rI = residueIndex[eI]
+
+                const residueKey = cantorPairing(rI, unit.id)
+                if (!seen.has(residueKey)) {
+                    const rciValue = rci.get(rI)
+                    if (rciValue !== undefined) {
+                        sum += rciValue
+                        seen.add(residueKey)
+                    }
+                }
+            })
+        }
 
-            const residueIndex = e.unit.model.atomicHierarchy.residueAtomSegments.index
-            const issues = geometryIssues.get(residueIndex[e.unit.elements[OrderedSet.start(e.indices)]])
-            if (!issues || issues.size === 0) return 'RCSB Geometry Quality: no issues';
+        if (seen.size === 0) return
 
-            const label: string[] = []
-            issues.forEach(i => label.push(i))
-            return `RCSB Geometry Quality: ${label.join(', ')}`;
+        const residueCount = `<small>(${seen.size} ${seen.size > 1 ? 'Residues' : 'Residue'})</small>`
+        const rciAvg = sum / seen.size
 
-        default: return;
+        return `Random Coil Index ${residueCount}: ${rciAvg.toFixed(2)}`
     }
 }

+ 4 - 3
src/mol-theme/label.ts

@@ -110,11 +110,12 @@ function _structureElementStatsLabel(stats: StructureElement.Stats, countsOnly =
     }
 }
 
-export function bondLabel(bond: Bond.Location): string {
+export function bondLabel(bond: Bond.Location, options: Partial<LabelOptions> = {}): string {
+    const o = { ...DefaultLabelOptions, ...options }
     const locA = StructureElement.Location.create(bond.aUnit, bond.aUnit.elements[bond.aIndex])
     const locB = StructureElement.Location.create(bond.bUnit, bond.bUnit.elements[bond.bIndex])
-    const labelA = _elementLabel(locA)
-    const labelB = _elementLabel(locB)
+    const labelA = _elementLabel(locA, o.granularity, o.hidePrefix)
+    const labelB = _elementLabel(locB, o.granularity, o.hidePrefix)
     let offset = 0
     for (let i = 0, il = Math.min(labelA.length, labelB.length); i < il; ++i) {
         if (labelA[i] === labelB[i]) offset += 1