@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
|
@@ -25,6 +25,10 @@ import {
|
|
|
25
25
|
DATE_TYPES,
|
|
26
26
|
ConceptRef,
|
|
27
27
|
} from 'glossarist';
|
|
28
|
+
import {
|
|
29
|
+
isPartitivePresence,
|
|
30
|
+
isPartitiveCount,
|
|
31
|
+
} from '../utils/partitive-multiplicity';
|
|
28
32
|
import {
|
|
29
33
|
LetterSymbol,
|
|
30
34
|
GrammarInfo,
|
|
@@ -166,20 +170,22 @@ interface JsonLdLocalizedConcept {
|
|
|
166
170
|
|
|
167
171
|
interface JsonLdPartitiveMember {
|
|
168
172
|
'gl:ref'?: JsonLdRef;
|
|
173
|
+
/** ISO 704:2022 MECE: presence (required | optional) */
|
|
174
|
+
'gl:presence'?: string;
|
|
175
|
+
/** ISO 704:2022 MECE: count (exactly_one | at_least_one | multiple) */
|
|
176
|
+
'gl:count'?: string;
|
|
177
|
+
/** ISO 704:2022: delimiting part (3× stroke width in diagrams) */
|
|
178
|
+
'gl:isDelimiting'?: boolean;
|
|
179
|
+
/** Legacy v3 field — one-string multiplicity, split into presence+count */
|
|
180
|
+
'gl:multiplicity'?: string;
|
|
181
|
+
/** Legacy v2 field — migrated to presence+count */
|
|
169
182
|
'gl:certainty'?: string;
|
|
170
183
|
}
|
|
171
184
|
|
|
172
|
-
interface JsonLdTypeSharedPlurality {
|
|
173
|
-
'gl:isShared'?: boolean;
|
|
174
|
-
'gl:isUncertain'?: boolean;
|
|
175
|
-
'gl:sharedType'?: JsonLdRef;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
185
|
interface JsonLdPartitiveRelation {
|
|
179
186
|
'gl:comprehensive'?: JsonLdRef;
|
|
180
187
|
'gl:hasPartitive'?: JsonLdPartitiveMember[];
|
|
181
188
|
'gl:completeness'?: string;
|
|
182
|
-
'gl:hasPlurality'?: JsonLdTypeSharedPlurality;
|
|
183
189
|
'gl:criterion'?: Record<string, string> | string;
|
|
184
190
|
}
|
|
185
191
|
|
|
@@ -592,7 +598,23 @@ function mapPartitiveRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<stri
|
|
|
592
598
|
const ref = m['gl:ref'] ? mapRefFromJsonLd(m['gl:ref']) : null;
|
|
593
599
|
if (!ref) return null;
|
|
594
600
|
const out: Record<string, unknown> = { ref };
|
|
595
|
-
|
|
601
|
+
// ISO 704:2022 MECE: prefer presence × count from JSON-LD. Fall back
|
|
602
|
+
// to migrating the legacy one-string `multiplicity` or v2 `certainty`
|
|
603
|
+
// so data in transit from older glossarist versions still loads.
|
|
604
|
+
const presence = m['gl:presence'];
|
|
605
|
+
const count = m['gl:count'];
|
|
606
|
+
if (isPartitivePresence(presence) && isPartitiveCount(count)) {
|
|
607
|
+
out.presence = presence;
|
|
608
|
+
out.count = count;
|
|
609
|
+
} else {
|
|
610
|
+
const raw = m['gl:multiplicity'] ?? splitLegacyCertainty(m['gl:certainty']);
|
|
611
|
+
if (raw) {
|
|
612
|
+
const parts = splitMultiplicity(raw);
|
|
613
|
+
out.presence = parts.presence;
|
|
614
|
+
out.count = parts.count;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
if (m['gl:isDelimiting'] === true) out.is_delimiting = true;
|
|
596
618
|
return out;
|
|
597
619
|
})
|
|
598
620
|
.filter((m): m is Record<string, unknown> => m !== null);
|
|
@@ -608,17 +630,6 @@ function mapPartitiveRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<stri
|
|
|
608
630
|
|
|
609
631
|
if (r['gl:completeness']) out.completeness = r['gl:completeness'];
|
|
610
632
|
|
|
611
|
-
const plurality = r['gl:hasPlurality'];
|
|
612
|
-
if (plurality && typeof plurality['gl:isShared'] === 'boolean') {
|
|
613
|
-
const p: Record<string, unknown> = { isShared: plurality['gl:isShared'] };
|
|
614
|
-
if (typeof plurality['gl:isUncertain'] === 'boolean') {
|
|
615
|
-
p.isUncertain = plurality['gl:isUncertain'];
|
|
616
|
-
}
|
|
617
|
-
const sharedType = plurality['gl:sharedType'] ? mapRefFromJsonLd(plurality['gl:sharedType']) : null;
|
|
618
|
-
if (sharedType) p.sharedType = sharedType;
|
|
619
|
-
out.plurality = p;
|
|
620
|
-
}
|
|
621
|
-
|
|
622
633
|
if (r['gl:criterion']) {
|
|
623
634
|
out.criterion = typeof r['gl:criterion'] === 'string'
|
|
624
635
|
? { default: r['gl:criterion'] }
|
|
@@ -628,6 +639,24 @@ function mapPartitiveRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<stri
|
|
|
628
639
|
return out;
|
|
629
640
|
}
|
|
630
641
|
|
|
642
|
+
/** Migrate legacy v2 certainty → old v3 multiplicity string. */
|
|
643
|
+
function splitLegacyCertainty(certainty: string | undefined): string | null {
|
|
644
|
+
if (!certainty) return null;
|
|
645
|
+
return certainty === 'possible' ? 'optional' : 'compulsory';
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Split the legacy one-string multiplicity into MECE presence × count. */
|
|
649
|
+
function splitMultiplicity(m: string): { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' } {
|
|
650
|
+
const LEGACY_MAP: Record<string, { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' }> = {
|
|
651
|
+
compulsory: { presence: 'required', count: 'exactly_one' },
|
|
652
|
+
optional: { presence: 'optional', count: 'exactly_one' },
|
|
653
|
+
compulsory_multiple: { presence: 'required', count: 'multiple' },
|
|
654
|
+
optional_multiple: { presence: 'optional', count: 'multiple' },
|
|
655
|
+
compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
|
|
656
|
+
};
|
|
657
|
+
return LEGACY_MAP[m] ?? { presence: 'required', count: 'exactly_one' };
|
|
658
|
+
}
|
|
659
|
+
|
|
631
660
|
function conceptFromJsonLd(doc: JsonLdConcept): Concept {
|
|
632
661
|
const id = String(doc['gl:identifier'] ?? doc['@id']?.split('/').pop() ?? '');
|
|
633
662
|
const localizations: Record<string, unknown> = {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Local module augmentation for glossarist 0.4.
|
|
1
|
+
// Local module augmentation for glossarist 0.4.26.
|
|
2
2
|
//
|
|
3
3
|
// Upstream's published src/models/index.d.ts declares ZERO classes for the
|
|
4
4
|
// non-verbal hierarchy (Figure, Table, Formula, FigureImage, NonVerbalEntity,
|
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
//
|
|
10
10
|
// This file declares the runtime shape so consumer code can be type-checked.
|
|
11
11
|
// DELETE this file when upstream ships proper declarations — tracked by
|
|
12
|
-
// PR glossarist/glossarist-js#31
|
|
12
|
+
// PR glossarist/glossarist-js#31.
|
|
13
13
|
//
|
|
14
|
-
// v2 PartitiveRelation block (below) tracks glossarist 0.4.
|
|
15
|
-
// exports PartitiveRelation/PartitiveMember/
|
|
16
|
-
//
|
|
17
|
-
//
|
|
14
|
+
// v2 PartitiveRelation block (below) tracks glossarist 0.4.26 — the runtime
|
|
15
|
+
// exports PartitiveRelation/PartitiveMember from src/models/index.js, but
|
|
16
|
+
// the d.ts has not been updated. Remove the v2 block when upstream PRs the
|
|
17
|
+
// d.ts changes.
|
|
18
18
|
|
|
19
19
|
import type { ConceptSource, Citation, GlossaristModel } from 'glossarist';
|
|
20
|
+
import type { PartitivePresence, PartitiveCount } from '../../utils/partitive-multiplicity';
|
|
20
21
|
|
|
21
22
|
declare module 'glossarist' {
|
|
22
23
|
class RegistrableModel extends GlossaristModel {
|
|
@@ -136,8 +137,9 @@ declare module 'glossarist' {
|
|
|
136
137
|
static fromJSON(data: Record<string, unknown>): BibliographyData;
|
|
137
138
|
}
|
|
138
139
|
|
|
139
|
-
// ──
|
|
140
|
+
// ── PartitiveRelation (glossarist 0.4.26 — MECE presence × count) ─
|
|
140
141
|
// Runtime exports these from src/models/index.js; d.ts is stale.
|
|
142
|
+
// (Tracked by PR glossarist/glossarist-js#31.)
|
|
141
143
|
|
|
142
144
|
type Completeness = 'complete' | 'partial';
|
|
143
145
|
const COMPLETENESS: { readonly COMPLETE: 'complete'; readonly PARTIAL: 'partial' };
|
|
@@ -145,52 +147,63 @@ declare module 'glossarist' {
|
|
|
145
147
|
const DEFAULT_COMPLETENESS: Completeness;
|
|
146
148
|
function isValidCompleteness(value: unknown): value is Completeness;
|
|
147
149
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
function
|
|
150
|
+
const PARTITIVE_PRESENCE: { readonly REQUIRED: 'required'; readonly OPTIONAL: 'optional' } & { readonly VALUES: readonly ['required', 'optional'] };
|
|
151
|
+
type PartitivePresence = (typeof PARTITIVE_PRESENCE.VALUES)[number];
|
|
152
|
+
const PARTITIVE_PRESENCE_VALUES: readonly PartitivePresence[];
|
|
153
|
+
const DEFAULT_PRESENCE: PartitivePresence;
|
|
154
|
+
function isValidPresence(value: unknown): value is PartitivePresence;
|
|
153
155
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
156
|
+
const PARTITIVE_COUNT: { readonly EXACTLY_ONE: 'exactly_one'; readonly AT_LEAST_ONE: 'at_least_one'; readonly MULTIPLE: 'multiple' } & { readonly VALUES: readonly ['exactly_one', 'at_least_one', 'multiple'] };
|
|
157
|
+
type PartitiveCount = (typeof PARTITIVE_COUNT.VALUES)[number];
|
|
158
|
+
const PARTITIVE_COUNT_VALUES: readonly PartitiveCount[];
|
|
159
|
+
const DEFAULT_COUNT: PartitiveCount;
|
|
160
|
+
function isValidCount(value: unknown): value is PartitiveCount;
|
|
161
|
+
|
|
162
|
+
type Multiplicity =
|
|
163
|
+
| 'compulsory' | 'optional' | 'compulsory_multiple'
|
|
164
|
+
| 'optional_multiple' | 'compulsory_at_least_one';
|
|
165
|
+
const MULTIPLICITY: Record<string, Multiplicity>;
|
|
166
|
+
const MULTIPLICITY_VALUES: readonly Multiplicity[];
|
|
167
|
+
const DEFAULT_MULTIPLICITY: Multiplicity;
|
|
168
|
+
function isValidMultiplicity(value: unknown): value is Multiplicity;
|
|
169
|
+
function multiplicityFromPair(presence: PartitivePresence, count: PartitiveCount): Multiplicity;
|
|
170
|
+
function pairFromMultiplicity(name: Multiplicity): { presence: PartitivePresence; count: PartitiveCount };
|
|
171
|
+
|
|
172
|
+
interface Concept {
|
|
173
|
+
readonly partitiveRelations: PartitiveRelation[];
|
|
169
174
|
}
|
|
170
175
|
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
// Re-exposed here so callers importing from the top-level `glossarist`
|
|
177
|
+
// entry get the MECE-augmented shape, not the stale d.ts type.
|
|
178
|
+
export class PartitiveMember extends GlossaristModel {
|
|
179
|
+
constructor(data?: {
|
|
180
|
+
ref?: ConceptRef;
|
|
181
|
+
presence?: PartitivePresence;
|
|
182
|
+
count?: PartitiveCount;
|
|
183
|
+
is_delimiting?: boolean;
|
|
184
|
+
});
|
|
173
185
|
readonly ref: ConceptRef;
|
|
174
|
-
readonly
|
|
175
|
-
readonly
|
|
176
|
-
readonly
|
|
177
|
-
|
|
186
|
+
readonly presence: PartitivePresence;
|
|
187
|
+
readonly count: PartitiveCount;
|
|
188
|
+
readonly is_delimiting: boolean;
|
|
189
|
+
readonly isRequired: boolean;
|
|
190
|
+
readonly isOptional: boolean;
|
|
191
|
+
get isDelimiting(): boolean;
|
|
192
|
+
toJSON(): { ref: ReturnType<ConceptRef['toJSON']>; presence?: PartitivePresence; count?: PartitiveCount; is_delimiting?: boolean };
|
|
178
193
|
static fromJSON(data: Record<string, unknown>): PartitiveMember;
|
|
179
194
|
static identityOf(value: unknown): string;
|
|
180
195
|
}
|
|
181
196
|
|
|
182
|
-
class PartitiveRelation extends GlossaristModel {
|
|
197
|
+
export class PartitiveRelation extends GlossaristModel {
|
|
183
198
|
constructor(data?: {
|
|
184
199
|
comprehensive?: ConceptRef;
|
|
185
200
|
partitives?: PartitiveMember[];
|
|
186
201
|
completeness?: Completeness;
|
|
187
|
-
plurality?: TypeSharedPlurality;
|
|
188
202
|
criterion?: Record<string, string> | string;
|
|
189
203
|
});
|
|
190
204
|
readonly comprehensive: ConceptRef;
|
|
191
205
|
readonly partitives: PartitiveMember[];
|
|
192
206
|
readonly completeness: Completeness;
|
|
193
|
-
readonly plurality: TypeSharedPlurality | null;
|
|
194
207
|
readonly criterion: Record<string, string> | null;
|
|
195
208
|
readonly isComplete: boolean;
|
|
196
209
|
readonly isPartial: boolean;
|
|
@@ -201,16 +214,11 @@ declare module 'glossarist' {
|
|
|
201
214
|
comprehensive: ReturnType<ConceptRef['toJSON']>;
|
|
202
215
|
partitives: ReturnType<PartitiveMember['toJSON']>[];
|
|
203
216
|
completeness: Completeness;
|
|
204
|
-
plurality?: ReturnType<TypeSharedPlurality['toJSON']>;
|
|
205
217
|
criterion?: Record<string, string>;
|
|
206
218
|
};
|
|
207
219
|
static fromJSON(data: Record<string, unknown>): PartitiveRelation;
|
|
208
220
|
static identityOf(value: unknown): string;
|
|
209
221
|
}
|
|
210
|
-
|
|
211
|
-
interface Concept {
|
|
212
|
-
readonly partitiveRelations: PartitiveRelation[];
|
|
213
|
-
}
|
|
214
222
|
}
|
|
215
223
|
|
|
216
224
|
// Re-declare the same exports under the 'glossarist/models' subpath so
|
|
@@ -223,11 +231,27 @@ declare module 'glossarist/models' {
|
|
|
223
231
|
export const DEFAULT_COMPLETENESS: Completeness;
|
|
224
232
|
export function isValidCompleteness(value: unknown): value is Completeness;
|
|
225
233
|
|
|
226
|
-
export
|
|
227
|
-
export
|
|
228
|
-
export const
|
|
229
|
-
export const
|
|
230
|
-
export function
|
|
234
|
+
export const PARTITIVE_PRESENCE: { readonly REQUIRED: 'required'; readonly OPTIONAL: 'optional' } & { readonly VALUES: readonly ['required', 'optional'] };
|
|
235
|
+
export type PartitivePresence = (typeof PARTITIVE_PRESENCE.VALUES)[number];
|
|
236
|
+
export const PARTITIVE_PRESENCE_VALUES: readonly PartitivePresence[];
|
|
237
|
+
export const DEFAULT_PRESENCE: PartitivePresence;
|
|
238
|
+
export function isValidPresence(value: unknown): value is PartitivePresence;
|
|
239
|
+
|
|
240
|
+
export const PARTITIVE_COUNT: { readonly EXACTLY_ONE: 'exactly_one'; readonly AT_LEAST_ONE: 'at_least_one'; readonly MULTIPLE: 'multiple' } & { readonly VALUES: readonly ['exactly_one', 'at_least_one', 'multiple'] };
|
|
241
|
+
export type PartitiveCount = (typeof PARTITIVE_COUNT.VALUES)[number];
|
|
242
|
+
export const PARTITIVE_COUNT_VALUES: readonly PartitiveCount[];
|
|
243
|
+
export const DEFAULT_COUNT: PartitiveCount;
|
|
244
|
+
export function isValidCount(value: unknown): value is PartitiveCount;
|
|
245
|
+
|
|
246
|
+
export type Multiplicity =
|
|
247
|
+
| 'compulsory' | 'optional' | 'compulsory_multiple'
|
|
248
|
+
| 'optional_multiple' | 'compulsory_at_least_one';
|
|
249
|
+
export const MULTIPLICITY: Record<string, Multiplicity>;
|
|
250
|
+
export const MULTIPLICITY_VALUES: readonly Multiplicity[];
|
|
251
|
+
export const DEFAULT_MULTIPLICITY: Multiplicity;
|
|
252
|
+
export function isValidMultiplicity(value: unknown): value is Multiplicity;
|
|
253
|
+
export function multiplicityFromPair(presence: PartitivePresence, count: PartitiveCount): Multiplicity;
|
|
254
|
+
export function pairFromMultiplicity(name: Multiplicity): { presence: PartitivePresence; count: PartitiveCount };
|
|
231
255
|
|
|
232
256
|
export class TypeSharedPlurality extends GlossaristModel {
|
|
233
257
|
constructor(data?: {
|
|
@@ -246,13 +270,23 @@ declare module 'glossarist/models' {
|
|
|
246
270
|
static fromJSON(data: Record<string, unknown>): TypeSharedPlurality;
|
|
247
271
|
}
|
|
248
272
|
|
|
273
|
+
// ── PartitiveRelation (glossarist 0.4.26 — MECE presence × count) ─
|
|
274
|
+
|
|
249
275
|
export class PartitiveMember extends GlossaristModel {
|
|
250
|
-
constructor(data?: {
|
|
276
|
+
constructor(data?: {
|
|
277
|
+
ref?: ConceptRef;
|
|
278
|
+
presence?: PartitivePresence;
|
|
279
|
+
count?: PartitiveCount;
|
|
280
|
+
is_delimiting?: boolean;
|
|
281
|
+
});
|
|
251
282
|
readonly ref: ConceptRef;
|
|
252
|
-
readonly
|
|
253
|
-
readonly
|
|
254
|
-
readonly
|
|
255
|
-
|
|
283
|
+
readonly presence: PartitivePresence;
|
|
284
|
+
readonly count: PartitiveCount;
|
|
285
|
+
readonly is_delimiting: boolean;
|
|
286
|
+
readonly isRequired: boolean;
|
|
287
|
+
readonly isOptional: boolean;
|
|
288
|
+
get isDelimiting(): boolean;
|
|
289
|
+
toJSON(): { ref: ReturnType<ConceptRef['toJSON']>; presence?: PartitivePresence; count?: PartitiveCount; is_delimiting?: boolean };
|
|
256
290
|
static fromJSON(data: Record<string, unknown>): PartitiveMember;
|
|
257
291
|
static identityOf(value: unknown): string;
|
|
258
292
|
}
|
|
@@ -262,13 +296,11 @@ declare module 'glossarist/models' {
|
|
|
262
296
|
comprehensive?: ConceptRef;
|
|
263
297
|
partitives?: PartitiveMember[];
|
|
264
298
|
completeness?: Completeness;
|
|
265
|
-
plurality?: TypeSharedPlurality;
|
|
266
299
|
criterion?: Record<string, string> | string;
|
|
267
300
|
});
|
|
268
301
|
readonly comprehensive: ConceptRef;
|
|
269
302
|
readonly partitives: PartitiveMember[];
|
|
270
303
|
readonly completeness: Completeness;
|
|
271
|
-
readonly plurality: TypeSharedPlurality | null;
|
|
272
304
|
readonly criterion: Record<string, string> | null;
|
|
273
305
|
readonly isComplete: boolean;
|
|
274
306
|
readonly isPartial: boolean;
|
|
@@ -279,7 +311,6 @@ declare module 'glossarist/models' {
|
|
|
279
311
|
comprehensive: ReturnType<ConceptRef['toJSON']>;
|
|
280
312
|
partitives: ReturnType<PartitiveMember['toJSON']>[];
|
|
281
313
|
completeness: Completeness;
|
|
282
|
-
plurality?: ReturnType<TypeSharedPlurality['toJSON']>;
|
|
283
314
|
criterion?: Record<string, string>;
|
|
284
315
|
};
|
|
285
316
|
static fromJSON(data: Record<string, unknown>): PartitiveRelation;
|
package/src/adapters/types.ts
CHANGED
|
@@ -168,20 +168,15 @@ export interface PartitiveRelationWire {
|
|
|
168
168
|
comprehensive: string;
|
|
169
169
|
partitives: PartitiveMemberWire[];
|
|
170
170
|
completeness: 'complete' | 'partial';
|
|
171
|
-
plurality: TypeSharedPluralityWire | null;
|
|
172
171
|
criterion?: Record<string, string>;
|
|
173
172
|
register: string;
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
export interface PartitiveMemberWire {
|
|
177
176
|
uri: string;
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
export interface TypeSharedPluralityWire {
|
|
182
|
-
isShared: boolean;
|
|
183
|
-
isUncertain: boolean;
|
|
184
|
-
sharedType?: string | null;
|
|
177
|
+
presence: 'required' | 'optional';
|
|
178
|
+
count: 'exactly_one' | 'at_least_one' | 'multiple';
|
|
179
|
+
isDelimiting: boolean;
|
|
185
180
|
}
|
|
186
181
|
|
|
187
182
|
export interface GraphNode {
|
|
@@ -1,46 +1,65 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/**
|
|
3
3
|
* PartitiveRelationDiagram — renders a single PartitiveRelation as an
|
|
4
|
-
* ISO 704 rake/bracket diagram in SVG.
|
|
4
|
+
* ISO 704:2022 rake/bracket diagram in SVG.
|
|
5
5
|
*
|
|
6
|
-
* Layout
|
|
6
|
+
* Layout:
|
|
7
7
|
*
|
|
8
8
|
* [comprehensive]
|
|
9
9
|
* │
|
|
10
|
-
* │ ←
|
|
11
|
-
* ┌───────────────┴───────────────┐ ←
|
|
10
|
+
* │ ← stem (always single solid — relation frame)
|
|
11
|
+
* ┌───────────────┴───────────────┐ ← spine (always single solid — relation frame)
|
|
12
12
|
* │ │ │
|
|
13
|
-
* │ │ │ ←
|
|
13
|
+
* │ │ │ ← drops (per-member multiplicity + delimiting)
|
|
14
14
|
* [part1] [part2] [part3]
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
16
|
+
* Per ISO 704:2022, each drop's line style encodes its multiplicity +
|
|
17
|
+
* is_delimiting (see utils/partitive-multiplicity.ts for the registry):
|
|
18
|
+
*
|
|
19
|
+
* multiplicity is_delimiting visual
|
|
20
|
+
* ------------------- -------------- --------------------------------
|
|
21
|
+
* compulsory false 1 solid @ 1.5px
|
|
22
|
+
* compulsory true 1 solid @ 4.5px (bold, 3× width)
|
|
23
|
+
* optional false 1 dashed @ 1.5px
|
|
24
|
+
* optional true 1 dashed @ 4.5px
|
|
25
|
+
* compulsory_multiple false 2 solid @ 1.5px
|
|
26
|
+
* compulsory_multiple true 2 solid @ 4.5px
|
|
27
|
+
* optional_multiple false 2 dashed @ 1.5px
|
|
28
|
+
* optional_multiple true 2 dashed @ 4.5px
|
|
29
|
+
* compulsory_at_least_one false 1 solid + 1 dashed @ 1.5px
|
|
30
|
+
* compulsory_at_least_one true 1 solid + 1 dashed @ 4.5px
|
|
31
|
+
*
|
|
32
|
+
* completeness === 'partial' : spine extends past the last tooth
|
|
33
|
+
* (continued backline — "more exist but
|
|
34
|
+
* not shown")
|
|
23
35
|
*
|
|
24
36
|
* Node sizing is designation-aware: the longest label drives the node
|
|
25
37
|
* width (clamped to [MIN_NODE_W, MAX_NODE_W]); labels that don't fit
|
|
26
|
-
* wrap to a second line via tspan.
|
|
27
|
-
* designations ('system of quantities') instead of bare IDs ('1.3').
|
|
38
|
+
* wrap to a second line via tspan.
|
|
28
39
|
*/
|
|
29
40
|
import { computed } from 'vue';
|
|
30
|
-
import
|
|
41
|
+
import {
|
|
42
|
+
rakeStrokeStyle,
|
|
43
|
+
type PartitivePresence,
|
|
44
|
+
type PartitiveCount,
|
|
45
|
+
type RakeStrokeStyle,
|
|
46
|
+
} from '../utils/partitive-multiplicity';
|
|
31
47
|
|
|
32
48
|
export interface PartitiveMemberLabeled {
|
|
33
49
|
uri: string;
|
|
34
50
|
label: string;
|
|
35
|
-
|
|
51
|
+
presence: PartitivePresence;
|
|
52
|
+
count: PartitiveCount;
|
|
53
|
+
isDelimiting: boolean;
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
const props = withDefaults(defineProps<{
|
|
39
57
|
comprehensiveLabel: string;
|
|
40
58
|
partitives: PartitiveMemberLabeled[];
|
|
41
59
|
completeness: 'complete' | 'partial';
|
|
42
|
-
plurality: TypeSharedPluralityWire | null;
|
|
43
60
|
criterion?: string | null;
|
|
61
|
+
/** Optional per-member tooltip string. */
|
|
62
|
+
memberTooltip?: (m: { presence: PartitivePresence; count: PartitiveCount; isDelimiting: boolean }) => string;
|
|
44
63
|
}>(), {
|
|
45
64
|
criterion: null,
|
|
46
65
|
});
|
|
@@ -59,29 +78,19 @@ const SPINE_TO_NODE = 32;
|
|
|
59
78
|
const GAP_X = 16;
|
|
60
79
|
const PADDING_X = 20;
|
|
61
80
|
const PADDING_Y = 14;
|
|
62
|
-
const DOUBLE_GAP = 3;
|
|
81
|
+
const DOUBLE_GAP = 3; // gap between the two lines of a close-set double
|
|
63
82
|
const PARTIAL_TAIL = 32;
|
|
64
83
|
const DASH_PATTERN = '4 3';
|
|
65
|
-
const CHAR_W = 6.2;
|
|
66
|
-
const NODE_TEXT_PAD = 16;
|
|
67
|
-
|
|
68
|
-
type LineVariant = 'single' | 'double-solid' | 'solid-plus-dashed';
|
|
69
|
-
|
|
70
|
-
const lineVariant = computed<LineVariant>(() => {
|
|
71
|
-
if (!props.plurality?.isShared) return 'single';
|
|
72
|
-
return props.plurality.isUncertain ? 'solid-plus-dashed' : 'double-solid';
|
|
73
|
-
});
|
|
84
|
+
const CHAR_W = 6.2; // approx for 11px sans-serif
|
|
85
|
+
const NODE_TEXT_PAD = 16;
|
|
86
|
+
const FRAME_STROKE_WIDTH = 1.4; // stem + spine use this width always
|
|
74
87
|
|
|
75
88
|
/** Estimated pixel width of a string at 11px sans-serif. */
|
|
76
89
|
function estimateTextWidth(text: string): number {
|
|
77
90
|
return text.length * CHAR_W;
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
/**
|
|
81
|
-
* Wrap a label to at most 2 lines, breaking on word boundaries.
|
|
82
|
-
* If a single word exceeds the limit, it stays on its own line and
|
|
83
|
-
* gets truncated with an ellipsis.
|
|
84
|
-
*/
|
|
93
|
+
/** Wrap a label to at most 2 lines, breaking on word boundaries. */
|
|
85
94
|
function wrapLabel(text: string, maxCharsPerLine: number): string[] {
|
|
86
95
|
if (text.length <= maxCharsPerLine) return [text];
|
|
87
96
|
const words = text.split(/\s+/);
|
|
@@ -89,7 +98,6 @@ function wrapLabel(text: string, maxCharsPerLine: number): string[] {
|
|
|
89
98
|
let current = '';
|
|
90
99
|
for (const w of words) {
|
|
91
100
|
if (w.length > maxCharsPerLine) {
|
|
92
|
-
// word itself too long — flush current, push truncated word
|
|
93
101
|
if (current) { lines.push(current); current = ''; }
|
|
94
102
|
lines.push(w.slice(0, Math.max(1, maxCharsPerLine - 1)) + '…');
|
|
95
103
|
continue;
|
|
@@ -132,7 +140,12 @@ const layout = computed(() => {
|
|
|
132
140
|
const partitiveSlots = props.partitives.map((member, i) => {
|
|
133
141
|
const x = PADDING_X + nodeW / 2 + i * (nodeW + GAP_X);
|
|
134
142
|
const labelLines = wrapped[i + 1]; // index 0 is comprehensive
|
|
135
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
member,
|
|
145
|
+
x,
|
|
146
|
+
labelLines,
|
|
147
|
+
style: rakeStrokeStyle(member.presence, member.count, member.isDelimiting),
|
|
148
|
+
};
|
|
136
149
|
});
|
|
137
150
|
|
|
138
151
|
const leftX = partitiveSlots[0]?.x ?? centerX;
|
|
@@ -157,19 +170,38 @@ const layout = computed(() => {
|
|
|
157
170
|
};
|
|
158
171
|
});
|
|
159
172
|
|
|
160
|
-
|
|
161
|
-
|
|
173
|
+
/**
|
|
174
|
+
* Build SVG stroke attributes for the relation frame (stem + spine).
|
|
175
|
+
*
|
|
176
|
+
* The frame is structural — always a single solid line at standard
|
|
177
|
+
* width. Per-member multiplicity + delimiting are encoded on the drops
|
|
178
|
+
* only (MECE: frame vs drops).
|
|
179
|
+
*/
|
|
180
|
+
const frameStrokeAttrs = computed(() => ({
|
|
181
|
+
stroke: 'currentColor',
|
|
182
|
+
'stroke-width': FRAME_STROKE_WIDTH,
|
|
183
|
+
}));
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Build SVG stroke attributes for one of a drop's parallel lines.
|
|
187
|
+
* The drop may have 1 or 2 parallel lines per its multiplicity.
|
|
188
|
+
*/
|
|
189
|
+
function dropStrokeAttrs(style: RakeStrokeStyle, dashed: boolean, opacity = 1) {
|
|
162
190
|
return {
|
|
163
191
|
stroke: 'currentColor',
|
|
164
|
-
'stroke-width':
|
|
192
|
+
'stroke-width': style.strokeWidth,
|
|
165
193
|
opacity,
|
|
166
|
-
...(
|
|
194
|
+
...(dashed ? { 'stroke-dasharray': DASH_PATTERN } : {}),
|
|
167
195
|
};
|
|
168
196
|
}
|
|
169
197
|
|
|
170
198
|
function emitNav(uri: string) {
|
|
171
199
|
emit('navigate', uri);
|
|
172
200
|
}
|
|
201
|
+
|
|
202
|
+
function tooltipFor(member: PartitiveMemberLabeled): string | undefined {
|
|
203
|
+
return props.memberTooltip?.({ presence: member.presence, count: member.count, isDelimiting: member.isDelimiting });
|
|
204
|
+
}
|
|
173
205
|
</script>
|
|
174
206
|
|
|
175
207
|
<template>
|
|
@@ -212,71 +244,53 @@ function emitNav(uri: string) {
|
|
|
212
244
|
class="text-[9px] italic fill-emerald-800 dark:fill-emerald-200/80"
|
|
213
245
|
>{{ criterion }}</text>
|
|
214
246
|
|
|
215
|
-
<!-- Stem
|
|
247
|
+
<!-- Stem: comprehensive → spine (single solid frame) -->
|
|
216
248
|
<line
|
|
217
|
-
v-if="lineVariant === 'single'"
|
|
218
249
|
:x1="layout.centerX" :y1="layout.compY + layout.nodeH / 2"
|
|
219
250
|
:x2="layout.centerX" :y2="layout.spineY"
|
|
220
|
-
v-bind="
|
|
251
|
+
v-bind="frameStrokeAttrs"
|
|
221
252
|
/>
|
|
222
|
-
<template v-else>
|
|
223
|
-
<line
|
|
224
|
-
:x1="layout.centerX - DOUBLE_GAP / 2" :y1="layout.compY + layout.nodeH / 2"
|
|
225
|
-
:x2="layout.centerX - DOUBLE_GAP / 2" :y2="layout.spineY"
|
|
226
|
-
v-bind="strokeAttr('double-solid')"
|
|
227
|
-
/>
|
|
228
|
-
<line
|
|
229
|
-
:x1="layout.centerX + DOUBLE_GAP / 2" :y1="layout.compY + layout.nodeH / 2"
|
|
230
|
-
:x2="layout.centerX + DOUBLE_GAP / 2" :y2="layout.spineY"
|
|
231
|
-
v-bind="strokeAttr(lineVariant, 1, lineVariant === 'solid-plus-dashed' ? 'dash' : undefined)"
|
|
232
|
-
/>
|
|
233
|
-
</template>
|
|
234
253
|
|
|
235
|
-
<!-- Spine
|
|
254
|
+
<!-- Spine: leftmost tooth → rightmost tooth (single solid frame;
|
|
255
|
+
extended for completeness: 'partial') -->
|
|
236
256
|
<line
|
|
237
|
-
v-if="lineVariant === 'single'"
|
|
238
257
|
:x1="layout.leftX" :y1="layout.spineY"
|
|
239
258
|
:x2="layout.spineEndX" :y2="layout.spineY"
|
|
240
|
-
v-bind="
|
|
259
|
+
v-bind="frameStrokeAttrs"
|
|
241
260
|
/>
|
|
242
|
-
<template v-else>
|
|
243
|
-
<line
|
|
244
|
-
:x1="layout.leftX" :y1="layout.spineY - DOUBLE_GAP / 2"
|
|
245
|
-
:x2="layout.spineEndX" :y2="layout.spineY - DOUBLE_GAP / 2"
|
|
246
|
-
v-bind="strokeAttr('double-solid')"
|
|
247
|
-
/>
|
|
248
|
-
<line
|
|
249
|
-
:x1="layout.leftX" :y1="layout.spineY + DOUBLE_GAP / 2"
|
|
250
|
-
:x2="layout.spineEndX" :y2="layout.spineY + DOUBLE_GAP / 2"
|
|
251
|
-
v-bind="strokeAttr(lineVariant, 1, lineVariant === 'solid-plus-dashed' ? 'dash' : undefined)"
|
|
252
|
-
/>
|
|
253
|
-
</template>
|
|
254
261
|
|
|
255
262
|
<!-- Drops + partitive nodes -->
|
|
256
263
|
<g v-for="(slot, i) in layout.partitiveSlots" :key="i">
|
|
264
|
+
<!-- Primary drop line -->
|
|
257
265
|
<line
|
|
258
266
|
:x1="slot.x" :y1="layout.spineY"
|
|
259
267
|
:x2="slot.x" :y2="layout.partY - layout.nodeH / 2"
|
|
260
|
-
v-bind="
|
|
268
|
+
v-bind="dropStrokeAttrs(slot.style, slot.style.primaryDashed)"
|
|
269
|
+
/>
|
|
270
|
+
<!-- Secondary drop line (only for lineCount === 2; e.g. *_multiple, compulsory_at_least_one) -->
|
|
271
|
+
<line
|
|
272
|
+
v-if="slot.style.lineCount === 2"
|
|
273
|
+
:x1="slot.x + DOUBLE_GAP" :y1="layout.spineY"
|
|
274
|
+
:x2="slot.x + DOUBLE_GAP" :y2="layout.partY - layout.nodeH / 2"
|
|
275
|
+
v-bind="dropStrokeAttrs(slot.style, slot.style.secondaryDashed)"
|
|
261
276
|
/>
|
|
262
|
-
<g class="cursor-pointer" @click="emitNav(slot.member.uri)">
|
|
277
|
+
<g class="cursor-pointer" :title="tooltipFor(slot.member)" @click="emitNav(slot.member.uri)">
|
|
263
278
|
<rect
|
|
264
279
|
:x="slot.x - layout.nodeW / 2"
|
|
265
280
|
:y="layout.partY - layout.nodeH / 2"
|
|
266
281
|
:width="layout.nodeW"
|
|
267
282
|
:height="layout.nodeH"
|
|
268
283
|
rx="6"
|
|
269
|
-
:class="slot.
|
|
270
|
-
? 'fill-emerald-
|
|
284
|
+
:class="slot.style.strokeWidth > 2
|
|
285
|
+
? 'fill-emerald-100 dark:fill-emerald-900/50 stroke-current'
|
|
271
286
|
: 'fill-emerald-50 dark:fill-emerald-900/30 stroke-current'"
|
|
272
|
-
:stroke-
|
|
273
|
-
stroke-width="1.2"
|
|
287
|
+
:stroke-width="slot.style.strokeWidth > 2 ? 2.4 : 1.2"
|
|
274
288
|
/>
|
|
275
289
|
<text
|
|
276
290
|
:x="slot.x"
|
|
277
291
|
text-anchor="middle"
|
|
278
|
-
:class="slot.
|
|
279
|
-
? 'text-[11px] fill-emerald-
|
|
292
|
+
:class="slot.style.strokeWidth > 2
|
|
293
|
+
? 'text-[11px] fill-emerald-900 dark:fill-emerald-50 font-medium'
|
|
280
294
|
: 'text-[11px] fill-emerald-900 dark:fill-emerald-100'"
|
|
281
295
|
>
|
|
282
296
|
<tspan
|