@glossarist/concept-browser 0.7.85 → 0.7.87
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/scripts/build-edges.js +50 -27
- package/scripts/generate-data.mjs +59 -5
- package/scripts/migrate-v1-to-v3.d.mts +6 -0
- package/scripts/migrate-v1-to-v3.mjs +176 -0
- package/src/__tests__/components/PartitiveRelationDiagram.test.ts +98 -136
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +123 -88
- package/src/__tests__/partitive-relation-styling.test.ts +12 -54
- package/src/__tests__/partitive-relation.test.ts +72 -90
- package/src/__tests__/scripts/migrate-v1-to-v3.d.mts +8 -0
- package/src/__tests__/scripts/migrate-v1-to-v3.test.ts +115 -0
- package/src/__tests__/utils/partitive-multiplicity.test.ts +137 -0
- package/src/adapters/model-bridge.ts +48 -19
- package/src/adapters/non-verbal/glossarist-augment.d.ts +85 -54
- package/src/adapters/types.ts +3 -8
- package/src/components/PartitiveRelationDiagram.vue +90 -76
- package/src/components/PartitiveRelationList.vue +27 -34
- package/src/components/RelationSphere.vue +58 -73
- package/src/composables/use-concept-edges.ts +26 -21
- package/src/utils/partitive-multiplicity.ts +99 -0
- package/src/utils/partitive-relation-styling.ts +29 -38
- package/src/views/ConceptView.vue +8 -13
- package/src/__tests__/model-bridge-partitive.test.ts +0 -146
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
3
|
* PartitiveRelationList — renders one-to-many partitive decompositions
|
|
4
|
-
* as ISO 704 rake diagrams. Each relation shows:
|
|
5
|
-
* - completeness badge (
|
|
6
|
-
* -
|
|
7
|
-
* - criterion as italic text
|
|
8
|
-
* - the rake diagram itself (PartitiveRelationDiagram)
|
|
4
|
+
* as ISO 704 rake diagrams. Each relation card shows:
|
|
5
|
+
* - completeness badge (Complete / Partial)
|
|
6
|
+
* - the comprehensive's designation as the card title
|
|
7
|
+
* - optional criterion as italic text
|
|
8
|
+
* - the rake diagram itself (PartitiveRelationDiagram), where each
|
|
9
|
+
* drop's line style encodes its multiplicity + is_delimiting per
|
|
10
|
+
* ISO 704:2022 (5 multiplicity values × 2 delimiting states).
|
|
9
11
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
12
|
+
* Header badges communicate metadata that the diagram's line styles
|
|
13
|
+
* also encode — for users who can't perceive the visual difference
|
|
14
|
+
* (screen readers, low vision, monochrome displays).
|
|
13
15
|
*/
|
|
14
16
|
import type { PartitiveRelationWire, Manifest } from '../adapters/types';
|
|
15
17
|
import { useRouter } from 'vue-router';
|
|
16
18
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
17
19
|
import { getFactory } from '../adapters/factory';
|
|
18
20
|
import { useI18n, locale } from '../i18n';
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
certaintyLabel,
|
|
22
|
-
} from '../utils/partitive-relation-styling';
|
|
21
|
+
import { completenessLabel } from '../utils/partitive-relation-styling';
|
|
22
|
+
import { presenceLabel, countLabel } from '../utils/partitive-multiplicity';
|
|
23
23
|
import PartitiveRelationDiagram, {
|
|
24
24
|
type PartitiveMemberLabeled,
|
|
25
25
|
} from './PartitiveRelationDiagram.vue';
|
|
@@ -77,25 +77,25 @@ function criterionText(criterion?: Record<string, string>): string | null {
|
|
|
77
77
|
?? null;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
label = `${label} (${t('partitive.plurality.uncertain') || 'uncertain'})`;
|
|
85
|
-
}
|
|
86
|
-
if (plurality.sharedType) {
|
|
87
|
-
label = `${label}: ${plurality.sharedType}`;
|
|
88
|
-
}
|
|
89
|
-
return label;
|
|
90
|
-
}
|
|
91
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Build a per-member view-model carrying the resolved designation plus
|
|
82
|
+
* the multiplicity + is_delimiting fields the diagram needs.
|
|
83
|
+
*/
|
|
92
84
|
function labeledMembers(rel: PartitiveRelationWire): PartitiveMemberLabeled[] {
|
|
93
85
|
return rel.partitives.map(m => ({
|
|
94
86
|
uri: m.uri,
|
|
95
87
|
label: designationFor(m.uri),
|
|
96
|
-
|
|
88
|
+
presence: m.presence,
|
|
89
|
+
count: m.count,
|
|
90
|
+
isDelimiting: m.isDelimiting,
|
|
97
91
|
}));
|
|
98
92
|
}
|
|
93
|
+
|
|
94
|
+
/** Tooltip text for a member, shown on hover. */
|
|
95
|
+
function memberTooltip(m: { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple'; isDelimiting: boolean }): string {
|
|
96
|
+
const label = `${presenceLabel(m.presence)} · ${countLabel(m.count).toLowerCase()}`;
|
|
97
|
+
return m.isDelimiting ? `Delimiting · ${label}` : label;
|
|
98
|
+
}
|
|
99
99
|
</script>
|
|
100
100
|
|
|
101
101
|
<template>
|
|
@@ -113,13 +113,6 @@ function labeledMembers(rel: PartitiveRelationWire): PartitiveMemberLabeled[] {
|
|
|
113
113
|
<span class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-emerald-200 dark:bg-emerald-800 text-emerald-900 dark:text-emerald-100 uppercase">
|
|
114
114
|
{{ completenessLabel(rel.completeness) }}
|
|
115
115
|
</span>
|
|
116
|
-
<span
|
|
117
|
-
v-if="rel.plurality && rel.plurality.isShared"
|
|
118
|
-
class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-amber-200 dark:bg-amber-800 text-amber-900 dark:text-amber-100 uppercase"
|
|
119
|
-
:title="pluralityBadge(rel.plurality)"
|
|
120
|
-
>
|
|
121
|
-
{{ pluralityBadge(rel.plurality) }}
|
|
122
|
-
</span>
|
|
123
116
|
<button
|
|
124
117
|
@click="navigate(rel.comprehensive)"
|
|
125
118
|
class="ml-auto text-sm font-medium text-emerald-900 dark:text-emerald-100 hover:underline truncate"
|
|
@@ -133,14 +126,14 @@ function labeledMembers(rel: PartitiveRelationWire): PartitiveMemberLabeled[] {
|
|
|
133
126
|
>
|
|
134
127
|
{{ criterionText(rel.criterion) }}
|
|
135
128
|
</div>
|
|
136
|
-
<!-- Rake diagram (ISO 704 partitive-relation convention) -->
|
|
129
|
+
<!-- Rake diagram (ISO 704:2022 partitive-relation convention) -->
|
|
137
130
|
<div class="px-3 py-3 overflow-x-auto">
|
|
138
131
|
<PartitiveRelationDiagram
|
|
139
132
|
:comprehensive-label="designationFor(rel.comprehensive)"
|
|
140
133
|
:partitives="labeledMembers(rel)"
|
|
141
134
|
:completeness="rel.completeness"
|
|
142
|
-
:plurality="rel.plurality"
|
|
143
135
|
:criterion="null"
|
|
136
|
+
:member-tooltip="memberTooltip"
|
|
144
137
|
@navigate="navigate"
|
|
145
138
|
/>
|
|
146
139
|
</div>
|
|
@@ -25,7 +25,8 @@ import { hashSeed, expandParams, portSide, portPoint, idToUriGet } from '../util
|
|
|
25
25
|
import { useI18n } from '../i18n';
|
|
26
26
|
|
|
27
27
|
const { t, locale } = useI18n();
|
|
28
|
-
import type { Concept, Manifest, GraphEdge, PartitiveRelationWire } from '../adapters/types';
|
|
28
|
+
import type { Concept, Manifest, GraphEdge, PartitiveRelationWire, PartitiveMemberWire } from '../adapters/types';
|
|
29
|
+
import { rakeStrokeStyle } from '../utils/partitive-multiplicity';
|
|
29
30
|
import { conceptUri } from '../adapters/model-bridge';
|
|
30
31
|
|
|
31
32
|
const props = defineProps<{
|
|
@@ -760,41 +761,33 @@ function drawEdges(cx: number, cy: number) {
|
|
|
760
761
|
}
|
|
761
762
|
|
|
762
763
|
/**
|
|
763
|
-
* Render PartitiveRelations as ISO 704
|
|
764
|
+
* Render PartitiveRelations as ISO 704:2022 rake brackets in the sphere.
|
|
764
765
|
*
|
|
765
|
-
* Each relation renders as a
|
|
766
|
+
* Each relation renders as a right-angle bracket with stem + spine + drops
|
|
767
|
+
* (geometry described in detail above). Stem and spine are the structural
|
|
768
|
+
* frame — always single solid at FRAME_STROKE_WIDTH.
|
|
766
769
|
*
|
|
767
|
-
*
|
|
768
|
-
*
|
|
769
|
-
* │ ← stem (along stem-axis)
|
|
770
|
-
* │
|
|
771
|
-
* ┌──────────●──────────┐ ← spine (perpendicular to stem-axis; right angle at junction)
|
|
772
|
-
* │ │ │
|
|
773
|
-
* │ │ │ ← drops (parallel to stem-axis; right angle at each tooth)
|
|
774
|
-
* [p1] [p2] [p3]
|
|
770
|
+
* Per-member multiplicity + is_delimiting is encoded on each DROP via
|
|
771
|
+
* rakeStrokeStyle() (see utils/partitive-multiplicity.ts):
|
|
775
772
|
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
*
|
|
779
|
-
*
|
|
780
|
-
*
|
|
781
|
-
*
|
|
782
|
-
*
|
|
773
|
+
* multiplicity is_delimiting drop visual
|
|
774
|
+
* ------------------- -------------- --------------------------------
|
|
775
|
+
* compulsory false 1 solid @ 1.5px
|
|
776
|
+
* compulsory true 1 solid @ 4.5px (3× bold)
|
|
777
|
+
* optional false 1 dashed @ 1.5px
|
|
778
|
+
* optional true 1 dashed @ 4.5px
|
|
779
|
+
* compulsory_multiple false 2 solid @ 1.5px
|
|
780
|
+
* compulsory_multiple true 2 solid @ 4.5px
|
|
781
|
+
* optional_multiple false 2 dashed @ 1.5px
|
|
782
|
+
* optional_multiple true 2 dashed @ 4.5px
|
|
783
|
+
* compulsory_at_least_one false 1 solid + 1 dashed @ 1.5px
|
|
784
|
+
* compulsory_at_least_one true 1 solid + 1 dashed @ 4.5px
|
|
783
785
|
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
*
|
|
787
|
-
* ISO 704 line conventions:
|
|
788
|
-
* - default : single solid line per segment
|
|
789
|
-
* - plurality.isShared && !isUncertain : TWO parallel solid lines (close-set)
|
|
790
|
-
* - plurality.isShared && isUncertain : one solid + one dashed line
|
|
791
|
-
* - completeness === 'partial' : spine extends past last tooth
|
|
792
|
-
* (continued backline — "more exist")
|
|
793
|
-
*
|
|
794
|
-
* Per-member certainty 'possible' → that drop is dashed + reduced opacity
|
|
795
|
-
* (stem and spine keep relation style).
|
|
786
|
+
* completeness === 'partial' : spine extends past last tooth (continued
|
|
787
|
+
* backline — "more exist but not shown")
|
|
796
788
|
*
|
|
797
789
|
* Diamond marker at the comprehensive end signals the hyperedge origin.
|
|
790
|
+
* Junction marker at the spine midpoint signals the right-angle corner.
|
|
798
791
|
*/
|
|
799
792
|
function drawRakeBundles(
|
|
800
793
|
svg: SVGSVGElement,
|
|
@@ -806,6 +799,7 @@ function drawRakeBundles(
|
|
|
806
799
|
const color = uiStore.isDark ? '#2dd4bf' : '#0d9488';
|
|
807
800
|
const DOUBLE_GAP = 4;
|
|
808
801
|
const DASH = '4 3';
|
|
802
|
+
const FRAME_STROKE_WIDTH = 1.4;
|
|
809
803
|
const SPINE_FRACTION = 0.55; // spine sits 55% from comp toward centroid
|
|
810
804
|
const PARTIAL_TAIL = 28; // px extension for completeness: 'partial'
|
|
811
805
|
|
|
@@ -824,7 +818,7 @@ function drawRakeBundles(
|
|
|
824
818
|
const p = pos.get(nid);
|
|
825
819
|
return p ? { member, pos: p } : null;
|
|
826
820
|
})
|
|
827
|
-
.filter((x): x is { member:
|
|
821
|
+
.filter((x): x is { member: PartitiveMemberWire; pos: {x: number; y: number} } => x !== null);
|
|
828
822
|
|
|
829
823
|
if (memberPositions.length === 0) continue;
|
|
830
824
|
|
|
@@ -850,7 +844,14 @@ function drawRakeBundles(
|
|
|
850
844
|
const along = vx * spineDirX + vy * spineDirY;
|
|
851
845
|
const toothX = spineCx + spineDirX * along;
|
|
852
846
|
const toothY = spineCy + spineDirY * along;
|
|
853
|
-
return {
|
|
847
|
+
return {
|
|
848
|
+
member,
|
|
849
|
+
partitivePos: pPos,
|
|
850
|
+
toothX,
|
|
851
|
+
toothY,
|
|
852
|
+
along,
|
|
853
|
+
style: rakeStrokeStyle(member.presence, member.count, member.isDelimiting),
|
|
854
|
+
};
|
|
854
855
|
});
|
|
855
856
|
|
|
856
857
|
// 4. Spine extents (leftmost to rightmost tooth along spine-axis)
|
|
@@ -867,23 +868,22 @@ function drawRakeBundles(
|
|
|
867
868
|
const spineEndX = spineCx + spineDirX * maxAlong;
|
|
868
869
|
const spineEndY = spineCy + spineDirY * maxAlong;
|
|
869
870
|
|
|
870
|
-
const isShared = !!rel.plurality?.isShared;
|
|
871
|
-
const isUncertain = !!rel.plurality?.isUncertain;
|
|
872
|
-
|
|
873
871
|
/**
|
|
874
|
-
* Draw a single straight segment
|
|
875
|
-
*
|
|
872
|
+
* Draw a single straight segment.
|
|
873
|
+
*
|
|
874
|
+
* Stem + spine use FRAME_STROKE_WIDTH (single solid frame).
|
|
875
|
+
* Drops use the per-member multiplicity + delimiting style from
|
|
876
|
+
* rakeStrokeStyle().
|
|
876
877
|
*/
|
|
877
878
|
const drawSegment = (
|
|
878
879
|
ax: number, ay: number, bx: number, by: number,
|
|
879
|
-
opts: {
|
|
880
|
+
opts: { width: number; dashed?: boolean; offset?: number; opacity?: number },
|
|
880
881
|
) => {
|
|
882
|
+
const opacity = opts.opacity ?? 0.85;
|
|
881
883
|
const offset = opts.offset ?? 0;
|
|
882
884
|
const dashed = opts.dashed ?? false;
|
|
883
|
-
const opacity = opts.opacity ?? 0.85;
|
|
884
885
|
const dx = bx - ax, dy = by - ay;
|
|
885
886
|
const len = Math.hypot(dx, dy) || 1;
|
|
886
|
-
// Perpendicular to THIS segment (not the spine-axis) for parallel offset
|
|
887
887
|
const px = -dy / len, py = dx / len;
|
|
888
888
|
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
889
889
|
line.setAttribute('class', 'rake-seg');
|
|
@@ -892,50 +892,35 @@ function drawRakeBundles(
|
|
|
892
892
|
line.setAttribute('x2', String(bx + px * offset));
|
|
893
893
|
line.setAttribute('y2', String(by + py * offset));
|
|
894
894
|
line.setAttribute('stroke', color);
|
|
895
|
-
line.setAttribute('stroke-width',
|
|
895
|
+
line.setAttribute('stroke-width', String(opts.width));
|
|
896
896
|
line.setAttribute('opacity', String(opacity));
|
|
897
|
-
line.setAttribute('stroke-linecap', 'square');
|
|
897
|
+
line.setAttribute('stroke-linecap', 'square');
|
|
898
898
|
if (dashed) line.setAttribute('stroke-dasharray', DASH);
|
|
899
899
|
svg.appendChild(line);
|
|
900
900
|
};
|
|
901
901
|
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
* Single solid (default), double solid (isShared),
|
|
905
|
-
* or solid + dashed (isShared + isUncertain).
|
|
906
|
-
*/
|
|
907
|
-
const drawSegmentWithPlurality = (
|
|
908
|
-
ax: number, ay: number, bx: number, by: number,
|
|
909
|
-
opts: { opacity?: number; perMemberPossible?: boolean } = {},
|
|
910
|
-
) => {
|
|
911
|
-
const opacity = opts.opacity ?? 0.85;
|
|
912
|
-
if (!isShared) {
|
|
913
|
-
drawSegment(ax, ay, bx, by, { opacity, dashed: opts.perMemberPossible });
|
|
914
|
-
} else {
|
|
915
|
-
drawSegment(ax, ay, bx, by, { offset: -DOUBLE_GAP / 2, opacity });
|
|
916
|
-
drawSegment(ax, ay, bx, by, {
|
|
917
|
-
offset: DOUBLE_GAP / 2,
|
|
918
|
-
opacity,
|
|
919
|
-
dashed: isUncertain || opts.perMemberPossible,
|
|
920
|
-
});
|
|
921
|
-
}
|
|
922
|
-
};
|
|
923
|
-
|
|
924
|
-
// 5. Stem: comp → spine center (along stem-axis)
|
|
925
|
-
drawSegmentWithPlurality(compPos.x, compPos.y, spineCx, spineCy);
|
|
902
|
+
// 5. Stem: comp → spine center (frame — single solid)
|
|
903
|
+
drawSegment(compPos.x, compPos.y, spineCx, spineCy, { width: FRAME_STROKE_WIDTH });
|
|
926
904
|
|
|
927
|
-
// 6. Spine: leftmost tooth → rightmost tooth (
|
|
928
|
-
// (skip if only one tooth — no horizontal extent)
|
|
905
|
+
// 6. Spine: leftmost tooth → rightmost tooth (frame — single solid)
|
|
929
906
|
if (Math.abs(maxAlong - minAlong) > 1) {
|
|
930
|
-
|
|
907
|
+
drawSegment(spineStartX, spineStartY, spineEndX, spineEndY, { width: FRAME_STROKE_WIDTH });
|
|
931
908
|
}
|
|
932
909
|
|
|
933
|
-
// 7. Drops: tooth → partitive (
|
|
910
|
+
// 7. Drops: tooth → partitive (per-member multiplicity + delimiting)
|
|
911
|
+
// Draw 1 or 2 parallel lines per the style.
|
|
934
912
|
for (const tooth of teeth) {
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
913
|
+
drawSegment(tooth.toothX, tooth.toothY, tooth.partitivePos.x, tooth.partitivePos.y, {
|
|
914
|
+
width: tooth.style.strokeWidth,
|
|
915
|
+
dashed: tooth.style.primaryDashed,
|
|
938
916
|
});
|
|
917
|
+
if (tooth.style.lineCount === 2) {
|
|
918
|
+
drawSegment(tooth.toothX, tooth.toothY, tooth.partitivePos.x, tooth.partitivePos.y, {
|
|
919
|
+
width: tooth.style.strokeWidth,
|
|
920
|
+
dashed: tooth.style.secondaryDashed,
|
|
921
|
+
offset: DOUBLE_GAP,
|
|
922
|
+
});
|
|
923
|
+
}
|
|
939
924
|
}
|
|
940
925
|
|
|
941
926
|
// 8. Junction marker at spine center (the right-angle corner)
|
|
@@ -1,19 +1,18 @@
|
|
|
1
1
|
import { computed, type ComputedRef } from 'vue';
|
|
2
2
|
import type { Router } from 'vue-router';
|
|
3
|
+
// glossarist-js's top-level d.ts is incomplete for v3 model fields.
|
|
4
|
+
// Import the MECE-augmented types from glossarist/models (extended by
|
|
5
|
+
// src/adapters/non-verbal/glossarist-augment.d.ts).
|
|
6
|
+
import type { Concept, RelatedConcept, ConceptRef } from 'glossarist';
|
|
3
7
|
import type {
|
|
4
|
-
Concept,
|
|
5
|
-
RelatedConcept,
|
|
6
8
|
PartitiveRelation as GlsPartitiveRelation,
|
|
7
9
|
PartitiveMember as GlsPartitiveMember,
|
|
8
|
-
|
|
9
|
-
ConceptRef,
|
|
10
|
-
} from 'glossarist';
|
|
10
|
+
} from 'glossarist/models';
|
|
11
11
|
import type {
|
|
12
12
|
Manifest,
|
|
13
13
|
GraphEdge,
|
|
14
14
|
PartitiveRelationWire,
|
|
15
15
|
PartitiveMemberWire,
|
|
16
|
-
TypeSharedPluralityWire,
|
|
17
16
|
} from '../adapters/types';
|
|
18
17
|
import { getFactory } from '../adapters/factory';
|
|
19
18
|
import { conceptUri } from '../adapters/model-bridge';
|
|
@@ -24,6 +23,7 @@ import { categorizeRelationship, relationshipLabel, INVERSE_RELATIONSHIPS } from
|
|
|
24
23
|
import { langLabel } from '../utils/lang';
|
|
25
24
|
import { escapeAttr } from '../utils/escape';
|
|
26
25
|
import { useI18n } from '../i18n';
|
|
26
|
+
import { type PartitivePresence, type PartitiveCount } from '../utils/partitive-multiplicity';
|
|
27
27
|
|
|
28
28
|
export interface EdgeDisplay {
|
|
29
29
|
uri: string;
|
|
@@ -190,7 +190,12 @@ export function useConceptEdges(
|
|
|
190
190
|
.map((m: GlsPartitiveMember): PartitiveMemberWire | null => {
|
|
191
191
|
const uri = resolveConceptRefUri(m.ref);
|
|
192
192
|
if (!uri || uri === source) return null;
|
|
193
|
-
return {
|
|
193
|
+
return {
|
|
194
|
+
uri,
|
|
195
|
+
presence: readPresence(m),
|
|
196
|
+
count: readCount(m),
|
|
197
|
+
isDelimiting: readIsDelimiting(m),
|
|
198
|
+
};
|
|
194
199
|
})
|
|
195
200
|
.filter((m: PartitiveMemberWire | null): m is PartitiveMemberWire => m !== null);
|
|
196
201
|
if (partitives.length === 0) return null;
|
|
@@ -200,7 +205,6 @@ export function useConceptEdges(
|
|
|
200
205
|
comprehensive,
|
|
201
206
|
partitives,
|
|
202
207
|
completeness: rel.completeness,
|
|
203
|
-
plurality: projectPlurality(rel.plurality),
|
|
204
208
|
criterion: rel.criterion ?? undefined,
|
|
205
209
|
register: registerId.value,
|
|
206
210
|
};
|
|
@@ -208,24 +212,25 @@ export function useConceptEdges(
|
|
|
208
212
|
.filter((r: PartitiveRelationWire | null): r is PartitiveRelationWire => r !== null);
|
|
209
213
|
});
|
|
210
214
|
|
|
211
|
-
function projectPlurality(p: GlsTypeSharedPlurality | null): TypeSharedPluralityWire | null {
|
|
212
|
-
if (!p) return null;
|
|
213
|
-
const sharedTypeRef: ConceptRef | null = p.sharedType;
|
|
214
|
-
const sharedType = sharedTypeRef
|
|
215
|
-
? [sharedTypeRef.source, sharedTypeRef.id].filter(Boolean).join(':') || null
|
|
216
|
-
: null;
|
|
217
|
-
return {
|
|
218
|
-
isShared: p.isShared,
|
|
219
|
-
isUncertain: p.isUncertain,
|
|
220
|
-
sharedType,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
215
|
function resolveConceptRefUri(ref: ConceptRef | null): string | null {
|
|
225
216
|
if (!ref) return null;
|
|
226
217
|
return resolveRefUri({ source: ref.source ?? null, id: ref.id ?? null });
|
|
227
218
|
}
|
|
228
219
|
|
|
220
|
+
// Read presence + count directly from the glossarist-js model
|
|
221
|
+
// (MECE native, since glossarist 0.4.26).
|
|
222
|
+
function readPresence(m: GlsPartitiveMember): PartitivePresence {
|
|
223
|
+
return m.presence;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readCount(m: GlsPartitiveMember): PartitiveCount {
|
|
227
|
+
return m.count;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function readIsDelimiting(m: GlsPartitiveMember): boolean {
|
|
231
|
+
return m.is_delimiting === true;
|
|
232
|
+
}
|
|
233
|
+
|
|
229
234
|
function resolveRefUri(ref: { source: string | null; id: string | null } | null): string | null {
|
|
230
235
|
const target = resolveRelatedRef(ref);
|
|
231
236
|
if (!target) return null;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISO 704:2022 §5.5.4.2 — partitive multiplicity model (MECE).
|
|
3
|
+
*
|
|
4
|
+
* Two independent axes:
|
|
5
|
+
*
|
|
6
|
+
* presence: 'required' | 'optional'
|
|
7
|
+
* Is this part always present?
|
|
8
|
+
*
|
|
9
|
+
* count: 'exactly_one' | 'at_least_one' | 'multiple'
|
|
10
|
+
* How many instances?
|
|
11
|
+
*
|
|
12
|
+
* 6 combinations, 5 visually distinct (optional + at_least_one
|
|
13
|
+
* collapses to 2-dashed = same as optional + multiple) and the
|
|
14
|
+
* invalid combo (optional, at_least_one) is rejected at construction
|
|
15
|
+
* by glossarist-js.
|
|
16
|
+
*
|
|
17
|
+
* Types are re-exported from glossarist-js — the model is the SSOT.
|
|
18
|
+
* The render-ready `rakeStrokeStyle(presence, count, isDelimiting)`
|
|
19
|
+
* helper is purely a browser-rendering concern; everything else
|
|
20
|
+
* (presence, count, is_delimiting fields, MULTIPLICITY name lookup via
|
|
21
|
+
* multiplicityFromPair, validity via isValidPresence / isValidCount)
|
|
22
|
+
* comes from `glossarist/models`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
PARTITIVE_PRESENCE,
|
|
27
|
+
PARTITIVE_COUNT,
|
|
28
|
+
multiplicityFromPair,
|
|
29
|
+
isValidPresence,
|
|
30
|
+
isValidCount,
|
|
31
|
+
type PartitivePresence,
|
|
32
|
+
type PartitiveCount,
|
|
33
|
+
} from 'glossarist/models';
|
|
34
|
+
|
|
35
|
+
// ── Two independent axes (re-exported SSOT) ────────────────────────────
|
|
36
|
+
|
|
37
|
+
export { PARTITIVE_PRESENCE, PARTITIVE_COUNT };
|
|
38
|
+
export type { PartitivePresence, PartitiveCount };
|
|
39
|
+
export { isValidPresence as isPartitivePresence, isValidCount as isPartitiveCount };
|
|
40
|
+
|
|
41
|
+
/** ISO 704 name for a (presence, count) pair. Throws on invalid combos. */
|
|
42
|
+
export const partitiveMultiplicityName = multiplicityFromPair;
|
|
43
|
+
|
|
44
|
+
// ── Render-ready stroke style (rendering-only concern) ─────────────────
|
|
45
|
+
|
|
46
|
+
export interface RakeStrokeStyle {
|
|
47
|
+
readonly lineCount: 1 | 2;
|
|
48
|
+
readonly primaryDashed: boolean;
|
|
49
|
+
readonly secondaryDashed: boolean;
|
|
50
|
+
readonly strokeWidth: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const NORMAL_STROKE_WIDTH = 1.5;
|
|
54
|
+
export const DELIMITING_STROKE_WIDTH = 4.5;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Compute the render-ready stroke style.
|
|
58
|
+
*
|
|
59
|
+
* Pure function — consumed by both the sidebar rake diagram
|
|
60
|
+
* (PartitiveRelationDiagram) and the sphere rake bundles
|
|
61
|
+
* (RelationSphere.drawRakeBundles).
|
|
62
|
+
*
|
|
63
|
+
* Derivation:
|
|
64
|
+
* presence=required → primary line is solid
|
|
65
|
+
* presence=optional → primary line is dashed
|
|
66
|
+
* count=exactly_one → 1 line
|
|
67
|
+
* count=multiple → 2 lines, both same style as primary
|
|
68
|
+
* count=at_least_one → 2 lines: primary + dashed ("possibly more")
|
|
69
|
+
*/
|
|
70
|
+
export function rakeStrokeStyle(
|
|
71
|
+
presence: PartitivePresence,
|
|
72
|
+
count: PartitiveCount,
|
|
73
|
+
isDelimiting: boolean,
|
|
74
|
+
): RakeStrokeStyle {
|
|
75
|
+
const primaryDashed = presence === 'optional';
|
|
76
|
+
const width = isDelimiting ? DELIMITING_STROKE_WIDTH : NORMAL_STROKE_WIDTH;
|
|
77
|
+
|
|
78
|
+
if (count === 'exactly_one') {
|
|
79
|
+
return { lineCount: 1, primaryDashed, secondaryDashed: false, strokeWidth: width };
|
|
80
|
+
}
|
|
81
|
+
// multiple or at_least_one
|
|
82
|
+
const secondaryDashed = count === 'at_least_one' ? true : primaryDashed;
|
|
83
|
+
return { lineCount: 2, primaryDashed, secondaryDashed, strokeWidth: width };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ── Human-readable labels (UI concern) ────────────────────────────────
|
|
87
|
+
|
|
88
|
+
export function presenceLabel(p: PartitivePresence): string {
|
|
89
|
+
return p === 'optional' ? 'Optional' : 'Required';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function countLabel(c: PartitiveCount): string {
|
|
93
|
+
switch (c) {
|
|
94
|
+
case 'exactly_one': return 'Exactly one';
|
|
95
|
+
case 'at_least_one': return 'At least one';
|
|
96
|
+
case 'multiple': return 'Multiple';
|
|
97
|
+
default: return '';
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Partitive relation styling — colors for one-to-many
|
|
2
|
+
* Partitive relation styling — colors and labels for one-to-many
|
|
3
|
+
* partitive decompositions.
|
|
3
4
|
*
|
|
4
5
|
* Independent of relationship-categories.ts (which handles binary edges).
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - plurality (type-shared, uncertain) adds accent badges
|
|
6
|
+
* The visual encoding for PartitiveRelation comes from two orthogonal
|
|
7
|
+
* axes per ISO 704:2022:
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* 1. completeness (complete / partial) — relation-level metadata,
|
|
10
|
+
* affects overall opacity. Partial relations have a continued
|
|
11
|
+
* backline ("more exist but aren't shown").
|
|
12
|
+
* 2. multiplicity + is_delimiting (per-member) — see
|
|
13
|
+
* partitive-multiplicity.ts for the registry + rakeStrokeStyle().
|
|
14
|
+
*
|
|
15
|
+
* This module owns:
|
|
16
|
+
* - relation-level color/badge (completeness-driven)
|
|
17
|
+
* - human-readable labels for badges
|
|
18
|
+
*
|
|
19
|
+
* It does NOT encode multiplicity visuals — those live in
|
|
20
|
+
* partitive-multiplicity.ts (separated so renderers can consume the
|
|
21
|
+
* pure rakeStrokeStyle() helper without pulling badge strings).
|
|
11
22
|
*/
|
|
12
23
|
|
|
13
|
-
import type { TypeSharedPluralityWire } from '../adapters/types';
|
|
14
|
-
|
|
15
24
|
const BASE_LIGHT = '#0d9488';
|
|
16
25
|
const BASE_DARK = '#2dd4bf';
|
|
17
|
-
const ACCENT_SHARED_LIGHT = '#3b82f6';
|
|
18
|
-
const ACCENT_SHARED_DARK = '#60a5fa';
|
|
19
|
-
const ACCENT_UNCERTAIN_LIGHT = '#f59e0b';
|
|
20
|
-
const ACCENT_UNCERTAIN_DARK = '#fbbf24';
|
|
21
26
|
|
|
22
27
|
export interface PartitiveRelationStyle {
|
|
23
28
|
color: string;
|
|
@@ -25,38 +30,24 @@ export interface PartitiveRelationStyle {
|
|
|
25
30
|
opacity: number;
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Relation-level style (color + opacity) driven by completeness.
|
|
35
|
+
*
|
|
36
|
+
* Per-member multiplicity + delimiting rendering is via
|
|
37
|
+
* `rakeStrokeStyle()` from partitive-multiplicity.ts, not here —
|
|
38
|
+
* this helper is for the relation's overall frame + header badges.
|
|
39
|
+
*/
|
|
28
40
|
export function partitiveRelationStyle(
|
|
29
41
|
completeness: 'complete' | 'partial',
|
|
30
|
-
plurality: TypeSharedPluralityWire | null,
|
|
31
42
|
isDark: boolean,
|
|
32
43
|
): PartitiveRelationStyle {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
badgeClass = 'badge-blue';
|
|
39
|
-
if (plurality.isUncertain) {
|
|
40
|
-
badgeClass = 'badge-yellow';
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return { color: base, badgeClass, opacity };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function pluralityColor(
|
|
48
|
-
plurality: TypeSharedPluralityWire,
|
|
49
|
-
isDark: boolean,
|
|
50
|
-
): string {
|
|
51
|
-
if (plurality.isUncertain) return isDark ? ACCENT_UNCERTAIN_DARK : ACCENT_UNCERTAIN_LIGHT;
|
|
52
|
-
if (plurality.isShared) return isDark ? ACCENT_SHARED_DARK : ACCENT_SHARED_LIGHT;
|
|
53
|
-
return isDark ? BASE_DARK : BASE_LIGHT;
|
|
44
|
+
return {
|
|
45
|
+
color: isDark ? BASE_DARK : BASE_LIGHT,
|
|
46
|
+
badgeClass: 'badge-teal',
|
|
47
|
+
opacity: completeness === 'partial' ? 0.6 : 1.0,
|
|
48
|
+
};
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
export function completenessLabel(completeness: 'complete' | 'partial'): string {
|
|
57
52
|
return completeness === 'partial' ? 'Partial' : 'Complete';
|
|
58
53
|
}
|
|
59
|
-
|
|
60
|
-
export function certaintyLabel(certainty: 'confirmed' | 'possible'): string {
|
|
61
|
-
return certainty === 'possible' ? 'Possible' : 'Confirmed';
|
|
62
|
-
}
|
|
@@ -5,6 +5,8 @@ import { useVocabularyStore } from '../stores/vocabulary';
|
|
|
5
5
|
import { conceptUri } from '../adapters/model-bridge';
|
|
6
6
|
import { UriRouter } from '../adapters/UriRouter';
|
|
7
7
|
import type { PartitiveRelationWire, PartitiveMemberWire } from '../adapters/types';
|
|
8
|
+
import type { PartitiveMember } from 'glossarist/models';
|
|
9
|
+
import type { PartitivePresence, PartitiveCount } from '../utils/partitive-multiplicity';
|
|
8
10
|
import ConceptDetail from '../components/ConceptDetail.vue';
|
|
9
11
|
import RelationSphere from '../components/RelationSphere.vue';
|
|
10
12
|
import ShortcutsModal from '../components/ShortcutsModal.vue';
|
|
@@ -80,15 +82,15 @@ const partitiveRelationsForSphere = computed<PartitiveRelationWire[]>(() => {
|
|
|
80
82
|
: null;
|
|
81
83
|
if (!compRef.id || !compTarget) continue;
|
|
82
84
|
const partitives: PartitiveMemberWire[] = rel.partitives
|
|
83
|
-
.map(member => {
|
|
85
|
+
.map((member): PartitiveMemberWire | null => {
|
|
84
86
|
const ref = member.ref;
|
|
85
87
|
if (!ref?.id) return null;
|
|
86
|
-
const
|
|
87
|
-
? activeRegisterId.value
|
|
88
|
-
: activeRegisterId.value;
|
|
89
|
-
const uri = UriRouter.buildConceptUri(m.uriBase, targetRegister, ref.id);
|
|
88
|
+
const uri = UriRouter.buildConceptUri(m.uriBase, activeRegisterId.value, ref.id);
|
|
90
89
|
if (uri === sourceUri) return null;
|
|
91
|
-
|
|
90
|
+
const presence: PartitivePresence = member.presence;
|
|
91
|
+
const count: PartitiveCount = member.count;
|
|
92
|
+
const isDelimiting = member.isDelimiting;
|
|
93
|
+
return { uri, presence, count, isDelimiting };
|
|
92
94
|
})
|
|
93
95
|
.filter((x): x is PartitiveMemberWire => x !== null);
|
|
94
96
|
if (partitives.length === 0) continue;
|
|
@@ -97,13 +99,6 @@ const partitiveRelationsForSphere = computed<PartitiveRelationWire[]>(() => {
|
|
|
97
99
|
comprehensive: compTarget,
|
|
98
100
|
partitives,
|
|
99
101
|
completeness: rel.completeness,
|
|
100
|
-
plurality: rel.plurality
|
|
101
|
-
? {
|
|
102
|
-
isShared: rel.plurality.isShared,
|
|
103
|
-
isUncertain: rel.plurality.isUncertain,
|
|
104
|
-
sharedType: null,
|
|
105
|
-
}
|
|
106
|
-
: null,
|
|
107
102
|
register: activeRegisterId.value,
|
|
108
103
|
});
|
|
109
104
|
}
|