@glossarist/concept-browser 0.7.78 → 0.7.80
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 +109 -39
- package/scripts/generate-data.mjs +63 -27
- package/src/__tests__/build-edges-redundancy.test.ts +101 -0
- package/src/__tests__/composables/use-concept-diff.test.ts +240 -0
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +132 -0
- package/src/__tests__/composables/use-dataset-stats.test.ts +96 -0
- package/src/__tests__/config/page-types.test.ts +82 -0
- package/src/__tests__/model-bridge-bridges.test.ts +3 -2
- package/src/__tests__/model-bridge-citation.test.ts +1 -1
- package/src/__tests__/ontology-registry.test.ts +2 -2
- package/src/__tests__/partitive-relation-styling.test.ts +82 -0
- package/src/__tests__/partitive-relation.test.ts +285 -0
- package/src/adapters/GraphDataSource.ts +16 -6
- package/src/adapters/dataset-stats.ts +13 -0
- package/src/adapters/non-verbal/glossarist-augment.d.ts +155 -4
- package/src/adapters/types.ts +27 -17
- package/src/components/AppSidebar.vue +14 -57
- package/src/components/ConceptDetail.vue +29 -0
- package/src/components/ConceptDiffModal.vue +120 -0
- package/src/components/ConceptDiffView.vue +192 -0
- package/src/components/ConceptEditionRail.vue +61 -36
- package/src/components/PartitiveRelationList.vue +139 -0
- package/src/components/groups/DatasetGroupRenderer.vue +14 -4
- package/src/components/groups/DefaultGroupSidebar.vue +28 -11
- package/src/components/groups/LineageGroupSidebar.vue +17 -29
- package/src/composables/use-concept-diff.ts +231 -0
- package/src/composables/use-concept-edges.ts +66 -45
- package/src/composables/use-dataset-stats.ts +52 -0
- package/src/config/page-types.ts +67 -0
- package/src/config/types.ts +6 -1
- package/src/config/use-site-config.ts +13 -29
- package/src/data/taxonomies.json +24 -1
- package/src/i18n/locales/eng.yml +6 -0
- package/src/utils/partitive-relation-styling.ts +62 -0
- package/src/views/DatasetView.vue +5 -3
- package/src/views/StatsView.vue +9 -22
- package/src/__tests__/hyperedge-styling.test.ts +0 -57
- package/src/__tests__/partitive-hyperedge.test.ts +0 -199
- package/src/components/PartitiveHyperedgeList.vue +0 -112
- package/src/islands/BibliographyIsland.vue +0 -26
- package/src/islands/ConceptDetailIsland.vue +0 -27
- package/src/islands/ConceptDiffIsland.vue +0 -214
- package/src/islands/ConceptIsland.vue +0 -102
- package/src/islands/GraphIsland.vue +0 -15
- package/src/islands/GroupRendererIsland.vue +0 -15
- package/src/islands/HeaderIsland.vue +0 -10
- package/src/islands/NonVerbalIsland.vue +0 -75
- package/src/islands/RelationSphereIsland.vue +0 -20
- package/src/islands/SearchIsland.vue +0 -23
- package/src/islands/SeriesCardIsland.vue +0 -12
- package/src/islands/ShortcutsIsland.vue +0 -13
- package/src/islands/SidebarIsland.vue +0 -15
- package/src/islands/SupersessionDiffIsland.vue +0 -267
- package/src/utils/hyperedge-styling.ts +0 -48
|
@@ -1,7 +1,20 @@
|
|
|
1
1
|
import { computed, type ComputedRef } from 'vue';
|
|
2
2
|
import type { Router } from 'vue-router';
|
|
3
|
-
import type {
|
|
4
|
-
|
|
3
|
+
import type {
|
|
4
|
+
Concept,
|
|
5
|
+
RelatedConcept,
|
|
6
|
+
PartitiveRelation as GlsPartitiveRelation,
|
|
7
|
+
PartitiveMember as GlsPartitiveMember,
|
|
8
|
+
TypeSharedPlurality as GlsTypeSharedPlurality,
|
|
9
|
+
ConceptRef,
|
|
10
|
+
} from 'glossarist';
|
|
11
|
+
import type {
|
|
12
|
+
Manifest,
|
|
13
|
+
GraphEdge,
|
|
14
|
+
PartitiveRelationWire,
|
|
15
|
+
PartitiveMemberWire,
|
|
16
|
+
TypeSharedPluralityWire,
|
|
17
|
+
} from '../adapters/types';
|
|
5
18
|
import { getFactory } from '../adapters/factory';
|
|
6
19
|
import { conceptUri } from '../adapters/model-bridge';
|
|
7
20
|
import { UriRouter } from '../adapters/UriRouter';
|
|
@@ -114,63 +127,71 @@ export function useConceptEdges(
|
|
|
114
127
|
});
|
|
115
128
|
return [...direct, ...derived].filter(edge => {
|
|
116
129
|
if (edge.type === 'broader_partitive' || edge.type === 'narrower_partitive') {
|
|
117
|
-
const
|
|
118
|
-
|
|
130
|
+
const refUri = resolveRefUri(edge.ref as { source: string | null; id: string | null } | null);
|
|
131
|
+
if (!refUri) return true;
|
|
132
|
+
const rels = conceptPartitiveRelations.value;
|
|
133
|
+
return !rels.some(rel =>
|
|
134
|
+
rel.comprehensive === refUri
|
|
135
|
+
|| rel.partitives.some(m => m.uri === refUri),
|
|
136
|
+
);
|
|
119
137
|
}
|
|
120
138
|
return true;
|
|
121
139
|
});
|
|
122
140
|
});
|
|
123
141
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
throw new Error(`Invalid partitive hyperedge marker: "${m}". Allowed: double, dashed`);
|
|
131
|
-
}
|
|
132
|
-
out.push(m as 'double' | 'dashed');
|
|
133
|
-
}
|
|
134
|
-
return out;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function resolveLocalizedContent(content: unknown): string | undefined {
|
|
138
|
-
if (content == null) return undefined;
|
|
139
|
-
if (typeof content === 'string') return content;
|
|
140
|
-
if (typeof content === 'object' && content !== null) {
|
|
141
|
-
const obj = content as Record<string, string>;
|
|
142
|
-
return obj[registerId.value] ?? obj.default ?? obj.eng ?? Object.values(obj)[0];
|
|
143
|
-
}
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Concept-level partitive hyperedges (one-to-many decompositions).
|
|
148
|
-
// Each hyperedge is resolved to concrete target URIs for display.
|
|
149
|
-
// Independent of binary `conceptRelated` — see TODO.hyperedge/00.
|
|
150
|
-
const conceptPartitiveHyperedges = computed<PartitiveHyperedge[]>(() => {
|
|
142
|
+
// Concept-level partitive relations (ISO 704 one-to-many decompositions).
|
|
143
|
+
// Reads the glossarist-js native model (`concept.partitiveRelations`)
|
|
144
|
+
// and projects each relation into a URI-resolved wire shape for display.
|
|
145
|
+
// Independent of binary `conceptRelated`.
|
|
146
|
+
// v2 shape per concept-model/TODO.partitive-relation-v2.
|
|
147
|
+
const conceptPartitiveRelations = computed<PartitiveRelationWire[]>(() => {
|
|
151
148
|
const source = conceptUriValue.value;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
const relations: readonly GlsPartitiveRelation[] = concept.value.partitiveRelations;
|
|
150
|
+
return relations
|
|
151
|
+
.map((rel): PartitiveRelationWire | null => {
|
|
152
|
+
const comprehensive = resolveConceptRefUri(rel.comprehensive);
|
|
155
153
|
if (!comprehensive) return null;
|
|
156
|
-
const
|
|
157
|
-
.map(
|
|
158
|
-
|
|
159
|
-
|
|
154
|
+
const partitives: PartitiveMemberWire[] = rel.partitives
|
|
155
|
+
.map((m: GlsPartitiveMember): PartitiveMemberWire | null => {
|
|
156
|
+
const uri = resolveConceptRefUri(m.ref);
|
|
157
|
+
if (!uri || uri === source) return null;
|
|
158
|
+
return { uri, certainty: m.certainty };
|
|
159
|
+
})
|
|
160
|
+
.filter((m: PartitiveMemberWire | null): m is PartitiveMemberWire => m !== null);
|
|
161
|
+
if (partitives.length === 0) return null;
|
|
162
|
+
|
|
160
163
|
return {
|
|
161
164
|
source,
|
|
162
165
|
comprehensive,
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
partitives,
|
|
167
|
+
completeness: rel.completeness,
|
|
168
|
+
plurality: projectPlurality(rel.plurality),
|
|
169
|
+
criterion: rel.criterion ?? undefined,
|
|
167
170
|
register: registerId.value,
|
|
168
171
|
};
|
|
169
172
|
})
|
|
170
|
-
.filter((
|
|
173
|
+
.filter((r: PartitiveRelationWire | null): r is PartitiveRelationWire => r !== null);
|
|
171
174
|
});
|
|
172
175
|
|
|
173
|
-
function
|
|
176
|
+
function projectPlurality(p: GlsTypeSharedPlurality | null): TypeSharedPluralityWire | null {
|
|
177
|
+
if (!p) return null;
|
|
178
|
+
const sharedTypeRef: ConceptRef | null = p.sharedType;
|
|
179
|
+
const sharedType = sharedTypeRef
|
|
180
|
+
? [sharedTypeRef.source, sharedTypeRef.id].filter(Boolean).join(':') || null
|
|
181
|
+
: null;
|
|
182
|
+
return {
|
|
183
|
+
isShared: p.isShared,
|
|
184
|
+
isUncertain: p.isUncertain,
|
|
185
|
+
sharedType,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function resolveConceptRefUri(ref: ConceptRef | null): string | null {
|
|
190
|
+
if (!ref) return null;
|
|
191
|
+
return resolveRefUri({ source: ref.source ?? null, id: ref.id ?? null });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function resolveRefUri(ref: { source: string | null; id: string | null } | null): string | null {
|
|
174
195
|
const target = resolveRelatedRef(ref);
|
|
175
196
|
if (!target) return null;
|
|
176
197
|
const m = store.manifests.get(target.registerId);
|
|
@@ -244,7 +265,7 @@ export function useConceptEdges(
|
|
|
244
265
|
edgeBadgeColor,
|
|
245
266
|
inverseEdgeType,
|
|
246
267
|
conceptRelated,
|
|
247
|
-
|
|
268
|
+
conceptPartitiveRelations,
|
|
248
269
|
resolveRelatedRef,
|
|
249
270
|
getResolvedRef,
|
|
250
271
|
relatedLabel,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useDatasetStats — loads stats.json for a dataset register.
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for the DatasetStats wire shape; both
|
|
5
|
+
* DatasetView (summary badges) and StatsView (full table) consume it.
|
|
6
|
+
*/
|
|
7
|
+
import { ref, watch, type Ref } from 'vue';
|
|
8
|
+
import type { DatasetStats } from '../adapters/dataset-stats';
|
|
9
|
+
|
|
10
|
+
export function useDatasetStats(
|
|
11
|
+
registerId: Ref<string> | (() => string),
|
|
12
|
+
baseUrl: string = import.meta.env.BASE_URL,
|
|
13
|
+
) {
|
|
14
|
+
const stats = ref<DatasetStats | null>(null);
|
|
15
|
+
const loading = ref(false);
|
|
16
|
+
const error = ref<string | null>(null);
|
|
17
|
+
|
|
18
|
+
const resolved = typeof registerId === 'function' ? registerId() : registerId.value;
|
|
19
|
+
|
|
20
|
+
async function load(id: string) {
|
|
21
|
+
if (!id) return;
|
|
22
|
+
loading.value = true;
|
|
23
|
+
error.value = null;
|
|
24
|
+
try {
|
|
25
|
+
const resp = await fetch(`${baseUrl}data/${id}/stats.json`);
|
|
26
|
+
if (resp.ok) {
|
|
27
|
+
stats.value = await resp.json() as DatasetStats;
|
|
28
|
+
} else {
|
|
29
|
+
stats.value = null;
|
|
30
|
+
}
|
|
31
|
+
} catch (e: unknown) {
|
|
32
|
+
stats.value = null;
|
|
33
|
+
error.value = e instanceof Error ? e.message : String(e);
|
|
34
|
+
} finally {
|
|
35
|
+
loading.value = false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (typeof registerId === 'function') {
|
|
40
|
+
// Re-fetch on app boot only; callers can call reload() to refresh.
|
|
41
|
+
load(resolved);
|
|
42
|
+
} else {
|
|
43
|
+
watch(registerId, id => load(id), { immediate: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function reload() {
|
|
47
|
+
const id = typeof registerId === 'function' ? registerId() : registerId.value;
|
|
48
|
+
load(id);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { stats, loading, error, reload };
|
|
52
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page type registry — OCP-compliant metadata for synthesized pages.
|
|
3
|
+
*
|
|
4
|
+
* Adding a new built-in page type = add one entry to PAGE_TYPES.
|
|
5
|
+
* `synthesizeGlobalPages` and `synthesizeDatasetPages` iterate the
|
|
6
|
+
* registry; no switches to edit.
|
|
7
|
+
*
|
|
8
|
+
* PageConfig.type stays a string — it tags the page's semantic kind
|
|
9
|
+
* for consumers (sidebar, future renderers). The router is the SSOT
|
|
10
|
+
* for route → view dispatch, not this registry.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the group-renderers.ts pattern: registry + lookup helper.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface PageTypeDefinition {
|
|
16
|
+
readonly type: string;
|
|
17
|
+
readonly route: string;
|
|
18
|
+
readonly title: string;
|
|
19
|
+
readonly icon: string;
|
|
20
|
+
readonly scope: 'global' | 'dataset';
|
|
21
|
+
/** When defined, the page is only synthesized when features[flag] is truthy
|
|
22
|
+
* (or, if the flag starts with '!', when features[flag.slice(1)] is falsy). */
|
|
23
|
+
readonly featureFlag?: string;
|
|
24
|
+
/** Synthesize automatically unless a declared page already uses this route. */
|
|
25
|
+
readonly autoSynthesize?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const PAGE_TYPES: readonly PageTypeDefinition[] = [
|
|
29
|
+
{ type: 'home', route: '', title: 'Home', icon: 'home', scope: 'global', autoSynthesize: true },
|
|
30
|
+
{ type: 'concepts', route: '', title: 'Concepts', icon: 'list', scope: 'dataset', autoSynthesize: true },
|
|
31
|
+
{ type: 'search', route: 'search', title: 'Search', icon: 'search', scope: 'global', featureFlag: '!search', autoSynthesize: true },
|
|
32
|
+
{ type: 'graph', route: 'graph', title: 'Graph', icon: 'graph', scope: 'global', featureFlag: '!graph', autoSynthesize: true },
|
|
33
|
+
{ type: 'ontology', route: 'ontology',title: 'Ontology', icon: 'schema', scope: 'global', featureFlag: '!ontology', autoSynthesize: true },
|
|
34
|
+
{ type: 'news', route: 'news', title: 'News', icon: 'newspaper', scope: 'global', featureFlag: 'news', autoSynthesize: true },
|
|
35
|
+
{ type: 'stats', route: 'stats', title: 'Statistics', icon: 'chart', scope: 'dataset', featureFlag: '!stats', autoSynthesize: true },
|
|
36
|
+
{ type: 'sources', route: 'sources', title: 'Sources', icon: 'database', scope: 'dataset', autoSynthesize: true },
|
|
37
|
+
{ type: 'about', route: 'about', title: 'About', icon: 'info', scope: 'dataset', featureFlag: '!about', autoSynthesize: true },
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
export type BuiltinPageType = typeof PAGE_TYPES[number]['type'];
|
|
41
|
+
|
|
42
|
+
const PAGE_TYPE_INDEX: ReadonlyMap<string, PageTypeDefinition> = new Map(
|
|
43
|
+
PAGE_TYPES.map(p => [p.type, p]),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export function pageTypeOf(type: string): PageTypeDefinition | undefined {
|
|
47
|
+
return PAGE_TYPE_INDEX.get(type);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function featureEnabled(features: Record<string, unknown> | undefined, flag: string): boolean {
|
|
51
|
+
if (flag.startsWith('!')) {
|
|
52
|
+
return features?.[flag.slice(1)] !== false;
|
|
53
|
+
}
|
|
54
|
+
return Boolean(features?.[flag]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Synthesize the auto-on pages for a given scope, respecting feature flags
|
|
58
|
+
* and skipping routes already declared by the user. */
|
|
59
|
+
export function synthesizePages(
|
|
60
|
+
scope: 'global' | 'dataset',
|
|
61
|
+
features: Record<string, unknown> | undefined,
|
|
62
|
+
declaredRoutes: ReadonlySet<string>,
|
|
63
|
+
): PageTypeDefinition[] {
|
|
64
|
+
return PAGE_TYPES.filter(p => p.scope === scope && p.autoSynthesize)
|
|
65
|
+
.filter(p => !p.featureFlag || featureEnabled(features, p.featureFlag))
|
|
66
|
+
.filter(p => !declaredRoutes.has(p.route));
|
|
67
|
+
}
|
package/src/config/types.ts
CHANGED
|
@@ -137,7 +137,12 @@ export const FORMAT_LABELS: Record<string, string> = {
|
|
|
137
137
|
|
|
138
138
|
// === Pages ===
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Page type tag. Any string is allowed — built-in tags are listed in
|
|
142
|
+
* `page-types.ts` PAGE_TYPES registry. Adding a new built-in type is
|
|
143
|
+
* a registry entry, not an edit to this union.
|
|
144
|
+
*/
|
|
145
|
+
export type PageType = string;
|
|
141
146
|
|
|
142
147
|
export interface PageConfig {
|
|
143
148
|
type: PageType;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ref, computed } from 'vue';
|
|
2
2
|
import type { PageConfig, SiteColors } from './types';
|
|
3
3
|
import type { DatasetGroup } from './types';
|
|
4
|
+
import { synthesizePages } from './page-types';
|
|
4
5
|
import { locale } from '../i18n';
|
|
5
6
|
|
|
6
7
|
export interface RuntimeSiteConfig {
|
|
@@ -128,43 +129,26 @@ function synthesizeGlobalPages(features?: Record<string, unknown>, pages?: PageC
|
|
|
128
129
|
const declared = pages?.filter(p => !p.datasetScoped) ?? [];
|
|
129
130
|
const declaredRoutes = new Set(declared.map(p => p.route));
|
|
130
131
|
|
|
131
|
-
const
|
|
132
|
-
{ type:
|
|
133
|
-
];
|
|
134
|
-
if (features?.search !== false && !declaredRoutes.has('search')) {
|
|
135
|
-
result.push({ type: 'custom', route: 'search', title: 'Search', icon: 'search' });
|
|
136
|
-
}
|
|
137
|
-
if (features?.graph !== false && !declaredRoutes.has('graph')) {
|
|
138
|
-
result.push({ type: 'custom', route: 'graph', title: 'Graph', icon: 'graph' });
|
|
139
|
-
}
|
|
140
|
-
if (features?.ontology !== false && !declaredRoutes.has('ontology')) {
|
|
141
|
-
result.push({ type: 'custom', route: 'ontology', title: 'Ontology', icon: 'schema' });
|
|
142
|
-
}
|
|
143
|
-
if (features?.news && !declaredRoutes.has('news')) {
|
|
144
|
-
result.push({ type: 'news', route: 'news', title: 'News', icon: 'newspaper' });
|
|
145
|
-
}
|
|
132
|
+
const synthesized = synthesizePages('global', features, declaredRoutes)
|
|
133
|
+
.map(p => ({ type: p.type, route: p.route, title: p.title, icon: p.icon }));
|
|
146
134
|
|
|
147
|
-
return [...
|
|
135
|
+
return [...synthesized, ...declared];
|
|
148
136
|
}
|
|
149
137
|
|
|
150
138
|
function synthesizeDatasetPages(features?: Record<string, unknown>, pages?: PageConfig[]): PageConfig[] {
|
|
151
139
|
const declared = pages?.filter(p => p.datasetScoped) ?? [];
|
|
152
140
|
const declaredRoutes = new Set(declared.map(p => p.route));
|
|
153
141
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
if (features?.about !== false && !declaredRoutes.has('about')) {
|
|
164
|
-
result.push({ type: 'about', route: 'about', title: 'About', icon: 'info', datasetScoped: true });
|
|
165
|
-
}
|
|
142
|
+
const synthesized = synthesizePages('dataset', features, declaredRoutes)
|
|
143
|
+
.map(p => ({
|
|
144
|
+
type: p.type,
|
|
145
|
+
route: p.route,
|
|
146
|
+
title: p.title,
|
|
147
|
+
icon: p.icon,
|
|
148
|
+
datasetScoped: true as const,
|
|
149
|
+
}));
|
|
166
150
|
|
|
167
|
-
return [...
|
|
151
|
+
return [...synthesized, ...declared];
|
|
168
152
|
}
|
|
169
153
|
|
|
170
154
|
export function useSiteConfig() {
|
package/src/data/taxonomies.json
CHANGED
|
@@ -45,6 +45,12 @@
|
|
|
45
45
|
"iri": "gloss:status/retired",
|
|
46
46
|
"prefLabel": "retired",
|
|
47
47
|
"definition": "The concept has been retired from active use."
|
|
48
|
+
},
|
|
49
|
+
"external": {
|
|
50
|
+
"id": "external",
|
|
51
|
+
"iri": "gloss:status/external",
|
|
52
|
+
"prefLabel": "external",
|
|
53
|
+
"definition": "An external concept: referenced but not defined here. May be resolved later when another dataset is loaded (concept-model TODO.partitive-relation-v2 item 06)."
|
|
48
54
|
}
|
|
49
55
|
},
|
|
50
56
|
"colors": {
|
|
@@ -54,7 +60,8 @@
|
|
|
54
60
|
"not_valid": "bg-red-50 text-red-700",
|
|
55
61
|
"invalid": "bg-red-50 text-red-700",
|
|
56
62
|
"superseded": "bg-red-50 text-red-700",
|
|
57
|
-
"retired": "badge-gray"
|
|
63
|
+
"retired": "badge-gray",
|
|
64
|
+
"external": "bg-purple-50 text-purple-700"
|
|
58
65
|
}
|
|
59
66
|
},
|
|
60
67
|
"entryStatus": {
|
|
@@ -264,6 +271,22 @@
|
|
|
264
271
|
"category": "lifecycle",
|
|
265
272
|
"inverseOf": "supersedes"
|
|
266
273
|
},
|
|
274
|
+
"provides": {
|
|
275
|
+
"id": "provides",
|
|
276
|
+
"iri": "gloss:rel/provides",
|
|
277
|
+
"prefLabel": "provides",
|
|
278
|
+
"definition": "This concept provides substance for an external concept (one we reference but don't define). Direction: subject → external. Per concept-model TODO.partitive-relation-v2 item 07.",
|
|
279
|
+
"category": "lifecycle",
|
|
280
|
+
"inverseOf": "provided_by"
|
|
281
|
+
},
|
|
282
|
+
"provided_by": {
|
|
283
|
+
"id": "provided_by",
|
|
284
|
+
"iri": "gloss:rel/provided_by",
|
|
285
|
+
"prefLabel": "provided by",
|
|
286
|
+
"definition": "This external concept is provided by (fulfilled by) the object concept. Inverse of provides.",
|
|
287
|
+
"category": "lifecycle",
|
|
288
|
+
"inverseOf": "provides"
|
|
289
|
+
},
|
|
267
290
|
"compare": {
|
|
268
291
|
"id": "compare",
|
|
269
292
|
"iri": "gloss:rel/compare",
|
package/src/i18n/locales/eng.yml
CHANGED
|
@@ -58,6 +58,12 @@ concept.example: Example
|
|
|
58
58
|
concept.sources: Sources
|
|
59
59
|
concept.designations: Designations
|
|
60
60
|
concept.relations: Relations
|
|
61
|
+
concept.compareWith: Compare with this edition
|
|
62
|
+
concept.compareEditions: Compare editions
|
|
63
|
+
concept.partitiveRelations: Partitive relations
|
|
64
|
+
partitive.relations: Partitive relations
|
|
65
|
+
partitive.plurality.shared: shared type
|
|
66
|
+
partitive.plurality.uncertain: uncertain
|
|
61
67
|
concept.classification: Classification
|
|
62
68
|
concept.domains: Domains
|
|
63
69
|
concept.tags: Tags
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Partitive relation styling — colors for one-to-many decompositions.
|
|
3
|
+
*
|
|
4
|
+
* Independent of relationship-categories.ts (which handles binary edges).
|
|
5
|
+
* Partitive relations have their own visual language:
|
|
6
|
+
* - completeness (complete/partial) affects opacity
|
|
7
|
+
* - plurality (type-shared, uncertain) adds accent badges
|
|
8
|
+
*
|
|
9
|
+
* v2 redesign: replaces v1 hyperedge-styling.ts per
|
|
10
|
+
* concept-model/TODO.partitive-relation-v2.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { TypeSharedPluralityWire } from '../adapters/types';
|
|
14
|
+
|
|
15
|
+
const BASE_LIGHT = '#0d9488';
|
|
16
|
+
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
|
+
|
|
22
|
+
export interface PartitiveRelationStyle {
|
|
23
|
+
color: string;
|
|
24
|
+
badgeClass: string;
|
|
25
|
+
opacity: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function partitiveRelationStyle(
|
|
29
|
+
completeness: 'complete' | 'partial',
|
|
30
|
+
plurality: TypeSharedPluralityWire | null,
|
|
31
|
+
isDark: boolean,
|
|
32
|
+
): PartitiveRelationStyle {
|
|
33
|
+
const base = isDark ? BASE_DARK : BASE_LIGHT;
|
|
34
|
+
const opacity = completeness === 'partial' ? 0.6 : 1.0;
|
|
35
|
+
|
|
36
|
+
let badgeClass = 'badge-teal';
|
|
37
|
+
if (plurality?.isShared) {
|
|
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;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function completenessLabel(completeness: 'complete' | 'partial'): string {
|
|
57
|
+
return completeness === 'partial' ? 'Partial' : 'Complete';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function certaintyLabel(certainty: 'confirmed' | 'possible'): string {
|
|
61
|
+
return certainty === 'possible' ? 'Possible' : 'Confirmed';
|
|
62
|
+
}
|
|
@@ -5,6 +5,7 @@ import { useVocabularyStore } from '../stores/vocabulary';
|
|
|
5
5
|
import { useUiStore } from '../stores/ui';
|
|
6
6
|
import { useDsStyle } from '../utils/dataset-style';
|
|
7
7
|
import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
8
|
+
import { useDatasetStats } from '../composables/use-dataset-stats';
|
|
8
9
|
import { FORMAT_LABELS } from '../config/types';
|
|
9
10
|
import { langName, langLabel, sortLanguages } from '../utils/lang';
|
|
10
11
|
import ConceptCard from '../components/ConceptCard.vue';
|
|
@@ -21,6 +22,7 @@ const store = useVocabularyStore();
|
|
|
21
22
|
const uiStore = useUiStore();
|
|
22
23
|
const { getStyle } = useDsStyle();
|
|
23
24
|
const { ensureLoaded, loading, localError } = useDatasetLoader(() => props.registerId);
|
|
25
|
+
const { stats: datasetStats } = useDatasetStats(() => props.registerId);
|
|
24
26
|
const { t } = useI18n();
|
|
25
27
|
const { localizedDatasetField } = useSiteConfig();
|
|
26
28
|
const route = useRoute();
|
|
@@ -331,10 +333,10 @@ function clearSection() {
|
|
|
331
333
|
<router-link :to="{ name: 'about', params: { registerId } }" class="badge badge-purple hover:opacity-80 transition-opacity">
|
|
332
334
|
{{ t('nav.about') }}
|
|
333
335
|
</router-link>
|
|
334
|
-
<router-link v-if="
|
|
335
|
-
{{
|
|
336
|
+
<router-link v-if="datasetStats?.sourceCount" :to="{ name: 'sources', params: { registerId } }" class="badge badge-gray hover:opacity-80 transition-opacity">
|
|
337
|
+
{{ datasetStats.sourceCount }} {{ t('dataset.sources') }}
|
|
336
338
|
</router-link>
|
|
337
|
-
<span v-if="
|
|
339
|
+
<span v-if="datasetStats?.relationshipCount" class="badge badge-yellow">{{ datasetStats.relationshipCount.toLocaleString() }} {{ t('dataset.relationships') }}</span>
|
|
338
340
|
</div>
|
|
339
341
|
</div>
|
|
340
342
|
|
package/src/views/StatsView.vue
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { computed } from 'vue';
|
|
3
3
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
4
4
|
import { useDsStyle } from '../utils/dataset-style';
|
|
5
5
|
import { useDatasetLoader } from '../composables/use-dataset-loader';
|
|
6
|
+
import { useDatasetStats } from '../composables/use-dataset-stats';
|
|
6
7
|
import { langName, langLabel } from '../utils/lang';
|
|
7
8
|
import { useI18n } from '../i18n';
|
|
8
9
|
|
|
@@ -11,6 +12,7 @@ const props = defineProps<{ registerId?: string }>();
|
|
|
11
12
|
const store = useVocabularyStore();
|
|
12
13
|
const { getColor } = useDsStyle();
|
|
13
14
|
const { loading, localError, ensureLoaded, resolvedId } = useDatasetLoader(() => props.registerId);
|
|
15
|
+
const { stats: statsData, reload: reloadStats } = useDatasetStats(resolvedId);
|
|
14
16
|
const { t } = useI18n();
|
|
15
17
|
|
|
16
18
|
const manifest = computed(() => store.manifests.get(resolvedId.value));
|
|
@@ -54,26 +56,6 @@ function coverageColor(ratio: number): string {
|
|
|
54
56
|
return 'bg-red-400';
|
|
55
57
|
}
|
|
56
58
|
|
|
57
|
-
interface StatsData {
|
|
58
|
-
sourceCount: number;
|
|
59
|
-
sources: Array<{ ref: string; types: string[]; conceptCount: number }>;
|
|
60
|
-
relationshipCount: number;
|
|
61
|
-
relationshipTypes: Record<string, number>;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const statsData = ref<StatsData | null>(null);
|
|
65
|
-
|
|
66
|
-
async function loadStats() {
|
|
67
|
-
if (!resolvedId.value) return;
|
|
68
|
-
const base = import.meta.env.BASE_URL;
|
|
69
|
-
try {
|
|
70
|
-
const resp = await fetch(`${base}data/${resolvedId.value}/stats.json`);
|
|
71
|
-
if (resp.ok) statsData.value = await resp.json();
|
|
72
|
-
} catch {}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
watch(() => resolvedId.value, () => { loadStats(); }, { immediate: true });
|
|
76
|
-
|
|
77
59
|
const relEntries = computed(() => {
|
|
78
60
|
if (!statsData.value?.relationshipTypes) return [];
|
|
79
61
|
return Object.entries(statsData.value.relationshipTypes)
|
|
@@ -92,6 +74,11 @@ function sourceTypeBadge(type: string): string {
|
|
|
92
74
|
if (type === 'lineage') return 'badge-yellow';
|
|
93
75
|
return 'badge-gray';
|
|
94
76
|
}
|
|
77
|
+
|
|
78
|
+
async function reloadAll() {
|
|
79
|
+
await ensureLoaded();
|
|
80
|
+
reloadStats();
|
|
81
|
+
}
|
|
95
82
|
</script>
|
|
96
83
|
|
|
97
84
|
<template>
|
|
@@ -115,7 +102,7 @@ function sourceTypeBadge(type: string): string {
|
|
|
115
102
|
<div class="card p-8 border-red-200 bg-red-50/50 text-center">
|
|
116
103
|
<p class="text-red-700 font-medium mb-1">{{ t('stats.failedToLoad') }}</p>
|
|
117
104
|
<p class="text-sm text-red-600/80 mb-4">{{ localError }}</p>
|
|
118
|
-
<button @click="
|
|
105
|
+
<button @click="reloadAll" class="btn-primary">{{ t('stats.retry') }}</button>
|
|
119
106
|
</div>
|
|
120
107
|
</template>
|
|
121
108
|
<template v-else-if="manifest">
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { hyperedgeStyle, markerColor, enumerationLabel } from '../utils/hyperedge-styling';
|
|
3
|
-
|
|
4
|
-
describe('hyperedge-styling', () => {
|
|
5
|
-
describe('hyperedgeStyle', () => {
|
|
6
|
-
it('returns full opacity for closed enumeration', () => {
|
|
7
|
-
expect(hyperedgeStyle('closed', [], false).opacity).toBe(1.0);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('returns reduced opacity for open enumeration', () => {
|
|
11
|
-
expect(hyperedgeStyle('open', [], false).opacity).toBe(0.6);
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('uses teal badge class for closed with no markers', () => {
|
|
15
|
-
expect(hyperedgeStyle('closed', [], false).badgeClass).toBe('badge-teal');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('uses blue badge class for double marker', () => {
|
|
19
|
-
expect(hyperedgeStyle('closed', ['double'], false).badgeClass).toBe('badge-blue');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('uses yellow badge class for dashed marker', () => {
|
|
23
|
-
expect(hyperedgeStyle('closed', ['dashed'], false).badgeClass).toBe('badge-yellow');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('returns different colors in dark mode', () => {
|
|
27
|
-
const light = hyperedgeStyle('closed', [], false);
|
|
28
|
-
const dark = hyperedgeStyle('closed', [], true);
|
|
29
|
-
expect(light.color).not.toBe(dark.color);
|
|
30
|
-
});
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe('markerColor', () => {
|
|
34
|
-
it('returns blue for double marker', () => {
|
|
35
|
-
expect(markerColor('double', false)).toBe('#3b82f6');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('returns amber for dashed marker', () => {
|
|
39
|
-
expect(markerColor('dashed', false)).toBe('#f59e0b');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('returns dark-mode variants', () => {
|
|
43
|
-
expect(markerColor('double', true)).toBe('#60a5fa');
|
|
44
|
-
expect(markerColor('dashed', true)).toBe('#fbbf24');
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('enumerationLabel', () => {
|
|
49
|
-
it('labels closed as "Closed (complete)"', () => {
|
|
50
|
-
expect(enumerationLabel('closed')).toBe('Closed (complete)');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('labels open as "Open (partial)"', () => {
|
|
54
|
-
expect(enumerationLabel('open')).toBe('Open (partial)');
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
});
|