From 81da4ede50f7ac31a1da17ee6e48f7f4a4888a28 Mon Sep 17 00:00:00 2001 From: kempersc Date: Wed, 7 Jan 2026 22:03:08 +0100 Subject: [PATCH] Add comprehensive slot visualization to LinkML viewer - Add standalone Slots section in visual view alongside Classes and Enums - Display slot_uri, range, identifier badge, description, pattern - Show examples with value/description pairs - Color-coded SKOS mapping tags (exact/close/narrow/broad/related) - Yellow highlighted comments section - Custodian type filtering works with slots - Shared renderSlotDetails() function for consistency --- .../src/lib/linkml/linkml-schema-service.ts | 6 + frontend/src/lib/linkml/schema-loader.ts | 14 ++ frontend/src/pages/LinkMLViewerPage.css | 156 ++++++++++++++++++ frontend/src/pages/LinkMLViewerPage.tsx | 122 ++++++++++++++ 4 files changed, 298 insertions(+) diff --git a/frontend/src/lib/linkml/linkml-schema-service.ts b/frontend/src/lib/linkml/linkml-schema-service.ts index 69a270b769..dd0d3a712a 100644 --- a/frontend/src/lib/linkml/linkml-schema-service.ts +++ b/frontend/src/lib/linkml/linkml-schema-service.ts @@ -18,6 +18,11 @@ function debugLog(...args: unknown[]): void { } } +export interface SlotExample { + value: string; + description?: string; +} + export interface SlotDefinition { name: string; description?: string; @@ -29,6 +34,7 @@ export interface SlotDefinition { identifier?: boolean; comments?: string[]; annotations?: Record; + examples?: SlotExample[]; // SKOS semantic mappings for ontology alignment exact_mappings?: string[]; close_mappings?: string[]; diff --git a/frontend/src/lib/linkml/schema-loader.ts b/frontend/src/lib/linkml/schema-loader.ts index ad488907b0..bd37f0ca03 100644 --- a/frontend/src/lib/linkml/schema-loader.ts +++ b/frontend/src/lib/linkml/schema-loader.ts @@ -6,6 +6,11 @@ import yaml from 'js-yaml'; +export interface SlotExample { + value: string; + description?: string; +} + export interface LinkMLSlot { name: string; description?: string; @@ -14,6 +19,15 @@ export interface LinkMLSlot { multivalued?: boolean; slot_uri?: string; pattern?: string; + identifier?: boolean; + comments?: string[]; + examples?: SlotExample[]; + // SKOS semantic mappings for ontology alignment + exact_mappings?: string[]; + close_mappings?: string[]; + narrow_mappings?: string[]; + broad_mappings?: string[]; + related_mappings?: string[]; } export interface LinkMLClass { diff --git a/frontend/src/pages/LinkMLViewerPage.css b/frontend/src/pages/LinkMLViewerPage.css index 0785fec313..9798795d96 100644 --- a/frontend/src/pages/LinkMLViewerPage.css +++ b/frontend/src/pages/LinkMLViewerPage.css @@ -1617,6 +1617,162 @@ color: var(--info-dark, #1565c0); } +/* Additional mapping type styles for SKOS semantic mappings */ +.linkml-viewer__tag--exact { + background: var(--success-light, #e8f5e9); + border-color: var(--success-color, #4caf50); + color: var(--success-dark, #2e7d32); +} + +.linkml-viewer__tag--narrow { + background: #fff3e0; + border-color: #ff9800; + color: #e65100; +} + +.linkml-viewer__tag--broad { + background: #fce4ec; + border-color: #e91e63; + color: #ad1457; +} + +.linkml-viewer__tag--related { + background: #f3e5f5; + border-color: #9c27b0; + color: #6a1b9a; +} + +/* Identifier badge */ +.linkml-viewer__badge--identifier { + background: var(--primary-color, #1976d2); + color: white; +} + +/* Slot URI display */ +.linkml-viewer__slot-uri { + margin: 0.5rem 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.linkml-viewer__uri-value { + background: var(--surface-secondary, #f5f5f5); + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.875rem; + color: var(--primary-color, #1976d2); +} + +/* Examples section */ +.linkml-viewer__examples { + margin: 0.75rem 0; +} + +.linkml-viewer__examples-list { + list-style: none; + padding: 0; + margin: 0.5rem 0 0 0; +} + +.linkml-viewer__example-item { + padding: 0.375rem 0.5rem; + margin: 0.25rem 0; + background: var(--surface-secondary, #f5f5f5); + border-radius: 4px; + border-left: 3px solid var(--primary-color, #1976d2); +} + +.linkml-viewer__example-value { + background: white; + padding: 0.125rem 0.375rem; + border-radius: 3px; + font-weight: 500; + color: var(--primary-dark, #0d47a1); +} + +.linkml-viewer__example-description { + color: var(--text-secondary, #616161); + font-size: 0.875rem; +} + +/* Comments section */ +.linkml-viewer__comments { + margin: 0.75rem 0; +} + +.linkml-viewer__comments-list { + list-style: none; + padding: 0; + margin: 0.5rem 0 0 0; +} + +.linkml-viewer__comment-item { + padding: 0.5rem; + margin: 0.25rem 0; + background: var(--warning-light, #fff8e1); + border-radius: 4px; + border-left: 3px solid var(--warning-color, #ffc107); + color: var(--text-primary, #212121); + font-size: 0.875rem; + font-style: italic; +} + +/* Dark mode support for new slot elements */ +[data-theme="dark"] .linkml-viewer__uri-value { + background: #374151; + color: #60a5fa; +} + +[data-theme="dark"] .linkml-viewer__example-item { + background: #374151; + border-left-color: #60a5fa; +} + +[data-theme="dark"] .linkml-viewer__example-value { + background: #1f2937; + color: #93c5fd; +} + +[data-theme="dark"] .linkml-viewer__example-description { + color: #9ca3af; +} + +[data-theme="dark"] .linkml-viewer__comment-item { + background: #422006; + border-left-color: #f59e0b; + color: #fcd34d; +} + +[data-theme="dark"] .linkml-viewer__tag--exact { + background: #064e3b; + border-color: #10b981; + color: #6ee7b7; +} + +[data-theme="dark"] .linkml-viewer__tag--narrow { + background: #451a03; + border-color: #f59e0b; + color: #fcd34d; +} + +[data-theme="dark"] .linkml-viewer__tag--broad { + background: #500724; + border-color: #ec4899; + color: #f9a8d4; +} + +[data-theme="dark"] .linkml-viewer__tag--related { + background: #4a044e; + border-color: #a855f7; + color: #d8b4fe; +} + +[data-theme="dark"] .linkml-viewer__badge--identifier { + background: #2563eb; + color: white; +} + /* Enum Values */ .linkml-viewer__enum-values { margin-top: 0.75rem; diff --git a/frontend/src/pages/LinkMLViewerPage.tsx b/frontend/src/pages/LinkMLViewerPage.tsx index c03864d37e..5b51b1dd8b 100644 --- a/frontend/src/pages/LinkMLViewerPage.tsx +++ b/frontend/src/pages/LinkMLViewerPage.tsx @@ -960,6 +960,14 @@ const TEXT = { en: 'Shows all slots (properties) defined for this class' }, noClassSlots: { nl: 'Deze klasse heeft geen slots.', en: 'This class has no slots.' }, + // Slot details - semantic URI and mappings + slotUri: { nl: 'Slot URI:', en: 'Slot URI:' }, + identifier: { nl: 'identifier', en: 'identifier' }, + examples: { nl: 'Voorbeelden:', en: 'Examples:' }, + narrowMappings: { nl: 'Specifiekere mappings:', en: 'Narrow Mappings:' }, + broadMappings: { nl: 'Bredere mappings:', en: 'Broad Mappings:' }, + relatedMappings: { nl: 'Gerelateerde mappings:', en: 'Related Mappings:' }, + comments: { nl: 'Opmerkingen:', en: 'Comments:' }, }; // Dynamically discover schema files from the modules directory @@ -2587,6 +2595,7 @@ const LinkMLViewerPage: React.FC = () => { {slot.name} {slot.required && {t('required')}} {slot.multivalued && {t('multivalued')}} + {slot.identifier && {t('identifier')}} {/* Show 3D polygon for ALL elements (including universal) in 3D mode for testing */} {use3DIndicator ? ( { ) )} + {/* Slot URI - semantic predicate */} + {slot.slot_uri && ( +
+ {t('slotUri')} + {slot.slot_uri} +
+ )} {slot.range && (
{t('range')} @@ -2639,6 +2655,84 @@ const LinkMLViewerPage: React.FC = () => { {slot.pattern}
)} + {/* Examples section */} + {slot.examples && slot.examples.length > 0 && ( +
+ {t('examples')} +
    + {slot.examples.map((example, idx) => ( +
  • + {example.value} + {example.description && ( + - {example.description} + )} +
  • + ))} +
+
+ )} + {/* Semantic Mappings - 5 types per LinkML spec */} + {slot.exact_mappings && slot.exact_mappings.length > 0 && ( +
+ {t('exactMappings')} +
+ {slot.exact_mappings.map(mapping => ( + {mapping} + ))} +
+
+ )} + {slot.close_mappings && slot.close_mappings.length > 0 && ( +
+ {t('closeMappings')} +
+ {slot.close_mappings.map(mapping => ( + {mapping} + ))} +
+
+ )} + {slot.narrow_mappings && slot.narrow_mappings.length > 0 && ( +
+ {t('narrowMappings')} +
+ {slot.narrow_mappings.map(mapping => ( + {mapping} + ))} +
+
+ )} + {slot.broad_mappings && slot.broad_mappings.length > 0 && ( +
+ {t('broadMappings')} +
+ {slot.broad_mappings.map(mapping => ( + {mapping} + ))} +
+
+ )} + {slot.related_mappings && slot.related_mappings.length > 0 && ( +
+ {t('relatedMappings')} +
+ {slot.related_mappings.map(mapping => ( + {mapping} + ))} +
+
+ )} + {/* Comments section */} + {slot.comments && slot.comments.length > 0 && ( +
+ {t('comments')} +
    + {slot.comments.map((comment, idx) => ( +
  • {comment}
  • + ))} +
+
+ )} ); }; @@ -2788,6 +2882,7 @@ const LinkMLViewerPage: React.FC = () => { const classes = extractClasses(schema); const enums = extractEnums(schema); + const slots = extractSlots(schema); // Count matching items when filter is active (for display purposes) const matchingClassCount = custodianTypeFilter.size > 0 @@ -2804,6 +2899,13 @@ const LinkMLViewerPage: React.FC = () => { }).length : enums.length; + const matchingSlotCount = custodianTypeFilter.size > 0 + ? slots.filter(slot => { + const types = slotCustodianTypes[slot.name] || getCustodianTypesForSlot(slot.name); + return types.some(t => custodianTypeFilter.has(t)); + }).length + : slots.length; + return (
{/* Schema Metadata */} @@ -2873,6 +2975,26 @@ const LinkMLViewerPage: React.FC = () => {
)} + {/* Slots */} + {slots.length > 0 && ( +
+ + {expandedSections.has('slots') && ( +
+ {slots.map(renderSlotDetails)} +
+ )} +
+ )} + {/* Enums */} {enums.length > 0 && (