@cavelang/cli 0.36.10 → 0.38.0
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/README.md +24 -1
- package/dist/internal/view/api.d.ts +58 -0
- package/dist/internal/view/api.d.ts.map +1 -1
- package/dist/internal/view/api.js +155 -0
- package/dist/internal/view/api.js.map +1 -1
- package/dist/internal/view/index.d.ts +4 -4
- package/dist/internal/view/index.d.ts.map +1 -1
- package/dist/internal/view/index.js +3 -3
- package/dist/internal/view/index.js.map +1 -1
- package/dist/internal/view/page.d.ts +6 -1
- package/dist/internal/view/page.d.ts.map +1 -1
- package/dist/internal/view/page.js +620 -68
- package/dist/internal/view/page.js.map +1 -1
- package/dist/internal/view/server.d.ts.map +1 -1
- package/dist/internal/view/server.js +28 -2
- package/dist/internal/view/server.js.map +1 -1
- package/dist/src/cli.d.ts +1 -1
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +15 -2
- package/dist/src/cli.js.map +1 -1
- package/dist/src/commands.d.ts +1 -1
- package/dist/src/commands.js +1 -1
- package/dist/src/commands.js.map +1 -1
- package/dist/src/main.js +14 -1
- package/dist/src/main.js.map +1 -1
- package/package.json +19 -19
- package/src/cli.ts +16 -2
- package/src/commands.ts +1 -1
- package/src/main.ts +14 -1
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
* routes over the read-only `/api/*` endpoints; claims render from
|
|
6
6
|
* *structured* row data (never by re-parsing text — no second grammar),
|
|
7
7
|
* and every entity name, claim key and row id links onward: entity 360,
|
|
8
|
-
* topic browse, belief-history timeline, lineage, search.
|
|
8
|
+
* relation graphs, topic browse, belief-history timeline, lineage, search.
|
|
9
|
+
*
|
|
10
|
+
* Each claim row leads with its statement and confidence; contexts, tags,
|
|
11
|
+
* comments, dates and the history/lineage links sit one click away in a
|
|
12
|
+
* native disclosure. Graphs are inline SVG laid out in the page by a small
|
|
13
|
+
* deterministic force simulation — still no library and no external fetch.
|
|
9
14
|
*
|
|
10
15
|
* `__CAVE_DB__`, `__CAVE_VERSION__`, and `__CAVE_SENSITIVITY__` are stamped by the server
|
|
11
16
|
* (HTML-escaped) when the page is requested.
|
|
@@ -22,13 +27,17 @@ export const page = `<!doctype html>
|
|
|
22
27
|
:root {
|
|
23
28
|
--bg: #ffffff; --fg: #1c1e21; --dim: #71767c; --line: #e5e7ea;
|
|
24
29
|
--accent: #0b62b8; --pill: #f1f3f5; --bad: #b3261e; --warn: #8a6100;
|
|
25
|
-
--bar: #cfe3f7; --mark: #fff2c4;
|
|
30
|
+
--bar: #cfe3f7; --mark: #fff2c4; --edge: #9aa1a8; --node: #c3c8cd;
|
|
31
|
+
--c0: #4f8fd9; --c1: #d9774f; --c2: #4fae7a; --c3: #b56fd0;
|
|
32
|
+
--c4: #c9a43a; --c5: #d05f87; --c6: #4fb3b8; --c7: #7f8ad6;
|
|
26
33
|
}
|
|
27
34
|
@media (prefers-color-scheme: dark) {
|
|
28
35
|
:root {
|
|
29
36
|
--bg: #111417; --fg: #e4e6e8; --dim: #969ca3; --line: #272c31;
|
|
30
37
|
--accent: #6db3f2; --pill: #1c2126; --bad: #ff8078; --warn: #dfb54a;
|
|
31
|
-
--bar: #1f3c5a; --mark: #4a3f14;
|
|
38
|
+
--bar: #1f3c5a; --mark: #4a3f14; --edge: #5d656d; --node: #4a525a;
|
|
39
|
+
--c0: #5d9de6; --c1: #e68a63; --c2: #5fc08b; --c3: #c483de;
|
|
40
|
+
--c4: #d8b54d; --c5: #e07299; --c6: #5ec4c9; --c7: #929ce6;
|
|
32
41
|
}
|
|
33
42
|
}
|
|
34
43
|
* { box-sizing: border-box; }
|
|
@@ -38,60 +47,84 @@ body {
|
|
|
38
47
|
}
|
|
39
48
|
a { color: var(--accent); text-decoration: none; }
|
|
40
49
|
a:hover { text-decoration: underline; }
|
|
41
|
-
a:focus-visible, input:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
50
|
+
a:focus-visible, input:focus-visible, button:focus-visible, summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
42
51
|
a:focus-visible { text-decoration: underline; }
|
|
43
52
|
header {
|
|
44
|
-
position: sticky; top: 0; display: flex; gap: 1rem; align-items: center;
|
|
53
|
+
position: sticky; top: 0; z-index: 5; display: flex; gap: 1rem; align-items: center;
|
|
45
54
|
padding: .55rem 1rem; background: var(--bg); border-bottom: 1px solid var(--line);
|
|
46
55
|
}
|
|
47
56
|
header .brand { font-weight: 700; color: var(--fg); }
|
|
57
|
+
header nav { display: flex; gap: .8rem; font-size: .9rem; }
|
|
48
58
|
header input[type=search] {
|
|
49
59
|
flex: 1; min-width: 0; max-width: 26rem; padding: .3rem .6rem; border: 1px solid var(--line);
|
|
50
60
|
border-radius: .4rem; background: var(--pill); color: var(--fg); font: inherit;
|
|
51
61
|
}
|
|
52
62
|
header label { color: var(--dim); font-size: .85rem; display: flex; gap: .35rem; align-items: center; }
|
|
63
|
+
#trail { max-width: 64rem; margin: 0 auto; padding: .35rem 1rem 0; font-size: .8rem; color: var(--dim); }
|
|
64
|
+
#trail:empty { display: none; }
|
|
65
|
+
#trail a { margin-left: .6ch; }
|
|
53
66
|
main { max-width: 64rem; margin: 0 auto; padding: 1rem 1rem 3rem; }
|
|
54
67
|
footer {
|
|
55
68
|
max-width: 64rem; margin: 0 auto; padding: .6rem 1rem 1.4rem;
|
|
56
69
|
color: var(--dim); font-size: .8rem; border-top: 1px solid var(--line);
|
|
57
70
|
}
|
|
58
|
-
h1 { font-size: 1.
|
|
71
|
+
h1 { font-size: 1.35rem; margin: .6rem 0 .3rem; word-break: break-word; }
|
|
59
72
|
h1 .kind { color: var(--dim); font-weight: 400; font-size: .85rem; }
|
|
60
|
-
section { margin: 1.
|
|
73
|
+
section { margin: 1.3rem 0; }
|
|
61
74
|
section h2 { font-size: .8rem; text-transform: uppercase; letter-spacing: .06em; color: var(--dim); margin: 0 0 .35rem; }
|
|
62
|
-
section h2 .n { font-weight: 400; }
|
|
75
|
+
section h2 .n, summary .n { font-weight: 400; color: var(--dim); }
|
|
76
|
+
.lede { color: var(--dim); margin: .1rem 0 .6rem; }
|
|
63
77
|
.none { color: var(--dim); font-size: .85rem; margin: .2rem 0; }
|
|
64
78
|
.err { color: var(--bad); }
|
|
65
|
-
.
|
|
66
|
-
.
|
|
79
|
+
button, .btn {
|
|
80
|
+
padding: .2rem .6rem; border: 1px solid var(--line); border-radius: .4rem; background: var(--pill);
|
|
81
|
+
color: var(--fg); font: inherit; font-size: .8rem; cursor: pointer;
|
|
82
|
+
}
|
|
83
|
+
button[aria-pressed=true] { border-color: var(--accent); color: var(--accent); }
|
|
84
|
+
.retry { padding: .35rem .7rem; font-size: inherit; }
|
|
85
|
+
.tiles { display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .5rem; }
|
|
67
86
|
.tile { background: var(--pill); border-radius: .5rem; padding: .45rem .8rem; min-width: 5.4rem; }
|
|
68
87
|
.tile b { display: block; font-size: 1.15rem; }
|
|
69
88
|
.tile span { color: var(--dim); font-size: .75rem; }
|
|
70
89
|
.chips { display: flex; flex-wrap: wrap; gap: .4rem; }
|
|
71
90
|
.chip { background: var(--pill); border-radius: 1rem; padding: .15rem .7rem; font-size: .85rem; }
|
|
72
91
|
.chip .n { color: var(--dim); }
|
|
92
|
+
.chip.type { border: 1px solid var(--line); background: transparent; }
|
|
93
|
+
.fold { margin: .5rem 0; border-top: 1px solid var(--line); padding-top: .4rem; }
|
|
94
|
+
.fold > summary { cursor: pointer; font-size: .8rem; text-transform: uppercase; letter-spacing: .06em; color: var(--dim); }
|
|
95
|
+
.fold > summary b { color: var(--fg); font-weight: 600; }
|
|
96
|
+
.fold[open] > summary { margin-bottom: .35rem; }
|
|
73
97
|
.claim {
|
|
74
|
-
display: flex; flex-wrap: wrap; gap: .45ch; align-items: baseline;
|
|
75
98
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
76
|
-
font-size: .84rem;
|
|
77
|
-
word-break: break-word;
|
|
99
|
+
font-size: .84rem; border-bottom: 1px dotted var(--line); word-break: break-word;
|
|
78
100
|
}
|
|
79
101
|
.claim:last-child { border-bottom: 0; }
|
|
80
|
-
.claim
|
|
102
|
+
.claim > summary { list-style: none; cursor: pointer; padding: .2rem 0; }
|
|
103
|
+
.claim > summary::-webkit-details-marker { display: none; }
|
|
104
|
+
.claim .line { display: flex; flex-wrap: wrap; gap: .45ch; align-items: baseline; }
|
|
105
|
+
.claim .more { margin-left: auto; color: var(--dim); font-size: .74rem; padding: 0 .3rem; border-radius: .3rem; }
|
|
106
|
+
.claim > summary:hover .more { background: var(--pill); color: var(--accent); }
|
|
107
|
+
.claim[open] .more { color: var(--accent); }
|
|
108
|
+
.claim.dim .line > :not(.more) { opacity: .55; }
|
|
109
|
+
.claim .meta {
|
|
110
|
+
display: flex; flex-wrap: wrap; gap: .3rem .9ch; align-items: baseline;
|
|
111
|
+
margin: 0 0 .4rem 1.2rem; padding: .3rem .6rem; border-left: 2px solid var(--line); font-size: .78rem;
|
|
112
|
+
}
|
|
113
|
+
.claim .meta a, .claim .meta time { color: var(--dim); }
|
|
114
|
+
.claim .meta a:hover { color: var(--accent); }
|
|
115
|
+
.claim .meta .key { flex-basis: 100%; color: var(--dim); font-size: .72rem; }
|
|
81
116
|
.v { font-weight: 600; }
|
|
82
117
|
.not { color: var(--bad); font-style: normal; font-weight: 700; }
|
|
83
118
|
.a { color: var(--dim); }
|
|
84
|
-
.
|
|
119
|
+
.self { color: var(--dim); }
|
|
85
120
|
.ctx, .tag { background: var(--pill); border-radius: .3rem; padding: 0 .35rem; font-size: .78rem; color: var(--dim); }
|
|
86
121
|
.ctx.src { color: var(--accent); }
|
|
87
122
|
.conf { color: var(--warn); }
|
|
88
123
|
.conf.retracted { color: var(--bad); }
|
|
89
124
|
.imp { color: var(--bad); font-weight: 700; }
|
|
90
125
|
.cmt { color: var(--dim); }
|
|
126
|
+
.hint { color: var(--dim); font-size: .74rem; }
|
|
91
127
|
.rel { color: var(--dim); font-size: .78rem; border: 1px solid var(--line); border-radius: .3rem; padding: 0 .3rem; }
|
|
92
|
-
.meta { margin-left: auto; white-space: nowrap; font-size: .74rem; }
|
|
93
|
-
.meta a, .meta time { color: var(--dim); margin-left: .6ch; }
|
|
94
|
-
.meta a:hover { color: var(--accent); }
|
|
95
128
|
.bar { display: inline-block; width: 7rem; height: .5rem; background: var(--pill); border-radius: .25rem; overflow: hidden; flex: none; }
|
|
96
129
|
.bar i { display: block; height: 100%; background: var(--bar); }
|
|
97
130
|
.when { color: var(--dim); font-size: .74rem; width: 6.2rem; flex: none; }
|
|
@@ -99,21 +132,69 @@ section h2 .n { font-weight: 400; }
|
|
|
99
132
|
.tree > li, .tree ul > li { margin: .15rem 0; }
|
|
100
133
|
.role { color: var(--dim); font-size: .74rem; text-transform: uppercase; }
|
|
101
134
|
.rep { color: var(--dim); font-size: .78rem; }
|
|
102
|
-
.aka { color: var(--dim); font-size: .85rem; }
|
|
135
|
+
.aka { color: var(--dim); font-size: .85rem; margin: .2rem 0; }
|
|
136
|
+
.neighbors { display: grid; grid-template-columns: max-content 1fr; gap: .3rem .8rem; align-items: baseline; }
|
|
137
|
+
.neighbors .vb { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: .78rem; color: var(--dim); text-align: right; }
|
|
103
138
|
mark { background: var(--mark); color: inherit; border-radius: .2rem; }
|
|
104
139
|
.linked { font-size: .8rem; color: var(--dim); margin-top: .3rem; }
|
|
140
|
+
.panel { border: 1px solid var(--line); border-radius: .6rem; overflow: hidden; position: relative; }
|
|
141
|
+
.panel .tools {
|
|
142
|
+
display: flex; flex-wrap: wrap; gap: .35rem; align-items: center; padding: .35rem .5rem;
|
|
143
|
+
border-bottom: 1px solid var(--line); font-size: .8rem; color: var(--dim);
|
|
144
|
+
}
|
|
145
|
+
.panel .tools .grow { flex: 1; }
|
|
146
|
+
.graph { display: block; width: 100%; height: 22rem; touch-action: none; user-select: none; cursor: grab; }
|
|
147
|
+
.graph.tall { height: min(70vh, 40rem); }
|
|
148
|
+
.graph .ed path { fill: none; stroke: var(--edge); stroke-width: 1.3; }
|
|
149
|
+
.graph .ed.neg path { stroke: var(--bad); stroke-dasharray: 4 3; }
|
|
150
|
+
.graph .ed text, .graph .n text {
|
|
151
|
+
font-size: 11px; fill: var(--dim); paint-order: stroke; stroke: var(--bg); stroke-width: 3px; stroke-linejoin: round;
|
|
152
|
+
}
|
|
153
|
+
.graph .ed.neg text { fill: var(--bad); }
|
|
154
|
+
.graph .n text { fill: var(--fg); font-size: 12px; }
|
|
155
|
+
.graph .n circle { fill: var(--node); stroke: var(--bg); stroke-width: 2; }
|
|
156
|
+
.graph .n.focus circle { stroke: var(--accent); stroke-width: 3; }
|
|
157
|
+
.graph .n.focus text { font-weight: 700; }
|
|
158
|
+
.graph .n { cursor: pointer; }
|
|
159
|
+
.graph .n:focus-visible circle { stroke: var(--accent); stroke-width: 3; }
|
|
160
|
+
.graph .n:focus { outline: none; }
|
|
161
|
+
.graph.dense .ed text { display: none; }
|
|
162
|
+
.graph.dense .ed.on text { display: inline; }
|
|
163
|
+
.graph.hl .n:not(.on), .graph.hl .ed:not(.on) { opacity: .15; }
|
|
164
|
+
.graph .ed.on path { stroke: var(--accent); stroke-width: 2; }
|
|
165
|
+
.graph .arrow { fill: var(--edge); }
|
|
166
|
+
.graph .c0 circle { fill: var(--c0); } .graph .c1 circle { fill: var(--c1); }
|
|
167
|
+
.graph .c2 circle { fill: var(--c2); } .graph .c3 circle { fill: var(--c3); }
|
|
168
|
+
.graph .c4 circle { fill: var(--c4); } .graph .c5 circle { fill: var(--c5); }
|
|
169
|
+
.graph .c6 circle { fill: var(--c6); } .graph .c7 circle { fill: var(--c7); }
|
|
170
|
+
.legend { display: flex; flex-wrap: wrap; gap: .3rem .9rem; padding: .35rem .6rem; font-size: .78rem; color: var(--dim); border-top: 1px solid var(--line); }
|
|
171
|
+
.legend:empty { display: none; }
|
|
172
|
+
.legend i { display: inline-block; width: .65rem; height: .65rem; border-radius: 50%; margin-right: .3rem; vertical-align: -.05rem; background: var(--node); }
|
|
173
|
+
.legend .c0 { background: var(--c0); } .legend .c1 { background: var(--c1); } .legend .c2 { background: var(--c2); }
|
|
174
|
+
.legend .c3 { background: var(--c3); } .legend .c4 { background: var(--c4); } .legend .c5 { background: var(--c5); }
|
|
175
|
+
.legend .c6 { background: var(--c6); } .legend .c7 { background: var(--c7); }
|
|
176
|
+
kbd { font: .75rem ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; border: 1px solid var(--line); border-radius: .25rem; padding: 0 .3rem; }
|
|
105
177
|
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
|
|
106
178
|
.skip-link { position: fixed; top: .5rem; left: .5rem; z-index: 10; padding: .6rem .9rem; background: var(--bg); border: 2px solid var(--accent); border-radius: .3rem; transform: translateY(-200%); }
|
|
107
179
|
.skip-link:focus { transform: translateY(0); }
|
|
180
|
+
@media (max-width: 40rem) {
|
|
181
|
+
header { gap: .6rem; flex-wrap: wrap; }
|
|
182
|
+
header input[type=search] { order: 5; flex-basis: 100%; max-width: none; }
|
|
183
|
+
.graph { height: 18rem; }
|
|
184
|
+
.neighbors { grid-template-columns: 1fr; }
|
|
185
|
+
.neighbors .vb { text-align: left; }
|
|
186
|
+
}
|
|
108
187
|
</style>
|
|
109
188
|
</head>
|
|
110
189
|
<body>
|
|
111
190
|
<a class="skip-link" href="#view">Skip to content</a>
|
|
112
191
|
<header>
|
|
113
192
|
<a class="brand" href="#/">cave</a>
|
|
114
|
-
<
|
|
193
|
+
<nav aria-label="Views"><a href="#/">overview</a><a href="#/g">graph</a></nav>
|
|
194
|
+
<input id="q" type="search" aria-label="Search claims by phrase" aria-keyshortcuts="Enter /" enterkeyhint="search" placeholder="search phrase… (press / )">
|
|
115
195
|
<label><input id="aliases" type="checkbox"> aliases</label>
|
|
116
196
|
</header>
|
|
197
|
+
<nav id="trail" aria-label="Recently visited entities"></nav>
|
|
117
198
|
<div id="loading-status" class="sr-only" role="status"></div>
|
|
118
199
|
<main id="view" tabindex="-1" aria-busy="false"></main>
|
|
119
200
|
<footer>cave __CAVE_VERSION__ · __CAVE_DB__ · sensitivity ≤ __CAVE_SENSITIVITY__ · read-only view</footer>
|
|
@@ -187,21 +268,29 @@ function api (path, params) {
|
|
|
187
268
|
}
|
|
188
269
|
|
|
189
270
|
function isLiteral (name) { return name.charAt(0) === '"' || name.charAt(0) === BACKTICK }
|
|
190
|
-
function entityHtml (name) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
271
|
+
function entityHtml (name, self) {
|
|
272
|
+
if (isLiteral(name)) return '<span class="lit">' + esc(name) + '</span>'
|
|
273
|
+
if (self !== undefined && name === self) return '<span class="self">' + esc(name) + '</span>'
|
|
274
|
+
return '<a class="e" href="#/e/' + enc(name) + '">' + esc(name) + '</a>'
|
|
194
275
|
}
|
|
195
276
|
|
|
277
|
+
/*
|
|
278
|
+
* One claim: the statement and its confidence lead; provenance, tags, the
|
|
279
|
+
* comment, the date and the history/lineage links open from the row.
|
|
280
|
+
* opts.when/bar add a timeline date and confidence bar, opts.self dims the
|
|
281
|
+
* page's own entity, opts.rel and opts.note prefix an annotation.
|
|
282
|
+
*/
|
|
196
283
|
function claimHtml (c, opts) {
|
|
197
284
|
opts = opts || {}
|
|
198
285
|
var parts = []
|
|
286
|
+
if (opts.rel !== undefined) { parts.push('<span class="rel" title="declared inverse — same stored fact read from this side">' + esc(opts.rel) + '</span>') }
|
|
199
287
|
if (opts.when) { parts.push('<span class="when" title="' + esc(c.at) + '">' + esc(c.at.slice(0, 10)) + '</span>') }
|
|
200
288
|
if (opts.bar) { parts.push('<span class="bar" title="confidence ' + esc(pct(c.conf)) + '"><i style="width:' + (c.conf * 100) + '%"></i></span>') }
|
|
201
|
-
parts.push(
|
|
289
|
+
if (opts.note !== undefined) { parts.push('<span class="hint">' + esc(opts.note) + '</span>') }
|
|
290
|
+
parts.push(entityHtml(c.subject, opts.self))
|
|
202
291
|
parts.push('<span class="v">' + esc(c.verb) + '</span>' + (c.negated ? ' <em class="not">NOT</em>' : ''))
|
|
203
292
|
if (c.object !== undefined) {
|
|
204
|
-
parts.push(entityHtml(c.object))
|
|
293
|
+
parts.push(entityHtml(c.object, opts.self))
|
|
205
294
|
} else if (c.attribute !== undefined) {
|
|
206
295
|
parts.push('<span class="a">' + esc(c.attribute) + ':</span> <span class="val">' + esc(c.value) + '</span>')
|
|
207
296
|
} else if (c.value !== undefined) {
|
|
@@ -209,30 +298,34 @@ function claimHtml (c, opts) {
|
|
|
209
298
|
}
|
|
210
299
|
if (c.delta !== undefined) { parts.push('<span class="val">+/- ' + esc(c.delta) + '</span>') }
|
|
211
300
|
if (c.sigmaLevel !== undefined) { parts.push('<span class="val">(' + esc(c.sigmaLevel) + 'σ)</span>') }
|
|
301
|
+
if (c.conf === 0) {
|
|
302
|
+
parts.push('<span class="conf retracted" title="retracted">@ 0%</span>')
|
|
303
|
+
} else if (c.conf < 1) {
|
|
304
|
+
parts.push('<span class="conf">@ ' + esc(pct(c.conf)) + '</span>')
|
|
305
|
+
}
|
|
306
|
+
if (c.importance) { parts.push('<span class="imp" title="important">!</span>') }
|
|
307
|
+
var meta = []
|
|
308
|
+
if (c.comment !== undefined) { meta.push('<span class="cmt">; ' + esc(c.comment) + '</span>') }
|
|
212
309
|
c.contexts.forEach(function (ctx) {
|
|
213
310
|
var source = (c.sources || []).filter(function (item) { return item.context === ctx })[0]
|
|
214
311
|
var label = '@' + esc(ctx)
|
|
215
312
|
if (source && source.href) {
|
|
216
313
|
label = '<a href="' + esc(source.href) + '" target="_blank" rel="noopener noreferrer" title="' + esc(source.location) + '">' + label + '</a>'
|
|
217
314
|
}
|
|
218
|
-
|
|
315
|
+
meta.push('<span class="ctx' + (ctx.indexOf('src:') === 0 ? ' src' : '') + '">' + label + '</span>')
|
|
219
316
|
})
|
|
220
317
|
c.tags.forEach(function (tag) {
|
|
221
|
-
|
|
318
|
+
meta.push('<span class="tag">#' + esc(tag.key) + (tag.value !== undefined ? ':' + esc(tag.value) : '') + '</span>')
|
|
222
319
|
})
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
320
|
+
meta.push('<time title="' + esc(c.at) + '">' + esc(c.at.slice(0, 19).replace('T', ' ')) + '</time>')
|
|
321
|
+
meta.push('<a href="#/k/' + enc(c.key) + '" title="belief history of this fact">history</a>')
|
|
322
|
+
if (c.cites > 0 || c.citedBy > 0) {
|
|
323
|
+
meta.push('<a href="#/l/' + enc(c.id) + '" title="cites ' + c.cites + ', cited by ' + c.citedBy + '">lineage</a>')
|
|
227
324
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
'<
|
|
232
|
-
(c.cites > 0 || c.citedBy > 0 ? '<a href="#/l/' + enc(c.id) + '" title="cites ' + c.cites + ', cited by ' + c.citedBy + '">lineage</a>' : '') +
|
|
233
|
-
(opts.when ? '' : '<time title="' + esc(c.at) + '">' + esc(c.at.slice(0, 10)) + '</time>') +
|
|
234
|
-
'</span>'
|
|
235
|
-
return '<div class="claim' + (c.conf === 0 ? ' dim' : '') + '">' + parts.join(' ') + meta + '</div>'
|
|
325
|
+
var hints = (c.comment !== undefined ? 'note ' : '') + (c.cites > 0 ? 'why ' : '') + '…'
|
|
326
|
+
return '<details class="claim' + (c.conf === 0 ? ' dim' : '') + '"><summary><span class="line">' + parts.join(' ') +
|
|
327
|
+
'<span class="more" aria-hidden="true" title="details">' + esc(hints) + '</span></span></summary>' +
|
|
328
|
+
'<div class="meta">' + meta.join(' ') + '</div></details>'
|
|
236
329
|
}
|
|
237
330
|
|
|
238
331
|
function claimList (list, opts) {
|
|
@@ -241,6 +334,10 @@ function claimList (list, opts) {
|
|
|
241
334
|
function section (title, body, count) {
|
|
242
335
|
return '<section><h2>' + title + (count !== undefined ? ' <span class="n">(' + count + ')</span>' : '') + '</h2>' + body + '</section>'
|
|
243
336
|
}
|
|
337
|
+
function fold (title, body, count, open) {
|
|
338
|
+
return '<details class="fold"' + (open ? ' open' : '') + '><summary><b>' + title + '</b>' +
|
|
339
|
+
(count !== undefined ? ' <span class="n">(' + count + ')</span>' : '') + '</summary>' + body + '</details>'
|
|
340
|
+
}
|
|
244
341
|
function capNote (capped) {
|
|
245
342
|
return capped.items.length < capped.total ?
|
|
246
343
|
'<p class="none">…and ' + (capped.total - capped.items.length) + ' more (see cave check)</p>' : ''
|
|
@@ -248,6 +345,7 @@ function capNote (capped) {
|
|
|
248
345
|
function tile (value, label) {
|
|
249
346
|
return '<div class="tile"><b>' + value + '</b><span>' + label + '</span></div>'
|
|
250
347
|
}
|
|
348
|
+
function plural (n, word, many) { return n + ' ' + (n === 1 ? word : many || word + 's') }
|
|
251
349
|
|
|
252
350
|
function shapeProblem (violation) {
|
|
253
351
|
if (violation.actualCount === 0) return 'missing ' + violation.kind + ' ' + violation.name
|
|
@@ -262,77 +360,508 @@ function shapeProblem (violation) {
|
|
|
262
360
|
return problems.join('; ')
|
|
263
361
|
}
|
|
264
362
|
|
|
363
|
+
/*
|
|
364
|
+
* Relation graphs — inline SVG, laid out here; nodes are links onward.
|
|
365
|
+
* Dictionaries keyed by stored names are prototype-free: an entity may be
|
|
366
|
+
* called __proto__ or constructor.
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
function hashOf (text) {
|
|
370
|
+
var hash = 0
|
|
371
|
+
for (var i = 0; i < text.length; i++) hash = (hash * 31 + text.charCodeAt(i)) | 0
|
|
372
|
+
return Math.abs(hash)
|
|
373
|
+
}
|
|
374
|
+
/* The eight most common first types in this graph get distinct colours. */
|
|
375
|
+
function typeColors (nodes) {
|
|
376
|
+
var counts = Object.create(null)
|
|
377
|
+
nodes.forEach(function (node) { if (node.types.length) counts[node.types[0]] = (counts[node.types[0]] || 0) + 1 })
|
|
378
|
+
var colors = Object.create(null)
|
|
379
|
+
Object.keys(counts).sort(function (a, b) { return counts[b] - counts[a] || (a < b ? -1 : 1) }).slice(0, 8).forEach(function (type, i) {
|
|
380
|
+
colors[type] = ' c' + i
|
|
381
|
+
})
|
|
382
|
+
return colors
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function layoutGraph (data, width, height) {
|
|
386
|
+
var nodes = data.nodes
|
|
387
|
+
var n = nodes.length
|
|
388
|
+
var index = Object.create(null)
|
|
389
|
+
nodes.forEach(function (node, i) { index[node.name] = i })
|
|
390
|
+
// One spring per linked pair, however many relations it carries.
|
|
391
|
+
var seen = Object.create(null)
|
|
392
|
+
var links = []
|
|
393
|
+
var degree = nodes.map(function () { return 0 })
|
|
394
|
+
data.edges.forEach(function (edge) {
|
|
395
|
+
var s = index[edge.source], t = index[edge.target]
|
|
396
|
+
if (s === undefined || t === undefined || s === t) return
|
|
397
|
+
var id = Math.min(s, t) + ':' + Math.max(s, t)
|
|
398
|
+
if (seen[id]) return
|
|
399
|
+
seen[id] = true
|
|
400
|
+
links.push([s, t])
|
|
401
|
+
degree[s]++; degree[t]++
|
|
402
|
+
})
|
|
403
|
+
var cx = width / 2
|
|
404
|
+
var cy = height / 2
|
|
405
|
+
// Deterministic start: the focus centred, the rest on a golden-angle spiral.
|
|
406
|
+
var pos = nodes.map(function (node, i) {
|
|
407
|
+
if (node.name === data.focus) return { x: cx, y: cy }
|
|
408
|
+
var r = 24 * Math.sqrt(i + 1)
|
|
409
|
+
var angle = i * 2.39996 + (hashOf(node.name) % 100) / 100
|
|
410
|
+
return { x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle) }
|
|
411
|
+
})
|
|
412
|
+
// Velocity simulation: pairwise charge, springs on relations, mild gravity.
|
|
413
|
+
var span = Math.min(90, Math.max(36, Math.sqrt(width * height / Math.max(n, 1)) * 0.55))
|
|
414
|
+
var charge = span * span * 1.2
|
|
415
|
+
var vel = pos.map(function () { return { x: 0, y: 0 } })
|
|
416
|
+
var alpha = 1
|
|
417
|
+
for (var step = 0; step < 320; step++) {
|
|
418
|
+
for (var a = 0; a < n; a++) {
|
|
419
|
+
for (var b = a + 1; b < n; b++) {
|
|
420
|
+
var dx = pos[b].x - pos[a].x
|
|
421
|
+
var dy = pos[b].y - pos[a].y
|
|
422
|
+
var d2 = Math.max(dx * dx + dy * dy, 16)
|
|
423
|
+
var push = charge * alpha / d2
|
|
424
|
+
vel[a].x -= dx * push / Math.sqrt(d2); vel[a].y -= dy * push / Math.sqrt(d2)
|
|
425
|
+
vel[b].x += dx * push / Math.sqrt(d2); vel[b].y += dy * push / Math.sqrt(d2)
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
// Springs weaken with degree and move the less connected end more (as in
|
|
429
|
+
// d3-force), so a hub's many springs cannot add up and fling it.
|
|
430
|
+
links.forEach(function (link) {
|
|
431
|
+
var s = pos[link[0]], t = pos[link[1]]
|
|
432
|
+
var dx = t.x - s.x, dy = t.y - s.y
|
|
433
|
+
var d = Math.sqrt(dx * dx + dy * dy) || 1
|
|
434
|
+
var pull = (d - span) / d * alpha * 0.5 / Math.min(degree[link[0]], degree[link[1]])
|
|
435
|
+
var bias = degree[link[0]] / (degree[link[0]] + degree[link[1]])
|
|
436
|
+
vel[link[0]].x += dx * pull * (1 - bias); vel[link[0]].y += dy * pull * (1 - bias)
|
|
437
|
+
vel[link[1]].x -= dx * pull * bias; vel[link[1]].y -= dy * pull * bias
|
|
438
|
+
})
|
|
439
|
+
for (var i = 0; i < n; i++) {
|
|
440
|
+
// Gravity keeps disconnected clusters close enough to share the frame.
|
|
441
|
+
vel[i].x += (cx - pos[i].x) * 0.02 * alpha
|
|
442
|
+
vel[i].y += (cy - pos[i].y) * 0.02 * alpha * width / height
|
|
443
|
+
if (nodes[i].name === data.focus) { vel[i].x = 0; vel[i].y = 0; continue }
|
|
444
|
+
// Bound each step; a non-finite velocity is discarded rather than spread.
|
|
445
|
+
var speed = Math.sqrt(vel[i].x * vel[i].x + vel[i].y * vel[i].y)
|
|
446
|
+
if (!(speed < Infinity)) { vel[i].x = 0; vel[i].y = 0; speed = 0 }
|
|
447
|
+
if (speed > span) { vel[i].x *= span / speed; vel[i].y *= span / speed }
|
|
448
|
+
pos[i].x += vel[i].x; pos[i].y += vel[i].y
|
|
449
|
+
vel[i].x *= 0.6; vel[i].y *= 0.6
|
|
450
|
+
}
|
|
451
|
+
alpha *= 0.985
|
|
452
|
+
}
|
|
453
|
+
// Fit the result into the viewport, leaving room for labels.
|
|
454
|
+
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
|
|
455
|
+
pos.forEach(function (p) {
|
|
456
|
+
minX = Math.min(minX, p.x); maxX = Math.max(maxX, p.x)
|
|
457
|
+
minY = Math.min(minY, p.y); maxY = Math.max(maxY, p.y)
|
|
458
|
+
})
|
|
459
|
+
var padX = 70, padY = 30
|
|
460
|
+
// Each axis fills the frame, within a bounded stretch so shapes stay readable.
|
|
461
|
+
var scaleX = Math.min((width - 2 * padX) / Math.max(maxX - minX, 1), 2.5)
|
|
462
|
+
var scaleY = Math.min((height - 2 * padY) / Math.max(maxY - minY, 1), 2.5)
|
|
463
|
+
scaleX = Math.min(scaleX, scaleY * 3)
|
|
464
|
+
scaleY = Math.min(scaleY, scaleX * 3)
|
|
465
|
+
var offsetX = (width - (maxX - minX) * scaleX) / 2
|
|
466
|
+
var offsetY = (height - (maxY - minY) * scaleY) / 2
|
|
467
|
+
return pos.map(function (p) { return { x: offsetX + (p.x - minX) * scaleX, y: offsetY + (p.y - minY) * scaleY } })
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function radiusOf (node, focus) {
|
|
471
|
+
return node.name === focus ? 11 : Math.min(5 + Math.sqrt(node.degree) * 1.8, 13)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function drawGraph (target, data, opts) {
|
|
475
|
+
opts = opts || {}
|
|
476
|
+
if (data.nodes.length === 0) {
|
|
477
|
+
target.innerHTML = '<p class="none" style="padding:.6rem">no relations between entities to draw</p>'
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
var width = Math.max(target.clientWidth || 0, 320)
|
|
481
|
+
var height = opts.tall ? Math.max(Math.min(window.innerHeight * 0.7, 640), 320) : (width < 640 ? 288 : 352)
|
|
482
|
+
var pos = layoutGraph(data, width, height)
|
|
483
|
+
var colors = typeColors(data.nodes)
|
|
484
|
+
function colorOf (node) { return node.types.length ? colors[node.types[0]] || '' : '' }
|
|
485
|
+
var index = Object.create(null)
|
|
486
|
+
data.nodes.forEach(function (node, i) { index[node.name] = i })
|
|
487
|
+
// One path per ordered pair; verbs on the same pair share a label.
|
|
488
|
+
var pairs = Object.create(null)
|
|
489
|
+
var order = []
|
|
490
|
+
data.edges.forEach(function (edge) {
|
|
491
|
+
if (edge.source === edge.target) return
|
|
492
|
+
var id = edge.source + '\\u0000' + edge.target
|
|
493
|
+
if (!pairs[id]) { pairs[id] = { source: edge.source, target: edge.target, edges: [] }; order.push(id) }
|
|
494
|
+
pairs[id].edges.push(edge)
|
|
495
|
+
})
|
|
496
|
+
var dense = order.length > 40
|
|
497
|
+
var svg = '<svg class="graph' + (opts.tall ? ' tall' : '') + (dense ? ' dense' : '') + '" viewBox="0 0 ' + width + ' ' + height +
|
|
498
|
+
'" role="group" aria-label="' + esc(opts.label || 'relation graph') + '" style="height:' + height + 'px">' +
|
|
499
|
+
'<defs><marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">' +
|
|
500
|
+
'<path class="arrow" d="M0,0L10,5L0,10z"/></marker></defs><g class="vp">'
|
|
501
|
+
order.forEach(function (id, i) {
|
|
502
|
+
var pair = pairs[id]
|
|
503
|
+
var negated = pair.edges.every(function (edge) { return edge.negated })
|
|
504
|
+
var label = pair.edges.map(function (edge) {
|
|
505
|
+
return (edge.negated ? 'NOT ' : '') + edge.verb + (edge.conf < 1 ? ' ' + pct(edge.conf) : '')
|
|
506
|
+
}).join(', ')
|
|
507
|
+
var opacity = Math.max(0.35, Math.max.apply(null, pair.edges.map(function (edge) { return edge.conf })))
|
|
508
|
+
svg += '<g class="ed' + (negated ? ' neg' : '') + '" data-i="' + i + '" data-s="' + index[pair.source] + '" data-t="' + index[pair.target] +
|
|
509
|
+
'" data-back="' + (pairs[pair.target + '\\u0000' + pair.source] ? 1 : 0) + '" style="opacity:' + opacity + '">' +
|
|
510
|
+
'<title>' + esc(pair.source + ' ' + label + ' ' + pair.target) + '</title>' +
|
|
511
|
+
'<path marker-end="url(#arrow)"/><text text-anchor="middle">' + esc(label) + '</text></g>'
|
|
512
|
+
})
|
|
513
|
+
data.nodes.forEach(function (node, i) {
|
|
514
|
+
var r = radiusOf(node, data.focus)
|
|
515
|
+
var title = node.name + (node.types.length ? ' — IS ' + node.types.join(', ') : '') + ' — ' + plural(node.degree, 'relation')
|
|
516
|
+
svg += '<a class="n' + colorOf(node) + (node.name === data.focus ? ' focus' : '') + '" href="#/e/' + esc(enc(node.name)) +
|
|
517
|
+
'" data-i="' + i + '" data-r="' + r + '"><title>' + esc(title) + '</title><circle r="' + r + '"/>' +
|
|
518
|
+
'<text dy="' + (r + 13) + '" text-anchor="middle">' + esc(node.name.length > 28 ? node.name.slice(0, 27) + '…' : node.name) + '</text></a>'
|
|
519
|
+
})
|
|
520
|
+
svg += '</g></svg>'
|
|
521
|
+
var legendHtml = Object.keys(colors).map(function (type) {
|
|
522
|
+
return '<span><i class="' + colors[type].trim() + '"></i>' + esc(type) + '</span>'
|
|
523
|
+
}).join('')
|
|
524
|
+
target.innerHTML = svg + '<div class="legend">' + legendHtml + '</div>'
|
|
525
|
+
wireGraph(target.querySelector('svg'), data, pos)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function wireGraph (svg, data, pos) {
|
|
529
|
+
var nodeEls = Array.prototype.slice.call(svg.querySelectorAll('.n'))
|
|
530
|
+
var edgeEls = Array.prototype.slice.call(svg.querySelectorAll('.ed'))
|
|
531
|
+
var view0 = svg.getAttribute('viewBox').split(' ').map(Number)
|
|
532
|
+
var box = view0.slice()
|
|
533
|
+
function placeEdge (el) {
|
|
534
|
+
var s = pos[+el.getAttribute('data-s')]
|
|
535
|
+
var t = pos[+el.getAttribute('data-t')]
|
|
536
|
+
var rs = +nodeEls[+el.getAttribute('data-s')].getAttribute('data-r')
|
|
537
|
+
var rt = +nodeEls[+el.getAttribute('data-t')].getAttribute('data-r')
|
|
538
|
+
var dx = t.x - s.x, dy = t.y - s.y
|
|
539
|
+
var d = Math.sqrt(dx * dx + dy * dy) || 1
|
|
540
|
+
// Opposite directions on one pair bow apart instead of overlapping.
|
|
541
|
+
var bend = el.getAttribute('data-back') === '1' ? Math.min(28, d / 4) : 0
|
|
542
|
+
var mx = (s.x + t.x) / 2 - dy / d * bend
|
|
543
|
+
var my = (s.y + t.y) / 2 + dx / d * bend
|
|
544
|
+
var sx = s.x + (mx - s.x) / (Math.sqrt((mx - s.x) * (mx - s.x) + (my - s.y) * (my - s.y)) || 1) * rs
|
|
545
|
+
var sy = s.y + (my - s.y) / (Math.sqrt((mx - s.x) * (mx - s.x) + (my - s.y) * (my - s.y)) || 1) * rs
|
|
546
|
+
var tl = Math.sqrt((t.x - mx) * (t.x - mx) + (t.y - my) * (t.y - my)) || 1
|
|
547
|
+
var tx = t.x - (t.x - mx) / tl * (rt + 2)
|
|
548
|
+
var ty = t.y - (t.y - my) / tl * (rt + 2)
|
|
549
|
+
el.querySelector('path').setAttribute('d', 'M' + sx.toFixed(1) + ',' + sy.toFixed(1) + 'Q' + mx.toFixed(1) + ',' + my.toFixed(1) + ' ' + tx.toFixed(1) + ',' + ty.toFixed(1))
|
|
550
|
+
var text = el.querySelector('text')
|
|
551
|
+
text.setAttribute('x', ((s.x + t.x) / 2 - dy / d * bend / 2).toFixed(1))
|
|
552
|
+
text.setAttribute('y', ((s.y + t.y) / 2 + dx / d * bend / 2 - 3).toFixed(1))
|
|
553
|
+
}
|
|
554
|
+
function placeNode (el) {
|
|
555
|
+
var p = pos[+el.getAttribute('data-i')]
|
|
556
|
+
el.setAttribute('transform', 'translate(' + p.x.toFixed(1) + ',' + p.y.toFixed(1) + ')')
|
|
557
|
+
}
|
|
558
|
+
nodeEls.forEach(placeNode)
|
|
559
|
+
edgeEls.forEach(placeEdge)
|
|
560
|
+
|
|
561
|
+
function highlight (i) {
|
|
562
|
+
if (i === undefined) {
|
|
563
|
+
svg.classList.remove('hl')
|
|
564
|
+
nodeEls.concat(edgeEls).forEach(function (el) { el.classList.remove('on') })
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
svg.classList.add('hl')
|
|
568
|
+
nodeEls[i].classList.add('on')
|
|
569
|
+
edgeEls.forEach(function (el) {
|
|
570
|
+
var s = +el.getAttribute('data-s'), t = +el.getAttribute('data-t')
|
|
571
|
+
if (s === i || t === i) {
|
|
572
|
+
el.classList.add('on')
|
|
573
|
+
nodeEls[s === i ? t : s].classList.add('on')
|
|
574
|
+
}
|
|
575
|
+
})
|
|
576
|
+
}
|
|
577
|
+
nodeEls.forEach(function (el, i) {
|
|
578
|
+
el.addEventListener('mouseenter', function () { highlight(i) })
|
|
579
|
+
el.addEventListener('mouseleave', function () { highlight() })
|
|
580
|
+
el.addEventListener('focus', function () { highlight(i) })
|
|
581
|
+
el.addEventListener('blur', function () { highlight() })
|
|
582
|
+
})
|
|
583
|
+
|
|
584
|
+
function toGraph (event) {
|
|
585
|
+
var rect = svg.getBoundingClientRect()
|
|
586
|
+
return {
|
|
587
|
+
x: box[0] + (event.clientX - rect.left) / rect.width * box[2],
|
|
588
|
+
y: box[1] + (event.clientY - rect.top) / rect.height * box[3]
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function applyBox () { svg.setAttribute('viewBox', box.map(function (v) { return v.toFixed(1) }).join(' ')) }
|
|
592
|
+
var drag
|
|
593
|
+
var handled = false
|
|
594
|
+
svg.addEventListener('pointerdown', function (event) {
|
|
595
|
+
// Modified clicks keep the browser's own link handling (new tab, window).
|
|
596
|
+
if (event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) return
|
|
597
|
+
var node = event.target.closest ? event.target.closest('.n') : null
|
|
598
|
+
var start = toGraph(event)
|
|
599
|
+
drag = { node: node, start: start, client: { x: event.clientX, y: event.clientY }, box: box.slice(), moved: false }
|
|
600
|
+
svg.setPointerCapture(event.pointerId)
|
|
601
|
+
})
|
|
602
|
+
svg.addEventListener('pointermove', function (event) {
|
|
603
|
+
if (!drag) return
|
|
604
|
+
if (Math.abs(event.clientX - drag.client.x) + Math.abs(event.clientY - drag.client.y) > 4) drag.moved = true
|
|
605
|
+
if (!drag.moved) return
|
|
606
|
+
if (drag.node) {
|
|
607
|
+
var point = toGraph(event)
|
|
608
|
+
var i = +drag.node.getAttribute('data-i')
|
|
609
|
+
pos[i].x = point.x; pos[i].y = point.y
|
|
610
|
+
placeNode(drag.node)
|
|
611
|
+
edgeEls.forEach(function (el) {
|
|
612
|
+
if (+el.getAttribute('data-s') === i || +el.getAttribute('data-t') === i) placeEdge(el)
|
|
613
|
+
})
|
|
614
|
+
} else {
|
|
615
|
+
var rect = svg.getBoundingClientRect()
|
|
616
|
+
box[0] = drag.box[0] - (event.clientX - drag.client.x) / rect.width * box[2]
|
|
617
|
+
box[1] = drag.box[1] - (event.clientY - drag.client.y) / rect.height * box[3]
|
|
618
|
+
applyBox()
|
|
619
|
+
}
|
|
620
|
+
})
|
|
621
|
+
function endDrag (event) {
|
|
622
|
+
if (!drag) return
|
|
623
|
+
var moved = drag.moved
|
|
624
|
+
var node = drag.node
|
|
625
|
+
drag = undefined
|
|
626
|
+
if (svg.hasPointerCapture(event.pointerId)) svg.releasePointerCapture(event.pointerId)
|
|
627
|
+
handled = moved || !!node
|
|
628
|
+
// A pointer capture retargets the click; follow the node link explicitly.
|
|
629
|
+
if (!moved && node && event.type === 'pointerup') location.hash = node.getAttribute('href')
|
|
630
|
+
}
|
|
631
|
+
svg.addEventListener('pointerup', endDrag)
|
|
632
|
+
svg.addEventListener('pointercancel', endDrag)
|
|
633
|
+
// Pointer navigation happened on pointerup; keyboard Enter still clicks the link.
|
|
634
|
+
svg.addEventListener('click', function (event) {
|
|
635
|
+
if (handled) event.preventDefault()
|
|
636
|
+
handled = false
|
|
637
|
+
})
|
|
638
|
+
var panel = svg.parentNode.parentNode
|
|
639
|
+
function zoom (factor) {
|
|
640
|
+
var cx = box[0] + box[2] / 2, cy = box[1] + box[3] / 2
|
|
641
|
+
box[2] = Math.min(Math.max(box[2] * factor, view0[2] / 8), view0[2] * 4)
|
|
642
|
+
box[3] = box[2] * view0[3] / view0[2]
|
|
643
|
+
box[0] = cx - box[2] / 2; box[1] = cy - box[3] / 2
|
|
644
|
+
applyBox()
|
|
645
|
+
}
|
|
646
|
+
Array.prototype.forEach.call(panel.querySelectorAll('[data-zoom]'), function (button) {
|
|
647
|
+
button.onclick = function () {
|
|
648
|
+
var action = button.getAttribute('data-zoom')
|
|
649
|
+
if (action === 'reset') { box = view0.slice(); applyBox() } else zoom(action === 'in' ? 0.8 : 1.25)
|
|
650
|
+
}
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
function graphPanel (id, tools) {
|
|
655
|
+
return '<div class="panel"><div class="tools">' + tools + '<span class="grow"></span>' +
|
|
656
|
+
'<button type="button" data-zoom="in" aria-label="Zoom in">+</button>' +
|
|
657
|
+
'<button type="button" data-zoom="out" aria-label="Zoom out">−</button>' +
|
|
658
|
+
'<button type="button" data-zoom="reset">reset</button></div>' +
|
|
659
|
+
'<div id="' + id + '"><p class="none" style="padding:.6rem">drawing graph…</p></div></div>'
|
|
660
|
+
}
|
|
661
|
+
function depthButtons (depth) {
|
|
662
|
+
return '<span>hops</span>' + [1, 2, 3].map(function (d) {
|
|
663
|
+
return '<button type="button" data-depth="' + d + '" aria-pressed="' + (d === depth) + '">' + d + '</button>'
|
|
664
|
+
}).join('')
|
|
665
|
+
}
|
|
666
|
+
function graphCount (data) {
|
|
667
|
+
return plural(data.nodes.length, 'entity', 'entities') +
|
|
668
|
+
(data.total > data.nodes.length ? ' of ' + data.total + ' (most connected shown)' : '') +
|
|
669
|
+
' · ' + plural(data.edges.length, 'relation')
|
|
670
|
+
}
|
|
671
|
+
/* Fetches a neighborhood after its page renders; depth buttons redraw in place. */
|
|
672
|
+
function neighborhood (name, depth, opts) {
|
|
673
|
+
opts = opts || {}
|
|
674
|
+
var id = opts.id || 'egraph'
|
|
675
|
+
var target = view.querySelector('#' + id)
|
|
676
|
+
if (!target) return
|
|
677
|
+
var panel = target.parentNode
|
|
678
|
+
Array.prototype.forEach.call(panel.querySelectorAll('[data-depth]'), function (button) {
|
|
679
|
+
button.setAttribute('aria-pressed', String(+button.getAttribute('data-depth') === depth))
|
|
680
|
+
button.onclick = function () { neighborhood(name, +button.getAttribute('data-depth'), opts) }
|
|
681
|
+
})
|
|
682
|
+
var link = panel.querySelector('.open-graph')
|
|
683
|
+
if (link) link.setAttribute('href', '#/g/' + depth + '/' + enc(name))
|
|
684
|
+
later('#' + id, api('graph', { name: name, depth: depth, aliases: aliasBox.checked }), function (el, data) {
|
|
685
|
+
var count = panel.querySelector('.count')
|
|
686
|
+
if (count) count.innerHTML = graphCount(data)
|
|
687
|
+
drawGraph(el, data, { tall: opts.tall, label: 'relation graph around ' + name })
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/*
|
|
692
|
+
* Loads a secondary panel for the current view. Superseded navigation drops
|
|
693
|
+
* it, and so does a newer request for the same panel (a hop change while the
|
|
694
|
+
* previous graph is still loading), whatever order the responses arrive in.
|
|
695
|
+
*/
|
|
696
|
+
var panelRequests = Object.create(null)
|
|
697
|
+
function later (selector, promise, render) {
|
|
698
|
+
var current = navigation
|
|
699
|
+
var ticket = panelRequests[selector] = (panelRequests[selector] || 0) + 1
|
|
700
|
+
function live () { return current === navigation && panelRequests[selector] === ticket }
|
|
701
|
+
promise.then(function (data) {
|
|
702
|
+
var target = view.querySelector(selector)
|
|
703
|
+
if (live() && target) render(target, data)
|
|
704
|
+
}).catch(function (error) {
|
|
705
|
+
var target = view.querySelector(selector)
|
|
706
|
+
if (live() && target) target.innerHTML = '<p class="err" style="padding:.6rem">' + esc(error.message) + '</p>'
|
|
707
|
+
})
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/* Recently visited entities — this page session only, never stored. */
|
|
711
|
+
var trail = []
|
|
712
|
+
function visit (name) {
|
|
713
|
+
trail = trail.filter(function (item) { return item !== name })
|
|
714
|
+
trail.push(name)
|
|
715
|
+
if (trail.length > 8) trail.shift()
|
|
716
|
+
var nav = document.getElementById('trail')
|
|
717
|
+
if (!nav) return
|
|
718
|
+
var earlier = trail.slice(0, -1).reverse()
|
|
719
|
+
nav.innerHTML = earlier.length === 0 ? '' : 'visited' + earlier.map(function (item) {
|
|
720
|
+
return '<a href="#/e/' + enc(item) + '">' + esc(item) + '</a>'
|
|
721
|
+
}).join('<span aria-hidden="true"> ·</span>')
|
|
722
|
+
}
|
|
723
|
+
|
|
265
724
|
function dashboard (data) {
|
|
266
725
|
var cov = data.coverage
|
|
267
|
-
var html = ''
|
|
268
|
-
html +=
|
|
726
|
+
var html = '<h1>Overview</h1>'
|
|
727
|
+
html += '<p class="lede">' + plural(cov.entities, 'entity', 'entities') + ' · ' +
|
|
728
|
+
plural(cov.current, 'current belief') + ' · ' + plural(data.topics.length, 'topic') +
|
|
729
|
+
(cov.averageConfidence === null ? '' : ' · average confidence ' + esc(pct(cov.averageConfidence))) + '</p>'
|
|
730
|
+
html += fold('all coverage stats', '<div class="tiles">' +
|
|
269
731
|
tile(cov.rows, 'rows') + tile(cov.facts, 'facts') + tile(cov.current, 'current') +
|
|
270
732
|
tile(cov.entities, 'entities') + tile(cov.typedEntities, 'typed') +
|
|
271
733
|
tile(cov.averageConfidence === null ? '—' : esc(pct(cov.averageConfidence)), 'avg conf') +
|
|
272
734
|
tile(cov.retracted, 'retracted') + tile(cov.negated, 'negated') + tile(cov.lowConfidence, 'low conf') +
|
|
273
735
|
tile(cov.satisfied + '/' + cov.checks, 'shape checks') +
|
|
274
736
|
'</div>')
|
|
737
|
+
html += section('map', graphPanel('map', '<span class="count"></span>') +
|
|
738
|
+
'<p class="linked">The most connected entities and the current relations between them. Drag to rearrange, click an entity to open it, or <a href="#/g">open the full graph</a>.</p>')
|
|
275
739
|
if (data.topics.length > 0) {
|
|
276
740
|
html += section('topics', '<div class="chips">' + data.topics.map(function (topic) {
|
|
277
741
|
return '<a class="chip" href="#/t/' + enc(topic.name) + '">' + esc(topic.name) + ' <span class="n">' + topic.members + '</span></a>'
|
|
278
742
|
}).join('') + '</div>')
|
|
279
743
|
}
|
|
744
|
+
var attention = ''
|
|
280
745
|
if (data.violations.total > 0) {
|
|
281
|
-
|
|
282
|
-
return '<
|
|
283
|
-
'<span class="cmt">' + esc(shapeProblem(violation)) + '</span>' +
|
|
284
|
-
'<span class="
|
|
746
|
+
attention += fold('shape violations', data.violations.items.map(function (violation) {
|
|
747
|
+
return '<p class="claim" style="padding:.2rem 0;margin:0">' + entityHtml(violation.entity) +
|
|
748
|
+
' <span class="cmt">' + esc(shapeProblem(violation)) + '</span>' +
|
|
749
|
+
' <span class="hint">; ' + esc(violation.entity) + ' IS ' + esc(violation.via) + ', ' + esc(violation.type) + ' EXPECTS ' + esc(violation.name) + '</span></p>'
|
|
285
750
|
}).join('') + capNote(data.violations), data.violations.total)
|
|
286
751
|
}
|
|
287
752
|
if (data.review.total > 0) {
|
|
288
|
-
|
|
753
|
+
attention += fold('review candidates <span class="n">conf 0.3–0.7</span>', claimList(data.review.items) + capNote(data.review), data.review.total)
|
|
289
754
|
}
|
|
290
755
|
if (data.disagreements.total > 0) {
|
|
291
|
-
|
|
292
|
-
return '<p class="none">' + esc(disagreement.about) + ' across ' + disagreement.entities.map(entityHtml).join(', ') + '</p>' +
|
|
756
|
+
attention += fold('alias disagreements', data.disagreements.items.map(function (disagreement) {
|
|
757
|
+
return '<p class="none">' + esc(disagreement.about) + ' across ' + disagreement.entities.map(function (name) { return entityHtml(name) }).join(', ') + '</p>' +
|
|
293
758
|
claimList(disagreement.rows)
|
|
294
759
|
}).join('') + capNote(data.disagreements), data.disagreements.total)
|
|
295
760
|
}
|
|
296
761
|
if (data.stale.total > 0) {
|
|
297
|
-
|
|
298
|
-
return claimHtml(stale.row
|
|
762
|
+
attention += fold('stale', data.stale.items.map(function (stale) {
|
|
763
|
+
return claimHtml(stale.row, { note: stale.ageDays + 'd' })
|
|
299
764
|
}).join('') + capNote(data.stale), data.stale.total)
|
|
300
765
|
}
|
|
301
|
-
html += section('
|
|
766
|
+
if (attention !== '') html += section('needs attention', attention)
|
|
767
|
+
var recent = data.recent
|
|
768
|
+
html += section('recent', claimList(recent.slice(0, 10), { when: true }) +
|
|
769
|
+
(recent.length > 10 ? fold('older appends', claimList(recent.slice(10), { when: true }), recent.length - 10) : ''))
|
|
302
770
|
view.innerHTML = html
|
|
771
|
+
later('#map', api('graph', {}), function (el, graph) {
|
|
772
|
+
var count = view.querySelector('.panel .count')
|
|
773
|
+
if (count) count.innerHTML = graphCount(graph)
|
|
774
|
+
drawGraph(el, graph, { label: 'map of the most connected entities' })
|
|
775
|
+
})
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/* Neighbors grouped by relation, both directions — the quick way onward. */
|
|
779
|
+
function neighborsHtml (data) {
|
|
780
|
+
var groups = Object.create(null)
|
|
781
|
+
var order = []
|
|
782
|
+
function add (label, name) {
|
|
783
|
+
if (isLiteral(name) || name === data.name) return
|
|
784
|
+
if (!groups[label]) { groups[label] = []; order.push(label) }
|
|
785
|
+
if (groups[label].indexOf(name) < 0) groups[label].push(name)
|
|
786
|
+
}
|
|
787
|
+
data.out.forEach(function (c) { if (c.conf > 0) add((c.negated ? 'NOT ' : '') + c.verb + ' →', c.object) })
|
|
788
|
+
data.in.forEach(function (c) {
|
|
789
|
+
if (c.conf > 0) add(c.rel !== undefined ? (c.negated ? 'NOT ' : '') + c.rel + ' →' : '← ' + (c.negated ? 'NOT ' : '') + c.verb, c.subject)
|
|
790
|
+
})
|
|
791
|
+
if (order.length === 0) return ''
|
|
792
|
+
return section('related', '<div class="neighbors">' + order.map(function (label) {
|
|
793
|
+
return '<span class="vb">' + esc(label) + '</span><div class="chips">' + groups[label].map(function (name) {
|
|
794
|
+
return '<a class="chip" href="#/e/' + enc(name) + '">' + esc(name) + '</a>'
|
|
795
|
+
}).join('') + '</div>'
|
|
796
|
+
}).join('') + '</div>')
|
|
303
797
|
}
|
|
304
798
|
|
|
305
799
|
function entityPage (data) {
|
|
306
800
|
var html = '<h1>' + esc(data.name) + ' <span class="kind">entity</span></h1>'
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
801
|
+
var badges = data.types.map(function (name) {
|
|
802
|
+
return '<a class="chip type" href="#/e/' + enc(name) + '" title="' + esc(data.name) + ' IS ' + esc(name) + '">' + esc(name) + '</a>'
|
|
803
|
+
}).concat(data.topics.map(function (name) {
|
|
804
|
+
return '<a class="chip" href="#/t/' + enc(name) + '" title="topic">' + esc(name) + '</a>'
|
|
805
|
+
}))
|
|
806
|
+
if (badges.length > 0) html += '<div class="chips">' + badges.join('') + '</div>'
|
|
310
807
|
if (data.aliases.length > 1) {
|
|
311
|
-
html += '<p class="aka">aka ' + data.aliases.slice(1).map(entityHtml).join(', ') + '</p>'
|
|
808
|
+
html += '<p class="aka">aka ' + data.aliases.slice(1).map(function (name) { return entityHtml(name) }).join(', ') + '</p>'
|
|
312
809
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}).join('') + '</div>'
|
|
810
|
+
var relations = data.out.length + data.in.length
|
|
811
|
+
if (relations > 0) {
|
|
812
|
+
html += section('neighborhood', graphPanel('egraph', depthButtons(1) + ' <span class="count"></span> <a class="open-graph" href="#/g/1/' + enc(data.name) + '">full screen</a>'))
|
|
317
813
|
}
|
|
318
|
-
html +=
|
|
319
|
-
html += section('
|
|
320
|
-
html += section('relations
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
html +=
|
|
814
|
+
html += neighborsHtml(data)
|
|
815
|
+
html += section('facts', claimList(data.facts, { self: data.name }), data.facts.length || undefined)
|
|
816
|
+
html += section('relations', fold('as subject', claimList(data.out, { self: data.name }), data.out.length, data.out.length <= 12) +
|
|
817
|
+
fold('as object', data.in.length === 0 ? '<p class="none">none</p>' : data.in.map(function (c) {
|
|
818
|
+
return claimHtml(c, { self: data.name, rel: c.rel })
|
|
819
|
+
}).join(''), data.in.length, data.in.length <= 12))
|
|
820
|
+
html += fold('activity <span class="n">all rows about ' + esc(data.name) + ', newest first</span>', claimList(data.activity, { when: true, self: data.name }) +
|
|
821
|
+
(data.total > data.activity.length ? '<p class="none">showing the newest ' + data.activity.length + ' of ' + data.total + '</p>' : ''), data.total)
|
|
325
822
|
view.innerHTML = html
|
|
823
|
+
visit(data.name)
|
|
824
|
+
if (relations > 0) neighborhood(data.name, 1)
|
|
326
825
|
}
|
|
327
826
|
|
|
328
827
|
function topicPage (data) {
|
|
329
828
|
var html = '<h1>' + esc(data.name) + ' <span class="kind">topic</span></h1>'
|
|
829
|
+
html += '<p class="lede"><a href="#/e/' + enc(data.name) + '">open as entity</a></p>'
|
|
330
830
|
html += section('members', data.members.length === 0 ? '<p class="none">none</p>' :
|
|
331
831
|
'<div class="chips">' + data.members.map(function (name) {
|
|
332
832
|
return isLiteral(name) ? '<span class="chip">' + esc(name) + '</span>' :
|
|
333
833
|
'<a class="chip" href="#/e/' + enc(name) + '">' + esc(name) + '</a>'
|
|
334
834
|
}).join('') + '</div>', data.members.length)
|
|
835
|
+
if (data.members.length > 0) {
|
|
836
|
+
html += section('graph', graphPanel('egraph', depthButtons(1) + ' <span class="count"></span>'))
|
|
837
|
+
}
|
|
838
|
+
view.innerHTML = html
|
|
839
|
+
if (data.members.length > 0) neighborhood(data.name, 1)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function graphPage (data) {
|
|
843
|
+
var focus = data.focus
|
|
844
|
+
var html = focus === undefined ?
|
|
845
|
+
'<h1>Graph <span class="kind">most connected entities</span></h1>' :
|
|
846
|
+
'<h1>' + esc(focus) + ' <span class="kind">graph</span></h1>'
|
|
847
|
+
html += '<p class="lede">' + (focus === undefined ? '' : '<a href="#/e/' + enc(focus) + '">back to entity</a> · ') +
|
|
848
|
+
'Current relations between entities; negated ones dashed. Drag to rearrange or pan, hover to trace, click to open.</p>'
|
|
849
|
+
var depth = Math.max.apply(null, data.nodes.map(function (node) { return node.depth || 0 }).concat([1]))
|
|
850
|
+
var hops = location.hash.match(/^#\\/g\\/([1-3])\\//)
|
|
851
|
+
if (hops) depth = +hops[1]
|
|
852
|
+
html += graphPanel('fgraph', (focus === undefined ? '' : depthButtons(depth)) + ' <span class="count">' + graphCount(data) + '</span>')
|
|
853
|
+
html += fold('as a list', data.nodes.length === 0 ? '<p class="none">none</p>' : '<div class="neighbors">' + data.nodes.map(function (node) {
|
|
854
|
+
var out = data.edges.filter(function (edge) { return edge.source === node.name })
|
|
855
|
+
return '<span class="vb"><a href="#/e/' + enc(node.name) + '">' + esc(node.name) + '</a></span><span class="hint">' +
|
|
856
|
+
(out.length === 0 ? '—' : out.map(function (edge) {
|
|
857
|
+
return (edge.negated ? 'NOT ' : '') + esc(edge.verb) + ' ' + esc(edge.target)
|
|
858
|
+
}).join(', ')) + '</span>'
|
|
859
|
+
}).join('') + '</div>', data.nodes.length)
|
|
335
860
|
view.innerHTML = html
|
|
861
|
+
Array.prototype.forEach.call(view.querySelectorAll('[data-depth]'), function (button) {
|
|
862
|
+
button.onclick = function () { location.hash = '#/g/' + button.getAttribute('data-depth') + '/' + enc(focus) }
|
|
863
|
+
})
|
|
864
|
+
drawGraph(view.querySelector('#fgraph'), data, { tall: true, label: focus === undefined ? 'map of the most connected entities' : 'relation graph around ' + focus })
|
|
336
865
|
}
|
|
337
866
|
|
|
338
867
|
function historyPage (data) {
|
|
@@ -371,10 +900,19 @@ function lineagePage (data) {
|
|
|
371
900
|
|
|
372
901
|
function searchPage (text, data) {
|
|
373
902
|
view.innerHTML = '<h1>' + esc(text) + ' <span class="kind">search</span></h1>' +
|
|
903
|
+
'<div id="names"></div>' +
|
|
374
904
|
'<p>Search includes earlier revisions and retracted claims, newest first. Open a claim’s history to follow its changes.</p>' +
|
|
375
905
|
(data.length === 100 ? '<p>Showing the newest 100 matches. More may exist; refine the search phrase to narrow the results.</p>' : '') +
|
|
376
906
|
section('matches', claimList(data, { when: true }), data.length)
|
|
377
907
|
}
|
|
908
|
+
/* Entity names matching the phrase, offered above the claim matches. */
|
|
909
|
+
function namesFor (text) {
|
|
910
|
+
later('#names', api('names', { q: text }), function (el, list) {
|
|
911
|
+
el.innerHTML = list.length === 0 ? '' : section('entities', '<div class="chips">' + list.map(function (name) {
|
|
912
|
+
return '<a class="chip" href="#/e/' + enc(name) + '">' + esc(name) + '</a>'
|
|
913
|
+
}).join('') + '</div>', list.length)
|
|
914
|
+
})
|
|
915
|
+
}
|
|
378
916
|
|
|
379
917
|
var navigation = 0
|
|
380
918
|
var renderedHash
|
|
@@ -455,12 +993,26 @@ function renderRoute () {
|
|
|
455
993
|
} else if ((match = hash.match(/^#\\/s\\/(.+)$/))) {
|
|
456
994
|
var text = decodeURIComponent(match[1])
|
|
457
995
|
if (changedRoute) searchBox.value = text
|
|
458
|
-
show(api('search', { q: text }), function (data) { searchPage(text, data) })
|
|
996
|
+
show(api('search', { q: text }), function (data) { searchPage(text, data); namesFor(text) })
|
|
997
|
+
} else if ((match = hash.match(/^#\\/g\\/([1-3])\\/(.+)$/))) {
|
|
998
|
+
show(api('graph', { name: decodeURIComponent(match[2]), depth: match[1], aliases: aliases }), graphPage)
|
|
999
|
+
} else if (/^#\\/g\\/?$/.test(hash)) {
|
|
1000
|
+
show(api('graph'), graphPage)
|
|
459
1001
|
} else {
|
|
460
1002
|
show(api('overview'), dashboard)
|
|
461
1003
|
}
|
|
462
1004
|
}
|
|
463
1005
|
window.addEventListener('hashchange', route)
|
|
1006
|
+
// "/" jumps to search from anywhere outside a text field.
|
|
1007
|
+
document.addEventListener('keydown', function (event) {
|
|
1008
|
+
var target = event.target
|
|
1009
|
+
var typing = target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)
|
|
1010
|
+
if (event.key === '/' && !typing && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
1011
|
+
event.preventDefault()
|
|
1012
|
+
searchBox.focus()
|
|
1013
|
+
searchBox.select()
|
|
1014
|
+
}
|
|
1015
|
+
})
|
|
464
1016
|
route()
|
|
465
1017
|
</script>
|
|
466
1018
|
</body>
|