vis-chronicle 1.2.5 → 1.4.5

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 CHANGED
@@ -95,8 +95,6 @@ Item properties:
95
95
  * `general`: General query segment, usually for selecting the item and property.
96
96
  * `start`: Query segment for selecting the start value.
97
97
  * `end`: Query segment for selecting the end value.
98
- * `startPath`: Another way of writing Wikidata queries. TODO.
99
- * `endPath`: See `startPath`
100
98
  * `expectedDuration`: Describes the expected duration, for hinting if the start or end is missing.
101
99
  * `min`: The absolute minimum duration.
102
100
  * `max`: The absolute maximum duration.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vis-chronicle",
3
- "version": "1.2.5",
3
+ "version": "1.4.5",
4
4
  "description": "Generates JSON for populating a vis.js timeline from Wikidata queries.",
5
5
  "keywords": [
6
6
  "wikidata",
package/src/fetch.js CHANGED
@@ -37,12 +37,11 @@ if (!outputFile)
37
37
 
38
38
  var metadataOutputFile = values["out-metadata"]
39
39
 
40
- const moment = require('moment')
41
40
  const fs = require('fs');
42
41
  const wikidata = require('./wikidata.js')
43
42
  const renderer = require('./render.js')
44
43
  const mypath = require("./mypath.js");
45
- const { flattenRelativeDate } = require('./index.js');
44
+ const { flattenRelativeDate, rangeUnion, rangeUnionAdv } = require('./relativeDates.js');
46
45
  const wikidataToRange2 = require('./wikidataToRange.js')
47
46
 
48
47
  function wikidataToRange(param)
@@ -57,40 +56,6 @@ wikidata.verboseLogging = values["verbose"]
57
56
  wikidata.setLang(values["lang"])
58
57
  wikidata.initialize()
59
58
 
60
- function rangeUnionAdv(value, min, max)
61
- {
62
- var aggregate = {}
63
- if (min)
64
- {
65
- assert(min.min)
66
- aggregate.min = min.min
67
- }
68
- else if (value && value.min)
69
- {
70
- aggregate.min = value.min
71
- }
72
- if (max)
73
- {
74
- assert(max.max)
75
- aggregate.max = max.max
76
- }
77
- else if (value && value.max)
78
- {
79
- aggregate.max = value.max
80
- }
81
- return aggregate
82
- }
83
-
84
- function rangeUnion(a, b)
85
- {
86
- if (!a) return b
87
- if (!b) return a
88
- return {
89
- min: a.min && b.min ? moment.min(a.min, b.min) : a.min || b.min,
90
- max: a.max && b.max ? moment.max(a.max, b.max) : a.max || b.max
91
- }
92
- }
93
-
94
59
  async function entryPoint() {}
95
60
 
96
61
  entryPoint()
@@ -112,7 +77,6 @@ entryPoint()
112
77
  var templateItem = wikidata.inputSpec.items[i]
113
78
  if (templateItem.itemQuery || templateItem.items)
114
79
  {
115
- //TODO: caching for item queries
116
80
  wikidata.inputSpec.items.splice(i, 1)
117
81
  const newItems = await wikidata.createTemplateItems(templateItem)
118
82
  for (const newItem of newItems)
@@ -200,8 +164,16 @@ entryPoint()
200
164
  {
201
165
  if (item.finished) continue
202
166
 
203
- if (item.startPath) pathQueries.push(item.startPath)
204
- if (item.endPath) pathQueries.push(item.endPath)
167
+ if (item.startPath)
168
+ {
169
+ item.startPath = wikidata.replaceQueryWildcards(item.startPath, item)
170
+ pathQueries.push(item.startPath)
171
+ }
172
+ if (item.endPath)
173
+ {
174
+ item.endPath = wikidata.replaceQueryWildcards(item.endPath, item)
175
+ pathQueries.push(item.endPath)
176
+ }
205
177
 
206
178
  // the bundle key is the queries, as well as any wildcard parameters
207
179
  const keyObject = {}
package/src/index.js CHANGED
@@ -3,5 +3,5 @@ module.exports = {
3
3
 
4
4
  SparqlBuilder: require('./sparql-builder'),
5
5
  Wikidata: require('./wikidata.js'),
6
- flattenRelativeDate: require('./relativeDates.js')
7
6
  }
7
+ module.exports = Object.assign(module.exports, require('./relativeDates.js'))
@@ -2,6 +2,8 @@
2
2
  const moment = require('moment')
3
3
  const { toJewishDate, toGregorianDate, getIndexByJewishMonth } = require("jewish-date");
4
4
  const wikidata = require('./wikidata');
5
+ const wikidataToRange = require('./wikidataToRange');
6
+ const assert = require("node:assert/strict")
5
7
 
6
8
  function momentToHDate(inMoment)
7
9
  {
@@ -24,54 +26,182 @@ function durationToWikidataPrecision(duration)
24
26
  else return 9
25
27
  }
26
28
 
27
- // Flattens a relative date string into a hard date string
28
- module.exports = function flattenRelativeDate(wikidataCache, dateString)
29
+ /**
30
+ * Combines three {min, max} objects into a range containing all of them.
31
+ * @param {*} value Is overridden by min.min and max.max if they are present.
32
+ * @param {*} min
33
+ * @param {*} max
34
+ * @returns
35
+ */
36
+ function rangeUnionAdv(value, min, max)
29
37
  {
30
- if (dateString == '') return null
38
+ var aggregate = {}
39
+ if (min)
40
+ {
41
+ assert(min.min)
42
+ aggregate.min = min.min
43
+ }
44
+ else if (value && value.min)
45
+ {
46
+ aggregate.min = value.min
47
+ }
48
+ if (max)
49
+ {
50
+ assert(max.max)
51
+ aggregate.max = max.max
52
+ }
53
+ else if (value && value.max)
54
+ {
55
+ aggregate.max = value.max
56
+ }
57
+ return aggregate
58
+ }
59
+
60
+ /**
61
+ * Combines two {min, max} objects into a range containing both of them.
62
+ * @param {*} a
63
+ * @param {*} b
64
+ * @returns
65
+ */
66
+ function rangeUnion(a, b)
67
+ {
68
+ if (!a) return b
69
+ if (!b) return a
70
+ return {
71
+ min: a.min && b.min ? moment.min(a.min, b.min) : a.min || b.min,
72
+ max: a.max && b.max ? moment.max(a.max, b.max) : a.max || b.max
73
+ }
74
+ }
75
+
76
+
77
+ /**
78
+ * Breaks down a relative date path into its components.
79
+ * @param {*} dateString
80
+ * @returns An array of strings, or null.
81
+ */
82
+ function breakRelativeDate(dateString)
83
+ {
84
+ if (!dateString) return null
31
85
 
32
86
  // parse out relative date components
33
- var relSplit
34
87
  var match = dateString.match(wikidata.pathQueryRegex)
35
- if (match)
88
+ if (!match)
36
89
  {
37
- relSplit = match.slice(1)
90
+ console.error(`Failed to parse relative date '${dateString}'.`)
91
+ return null
92
+ }
93
+
94
+ const dateComponents = [ match[1] ]
95
+ var opStartIndex = 0
96
+ const operatorString = match[2]
97
+ if (operatorString.length > 0)
98
+ {
99
+ for (var i = 1; i < operatorString.length; i++)
100
+ {
101
+ if (operatorString[i] == '+' || operatorString[i] == '>')
102
+ {
103
+ dateComponents.push(operatorString.substring(opStartIndex, i))
104
+ opStartIndex = i
105
+ }
106
+ }
107
+ dateComponents.push(operatorString.substring(opStartIndex, i))
108
+ }
109
+ return dateComponents
110
+ }
111
+
112
+ /**
113
+ * Flattens a relative date path string into a hard date.
114
+ * @param {*} wikidataCache
115
+ * @param {*} dateString
116
+ * @param {*} params { returnRange:BOOL }. Result will be a range { min:STRING, max:STRING } instead.
117
+ * @returns An object like { value:STRING, precision:INT }, or null
118
+ */
119
+ function flattenRelativeDate(wikidataCache, dateString, params)
120
+ {
121
+ var flatMoment = flattenRelativeDateToMoment(wikidataCache, dateString)
122
+
123
+ // if allowed and necessary, produce a range instead of a single value
124
+ if (params?.returnRange)
125
+ {
126
+ var parsedPath = breakRelativeDate(dateString)
127
+ if (!parsedPath) return { value: null }
128
+
129
+ const basePath = parsedPath[0]
130
+ const wdpk = basePath.substring(0, basePath.lastIndexOf(':'))
131
+ const qual = basePath.substring(basePath.lastIndexOf(':') + 1)
132
+ var minQuery
133
+ var maxQuery
134
+ switch (qual)
135
+ {
136
+ case "P580":
137
+ {
138
+ minQuery = `${wdpk}:P1319` // earliest date
139
+ maxQuery = `${wdpk}:P8555` // latest start date
140
+ break
141
+ }
142
+ case "P582":
143
+ {
144
+ minQuery = `${wdpk}:P8554` // earliest end date
145
+ maxQuery = `${wdpk}:P1326` // latest date
146
+ break
147
+ }
148
+ }
149
+ if (minQuery && maxQuery)
150
+ {
151
+ const minMoment = flattenRelativeDateToMoment(wikidataCache, minQuery + parsedPath.slice(1).join(''))
152
+ const maxMoment = flattenRelativeDateToMoment(wikidataCache, maxQuery + parsedPath.slice(1).join(''))
153
+ const value = wikidataToRange(flatMoment)
154
+ const minRange = wikidataToRange(minMoment)
155
+ const maxRange = wikidataToRange(maxMoment)
156
+ const aggregateRange = rangeUnionAdv(value, minRange, maxRange)
157
+ return {
158
+ min: aggregateRange.min ? aggregateRange.min.format('YYYYYY-MM-DDThh:mm:ss') : null,
159
+ max: aggregateRange.max ? aggregateRange.max.format('YYYYYY-MM-DDThh:mm:ss') : null
160
+ }
161
+ }
162
+ else
163
+ {
164
+ const flatRange = wikidataToRange(flatMoment)
165
+ return flatRange
166
+ ? { min: flatRange.min.format('YYYYYY-MM-DDThh:mm:ss'), max: flatRange.max.format('YYYYYY-MM-DDThh:mm:ss') }
167
+ : { value: null }
168
+ }
38
169
  }
39
170
  else
40
171
  {
41
- console.error(`Failed to parse relative date '${dateString}'.`)
42
- return null
172
+ if (flatMoment && flatMoment.value)
173
+ {
174
+ flatMoment.value = flatMoment.value.format('YYYYYY-MM-DDThh:mm:ss')
175
+ }
176
+ return flatMoment
43
177
  }
178
+ }
44
179
 
45
- if (!relSplit[0])
180
+ /**
181
+ * Flattens a relative date path string into a moment.
182
+ * @param {*} wikidataCache
183
+ * @param {*} dateString
184
+ * @returns An object like { value:MOMENT, precision:INT }, or null
185
+ */
186
+ function flattenRelativeDateToMoment(wikidataCache, dateString)
187
+ {
188
+ var parsedPath = breakRelativeDate(dateString)
189
+ if (!parsedPath)
46
190
  {
47
191
  return null
48
192
  }
49
- else if (!wikidataCache[relSplit[0]])
193
+ else if (!wikidataCache[parsedPath[0]])
50
194
  {
51
- console.error(`Date for '${relSplit[0]}' wasn't cached.`)
195
+ console.error(`Date for '${parsedPath[0]}' wasn't cached.`)
52
196
  return null
53
197
  }
54
198
  else
55
199
  {
56
- const cacheEntry = wikidataCache[relSplit[0]]
57
- if (cacheEntry.value && relSplit.length > 1)
200
+ const cacheEntry = wikidataCache[parsedPath[0]]
201
+ if (cacheEntry.value && parsedPath.length > 1)
58
202
  {
59
203
  // break up operators
60
- const dateOperators = []
61
- var opStartIndex = 0
62
- const operatorString = relSplit[1]
63
- if (operatorString.length > 0)
64
- {
65
- for (var i = 1; i < operatorString.length; i++)
66
- {
67
- if (operatorString[i] == '+' || operatorString[i] == '>')
68
- {
69
- dateOperators.push(operatorString.substring(opStartIndex, i))
70
- opStartIndex = i
71
- }
72
- }
73
- dateOperators.push(operatorString.substring(opStartIndex, i))
74
- }
204
+ const dateOperators = parsedPath.slice(1)
75
205
 
76
206
  // handle relative segments of date
77
207
  // About precision:
@@ -146,14 +276,15 @@ module.exports = function flattenRelativeDate(wikidataCache, dateString)
146
276
 
147
277
  //console.log(`${component} (${precision}): ${momentDate}`)
148
278
  }
149
- return {
150
- value: momentDate.format('YYYYYY-MM-DDThh:mm:ss'),
151
- precision: precision
152
- }
279
+ return { value: momentDate, precision: precision }
153
280
  }
154
281
  else
155
282
  {
156
- return {...cacheEntry}
283
+ const cachedOut = {...cacheEntry}
284
+ if (cachedOut.value) cachedOut.value = moment(cachedOut.value)
285
+ return cachedOut
157
286
  }
158
287
  }
159
- }
288
+ }
289
+
290
+ module.exports = { breakRelativeDate, flattenRelativeDate, rangeUnion, rangeUnionAdv }
package/src/render.js CHANGED
@@ -461,7 +461,12 @@ renderer.produceOutput = function(inputSpec, items)
461
461
  {
462
462
  const labelItem = {...outputItem}
463
463
  labelItem.id += "-label"
464
- labelItem.className = [labelItem.className, "visc-toplabel"].join(' '),
464
+
465
+ var classes = labelItem.className.split(' ')
466
+ classes = classes.filter(c => c != "visc-left-connection" && c != "visc-right-connection")
467
+ classes.push("visc-toplabel")
468
+ labelItem.className = classes.join(' ')
469
+
465
470
  outputObject.items.push(labelItem)
466
471
  outputItem.content = outputItem.content ? "&nbsp;" : ""
467
472
  }
package/src/wikidata.js CHANGED
@@ -2,6 +2,7 @@
2
2
  const mypath = require("./mypath.js")
3
3
  const fs = require('fs');
4
4
  const nodepath = require('node:path');
5
+ const moment = require('moment')
5
6
  const globalData = require("./global-data.json")
6
7
  const assert = require('node:assert/strict')
7
8
  const SparqlBuilder = require("./sparql-builder.js")
@@ -12,9 +13,9 @@ const wikidata = module.exports = {
12
13
  verboseLogging: false,
13
14
 
14
15
  /**
15
- *
16
+ * Caches results for SPARQL queries, using the query string as the key.
16
17
  */
17
- cache: {},
18
+ queryCache: {},
18
19
 
19
20
  /**
20
21
  * Caches results for path queries, using the path as the key.
@@ -25,9 +26,9 @@ const wikidata = module.exports = {
25
26
  cacheBuster: undefined,
26
27
 
27
28
  /**
28
- * Relative path to the term cache file.
29
+ * Relative path to the query cache file.
29
30
  */
30
- termCacheFile: "intermediate/wikidata-term-cache.json",
31
+ queryCacheFile: "intermediate/sparql-query-cache.json",
31
32
 
32
33
  /**
33
34
  * Relative path to the path cache file.
@@ -126,8 +127,8 @@ const wikidata = module.exports = {
126
127
  {
127
128
  try
128
129
  {
129
- const contents = await fs.promises.readFile(this.termCacheFile)
130
- this.cache = JSON.parse(contents)
130
+ const contents = await fs.promises.readFile(this.queryCacheFile)
131
+ this.queryCache = JSON.parse(contents)
131
132
  }
132
133
  catch
133
134
  {
@@ -147,11 +148,11 @@ const wikidata = module.exports = {
147
148
 
148
149
  writeCache: async function()
149
150
  {
150
- await mypath.ensureDirectoryForFile(this.termCacheFile)
151
+ await mypath.ensureDirectoryForFile(this.queryCacheFile)
151
152
 
152
- fs.writeFile(this.termCacheFile, JSON.stringify(this.cache), err => {
153
+ fs.writeFile(this.queryCacheFile, JSON.stringify(this.queryCache), err => {
153
154
  if (err) {
154
- console.error(`Error writing wikidata term cache:`)
155
+ console.error(`Error writing wikidata query cache:`)
155
156
  console.error(err)
156
157
  }
157
158
  })
@@ -184,25 +185,32 @@ const wikidata = module.exports = {
184
185
  }
185
186
  },
186
187
 
187
- postprocessQueryTerm: function(context, term, item)
188
+ replaceQueryWildcards: function(term, item, itemPrefix = "")
188
189
  {
189
- if (!term)
190
- {
191
- return term;
192
- }
193
-
194
190
  // replace query wildcards
195
191
  for (const key in item)
196
192
  {
197
193
  var insertValue = item[key]
198
194
  if (typeof insertValue === "string" && insertValue.startsWith("Q"))
199
- insertValue = "wd:" + insertValue
195
+ insertValue = itemPrefix + insertValue
200
196
  term = term.replaceAll(`{${key}}`, insertValue)
201
197
  }
202
198
 
203
199
  // detect unreplaced wildcards
204
200
  //TODO:
205
201
 
202
+ return term
203
+ },
204
+
205
+ preprocessQueryTerm: function(context, term, item)
206
+ {
207
+ if (!term)
208
+ {
209
+ return term
210
+ }
211
+
212
+ term = this.replaceQueryWildcards(term, item, "wd:")
213
+
206
214
  // terminate term
207
215
  if (!term.trim().endsWith("."))
208
216
  {
@@ -238,7 +246,7 @@ const wikidata = module.exports = {
238
246
 
239
247
  //TODO: validate query has required wildcards
240
248
 
241
- queryTerm = this.postprocessQueryTerm(inQueryTerm, queryTerm, item)
249
+ queryTerm = this.preprocessQueryTerm(inQueryTerm, queryTerm, item)
242
250
  return queryTerm
243
251
  },
244
252
 
@@ -275,7 +283,7 @@ const wikidata = module.exports = {
275
283
  if (typeof queryTerm === 'string' || queryTerm instanceof String)
276
284
  {
277
285
  return {
278
- value: this.postprocessQueryTerm(inQueryTerm, queryTerm, item),
286
+ value: this.preprocessQueryTerm(inQueryTerm, queryTerm, item),
279
287
  min: "?_prop pqv:P1319 ?_min_value.",
280
288
  max: "?_prop pqv:P1326 ?_max_value."
281
289
  }
@@ -285,7 +293,7 @@ const wikidata = module.exports = {
285
293
  const result = {}
286
294
  for (const key in queryTerm)
287
295
  {
288
- result[key] = this.postprocessQueryTerm(inQueryTerm, queryTerm[key], item)
296
+ result[key] = this.preprocessQueryTerm(inQueryTerm, queryTerm[key], item)
289
297
  }
290
298
  return result
291
299
  }
@@ -393,15 +401,7 @@ const wikidata = module.exports = {
393
401
  queryBuilder.addOptionalQueryTerm(`${propVar} wikibase:rank ${rankVar}.`)
394
402
 
395
403
  const query = queryBuilder.build()
396
-
397
- // read cache
398
- const cacheKey = query
399
- if (!this.skipCache && !item.skipCache && this.cache[cacheKey])
400
- {
401
- return this.cache[cacheKey]
402
- }
403
-
404
- const data = await this.runQuery(query)
404
+ const data = await this.runQuery(query, item.skipCache)
405
405
  console.log(`\tQuery for ${item.id} returned ${data.results.bindings.length} results.`)
406
406
 
407
407
  const readBinding = function(binding)
@@ -487,10 +487,18 @@ const wikidata = module.exports = {
487
487
  }
488
488
  }
489
489
 
490
- this.cache[cacheKey] = result;
491
490
  return result;
492
491
  },
493
492
 
493
+ /**
494
+ * Formats the provided Wikidata date string as ISO 8601 with a six-digit year.
495
+ */
496
+ normalizeWikidataDate: function(dateStr)
497
+ {
498
+ const date = moment(dateStr, 'Y-MM-DDThh:mm:ss')
499
+ return date.format('YYYYYY-MM-DDThh:mm:ss')
500
+ },
501
+
494
502
  /**
495
503
  * Runs an unsorted list of path queries.
496
504
  */
@@ -557,7 +565,7 @@ const wikidata = module.exports = {
557
565
  for (const binding of data.results.bindings)
558
566
  {
559
567
  const key = this.extractQidFromUrl(binding['item'].value)
560
- this.pathCache[key] = { value: binding['date'].value, precision: binding['precision'].value }
568
+ this.pathCache[key] = { value: this.normalizeWikidataDate(binding['date'].value), precision: binding['precision'].value }
561
569
  foundKeys.add(key)
562
570
  }
563
571
 
@@ -600,7 +608,7 @@ const wikidata = module.exports = {
600
608
  const qid = this.extractQidFromUrl(binding['item'].value)
601
609
  const pid = this.extractQidFromUrl(binding['p'].value)
602
610
  const key = `${qid}:${pid}`
603
- this.pathCache[key] = { value: binding['date'].value, precision: binding['precision'].value }
611
+ this.pathCache[key] = { value: this.normalizeWikidataDate(binding['date'].value), precision: binding['precision'].value }
604
612
  foundKeys.add(key)
605
613
  }
606
614
 
@@ -620,6 +628,25 @@ const wikidata = module.exports = {
620
628
  for (const wdProp of queries)
621
629
  {
622
630
  if (!this.pathCache[wdProp]) qualsToQuery.push(wdProp)
631
+
632
+ // grab corresponding min/max props as well
633
+ const split = wdProp.split(':')
634
+ const wdpk = split.slice(0, 3).join(':')
635
+ switch (split[3])
636
+ {
637
+ case "P580":
638
+ query = `${wdpk}:P1319` // earliest date
639
+ if (!this.pathCache[query]) qualsToQuery.push(query)
640
+ query = `${wdpk}:P8555` // latest start date
641
+ if (!this.pathCache[query]) qualsToQuery.push(query)
642
+ break;
643
+ case "P582":
644
+ query = `${wdpk}:P8554` // earliest end date
645
+ if (!this.pathCache[query]) qualsToQuery.push(query)
646
+ query = `${wdpk}:P1326` // latest date
647
+ if (!this.pathCache[query]) qualsToQuery.push(query)
648
+ break;
649
+ }
623
650
  }
624
651
  if (qualsToQuery.length > 0)
625
652
  {
@@ -646,7 +673,7 @@ const wikidata = module.exports = {
646
673
  const valId = this.extractQidFromUrl(binding['value'].value)
647
674
  const qualId = this.extractQidFromUrl(binding['q'].value)
648
675
  const key = `${itemId}:${propId}:${valId}:${qualId}`
649
- this.pathCache[key] = { value: binding['date'].value, precision: binding['precision'].value }
676
+ this.pathCache[key] = { value: this.normalizeWikidataDate(binding['date'].value), precision: binding['precision'].value }
650
677
  foundKeys.add(key)
651
678
  }
652
679
 
@@ -695,7 +722,11 @@ const wikidata = module.exports = {
695
722
  queryBuilder.addWikibaseLabel(this.lang)
696
723
 
697
724
  const query = queryBuilder.build()
698
- const data = await this.runQuery(query)
725
+ const data = await this.runQuery(query, templateItem.skipCache)
726
+ if (!data)
727
+ {
728
+ throw 'No response data from Wikidata query.'
729
+ }
699
730
 
700
731
  const newItems = []
701
732
 
@@ -723,8 +754,13 @@ const wikidata = module.exports = {
723
754
  },
724
755
 
725
756
  // runs a SPARQL query
726
- runQuery: async function(query)
757
+ runQuery: async function(query, skipCache = false)
727
758
  {
759
+ if (!skipCache && !this.skipCache && this.queryCache[query])
760
+ {
761
+ return this.queryCache[query]
762
+ }
763
+
728
764
  if (this.verboseLogging) console.log(query)
729
765
 
730
766
  assert(this.options)
@@ -739,6 +775,7 @@ const wikidata = module.exports = {
739
775
  else
740
776
  {
741
777
  const data = await response.json()
778
+ this.queryCache[query] = data
742
779
  return data
743
780
  }
744
781
  }