conceptql 0.6.0 → 0.7.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +176 -0
- data/README.md +4 -2
- data/assets/diagram/diagram.css +331 -0
- data/assets/diagram/fonts/OFL.txt +93 -0
- data/assets/diagram/fonts/Roboto-400.ttf +0 -0
- data/assets/diagram/fonts/Roboto-700.ttf +0 -0
- data/assets/diagram/fonts/Roboto-Italic-400.ttf +0 -0
- data/assets/diagram/fonts/Roboto-Italic-Latin.woff2 +0 -0
- data/assets/diagram/fonts/Roboto-Normal-Latin.woff2 +0 -0
- data/assets/diagram/operators.svg +224 -0
- data/config/vocabularies.csv +85 -85
- data/docs/spec/README.md +2624 -0
- data/docs/spec/examples/after-operator.json +562 -0
- data/docs/spec/examples/any-overlap-operator.json +106 -0
- data/docs/spec/examples/before-operator.json +437 -0
- data/docs/spec/examples/co-reported-operator.json +168 -0
- data/docs/spec/examples/contains-operator.json +97 -0
- data/docs/spec/examples/date-range-operator.json +275 -0
- data/docs/spec/examples/death-operator.json +103 -0
- data/docs/spec/examples/during-operator.json +178 -0
- data/docs/spec/examples/episode-operator.json +80 -0
- data/docs/spec/examples/equal-operator.json +79 -0
- data/docs/spec/examples/ethnicity-operator.json +86 -0
- data/docs/spec/examples/except-operator.json +536 -0
- data/docs/spec/examples/filter-operator.json +218 -0
- data/docs/spec/examples/first-operator.json +148 -0
- data/docs/spec/examples/gender-operator.json +257 -0
- data/docs/spec/examples/hospice-operator.json +40 -0
- data/docs/spec/examples/hospitalization-operator.json +131 -0
- data/docs/spec/examples/information-periods-operator.json +131 -0
- data/docs/spec/examples/last-operator.json +148 -0
- data/docs/spec/examples/match-operator.json +186 -0
- data/docs/spec/examples/nth-occurrence-operator.json +300 -0
- data/docs/spec/examples/numeric-filter-operator.json +77 -0
- data/docs/spec/examples/one-in-two-out-operator.json +158 -0
- data/docs/spec/examples/person-filter-operator.json +919 -0
- data/docs/spec/examples/person-operator.json +274 -0
- data/docs/spec/examples/place-of-service-filter-operator.json +57 -0
- data/docs/spec/examples/provenance-operator.json +148 -0
- data/docs/spec/examples/provider-filter-operator.json +77 -0
- data/docs/spec/examples/race-operator.json +131 -0
- data/docs/spec/examples/recall-operator.json +289 -0
- data/docs/spec/examples/snf-operator.json +40 -0
- data/docs/spec/examples/time-window-operator.json +736 -0
- data/docs/spec/examples/trim-date-end-operator.json +201 -0
- data/docs/spec/examples/trim-date-start-operator.json +201 -0
- data/docs/spec/examples/union-operator.json +547 -0
- data/docs/spec/examples/vocabulary-operator.json +232 -0
- data/lib/conceptql/behaviors/metadatable.rb +68 -2
- data/lib/conceptql/behaviors/provenanceable.rb +4 -1
- data/lib/conceptql/behaviors/utilizable.rb +3 -0
- data/lib/conceptql/code_case_mismatch_error.rb +50 -0
- data/lib/conceptql/data_model/gdm.rb +23 -2
- data/lib/conceptql/diagram_renderer/app.rb +59 -0
- data/lib/conceptql/diagram_renderer/configuration.rb +87 -0
- data/lib/conceptql/diagram_renderer/diagram_data.rb +80 -0
- data/lib/conceptql/diagram_renderer/drawing.rb +255 -0
- data/lib/conceptql/diagram_renderer/geometry.rb +380 -0
- data/lib/conceptql/diagram_renderer/input_error.rb +11 -0
- data/lib/conceptql/diagram_renderer/js_format.rb +112 -0
- data/lib/conceptql/diagram_renderer/resvg.rb +120 -0
- data/lib/conceptql/diagram_renderer/sprite.rb +73 -0
- data/lib/conceptql/diagram_renderer/static.rb +70 -0
- data/lib/conceptql/diagram_renderer/svg.rb +73 -0
- data/lib/conceptql/diagram_renderer/tree_layout.rb +287 -0
- data/lib/conceptql/diagram_renderer.rb +82 -0
- data/lib/conceptql/lexicon/lexicon_strategy.rb +16 -0
- data/lib/conceptql/lexicon.rb +63 -11
- data/lib/conceptql/nodifier.rb +5 -3
- data/lib/conceptql/operators/after.rb +15 -1
- data/lib/conceptql/operators/any_overlap.rb +6 -1
- data/lib/conceptql/operators/before.rb +11 -1
- data/lib/conceptql/operators/binary_operator_operator.rb +12 -2
- data/lib/conceptql/operators/co_reported.rb +9 -4
- data/lib/conceptql/operators/contains.rb +6 -1
- data/lib/conceptql/operators/count.rb +2 -0
- data/lib/conceptql/operators/date_range.rb +15 -3
- data/lib/conceptql/operators/death.rb +5 -1
- data/lib/conceptql/operators/dedup.rb +2 -0
- data/lib/conceptql/operators/during.rb +6 -1
- data/lib/conceptql/operators/episode.rb +13 -2
- data/lib/conceptql/operators/equal.rb +11 -1
- data/lib/conceptql/operators/ethnicity.rb +19 -7
- data/lib/conceptql/operators/except.rb +10 -1
- data/lib/conceptql/operators/filter.rb +6 -1
- data/lib/conceptql/operators/first.rb +19 -9
- data/lib/conceptql/operators/from.rb +3 -0
- data/lib/conceptql/operators/gender.rb +10 -2
- data/lib/conceptql/operators/hospice.rb +7 -1
- data/lib/conceptql/operators/hospitalization.rb +6 -1
- data/lib/conceptql/operators/information_periods.rb +26 -7
- data/lib/conceptql/operators/invalid.rb +4 -0
- data/lib/conceptql/operators/last.rb +19 -9
- data/lib/conceptql/operators/match.rb +6 -1
- data/lib/conceptql/operators/multiple_vocabularies.rb +17 -3
- data/lib/conceptql/operators/numeric.rb +4 -0
- data/lib/conceptql/operators/numeric_filter.rb +13 -3
- data/lib/conceptql/operators/occurrence.rb +33 -12
- data/lib/conceptql/operators/one_in_two_out.rb +22 -5
- data/lib/conceptql/operators/operator.rb +8 -4
- data/lib/conceptql/operators/person.rb +5 -1
- data/lib/conceptql/operators/person_filter.rb +14 -1
- data/lib/conceptql/operators/place_of_service_filter.rb +14 -3
- data/lib/conceptql/operators/provenance.rb +17 -8
- data/lib/conceptql/operators/provider_filter.rb +18 -6
- data/lib/conceptql/operators/race.rb +14 -9
- data/lib/conceptql/operators/read.rb +11 -1
- data/lib/conceptql/operators/recall.rb +34 -2
- data/lib/conceptql/operators/snf.rb +7 -1
- data/lib/conceptql/operators/sum.rb +4 -0
- data/lib/conceptql/operators/temporal_operator.rb +4 -2
- data/lib/conceptql/operators/time_window.rb +27 -4
- data/lib/conceptql/operators/trim_date.rb +6 -14
- data/lib/conceptql/operators/trim_date_end.rb +9 -3
- data/lib/conceptql/operators/trim_date_start.rb +9 -3
- data/lib/conceptql/operators/union.rb +13 -1
- data/lib/conceptql/operators/vocabulary.rb +80 -6
- data/lib/conceptql/paths.rb +9 -1
- data/lib/conceptql/version.rb +1 -1
- data/lib/conceptql/vocabularies/drg_systems.rb +3 -2
- data/lib/conceptql/vocabularies/entry.rb +14 -11
- data/lib/conceptql/vocabularies/vocabulary_class_factory.rb +2 -1
- data/lib/conceptql.rb +3 -0
- metadata +75 -2
- data/doc/spec.md +0 -2091
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
{
|
|
2
|
+
"spec_id": "vocabulary-operator",
|
|
3
|
+
"examples": [
|
|
4
|
+
{
|
|
5
|
+
"title": "All records with the ICD-9-CM diagnosis code 412 (old myocardial infarction)",
|
|
6
|
+
"statement": ["icd9cm", "412"],
|
|
7
|
+
"help": true,
|
|
8
|
+
"columns": [
|
|
9
|
+
"person_id",
|
|
10
|
+
"criterion_id",
|
|
11
|
+
"criterion_table",
|
|
12
|
+
"criterion_domain",
|
|
13
|
+
"start_date",
|
|
14
|
+
"end_date",
|
|
15
|
+
"source_value"
|
|
16
|
+
],
|
|
17
|
+
"rows": [
|
|
18
|
+
[
|
|
19
|
+
11,
|
|
20
|
+
82614,
|
|
21
|
+
"observations",
|
|
22
|
+
"condition_occurrence",
|
|
23
|
+
"2008-10-29",
|
|
24
|
+
"2008-10-29",
|
|
25
|
+
"412"
|
|
26
|
+
],
|
|
27
|
+
[
|
|
28
|
+
17,
|
|
29
|
+
27269,
|
|
30
|
+
"observations",
|
|
31
|
+
"condition_occurrence",
|
|
32
|
+
"2008-08-25",
|
|
33
|
+
"2008-08-25",
|
|
34
|
+
"412"
|
|
35
|
+
],
|
|
36
|
+
[
|
|
37
|
+
17,
|
|
38
|
+
46165,
|
|
39
|
+
"observations",
|
|
40
|
+
"condition_occurrence",
|
|
41
|
+
"2009-04-30",
|
|
42
|
+
"2009-04-30",
|
|
43
|
+
"412"
|
|
44
|
+
],
|
|
45
|
+
[
|
|
46
|
+
38,
|
|
47
|
+
40556,
|
|
48
|
+
"observations",
|
|
49
|
+
"condition_occurrence",
|
|
50
|
+
"2010-02-12",
|
|
51
|
+
"2010-02-12",
|
|
52
|
+
"412"
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
54,
|
|
56
|
+
74606,
|
|
57
|
+
"observations",
|
|
58
|
+
"condition_occurrence",
|
|
59
|
+
"2008-06-05",
|
|
60
|
+
"2008-06-05",
|
|
61
|
+
"412"
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
60,
|
|
65
|
+
986,
|
|
66
|
+
"observations",
|
|
67
|
+
"condition_occurrence",
|
|
68
|
+
"2009-07-19",
|
|
69
|
+
"2009-07-22",
|
|
70
|
+
"412"
|
|
71
|
+
],
|
|
72
|
+
[
|
|
73
|
+
66,
|
|
74
|
+
16171,
|
|
75
|
+
"observations",
|
|
76
|
+
"condition_occurrence",
|
|
77
|
+
"2009-07-25",
|
|
78
|
+
"2009-07-25",
|
|
79
|
+
"412"
|
|
80
|
+
],
|
|
81
|
+
[
|
|
82
|
+
73,
|
|
83
|
+
52843,
|
|
84
|
+
"observations",
|
|
85
|
+
"condition_occurrence",
|
|
86
|
+
"2008-11-16",
|
|
87
|
+
"2008-11-16",
|
|
88
|
+
"412"
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
77,
|
|
92
|
+
58165,
|
|
93
|
+
"observations",
|
|
94
|
+
"condition_occurrence",
|
|
95
|
+
"2010-10-06",
|
|
96
|
+
"2010-10-06",
|
|
97
|
+
"412"
|
|
98
|
+
],
|
|
99
|
+
[
|
|
100
|
+
81,
|
|
101
|
+
1405,
|
|
102
|
+
"observations",
|
|
103
|
+
"condition_occurrence",
|
|
104
|
+
"2009-01-28",
|
|
105
|
+
"2009-01-30",
|
|
106
|
+
"412"
|
|
107
|
+
]
|
|
108
|
+
],
|
|
109
|
+
"tree": {
|
|
110
|
+
"format": "conceptql-diagram/v1",
|
|
111
|
+
"statements": [
|
|
112
|
+
{
|
|
113
|
+
"name": "icd9cm",
|
|
114
|
+
"base": "selection",
|
|
115
|
+
"humanName": "ICD-9 CM",
|
|
116
|
+
"vocabularyId": "ICD9CM",
|
|
117
|
+
"outputTypes": ["condition_occurrence"],
|
|
118
|
+
"parameters": {},
|
|
119
|
+
"values": ["412"],
|
|
120
|
+
"children": [],
|
|
121
|
+
"counts": {
|
|
122
|
+
"condition_occurrence": {
|
|
123
|
+
"rows": 48,
|
|
124
|
+
"n": 37
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
]
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
"title": "All MS-DRG 205 records: other respiratory system diagnoses with a major complication or comorbidity",
|
|
133
|
+
"statement": ["ms_drg", "205"],
|
|
134
|
+
"help": true,
|
|
135
|
+
"columns": [
|
|
136
|
+
"person_id",
|
|
137
|
+
"criterion_id",
|
|
138
|
+
"criterion_table",
|
|
139
|
+
"criterion_domain",
|
|
140
|
+
"start_date",
|
|
141
|
+
"end_date",
|
|
142
|
+
"source_value"
|
|
143
|
+
],
|
|
144
|
+
"rows": [
|
|
145
|
+
[
|
|
146
|
+
17,
|
|
147
|
+
1365,
|
|
148
|
+
"observations",
|
|
149
|
+
"procedure_occurrence",
|
|
150
|
+
"2010-06-02",
|
|
151
|
+
"2010-06-06",
|
|
152
|
+
"205"
|
|
153
|
+
],
|
|
154
|
+
[
|
|
155
|
+
68,
|
|
156
|
+
18,
|
|
157
|
+
"observations",
|
|
158
|
+
"procedure_occurrence",
|
|
159
|
+
"2008-04-19",
|
|
160
|
+
"2008-04-23",
|
|
161
|
+
"205"
|
|
162
|
+
],
|
|
163
|
+
[
|
|
164
|
+
95,
|
|
165
|
+
603,
|
|
166
|
+
"observations",
|
|
167
|
+
"procedure_occurrence",
|
|
168
|
+
"2008-08-27",
|
|
169
|
+
"2008-09-01",
|
|
170
|
+
"205"
|
|
171
|
+
]
|
|
172
|
+
],
|
|
173
|
+
"tree": {
|
|
174
|
+
"format": "conceptql-diagram/v1",
|
|
175
|
+
"statements": [
|
|
176
|
+
{
|
|
177
|
+
"name": "ms_drg",
|
|
178
|
+
"base": "selection",
|
|
179
|
+
"humanName": "MS-DRG",
|
|
180
|
+
"vocabularyId": "MS-DRG",
|
|
181
|
+
"outputTypes": ["procedure_occurrence"],
|
|
182
|
+
"parameters": {},
|
|
183
|
+
"values": ["205"],
|
|
184
|
+
"children": [],
|
|
185
|
+
"counts": {
|
|
186
|
+
"procedure_occurrence": {
|
|
187
|
+
"rows": 3,
|
|
188
|
+
"n": 3
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"title": "All CMS-DRG 205 records (a liver disorder)",
|
|
197
|
+
"statement": ["cms_drg", "205"],
|
|
198
|
+
"help": false,
|
|
199
|
+
"columns": [
|
|
200
|
+
"person_id",
|
|
201
|
+
"criterion_id",
|
|
202
|
+
"criterion_table",
|
|
203
|
+
"criterion_domain",
|
|
204
|
+
"start_date",
|
|
205
|
+
"end_date",
|
|
206
|
+
"source_value"
|
|
207
|
+
],
|
|
208
|
+
"rows": [],
|
|
209
|
+
"tree": {
|
|
210
|
+
"format": "conceptql-diagram/v1",
|
|
211
|
+
"statements": [
|
|
212
|
+
{
|
|
213
|
+
"name": "cms_drg",
|
|
214
|
+
"base": "selection",
|
|
215
|
+
"humanName": "CMS-DRG",
|
|
216
|
+
"vocabularyId": "CMS-DRG",
|
|
217
|
+
"outputTypes": ["procedure_occurrence"],
|
|
218
|
+
"parameters": {},
|
|
219
|
+
"values": ["205"],
|
|
220
|
+
"children": [],
|
|
221
|
+
"counts": {
|
|
222
|
+
"procedure_occurrence": {
|
|
223
|
+
"rows": 0,
|
|
224
|
+
"n": 0
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
]
|
|
232
|
+
}
|
|
@@ -47,22 +47,71 @@ module ConceptQL
|
|
|
47
47
|
@deprecated = message
|
|
48
48
|
end
|
|
49
49
|
|
|
50
|
+
# Marks an operator as hidden. It still runs, but ConceptQL.metadata
|
|
51
|
+
# (Nodifier#to_metadata) leaves it out, so an editor does not offer it.
|
|
52
|
+
# An operator is not hidden unless it says so. A subclass does not
|
|
53
|
+
# inherit the mark.
|
|
54
|
+
def hidden
|
|
55
|
+
@hidden = true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def hidden?
|
|
59
|
+
@hidden || false
|
|
60
|
+
end
|
|
61
|
+
|
|
50
62
|
def predominant_domains(*values)
|
|
51
63
|
return @predominant_domains if values.empty?
|
|
52
64
|
|
|
53
65
|
@predominant_domains = values
|
|
54
66
|
end
|
|
55
67
|
|
|
68
|
+
# An argument or option can carry two texts. instructions: is the form
|
|
69
|
+
# hint for typing a value. meaning: says what the argument or option
|
|
70
|
+
# does. Both are plain text, because the diagram editor escapes them.
|
|
56
71
|
def argument(name, options = {})
|
|
57
72
|
(@arguments ||= [])
|
|
58
73
|
@arguments << [ name, auto_label(name, options) ]
|
|
59
74
|
end
|
|
60
75
|
|
|
76
|
+
# The options that one argument was declared with
|
|
77
|
+
def argument_options(name)
|
|
78
|
+
(@arguments || []).to_h.fetch(name)
|
|
79
|
+
end
|
|
80
|
+
|
|
61
81
|
def option(name, options = {})
|
|
62
82
|
@options ||= {}
|
|
63
83
|
@options[name] = auto_label(name, options)
|
|
64
84
|
end
|
|
65
85
|
|
|
86
|
+
# Replaces the meaning: text of an option, usually one that this class
|
|
87
|
+
# inherited. The option hash is shared with the parent class (see
|
|
88
|
+
# #inherited), so this stores a copy and leaves the parent's text alone.
|
|
89
|
+
# An option that the class does not have raises KeyError.
|
|
90
|
+
def option_meaning(name, text)
|
|
91
|
+
@options[name] = @options.fetch(name).merge(meaning: text)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# The plain text that says which records the operator returns. A
|
|
95
|
+
# subclass does not inherit it.
|
|
96
|
+
def returns(value = nil)
|
|
97
|
+
return @returns unless value
|
|
98
|
+
|
|
99
|
+
@returns = value
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Adds an example: a title and a ConceptQL statement. help: false keeps
|
|
103
|
+
# the example out of an editor's help, for example because it returns no
|
|
104
|
+
# rows on the test data. The spec build generates the results and the
|
|
105
|
+
# diagram, so they are never written here. A subclass does not inherit
|
|
106
|
+
# the examples, because they would show the wrong operator.
|
|
107
|
+
def example(title, statement, help: true)
|
|
108
|
+
(@examples ||= []) << { title: title, statement: statement, help: help }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def examples
|
|
112
|
+
@examples || []
|
|
113
|
+
end
|
|
114
|
+
|
|
66
115
|
# Takes an option that a subclass inherited out of its metadata
|
|
67
116
|
def remove_option(name)
|
|
68
117
|
@options.delete(name)
|
|
@@ -93,11 +142,16 @@ module ConceptQL
|
|
|
93
142
|
@basic_type = value
|
|
94
143
|
end
|
|
95
144
|
|
|
145
|
+
# min_upstreams and max_upstreams say how many positional upstreams pass
|
|
146
|
+
# validation (99 means no limit). A validate_*_upstream check sets them
|
|
147
|
+
# again when to_metadata runs (derive_metadata_from_validations).
|
|
96
148
|
def allows_many_upstreams
|
|
149
|
+
@min_upstreams = 0
|
|
97
150
|
@max_upstreams = 99
|
|
98
151
|
end
|
|
99
152
|
|
|
100
153
|
def allows_one_upstream
|
|
154
|
+
@min_upstreams = 0
|
|
101
155
|
@max_upstreams = 1
|
|
102
156
|
end
|
|
103
157
|
|
|
@@ -155,7 +209,7 @@ module ConceptQL
|
|
|
155
209
|
short_name: short_name,
|
|
156
210
|
long_name: long_name,
|
|
157
211
|
operation: just_class_name.underscore,
|
|
158
|
-
min_upstreams: @
|
|
212
|
+
min_upstreams: @min_upstreams || 0,
|
|
159
213
|
max_upstreams: @max_upstreams || 0,
|
|
160
214
|
arguments: @arguments || [],
|
|
161
215
|
options: process_options(@options),
|
|
@@ -167,10 +221,22 @@ module ConceptQL
|
|
|
167
221
|
deprecated: (true if deprecated), # nil for an operator that is not deprecated
|
|
168
222
|
arity: arity,
|
|
169
223
|
aliases: @aliases,
|
|
170
|
-
spec_id: spec_id
|
|
224
|
+
spec_id: spec_id,
|
|
225
|
+
returns: get_returns,
|
|
226
|
+
examples: examples_for(name)
|
|
171
227
|
}
|
|
172
228
|
end
|
|
173
229
|
|
|
230
|
+
# The returns text that the metadata serves
|
|
231
|
+
def get_returns
|
|
232
|
+
returns
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# The examples that the metadata of the operator with this name serves
|
|
236
|
+
def examples_for(_name)
|
|
237
|
+
examples
|
|
238
|
+
end
|
|
239
|
+
|
|
174
240
|
def arity
|
|
175
241
|
case basic_type
|
|
176
242
|
when :set
|
|
@@ -76,8 +76,11 @@ module ConceptQL
|
|
|
76
76
|
[ dm.file_provenance_types_vocab, dm.code_provenance_types_vocab ].flatten
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
# The keywords come from the statement, whose validation reports a bad
|
|
80
|
+
# one (find_bad_keywords), so the lookup does not raise
|
|
81
|
+
# CodeCaseMismatchError.
|
|
79
82
|
def prov_of(db, vocabularies, ancestors)
|
|
80
|
-
dm.descendants_of(db, dm.concept_ids(db, vocabularies, ancestors))
|
|
83
|
+
dm.descendants_of(db, dm.concept_ids(db, vocabularies, ancestors, check_case: false))
|
|
81
84
|
end
|
|
82
85
|
|
|
83
86
|
def plain_condition(db, codes)
|
|
@@ -55,6 +55,9 @@ module ConceptQL
|
|
|
55
55
|
db[name].with(name, list, Scope::SHARED_TABLE => true)
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
+
# collection_type and "primary" are ConceptQL's own codes, so their
|
|
59
|
+
# lookups check case: a wrong case raises CodeCaseMismatchError instead
|
|
60
|
+
# of selecting nothing (conceptql-9okk, bead conceptql-rz8s).
|
|
58
61
|
def gdm_it(db)
|
|
59
62
|
source_type_id = dm.concept_ids(db, dm.file_provenance_types_vocab, collection_type)
|
|
60
63
|
all_source_type_ids = dm.descendants_of(db, source_type_id)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
# A code lookup got codes that match no stored code exactly, but match a
|
|
5
|
+
# stored code of the same vocabulary when case is ignored (bead
|
|
6
|
+
# conceptql-rz8s). ConceptQL compares codes case-exactly (seed conceptql-oji
|
|
7
|
+
# ruling "b"), so without this error the lookup returns no concept for those
|
|
8
|
+
# codes, and the caller loses rows without a word. t_shank asked GDM_PROV for
|
|
9
|
+
# "line" when the vocabulary stores "LINE".
|
|
10
|
+
#
|
|
11
|
+
# The public lookups of ConceptQL::Lexicon (and of the DataModel, which
|
|
12
|
+
# delegates to it) raise it. A vocabulary operator in a statement does not:
|
|
13
|
+
# it passes check_case: false, and warns about the code instead.
|
|
14
|
+
class CodeCaseMismatchError < ArgumentError
|
|
15
|
+
# One code of one vocabulary. +stored+ is every spelling of the code that
|
|
16
|
+
# the vocabulary stores, sorted.
|
|
17
|
+
Mismatch = Data.define(:vocabulary_id, :code, :stored) do
|
|
18
|
+
def to_s
|
|
19
|
+
"The vocabulary #{vocabulary_id} stores #{code.inspect} as #{stored.map(&:inspect).join(" or ")}."
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
attr_reader :mismatches
|
|
24
|
+
|
|
25
|
+
def initialize(mismatches)
|
|
26
|
+
@mismatches = mismatches
|
|
27
|
+
super([
|
|
28
|
+
"A code lookup got codes that their vocabulary stores only in another case.",
|
|
29
|
+
"ConceptQL compares codes case-exactly, so these codes would match no concept:",
|
|
30
|
+
*mismatches.map { |mismatch| " - #{mismatch}" },
|
|
31
|
+
"Pass each code as its vocabulary stores it."
|
|
32
|
+
].join("\n"))
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# The mismatches among +codes+, given +rows+: the [vocabulary_id,
|
|
36
|
+
# concept_code] pairs of the vocabularies' codes that equal one of the
|
|
37
|
+
# codes when case is ignored. A code that some vocabulary stores exactly
|
|
38
|
+
# is no mismatch. Neither is a code that no vocabulary stores in any case.
|
|
39
|
+
def self.mismatches(codes, rows)
|
|
40
|
+
codes.map(&:to_s).uniq.flat_map do |code|
|
|
41
|
+
same = rows.select { |_, stored| stored.casecmp?(code) }
|
|
42
|
+
next [] if same.empty? || same.any? { |_, stored| stored == code }
|
|
43
|
+
|
|
44
|
+
same.group_by(&:first).sort.map do |vocabulary_id, pairs|
|
|
45
|
+
Mismatch.new(vocabulary_id, code, pairs.map(&:last).uniq.sort)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -127,10 +127,31 @@ module ConceptQL
|
|
|
127
127
|
end
|
|
128
128
|
end
|
|
129
129
|
|
|
130
|
-
|
|
130
|
+
# Each argument is an information type concept ID or a concept code of
|
|
131
|
+
# information_types_vocab. A statement may mix both. A code that names
|
|
132
|
+
# no concept adds no ID, so it matches no period: it does not become
|
|
133
|
+
# concept ID 0. With only unknown codes, the clause matches nothing.
|
|
134
|
+
# Validation warns about an unknown code, so the lookup does not raise
|
|
135
|
+
# CodeCaseMismatchError.
|
|
136
|
+
def information_period_where_clause(db, arguments)
|
|
131
137
|
return if arguments.empty?
|
|
132
138
|
|
|
133
|
-
|
|
139
|
+
codes = information_type_codes(arguments)
|
|
140
|
+
ids = (arguments - codes).map(&:to_i)
|
|
141
|
+
ids += concept_ids(db, information_types_vocab, codes, check_case: false) unless codes.empty?
|
|
142
|
+
{ information_type_concept_id: ids }
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The arguments that are not concept IDs, as strings. A concept ID is
|
|
146
|
+
# an Integer or a string of digits.
|
|
147
|
+
def information_type_codes(arguments)
|
|
148
|
+
arguments.reject { |arg| arg.is_a?(Integer) || arg.to_s.match?(/\A\d+\z/) }.map(&:to_s)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# The vocabulary of the information type concepts, such as PARTD
|
|
152
|
+
# (Medicare Part D). The GDM and the OHDSI vocabulary tables both name it so.
|
|
153
|
+
def information_types_vocab
|
|
154
|
+
"GDM_INFORMATION_TYPE"
|
|
134
155
|
end
|
|
135
156
|
|
|
136
157
|
def code_provenance_type(_query, _domain)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module DiagramRenderer
|
|
5
|
+
# The in-app SVG of a diagram: the <svg class="vis"> element that
|
|
6
|
+
# frontend/lib/conceptql-diagram/src/renderer.js builds (its constructor,
|
|
7
|
+
# then update() at duration 0), for a page to show and script. The root
|
|
8
|
+
# svg, the injected <style>, the hidden copy of the icon sprite, the
|
|
9
|
+
# gradients and the Drawing with hooks (data-node-id, data-insert-before,
|
|
10
|
+
# data-diagram-key; see Drawing).
|
|
11
|
+
#
|
|
12
|
+
# Fit-to-view stays in the browser: renderer.js measures the drawing with
|
|
13
|
+
# getBBox and sets the .vis-algorithm transform, so this SVG has none.
|
|
14
|
+
#
|
|
15
|
+
# ConceptQL::DiagramRenderer::App.new(configuration).render(tree, lock_nodes: true) # => SVG text
|
|
16
|
+
class App
|
|
17
|
+
# XMLSerializer leaves out a namespace declaration that the element
|
|
18
|
+
# already inherits. The sprite <div> is in the SVG namespace (d3 appends
|
|
19
|
+
# it to the svg), so the browser's copy of the sprite has no
|
|
20
|
+
# xmlns="http://www.w3.org/2000/svg".
|
|
21
|
+
INHERITED_XMLNS = /\s+xmlns\s*=\s*(?:"#{Regexp.escape(Static::SVG_NS)}"|'#{Regexp.escape(Static::SVG_NS)}')/
|
|
22
|
+
START_TAG = /<[^\s!?\/>][^>]*>/
|
|
23
|
+
|
|
24
|
+
def initialize(configuration)
|
|
25
|
+
@configuration = configuration
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# tree is a diagram tree whose nodes have ids, isSelected and
|
|
29
|
+
# isAttachable (what Ember passes to the renderer, or DiagramData.from's
|
|
30
|
+
# output). It is drawn as it is. styles and icons are the renderer's
|
|
31
|
+
# options of those names: the CSS and the sprite markup (default: the
|
|
32
|
+
# configuration's).
|
|
33
|
+
def render(tree, styles: @configuration.styles, icons: @configuration.icons, lock_nodes: false, summary: false,
|
|
34
|
+
show_counts: false, show_options: false, css_transforms: false)
|
|
35
|
+
drawing = Drawing.new(tree, show_counts:, show_options:, hooks: true, lock_nodes:, css_transforms:)
|
|
36
|
+
Svg.element("svg", {
|
|
37
|
+
"xmlns" => Static::SVG_NS,
|
|
38
|
+
"xmlns:xlink" => Static::XLINK_NS,
|
|
39
|
+
"class" => lock_nodes ? "vis is-new" : "vis",
|
|
40
|
+
"height" => "100%",
|
|
41
|
+
"width" => "100%",
|
|
42
|
+
"viewBox" => summary ? "0 0 800 500" : nil
|
|
43
|
+
}, [
|
|
44
|
+
Svg.element("style", {}, styles.nil? ? [] : [ styles ]),
|
|
45
|
+
Svg.element("div", { "style" => "display: none;" }, icons.nil? || icons.empty? ? [] : [ Svg::Raw.new(serialized(icons)) ]),
|
|
46
|
+
Svg.element("defs", {}, drawing.gradients),
|
|
47
|
+
drawing.algorithm
|
|
48
|
+
]).to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# The sprite markup as the browser serializes the <div> after its
|
|
54
|
+
# innerHTML is set. The markup must otherwise be in serialized form, as
|
|
55
|
+
# the sprite in frontend/index.html is.
|
|
56
|
+
def serialized(icons) = icons.gsub(START_TAG) { _1.gsub(INHERITED_XMLNS, "") }
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ConceptQL
|
|
4
|
+
module DiagramRenderer
|
|
5
|
+
# Where the renderer's assets are. Each path defaults to the copy that the
|
|
6
|
+
# gem ships under assets/diagram/ (ASSETS), so a host needs no configure
|
|
7
|
+
# call. A host that has its own copy of an asset sets that path:
|
|
8
|
+
#
|
|
9
|
+
# ConceptQL::DiagramRenderer.configure do |config|
|
|
10
|
+
# config.styles_path = "my_diagram.css"
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# - sprite_path: a file that holds the operator icon <symbol>s (Sprite):
|
|
14
|
+
# assets/diagram/operators.svg.
|
|
15
|
+
# - styles_path: the diagram CSS: assets/diagram/diagram.css.
|
|
16
|
+
# - webfonts_dir: the WEBFONTS woff2 files, embedded in the SVG's <style>:
|
|
17
|
+
# assets/diagram/fonts.
|
|
18
|
+
# - resvg_fonts_dir: the Resvg::FONT_FILES TTF files for resvg:
|
|
19
|
+
# assets/diagram/fonts.
|
|
20
|
+
# - resvg_program: the resvg executable, "resvg" (on PATH) by default.
|
|
21
|
+
#
|
|
22
|
+
# The files are read once, when first needed.
|
|
23
|
+
class Configuration
|
|
24
|
+
# The gem's asset directory.
|
|
25
|
+
ASSETS = ConceptQL.root.join("assets", "diagram")
|
|
26
|
+
|
|
27
|
+
# The Roboto webfonts and their font-style (loadAssets in
|
|
28
|
+
# cli/src/render-static.js).
|
|
29
|
+
WEBFONTS = [ [ "Roboto-Normal-Latin.woff2", "normal" ], [ "Roboto-Italic-Latin.woff2", "italic" ] ].freeze
|
|
30
|
+
|
|
31
|
+
# The rule for the root svg after the diagram CSS. animation: none is
|
|
32
|
+
# required: the diagram CSS fades .vis in after a delay, and an SVG shown
|
|
33
|
+
# as an image runs its CSS animations, so anything that snapshots the
|
|
34
|
+
# file early (thumbnailers, PDF converters) gets a blank.
|
|
35
|
+
ROOT_RULE = %(svg.vis { font: normal 15px/1.5 "Roboto", sans-serif; animation: none; cursor: default; }\n)
|
|
36
|
+
|
|
37
|
+
# An asset path is not set.
|
|
38
|
+
class Missing < StandardError; end
|
|
39
|
+
|
|
40
|
+
attr_accessor :sprite_path, :styles_path, :webfonts_dir, :resvg_fonts_dir, :resvg_program
|
|
41
|
+
|
|
42
|
+
def initialize
|
|
43
|
+
@sprite_path = ASSETS.join("operators.svg").to_s
|
|
44
|
+
@styles_path = ASSETS.join("diagram.css").to_s
|
|
45
|
+
@webfonts_dir = ASSETS.join("fonts").to_s
|
|
46
|
+
@resvg_fonts_dir = ASSETS.join("fonts").to_s
|
|
47
|
+
@resvg_program = "resvg"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The sprite block in an HTML page (loadAssets in cli/src/render-static.js).
|
|
51
|
+
OPERATORS = %r{<div class="operators"[^>]*>(.*?)</div>\s*(?:<|\z)}m
|
|
52
|
+
|
|
53
|
+
def sprite = @sprite ||= Sprite.new(sprite_markup)
|
|
54
|
+
|
|
55
|
+
def styles = @styles ||= File.read(path(:styles_path))
|
|
56
|
+
|
|
57
|
+
# The icon markup that the in-app SVG copies (App): the content of the
|
|
58
|
+
# <div class="operators"> block when the sprite file has one, else the
|
|
59
|
+
# whole file.
|
|
60
|
+
def icons = @icons ||= sprite_markup[OPERATORS, 1] || sprite_markup
|
|
61
|
+
|
|
62
|
+
# The text of the standalone SVG's <style>: the embedded Roboto faces, the
|
|
63
|
+
# diagram CSS and ROOT_RULE (standaloneStyleText in
|
|
64
|
+
# cli/src/render-static.js).
|
|
65
|
+
def standalone_style_text
|
|
66
|
+
@standalone_style_text ||= "#{font_faces}\n#{styles}\n#{ROOT_RULE}"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def resvg = Resvg.new(fonts_dir: path(:resvg_fonts_dir), program: resvg_program)
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def sprite_markup = @sprite_markup ||= File.read(path(:sprite_path))
|
|
74
|
+
|
|
75
|
+
def font_faces
|
|
76
|
+
WEBFONTS.map do |file, style|
|
|
77
|
+
data = [ File.binread(File.join(path(:webfonts_dir), file)) ].pack("m0")
|
|
78
|
+
%(@font-face { font-family: "Roboto"; font-style: #{style}; font-weight: 100 900; src: url(data:font/woff2;base64,#{data}) format("woff2"); })
|
|
79
|
+
end.join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def path(name)
|
|
83
|
+
public_send(name)&.to_s or raise Missing, "ConceptQL::DiagramRenderer: #{name} is not set (see ConceptQL::DiagramRenderer.configure)"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module ConceptQL
|
|
6
|
+
module DiagramRenderer
|
|
7
|
+
# The input handling of the renderer: toDiagramData in
|
|
8
|
+
# frontend/lib/conceptql-diagram/cli/src/render-static.js.
|
|
9
|
+
#
|
|
10
|
+
# The input is parsed JSON (hashes with string keys) in one of three shapes:
|
|
11
|
+
#
|
|
12
|
+
# - a bare node: {"name" => "icd9", ...}
|
|
13
|
+
# - an array of statements: [{"name" => ...}, ...]
|
|
14
|
+
# - {"format" => "conceptql-diagram/v1", "statements" => [...]}, which is
|
|
15
|
+
# what `conceptql render_json` prints
|
|
16
|
+
#
|
|
17
|
+
# The statements go under a synthetic root with path ids (n0, n0.0, ...).
|
|
18
|
+
# Each node gets outputTypes ["unknown"] when it has none, parameters {}
|
|
19
|
+
# when it has none, and isSelected and isAttachable false. Other keys stay
|
|
20
|
+
# as they are. A bad input raises InputError.
|
|
21
|
+
#
|
|
22
|
+
# The input may also have symbol keys, as ConceptQL::Diagram.render's
|
|
23
|
+
# output has: from writes the input as JSON and reads it back first, so
|
|
24
|
+
# it reads every input as parsed JSON.
|
|
25
|
+
module DiagramData
|
|
26
|
+
FORMAT = "conceptql-diagram/v1"
|
|
27
|
+
|
|
28
|
+
module_function
|
|
29
|
+
|
|
30
|
+
def from(input)
|
|
31
|
+
statements = statements_of(JSON.parse(JSON.generate(input)))
|
|
32
|
+
raise InputError, "no statements to render" unless statements.is_a?(Array) && !statements.empty?
|
|
33
|
+
|
|
34
|
+
children = statements.each_with_index.map { |statement, i| assign(statement, "n#{i}") }
|
|
35
|
+
{
|
|
36
|
+
"name" => "root",
|
|
37
|
+
"id" => "root",
|
|
38
|
+
"isSelected" => false,
|
|
39
|
+
"isAttachable" => false,
|
|
40
|
+
"outputTypes" => children.flat_map { _1["outputTypes"] }.uniq,
|
|
41
|
+
"children" => children
|
|
42
|
+
}
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def statements_of(input)
|
|
46
|
+
if input.is_a?(Array)
|
|
47
|
+
input
|
|
48
|
+
elsif input.is_a?(Hash) && input.key?("statements")
|
|
49
|
+
unless input["format"] == FORMAT
|
|
50
|
+
format = input.key?("format") ? JSON.generate(input["format"]) : "undefined"
|
|
51
|
+
raise InputError, "unsupported format #{format}; expected \"#{FORMAT}\""
|
|
52
|
+
end
|
|
53
|
+
input["statements"]
|
|
54
|
+
elsif input.is_a?(Hash) && input["name"].is_a?(String)
|
|
55
|
+
[ input ]
|
|
56
|
+
else
|
|
57
|
+
raise InputError, 'input must be a diagram node, an array of nodes, or {"format": "conceptql-diagram/v1", "statements": [...]}'
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def assign(node, id)
|
|
62
|
+
raise InputError, "node #{id} has no \"name\"" unless node.is_a?(Hash) && node["name"].is_a?(String)
|
|
63
|
+
|
|
64
|
+
output_types = node["outputTypes"]
|
|
65
|
+
parameters = node["parameters"]
|
|
66
|
+
children = node["children"]
|
|
67
|
+
node.merge(
|
|
68
|
+
"id" => id,
|
|
69
|
+
"outputTypes" => JsFormat.truthy?(output_types) && JsFormat.truthy?(JsFormat.length(output_types)) ? output_types : [ "unknown" ],
|
|
70
|
+
"parameters" => JsFormat.truthy?(parameters) ? parameters : {},
|
|
71
|
+
"isSelected" => false,
|
|
72
|
+
"isAttachable" => false,
|
|
73
|
+
"children" => (JsFormat.truthy?(children) ? children : []).each_with_index.map { |child, i| assign(child, "#{id}.#{i}") }
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private_class_method :statements_of, :assign
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|