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
This commit is contained in:
kempersc 2026-01-07 22:03:08 +01:00
parent f8421a2903
commit 81da4ede50
4 changed files with 298 additions and 0 deletions

View file

@ -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<string, unknown>;
examples?: SlotExample[];
// SKOS semantic mappings for ontology alignment
exact_mappings?: string[];
close_mappings?: string[];

View file

@ -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 {

View file

@ -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;

View file

@ -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 && <span className="linkml-viewer__badge linkml-viewer__badge--required">{t('required')}</span>}
{slot.multivalued && <span className="linkml-viewer__badge linkml-viewer__badge--multi">{t('multivalued')}</span>}
{slot.identifier && <span className="linkml-viewer__badge linkml-viewer__badge--identifier">{t('identifier')}</span>}
{/* Show 3D polygon for ALL elements (including universal) in 3D mode for testing */}
{use3DIndicator ? (
<CustodianTypeIndicator3D
@ -2609,6 +2618,13 @@ const LinkMLViewerPage: React.FC = () => {
)
)}
</h4>
{/* Slot URI - semantic predicate */}
{slot.slot_uri && (
<div className="linkml-viewer__slot-uri">
<span className="linkml-viewer__label">{t('slotUri')}</span>
<code className="linkml-viewer__uri-value">{slot.slot_uri}</code>
</div>
)}
{slot.range && (
<div className="linkml-viewer__range">
<span className="linkml-viewer__label">{t('range')}</span>
@ -2639,6 +2655,84 @@ const LinkMLViewerPage: React.FC = () => {
<code>{slot.pattern}</code>
</div>
)}
{/* Examples section */}
{slot.examples && slot.examples.length > 0 && (
<div className="linkml-viewer__examples">
<span className="linkml-viewer__label">{t('examples')}</span>
<ul className="linkml-viewer__examples-list">
{slot.examples.map((example, idx) => (
<li key={idx} className="linkml-viewer__example-item">
<code className="linkml-viewer__example-value">{example.value}</code>
{example.description && (
<span className="linkml-viewer__example-description"> - {example.description}</span>
)}
</li>
))}
</ul>
</div>
)}
{/* Semantic Mappings - 5 types per LinkML spec */}
{slot.exact_mappings && slot.exact_mappings.length > 0 && (
<div className="linkml-viewer__mappings">
<span className="linkml-viewer__label">{t('exactMappings')}</span>
<div className="linkml-viewer__tag-list">
{slot.exact_mappings.map(mapping => (
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--exact">{mapping}</span>
))}
</div>
</div>
)}
{slot.close_mappings && slot.close_mappings.length > 0 && (
<div className="linkml-viewer__mappings">
<span className="linkml-viewer__label">{t('closeMappings')}</span>
<div className="linkml-viewer__tag-list">
{slot.close_mappings.map(mapping => (
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--close">{mapping}</span>
))}
</div>
</div>
)}
{slot.narrow_mappings && slot.narrow_mappings.length > 0 && (
<div className="linkml-viewer__mappings">
<span className="linkml-viewer__label">{t('narrowMappings')}</span>
<div className="linkml-viewer__tag-list">
{slot.narrow_mappings.map(mapping => (
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--narrow">{mapping}</span>
))}
</div>
</div>
)}
{slot.broad_mappings && slot.broad_mappings.length > 0 && (
<div className="linkml-viewer__mappings">
<span className="linkml-viewer__label">{t('broadMappings')}</span>
<div className="linkml-viewer__tag-list">
{slot.broad_mappings.map(mapping => (
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--broad">{mapping}</span>
))}
</div>
</div>
)}
{slot.related_mappings && slot.related_mappings.length > 0 && (
<div className="linkml-viewer__mappings">
<span className="linkml-viewer__label">{t('relatedMappings')}</span>
<div className="linkml-viewer__tag-list">
{slot.related_mappings.map(mapping => (
<span key={mapping} className="linkml-viewer__tag linkml-viewer__tag--mapping linkml-viewer__tag--related">{mapping}</span>
))}
</div>
</div>
)}
{/* Comments section */}
{slot.comments && slot.comments.length > 0 && (
<div className="linkml-viewer__comments">
<span className="linkml-viewer__label">{t('comments')}</span>
<ul className="linkml-viewer__comments-list">
{slot.comments.map((comment, idx) => (
<li key={idx} className="linkml-viewer__comment-item">{comment}</li>
))}
</ul>
</div>
)}
</div>
);
};
@ -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 (
<div className="linkml-viewer__visual">
{/* Schema Metadata */}
@ -2873,6 +2975,26 @@ const LinkMLViewerPage: React.FC = () => {
</div>
)}
{/* Slots */}
{slots.length > 0 && (
<div className="linkml-viewer__section">
<button
className="linkml-viewer__section-header"
onClick={() => toggleSection('slots')}
>
<span className="linkml-viewer__section-icon">
{expandedSections.has('slots') ? '▼' : '▶'}
</span>
{t('slots')} ({custodianTypeFilter.size > 0 ? `${matchingSlotCount}/${slots.length}` : slots.length})
</button>
{expandedSections.has('slots') && (
<div className="linkml-viewer__section-content">
{slots.map(renderSlotDetails)}
</div>
)}
</div>
)}
{/* Enums */}
{enums.length > 0 && (
<div className="linkml-viewer__section">