trevl 0.1.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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/NOTICE +9 -0
  4. data/README.md +431 -0
  5. data/Rakefile +10 -0
  6. data/assets/trendence-logo.svg +17 -0
  7. data/assets/trevl-logo.svg +20 -0
  8. data/docs/screenshots/01_bar_chart.png +0 -0
  9. data/docs/screenshots/02_column_chart.png +0 -0
  10. data/docs/screenshots/03_line_chart.png +0 -0
  11. data/docs/screenshots/04_pie_chart.png +0 -0
  12. data/docs/screenshots/05_computed_fields.png +0 -0
  13. data/docs/screenshots/06_postprocess_top_n.png +0 -0
  14. data/docs/screenshots/10_multi_series.png +0 -0
  15. data/examples/01_bar_chart.yml +16 -0
  16. data/examples/02_column_chart.yml +19 -0
  17. data/examples/03_line_chart.yml +21 -0
  18. data/examples/04_pie_chart.yml +15 -0
  19. data/examples/05_computed_fields.yml +26 -0
  20. data/examples/06_postprocess_top_n.yml +21 -0
  21. data/examples/07_score_kpi.yml +13 -0
  22. data/examples/08_table.yml +14 -0
  23. data/examples/09_template_inheritance.yml +25 -0
  24. data/examples/10_multi_series.yml +23 -0
  25. data/lib/trevl/auth/bearer_token.rb +23 -0
  26. data/lib/trevl/configuration.rb +24 -0
  27. data/lib/trevl/core_ext/hash.rb +37 -0
  28. data/lib/trevl/data_source/api.rb +86 -0
  29. data/lib/trevl/data_source/base.rb +19 -0
  30. data/lib/trevl/data_source/cube.rb +70 -0
  31. data/lib/trevl/data_source/static.rb +30 -0
  32. data/lib/trevl/data_source.rb +38 -0
  33. data/lib/trevl/errors.rb +9 -0
  34. data/lib/trevl/highcharts_asset.rb +45 -0
  35. data/lib/trevl/html_renderer.rb +91 -0
  36. data/lib/trevl/notebook/display.rb +53 -0
  37. data/lib/trevl/notebook.rb +41 -0
  38. data/lib/trevl/processor.rb +106 -0
  39. data/lib/trevl/renderer/data_fetcher.rb +63 -0
  40. data/lib/trevl/renderer/ref_parser.rb +61 -0
  41. data/lib/trevl/renderer/transform.rb +104 -0
  42. data/lib/trevl/renderer.rb +345 -0
  43. data/lib/trevl/schema/component.json +337 -0
  44. data/lib/trevl/schema/document.json +41 -0
  45. data/lib/trevl/template_store.rb +38 -0
  46. data/lib/trevl/validator.rb +96 -0
  47. data/lib/trevl/version.rb +5 -0
  48. data/lib/trevl.rb +99 -0
  49. data/llms.txt +171 -0
  50. data/notebooks/demo.ipynb +312 -0
  51. data/notebooks/exploration.ipynb +164 -0
  52. data/scripts/generate_screenshots.rb +107 -0
  53. metadata +137 -0
