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,14 @@
1
+ # Data table with columns. Use when you need tabular output instead of charts.
2
+ components:
3
+ - id: crew_manifest
4
+ type: table
5
+ api: demo
6
+ tableData:
7
+ headers: ["Pirate", "Role", "Grog Ration"]
8
+ columns:
9
+ - identifier: pirate
10
+ value: "$crew.data.name"
11
+ - identifier: role
12
+ value: "$crew.data.role"
13
+ - identifier: grog
14
+ value: "$crew.data.grog_ration"
@@ -0,0 +1,25 @@
1
+ # Template inheritance. Register a template first, then reference it.
2
+ # Component values override template values at the same path (deep merge).
3
+ #
4
+ # Ruby setup:
5
+ # Trevl.template_store.register("pirate_bar", {
6
+ # "highchartsData" => {
7
+ # "chart" => {"type" => "bar"},
8
+ # "colors" => ["#8B4513", "#DAA520"],
9
+ # "plotOptions" => {"bar" => {"borderRadius" => 6}},
10
+ # "credits" => {"enabled" => false}
11
+ # }
12
+ # })
13
+ components:
14
+ - id: plunder_template
15
+ type: chart
16
+ api: demo
17
+ template: pirate_bar
18
+ highchartsData:
19
+ title:
20
+ text: Plunder by Ship (uses pirate_bar template)
21
+ series:
22
+ - name: Plunder
23
+ data:
24
+ x: "$ships.data.ship"
25
+ y: "$ships.data.plunder"
@@ -0,0 +1,23 @@
1
+ # Multiple series from the same data source. Use for comparison charts.
2
+ # Each series maps different fields from the same rows.
3
+ components:
4
+ - id: grog_vs_rum
5
+ type: chart
6
+ api: demo
7
+ highchartsData:
8
+ chart:
9
+ type: column
10
+ title:
11
+ text: Grog vs Rum Sales at the SCUMM Bar
12
+ colors: ["#8B4513", "#DAA520"]
13
+ legend:
14
+ enabled: true
15
+ series:
16
+ - name: Grog (pints)
17
+ data:
18
+ x: "$sales.data.month"
19
+ y: "$sales.data.grog"
20
+ - name: Rum (shots)
21
+ data:
22
+ x: "$sales.data.month"
23
+ y: "$sales.data.rum"
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ module Auth
5
+ # Simple bearer token authentication.
6
+ #
7
+ # auth = Trevl::Auth::BearerToken.new("my-secret-token")
8
+ # Trevl::DataSource::Api.new(base_url: "...", auth: auth)
9
+ #
10
+ # Custom auth objects can be used instead — any object responding to
11
+ # #apply(headers, url:) will work.
12
+ #
13
+ class BearerToken
14
+ def initialize(token)
15
+ @token = token
16
+ end
17
+
18
+ def apply(headers, url: nil)
19
+ headers["Authorization"] = "Bearer #{@token}"
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ class Configuration
5
+ # Highcharts is commercially licensed and not bundled with this gem, so it
6
+ # is loaded from the CDN unless a local copy is configured.
7
+ HIGHCHARTS_CDN_URL = "https://code.highcharts.com/11.4.0/highcharts.js"
8
+
9
+ attr_accessor :logger, :highcharts_url, :highcharts_path, :highcharts_modules
10
+ attr_writer :template_store
11
+
12
+ def initialize
13
+ @logger = Logger.new($stderr, level: Logger::WARN)
14
+ @template_store = nil
15
+ @highcharts_url = HIGHCHARTS_CDN_URL
16
+ @highcharts_path = nil
17
+ @highcharts_modules = []
18
+ end
19
+
20
+ def template_store
21
+ @template_store ||= TemplateStore.new
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ module CoreExt
5
+ module Hash
6
+ def self.deep_dup(hash)
7
+ hash.each_with_object({}) do |(key, value), result|
8
+ result[key] = case value
9
+ when ::Hash then deep_dup(value)
10
+ when ::Array then value.map { |v| v.is_a?(::Hash) ? deep_dup(v) : v }
11
+ else value
12
+ end
13
+ end
14
+ end
15
+
16
+ def self.deep_stringify_keys(hash)
17
+ hash.each_with_object({}) do |(key, value), result|
18
+ result[key.to_s] = case value
19
+ when ::Hash then deep_stringify_keys(value)
20
+ when ::Array then value.map { |v| v.is_a?(::Hash) ? deep_stringify_keys(v) : v }
21
+ else value
22
+ end
23
+ end
24
+ end
25
+
26
+ def self.deep_merge(base, override)
27
+ base.merge(override) do |_key, base_val, override_val|
28
+ if base_val.is_a?(::Hash) && override_val.is_a?(::Hash)
29
+ deep_merge(base_val, override_val)
30
+ else
31
+ override_val
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+
5
+ module Trevl
6
+ module DataSource
7
+ # Generic REST API data source.
8
+ #
9
+ # Trevl::DataSource.register("my_api", Trevl::DataSource::Api.new(
10
+ # base_url: "https://api.example.com/v1",
11
+ # auth: Trevl::Auth::BearerToken.new("my-token")
12
+ # ))
13
+ #
14
+ class Api < Base
15
+ include HTTParty
16
+
17
+ EMPTY_RESULT = {"data" => [], "meta" => {}}.freeze
18
+
19
+ attr_reader :base_url
20
+
21
+ def initialize(base_url:, auth: nil, default_resource: nil, **)
22
+ super()
23
+ @base_url = base_url.to_s.chomp("/")
24
+ @auth = auth
25
+ @default_resource = default_resource
26
+ end
27
+
28
+ def fetch(endpoint, params = {}, resource: nil)
29
+ resource = (resource || @default_resource).to_s
30
+ url = build_url(resource, endpoint)
31
+ headers = build_headers(url)
32
+ body = params.is_a?(Hash) ? params : {}
33
+
34
+ Trevl.logger.info("[Trevl::DataSource::Api] POST #{url}")
35
+
36
+ response = self.class.post(url, body: body.to_json, headers: headers, timeout: 15)
37
+
38
+ unless response.success?
39
+ Trevl.logger.error("[Trevl::DataSource::Api] HTTP #{response.code} from #{url}")
40
+ return EMPTY_RESULT.dup
41
+ end
42
+
43
+ parse_response(response.body)
44
+ rescue JSON::ParserError, Timeout::Error, Errno::ECONNREFUSED => e
45
+ Trevl.logger.error("[Trevl::DataSource::Api] Error: #{e.message}")
46
+ EMPTY_RESULT.dup
47
+ end
48
+
49
+ private
50
+
51
+ def build_url(resource, endpoint)
52
+ parts = [@base_url]
53
+ parts << resource unless resource.empty?
54
+ parts << endpoint unless endpoint.nil? || endpoint.empty?
55
+ parts.join("/")
56
+ end
57
+
58
+ def build_headers(url)
59
+ headers = {"Content-Type" => "application/json", "Accept" => "application/json"}
60
+ @auth&.apply(headers, url: url)
61
+ headers
62
+ end
63
+
64
+ def parse_response(body)
65
+ data = JSON.parse(body)
66
+ meta = extract_meta(data)
67
+ rows = extract_rows(data)
68
+ {"data" => rows, "meta" => meta}
69
+ end
70
+
71
+ def extract_meta(data)
72
+ return {} unless data.is_a?(Hash)
73
+ m = data["metadata"] || data["meta"]
74
+ m.is_a?(Hash) ? m : {}
75
+ end
76
+
77
+ def extract_rows(data)
78
+ case data
79
+ when Array then data
80
+ when Hash then data["results"] || data["data"] || (data.key?("meta") ? [] : [data])
81
+ else []
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ module DataSource
5
+ class Base
6
+ def fetch(endpoint, params = {}, resource: nil)
7
+ raise NotImplementedError, "#{self.class}#fetch must be implemented"
8
+ end
9
+
10
+ # Returns the field names available for an endpoint by fetching a sample row.
11
+ # Override in subclasses for more efficient discovery (e.g. schema endpoints).
12
+ def field_names(endpoint, params = {}, resource: nil)
13
+ result = fetch(endpoint, params, resource: resource)
14
+ row = Array(result["data"]).first
15
+ row.is_a?(Hash) ? row.keys : []
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+
5
+ module Trevl
6
+ module DataSource
7
+ # Generic CubeJS data source.
8
+ #
9
+ # Trevl::DataSource.register("cube", Trevl::DataSource::Cube.new(
10
+ # url: "https://cube.example.com/cubejs-api/v1",
11
+ # token: ENV["CUBE_TOKEN"]
12
+ # ))
13
+ #
14
+ class Cube < Base
15
+ include HTTParty
16
+
17
+ EMPTY_RESULT = {"data" => [], "meta" => {}}.freeze
18
+ QUERY_KEYS = %w[dimensions measures filters timeDimensions order limit].freeze
19
+
20
+ def initialize(url:, token: nil, **)
21
+ super()
22
+ @url = url.to_s.chomp("/")
23
+ @token = token
24
+ end
25
+
26
+ def fetch(endpoint, params = {}, resource: nil)
27
+ query = build_query(params)
28
+ headers = {"Content-Type" => "application/json", "Accept" => "application/json"}
29
+ headers["Authorization"] = @token if @token
30
+
31
+ Trevl.logger.info("[Trevl::DataSource::Cube] Querying #{@url}/load")
32
+
33
+ response = self.class.post(
34
+ "#{@url}/load",
35
+ body: {query: query}.to_json,
36
+ headers: headers,
37
+ timeout: 30
38
+ )
39
+
40
+ unless response.success?
41
+ Trevl.logger.error("[Trevl::DataSource::Cube] HTTP #{response.code}: #{response.body}")
42
+ return EMPTY_RESULT.dup
43
+ end
44
+
45
+ data = JSON.parse(response.body)
46
+ rows = Array(data["data"]).map { |row| flatten_keys(row) }
47
+ {"data" => rows, "meta" => {}}
48
+ rescue JSON::ParserError, Timeout::Error, Errno::ECONNREFUSED => e
49
+ Trevl.logger.error("[Trevl::DataSource::Cube] Error: #{e.message}")
50
+ EMPTY_RESULT.dup
51
+ end
52
+
53
+ private
54
+
55
+ def build_query(params)
56
+ QUERY_KEYS.each_with_object({}) do |key, query|
57
+ val = params[key] || params[key.to_sym]
58
+ query[key] = val if val
59
+ end
60
+ end
61
+
62
+ def flatten_keys(row)
63
+ row.each_with_object({}) do |(key, value), flat|
64
+ flat_key = key.to_s.include?(".") ? key.to_s.split(".").last : key.to_s
65
+ flat[flat_key] = value
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ module DataSource
5
+ class Static < Base
6
+ attr_reader :data
7
+
8
+ def initialize(data: {}, **)
9
+ super()
10
+ @data = data
11
+ end
12
+
13
+ def fetch(endpoint, params = {}, resource: nil)
14
+ key = resource ? "#{resource}/#{endpoint}" : endpoint
15
+ result = @data[key] || @data[endpoint]
16
+
17
+ case result
18
+ when Array
19
+ {"data" => result, "meta" => {}}
20
+ when Hash
21
+ data = result["data"] || result[:data] || []
22
+ meta = result["meta"] || result[:meta] || {}
23
+ {"data" => Array(data), "meta" => meta.is_a?(Hash) ? meta : {}}
24
+ else
25
+ {"data" => [], "meta" => {}}
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ module DataSource
5
+ class << self
6
+ def register(name, source)
7
+ registry[name.to_s.strip.downcase] = source
8
+ end
9
+
10
+ def for(name, **)
11
+ key = name.to_s.strip.downcase
12
+ source = registry[key]
13
+ raise DataSourceError, "No data source registered for '#{name}'. Available: #{registry.keys.join(", ")}." unless source
14
+
15
+ source.is_a?(Class) ? source.new(**) : source
16
+ end
17
+
18
+ def all
19
+ registry.map do |name, source|
20
+ obj = source.is_a?(Class) ? source : source.class
21
+ {name: name, type: obj.name&.split("::")&.last || obj.to_s, source: source}
22
+ end
23
+ end
24
+
25
+ def names
26
+ registry.keys
27
+ end
28
+
29
+ def registry
30
+ @registry ||= {}
31
+ end
32
+
33
+ def reset!
34
+ @registry = {}
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ class Error < StandardError; end
5
+ class ParseError < Error; end
6
+ class ValidationError < Error; end
7
+ class DataSourceError < Error; end
8
+ class RenderError < Error; end
9
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ # Builds the <script> tags that load Highcharts into generated HTML.
5
+ #
6
+ # Highcharts is commercially licensed and is therefore not shipped with this
7
+ # gem. By default it is loaded from the official CDN. Point the configuration
8
+ # at local copies to keep the output self-contained and usable offline:
9
+ #
10
+ # Trevl.configure do |config|
11
+ # config.highcharts_path = Rails.root.join("vendor/highcharts/highcharts.js")
12
+ # end
13
+ #
14
+ # Using Highcharts requires a licence from Highsoft. See highcharts.com/license.
15
+ module HighchartsAsset
16
+ class << self
17
+ # Core library plus any configured modules, as ready-to-embed script tags.
18
+ def script_tags
19
+ config = Trevl.configuration
20
+ sources = [[config.highcharts_path, config.highcharts_url]] +
21
+ config.highcharts_modules.map { |m| [local?(m) ? m : nil, m] }
22
+
23
+ sources.map { |path, url| script_tag(path, url) }.join("\n")
24
+ end
25
+
26
+ private
27
+
28
+ # A local file is inlined so the page works without network access;
29
+ # anything else becomes a src reference.
30
+ def script_tag(path, url)
31
+ if path && File.exist?(path.to_s)
32
+ "<script>#{File.read(path.to_s)}</script>"
33
+ elsif path
34
+ raise Trevl::Error, "Highcharts JS not found at #{path}"
35
+ else
36
+ %(<script src="#{url}"></script>)
37
+ end
38
+ end
39
+
40
+ def local?(source)
41
+ !source.to_s.match?(%r{\Ahttps?://})
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ # Renders TREVL YAML to an HTML page that draws its charts with Highcharts.
5
+ # Highcharts itself is loaded from the CDN by default; configure
6
+ # `highcharts_path` to inline a local copy and get a self-contained file
7
+ # that works offline.
8
+ #
9
+ # html = Trevl.render_to_html(yaml_string)
10
+ # File.write("chart.html", html)
11
+ #
12
+ # For screenshots, open the HTML file with any headless browser:
13
+ # - Grover gem: Grover.new(html).to_png
14
+ # - Ferrum: browser.goto("file:///tmp/chart.html"); browser.screenshot
15
+ # - Playwright MCP: browser_navigate + browser_take_screenshot
16
+ # - CLI: npx puppeteer screenshot chart.html
17
+ #
18
+ class HtmlRenderer
19
+ def initialize(width: 800, height: 400, background: "#ffffff")
20
+ @width = width
21
+ @height = height
22
+ @background = background
23
+ end
24
+
25
+ # Renders TREVL YAML to a standalone HTML string.
26
+ # Returns a complete HTML document with all charts embedded.
27
+ def render(yaml_string, params: {}, data: nil, data_sources: {})
28
+ results = Trevl.render(yaml_string, params: params, data: data, data_sources: data_sources)
29
+ charts = results.select { |r| r["type"] == "chart" && r["highchartsData"] }
30
+ scores = results.select { |r| r["type"] == "score" && r["display"] }
31
+
32
+ build_html(charts, scores)
33
+ end
34
+
35
+ # Renders a single pre-rendered component hash to HTML.
36
+ def render_component(result)
37
+ case result["type"]
38
+ when "chart" then build_html([result], [])
39
+ when "score" then build_html([], [result])
40
+ else ""
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ def build_html(charts, scores)
47
+ <<~HTML
48
+ <!DOCTYPE html>
49
+ <html>
50
+ <head>
51
+ <meta charset="utf-8">
52
+ <style>
53
+ body { margin: 0; padding: 20px; background: #{@background}; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
54
+ .trevl-chart { width: #{@width}px; height: #{@height}px; margin: 0 auto 20px; }
55
+ .trevl-score { text-align: center; padding: 20px; margin: 0 auto 20px; }
56
+ .trevl-score-title { font-size: 14px; color: #6b7280; margin-bottom: 8px; }
57
+ .trevl-score-value { font-size: 48px; font-weight: 700; color: #1f2937; }
58
+ </style>
59
+ #{HighchartsAsset.script_tags}
60
+ </head>
61
+ <body>
62
+ #{scores.map { |s| score_html(s) }.join("\n")}
63
+ #{charts.each_with_index.map { |c, i| chart_html(c, i) }.join("\n")}
64
+ </body>
65
+ </html>
66
+ HTML
67
+ end
68
+
69
+ def chart_html(result, index)
70
+ container_id = "trevl_chart_#{index}"
71
+ <<~HTML
72
+ <div id="#{container_id}" class="trevl-chart"></div>
73
+ <script>Highcharts.chart('#{container_id}', #{result["highchartsData"].to_json});</script>
74
+ HTML
75
+ end
76
+
77
+ def score_html(result)
78
+ display = result["display"] || {}
79
+ value = display["value"]
80
+ unit = display["unit"] || ""
81
+ title = display.dig("header", "title") || ""
82
+
83
+ <<~HTML
84
+ <div class="trevl-score">
85
+ <div class="trevl-score-title">#{title}</div>
86
+ <div class="trevl-score-value">#{value}#{unit}</div>
87
+ </div>
88
+ HTML
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Trevl
4
+ class Notebook
5
+ class Display
6
+ def render_chart(highcharts_data, height: 400)
7
+ container_id = "trevl_#{SecureRandom.hex(6)}"
8
+
9
+ html = +""
10
+ html << highcharts_script_tag unless @highcharts_loaded
11
+ html << <<~HTML
12
+ <div id="#{container_id}" style="width:100%;height:#{height}px;"></div>
13
+ <script>
14
+ Highcharts.chart('#{container_id}', #{highcharts_data.to_json});
15
+ </script>
16
+ HTML
17
+
18
+ @highcharts_loaded = true
19
+ display_html(html)
20
+ end
21
+
22
+ def render_score(display_data)
23
+ value = display_data["value"]
24
+ unit = display_data["unit"] || ""
25
+ header = display_data["header"] || {}
26
+ title = header["title"] || ""
27
+
28
+ html = <<~HTML
29
+ <div style="text-align:center;padding:20px;font-family:sans-serif;">
30
+ <div style="font-size:14px;color:#6b7280;margin-bottom:8px;">#{title}</div>
31
+ <div style="font-size:48px;font-weight:700;color:#1f2937;">#{value}#{unit}</div>
32
+ </div>
33
+ HTML
34
+
35
+ display_html(html)
36
+ end
37
+
38
+ private
39
+
40
+ def highcharts_script_tag
41
+ "#{HighchartsAsset.script_tags}\n"
42
+ end
43
+
44
+ def display_html(html)
45
+ if defined?(IRuby)
46
+ IRuby.display(html, mime: "text/html")
47
+ else
48
+ puts html
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "notebook/display"
4
+
5
+ module Trevl
6
+ class Notebook
7
+ attr_reader :display
8
+
9
+ def initialize
10
+ @display = Display.new
11
+ end
12
+
13
+ # Render a TREVL YAML string as an interactive Highcharts chart in iRuby.
14
+ # Optionally pass static data to avoid API calls — inline data answers any
15
+ # api name and components without one (Trevl.render data: semantics).
16
+ def chart(yaml_string, params: {}, data: nil, height: 400)
17
+ results = Trevl.render(yaml_string, params: params, data: data)
18
+ results.each do |result|
19
+ next unless result["type"] == "chart" && result["highchartsData"]
20
+ @display.render_chart(result["highchartsData"], height: height)
21
+ end
22
+ nil
23
+ end
24
+
25
+ # Render a score component inline
26
+ def score(yaml_string, params: {}, data: nil)
27
+ results = Trevl.render(yaml_string, params: params, data: data)
28
+ results.each do |result|
29
+ next unless result["type"] == "score"
30
+ @display.render_score(result["display"] || result)
31
+ end
32
+ nil
33
+ end
34
+
35
+ # Fetch raw data from a registered data source
36
+ def fetch(source_name, endpoint, params = {}, resource: nil)
37
+ source = DataSource.for(source_name)
38
+ source.fetch(endpoint, params, resource: resource)
39
+ end
40
+ end
41
+ end