@@ -0,0 +1,337 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://trevl.trendence.com/schema/component.json",
4
+ "title": "TREVL Component",
5
+ "description": "Schema for a single TREVL visualization component",
6
+
7
+ "type": "object",
8
+ "required": ["id", "type"],
9
+
10
+ "properties": {
11
+ "id": {
12
+ "type": "string",
13
+ "description": "Unique component identifier"
14
+ },
15
+ "type": {
16
+ "type": "string",
17
+ "enum": ["chart", "score", "table", "text", "filter"],
18
+ "description": "Component type"
19
+ },
20
+ "name": {
21
+ "type": "string",
22
+ "description": "Human-readable component name"
23
+ },
24
+ "api": {
25
+ "type": "string",
26
+ "description": "Registered data source name"
27
+ },
28
+ "api-parameters": {
29
+ "$ref": "#/$defs/apiParameters"
30
+ },
31
+ "template": {
32
+ "type": "string",
33
+ "description": "Name of a registered template to inherit from"
34
+ },
35
+ "computed": {
36
+ "type": "array",
37
+ "items": { "$ref": "#/$defs/computedField" },
38
+ "description": "Per-row JavaScript transformations"
39
+ },
40
+ "postprocess": {
41
+ "type": "string",
42
+ "description": "JavaScript code to transform the full dataset. Must reassign $result to an Array."
43
+ },
44
+ "queries": {
45
+ "type": "array",
46
+ "items": { "$ref": "#/$defs/query" },
47
+ "description": "Query definitions (for Cube/X-Middle data sources)"
48
+ },
49
+ "highchartsData": {
50
+ "$ref": "#/$defs/highchartsData"
51
+ },
52
+ "display": {
53
+ "$ref": "#/$defs/display"
54
+ },
55
+ "tableData": {
56
+ "$ref": "#/$defs/tableData"
57
+ },
58
+ "filters": {
59
+ "type": "array",
60
+ "items": { "$ref": "#/$defs/filterDefinition" },
61
+ "description": "Filter definitions (for filter components)"
62
+ },
63
+ "text": {
64
+ "type": "string",
65
+ "description": "HTML content (for text components)"
66
+ },
67
+ "subheader": {
68
+ "type": "string"
69
+ },
70
+ "body": {
71
+ "type": "string"
72
+ }
73
+ },
74
+
75
+ "allOf": [
76
+ {
77
+ "if": { "properties": { "type": { "const": "chart" } } },
78
+ "then": {
79
+ "required": ["api", "highchartsData"],
80
+ "properties": {
81
+ "highchartsData": {
82
+ "required": ["series"]
83
+ }
84
+ }
85
+ }
86
+ },
87
+ {
88
+ "if": { "properties": { "type": { "const": "score" } } },
89
+ "then": {
90
+ "required": ["api", "display"]
91
+ }
92
+ },
93
+ {
94
+ "if": { "properties": { "type": { "const": "table" } } },
95
+ "then": {
96
+ "required": ["api", "tableData"]
97
+ }
98
+ },
99
+ {
100
+ "if": { "properties": { "type": { "const": "filter" } } },
101
+ "then": {
102
+ "required": ["api", "filters"]
103
+ }
104
+ }
105
+ ],
106
+
107
+ "$defs": {
108
+ "computedField": {
109
+ "type": "object",
110
+ "required": ["name", "code"],
111
+ "properties": {
112
+ "name": {
113
+ "type": "string",
114
+ "description": "Field name added to each row"
115
+ },
116
+ "code": {
117
+ "type": "string",
118
+ "description": "JavaScript expression. Return value is assigned to the field."
119
+ },
120
+ "arguments": {
121
+ "type": "object",
122
+ "additionalProperties": {
123
+ "type": ["string", "number", "boolean"]
124
+ },
125
+ "description": "Named arguments. Use $endpoint.data.field references as values."
126
+ }
127
+ },
128
+ "additionalProperties": false
129
+ },
130
+
131
+ "query": {
132
+ "type": "object",
133
+ "properties": {
134
+ "context": {
135
+ "type": "string",
136
+ "description": "Query context (e.g. target_group, compare_group)"
137
+ },
138
+ "dimensions": {
139
+ "type": "array",
140
+ "items": { "type": "string" }
141
+ },
142
+ "measures": {
143
+ "type": "array",
144
+ "items": { "type": "string" }
145
+ },
146
+ "filters": {
147
+ "type": "array",
148
+ "items": { "$ref": "#/$defs/queryFilter" }
149
+ },
150
+ "computed": {
151
+ "type": "array",
152
+ "items": { "$ref": "#/$defs/computedField" }
153
+ }
154
+ }
155
+ },
156
+
157
+ "queryFilter": {
158
+ "type": "object",
159
+ "required": ["member", "operator"],
160
+ "properties": {
161
+ "member": {
162
+ "type": "string",
163
+ "description": "Cube dimension/measure to filter on"
164
+ },
165
+ "operator": {
166
+ "type": "string",
167
+ "enum": ["equals", "notEquals", "gt", "gte", "lt", "lte", "afterDate", "beforeDate", "set", "notSet", "contains", "notContains"],
168
+ "description": "Filter operator"
169
+ },
170
+ "values": {
171
+ "type": "array",
172
+ "items": { "type": ["string", "number"] },
173
+ "description": "Static filter values"
174
+ },
175
+ "parameter": {
176
+ "type": "string",
177
+ "description": "Runtime parameter reference ($param_name)"
178
+ }
179
+ }
180
+ },
181
+
182
+ "apiParameters": {
183
+ "type": "object",
184
+ "description": "Parameters passed to the data source",
185
+ "additionalProperties": true
186
+ },
187
+
188
+ "highchartsData": {
189
+ "type": "object",
190
+ "description": "Highcharts configuration object",
191
+ "properties": {
192
+ "chart": {
193
+ "type": "object",
194
+ "properties": {
195
+ "type": {
196
+ "type": "string",
197
+ "enum": ["bar", "column", "line", "area", "pie", "scatter", "spline", "areaspline", "heatmap", "bubble", "waterfall", "funnel", "gauge", "solidgauge", "bullet"],
198
+ "description": "Highcharts chart type"
199
+ },
200
+ "height": { "type": ["number", "string"] },
201
+ "backgroundColor": { "type": "string" }
202
+ },
203
+ "additionalProperties": true
204
+ },
205
+ "title": {
206
+ "type": "object",
207
+ "properties": {
208
+ "text": { "type": "string" }
209
+ },
210
+ "additionalProperties": true
211
+ },
212
+ "series": {
213
+ "type": "array",
214
+ "items": { "$ref": "#/$defs/seriesDefinition" },
215
+ "description": "Data series definitions"
216
+ },
217
+ "xAxis": { "additionalProperties": true },
218
+ "yAxis": { "additionalProperties": true },
219
+ "colors": {
220
+ "type": "array",
221
+ "items": { "type": "string" }
222
+ },
223
+ "plotOptions": { "type": "object", "additionalProperties": true },
224
+ "legend": { "type": "object", "additionalProperties": true },
225
+ "tooltip": { "type": "object", "additionalProperties": true },
226
+ "credits": { "type": "object", "additionalProperties": true }
227
+ },
228
+ "additionalProperties": true
229
+ },
230
+
231
+ "seriesDefinition": {
232
+ "type": "object",
233
+ "properties": {
234
+ "name": {
235
+ "type": "string",
236
+ "description": "Series name. Can be a $reference."
237
+ },
238
+ "type": {
239
+ "type": "string",
240
+ "description": "Override chart type for this series"
241
+ },
242
+ "color": {
243
+ "type": "string"
244
+ },
245
+ "data": {
246
+ "type": "object",
247
+ "description": "Data point mapping. Keys (x, y, color, etc.) map to $references.",
248
+ "additionalProperties": {
249
+ "type": ["string", "number", "object"]
250
+ }
251
+ }
252
+ },
253
+ "additionalProperties": true
254
+ },
255
+
256
+ "display": {
257
+ "type": "object",
258
+ "description": "Display configuration (primarily for score components)",
259
+ "properties": {
260
+ "value": {
261
+ "type": ["string", "number"],
262
+ "description": "Value to display. Can be a $reference."
263
+ },
264
+ "unit": {
265
+ "type": "string",
266
+ "description": "Unit suffix (e.g. '%', ' EUR')"
267
+ },
268
+ "header": {
269
+ "type": "object",
270
+ "properties": {
271
+ "title": { "type": "string" },
272
+ "icon": { "type": "string" }
273
+ },
274
+ "additionalProperties": true
275
+ },
276
+ "format": {
277
+ "type": "object",
278
+ "properties": {
279
+ "minimumFractionDigits": { "type": "integer" },
280
+ "maximumFractionDigits": { "type": "integer" }
281
+ },
282
+ "additionalProperties": true
283
+ }
284
+ },
285
+ "additionalProperties": true
286
+ },
287
+
288
+ "tableData": {
289
+ "type": "object",
290
+ "required": ["columns"],
291
+ "properties": {
292
+ "headers": {
293
+ "type": "array",
294
+ "items": { "type": "string" },
295
+ "description": "Column header labels"
296
+ },
297
+ "columns": {
298
+ "type": "array",
299
+ "items": {
300
+ "type": "object",
301
+ "required": ["identifier", "value"],
302
+ "properties": {
303
+ "identifier": { "type": "string" },
304
+ "value": { "type": "string", "description": "Value or $reference" }
305
+ },
306
+ "additionalProperties": true
307
+ }
308
+ },
309
+ "index": { "type": "string" }
310
+ },
311
+ "additionalProperties": true
312
+ },
313
+
314
+ "filterDefinition": {
315
+ "type": "object",
316
+ "properties": {
317
+ "id": { "type": "string" },
318
+ "type": {
319
+ "type": "string",
320
+ "enum": ["select", "multiselect", "range", "date"],
321
+ "description": "Filter input type"
322
+ },
323
+ "label": { "type": "string" },
324
+ "options": {
325
+ "type": "object",
326
+ "description": "Option mapping with $references for value/label",
327
+ "additionalProperties": { "type": "string" }
328
+ },
329
+ "queries": {
330
+ "type": "array",
331
+ "items": { "$ref": "#/$defs/query" }
332
+ }
333
+ },
334
+ "additionalProperties": true
335
+ }
336
+ }
337
+ }
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://trevl.trendence.com/schema/document.json",
4
+ "title": "TREVL Document",
5
+ "description": "Schema for a TREVL YAML document — either a hash with 'components' key or a bare array of components",
6
+
7
+ "oneOf": [
8
+ {
9
+ "type": "object",
10
+ "required": ["components"],
11
+ "properties": {
12
+ "components": {
13
+ "type": "array",
14
+ "items": { "$ref": "component.json" },
15
+ "minItems": 1,
16
+ "description": "Array of TREVL components"
17
+ },
18
+ "parameters": {
19
+ "type": "array",
20
+ "items": {
21
+ "type": "object",
22
+ "required": ["id"],
23
+ "properties": {
24
+ "id": { "type": "string" },
25
+ "type": { "type": "string", "enum": ["single", "multi"] },
26
+ "sources": { "type": "array" }
27
+ }
28
+ },
29
+ "description": "Runtime parameter definitions"
30
+ }
31
+ },
32
+ "additionalProperties": true
33
+ },
34
+ {
35
+ "type": "array",
36
+ "items": { "$ref": "component.json" },
37
+ "minItems": 1,
38
+ "description": "Bare array of components (shorthand)"
39
+ }
40
+ ]
41
+ }
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ class TemplateStore
5
+ def initialize
6
+ @templates = {}
7
+ end
8
+
9
+ def register(name, template_hash)
10
+ @templates[name.to_s.downcase] = template_hash
11
+ end
12
+
13
+ def find(name)
14
+ @templates[name.to_s.downcase]
15
+ end
16
+
17
+ def load_yaml(yaml_string)
18
+ data = YAML.safe_load(yaml_string, permitted_classes: [Hash], aliases: true)
19
+ return unless data.is_a?(Hash)
20
+
21
+ data.each do |name, template_hash|
22
+ register(name, template_hash) if template_hash.is_a?(Hash)
23
+ end
24
+ end
25
+
26
+ def load_file(path)
27
+ load_yaml(File.read(path))
28
+ end
29
+
30
+ def names
31
+ @templates.keys
32
+ end
33
+
34
+ def clear
35
+ @templates.clear
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json_schemer"
4
+
5
+ module Trevl
6
+ class Validator
7
+ SCHEMA_DIR = File.expand_path("schema", __dir__)
8
+ COMPONENT_SCHEMA_PATH = File.join(SCHEMA_DIR, "component.json")
9
+ DOCUMENT_SCHEMA_PATH = File.join(SCHEMA_DIR, "document.json")
10
+
11
+ Result = Struct.new(:valid?, :errors, keyword_init: true) do
12
+ def to_s
13
+ return "Valid" if valid?
14
+ errors.map { |e| "- #{e}" }.join("\n")
15
+ end
16
+ end
17
+
18
+ def self.validate(yaml_string)
19
+ new.validate(yaml_string)
20
+ end
21
+
22
+ def self.validate_component(component_hash)
23
+ new.validate_component(component_hash)
24
+ end
25
+
26
+ def validate(yaml_string)
27
+ parsed = YAML.safe_load(yaml_string, permitted_classes: [Hash, Symbol], aliases: true)
28
+ validate_document(parsed)
29
+ rescue Psych::SyntaxError => e
30
+ Result.new(valid?: false, errors: ["YAML syntax error: #{e.message}"])
31
+ end
32
+
33
+ def validate_component(component_hash)
34
+ schema_errors = component_schemer.validate(component_hash).map { |e| format_error(e) }
35
+ Result.new(valid?: schema_errors.empty?, errors: schema_errors)
36
+ end
37
+
38
+ def validate_document(parsed)
39
+ if parsed.is_a?(Array)
40
+ validate_component_array(parsed)
41
+ elsif parsed.is_a?(Hash) && parsed["components"].is_a?(Array)
42
+ validate_component_array(parsed["components"])
43
+ elsif parsed.is_a?(Hash)
44
+ Result.new(valid?: false, errors: ["Document must have a 'components' array"])
45
+ else
46
+ Result.new(valid?: false, errors: ["Document must be a Hash or Array, got #{parsed.class}"])
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def validate_component_array(components)
53
+ all_errors = []
54
+ components.each_with_index do |comp, idx|
55
+ schema_errors = component_schemer.validate(comp).map { |e| format_error(e, component_index: idx, component_id: comp["id"]) }
56
+ all_errors.concat(schema_errors)
57
+ end
58
+ Result.new(valid?: all_errors.empty?, errors: all_errors)
59
+ end
60
+
61
+ def component_schemer
62
+ @component_schemer ||= JSONSchemer.schema(
63
+ JSON.parse(File.read(COMPONENT_SCHEMA_PATH))
64
+ )
65
+ end
66
+
67
+ def format_error(error, component_index: nil, component_id: nil)
68
+ path = error["data_pointer"]
69
+ msg = error["type"]
70
+ details = error["details"] || {}
71
+
72
+ prefix = if component_id
73
+ "[#{component_id}]"
74
+ elsif component_index
75
+ "[component ##{component_index}]"
76
+ else
77
+ ""
78
+ end
79
+
80
+ case msg
81
+ when "required"
82
+ missing = details["missing_keys"]&.join(", ") || "unknown"
83
+ "#{prefix} Missing required field(s): #{missing} (at #{path})"
84
+ when "enum"
85
+ "#{prefix} Invalid value at #{path}: must be one of #{error["schema"]["enum"].join(", ")}"
86
+ when "type"
87
+ "#{prefix} Wrong type at #{path}: expected #{error["schema"]["type"]}"
88
+ when "object"
89
+ "#{prefix} Schema error at #{path}: #{error["error"]}"
90
+ else
91
+ detail = error["error"] || error.fetch("message", msg)
92
+ "#{prefix} #{detail} (at #{path})"
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ VERSION = "0.1.0"
5
+ end
data/lib/trevl.rb ADDED
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+ require "logger"
6
+ require "securerandom"
7
+
8
+ require_relative "trevl/version"
9
+ require_relative "trevl/errors"
10
+ require_relative "trevl/configuration"
11
+ require_relative "trevl/highcharts_asset"
12
+ require_relative "trevl/core_ext/hash"
13
+ require_relative "trevl/template_store"
14
+ require_relative "trevl/data_source"
15
+ require_relative "trevl/data_source/base"
16
+ require_relative "trevl/data_source/static"
17
+ require_relative "trevl/data_source/api"
18
+ require_relative "trevl/data_source/cube"
19
+ require_relative "trevl/auth/bearer_token"
20
+ require_relative "trevl/processor"
21
+ require_relative "trevl/renderer"
22
+ require_relative "trevl/validator"
23
+ require_relative "trevl/html_renderer"
24
+
25
+ module Trevl
26
+ GEM_ROOT = File.expand_path("..", __dir__)
27
+
28
+ class << self
29
+ # --- Configuration ---
30
+
31
+ def configure
32
+ yield(configuration)
33
+ end
34
+
35
+ def configuration
36
+ @configuration ||= Configuration.new
37
+ end
38
+
39
+ def logger = configuration.logger
40
+ def template_store = configuration.template_store
41
+
42
+ # --- Core API ---
43
+
44
+ def parse(yaml_string)
45
+ Processor.new(yaml_string)
46
+ end
47
+
48
+ def validate(yaml_string)
49
+ Validator.validate(yaml_string)
50
+ end
51
+
52
+ # data: inline rows for this render call ({endpoint => rows}), wrapped in
53
+ # a Static source internally; answers any api name and components without
54
+ # one. data_sources: per-render sources ({name => instance}). Both take
55
+ # precedence over the global DataSource registry.
56
+ def render(yaml_string, params: {}, data: nil, data_sources: {})
57
+ components = parse(yaml_string).components || []
58
+ components.filter_map { |c| Renderer.new(c, params, data: data, data_sources: data_sources).render }
59
+ end
60
+
61
+ def render_to_html(yaml_string, params: {}, data: nil, data_sources: {}, width: 800, height: 400)
62
+ HtmlRenderer.new(width: width, height: height)
63
+ .render(yaml_string, params: params, data: data, data_sources: data_sources)
64
+ end
65
+
66
+ # --- AI tooling ---
67
+
68
+ def schema_reference
69
+ @schema_reference ||= File.read(File.join(GEM_ROOT, "llms.txt"))
70
+ end
71
+
72
+ def examples
73
+ @examples ||= load_examples
74
+ end
75
+
76
+ # --- Lifecycle ---
77
+
78
+ def reset!
79
+ @configuration = Configuration.new
80
+ @schema_reference = nil
81
+ @examples = nil
82
+ end
83
+
84
+ private
85
+
86
+ def load_examples
87
+ dir = File.join(GEM_ROOT, "examples")
88
+ return [] unless File.directory?(dir)
89
+
90
+ Dir.glob(File.join(dir, "*.yml")).sort.map do |path|
91
+ name = File.basename(path, ".yml")
92
+ content = File.read(path)
93
+ comment_lines = content.lines.take_while { |l| l.start_with?("#") }
94
+ description = comment_lines.map { |l| l.sub(/^#\s?/, "").chomp }.join(" ").strip
95
+ {name: name, description: description, yaml: content.sub(/\A(#[^\n]*\n)+/, "")}
96
+ end
97
+ end
98
+ end
99
+ end