glancer 1.0.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 +7 -0
- data/.github/workflows/ci.yml +96 -0
- data/.rubocop.yml +54 -0
- data/CHANGELOG.md +88 -0
- data/CLAUDE.md +115 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/README.md +354 -0
- data/app/assets/config/glancer_manifest.js +1 -0
- data/app/assets/javascripts/glancer/application.js +15 -0
- data/app/assets/javascripts/glancer/controllers/chat_controller.js +101 -0
- data/app/assets/javascripts/glancer/controllers/message_controller.js +1052 -0
- data/app/assets/javascripts/glancer/controllers/toast_controller.js +63 -0
- data/app/assets/stylesheets/glancer/application.css +350 -0
- data/app/assets/stylesheets/glancer/code-blocks.css +6 -0
- data/app/assets/stylesheets/glancer/list.css +31 -0
- data/app/assets/stylesheets/glancer/scrollbar.css +16 -0
- data/app/assets/stylesheets/glancer/table.css +97 -0
- data/app/controllers/glancer/application_controller.rb +33 -0
- data/app/controllers/glancer/chats_controller.rb +49 -0
- data/app/controllers/glancer/messages_controller.rb +144 -0
- data/app/controllers/glancer/schema_controller.rb +29 -0
- data/app/controllers/glancer/settings_controller.rb +23 -0
- data/app/helpers/glancer/application_helper.rb +17 -0
- data/app/jobs/glancer/application_job.rb +6 -0
- data/app/jobs/glancer/process_message_job.rb +38 -0
- data/app/models/glancer/audit.rb +12 -0
- data/app/models/glancer/chat.rb +8 -0
- data/app/models/glancer/code_version.rb +12 -0
- data/app/models/glancer/embedding.rb +6 -0
- data/app/models/glancer/message.rb +25 -0
- data/app/models/glancer/setting.rb +23 -0
- data/app/models/glancer/sql_version.rb +6 -0
- data/app/views/glancer/_data/_importmap.json.erb +7 -0
- data/app/views/glancer/chats/_chat_sidebar.html.erb +2 -0
- data/app/views/glancer/chats/_show.html.erb +52 -0
- data/app/views/glancer/chats/_sidebar_chat_list.html.erb +30 -0
- data/app/views/glancer/chats/index.html.erb +10 -0
- data/app/views/glancer/chats/show.html.erb +1 -0
- data/app/views/glancer/messages/_data_table.html.erb +268 -0
- data/app/views/glancer/messages/_execution_error.html.erb +26 -0
- data/app/views/glancer/messages/_form.html.erb +93 -0
- data/app/views/glancer/messages/_message.html.erb +206 -0
- data/app/views/glancer/messages/_message_info.html.erb +176 -0
- data/app/views/glancer/messages/_temp_form.html.erb +100 -0
- data/app/views/glancer/messages/create.turbo_stream.erb +25 -0
- data/app/views/glancer/schema/show.html.erb +123 -0
- data/app/views/glancer/settings/show.html.erb +306 -0
- data/app/views/glancer/shared/_icons.html.erb +126 -0
- data/app/views/layouts/glancer/application.html.erb +234 -0
- data/config/locales/glancer.en.yml +90 -0
- data/config/locales/glancer.es.yml +90 -0
- data/config/locales/glancer.pt-BR.yml +90 -0
- data/config/routes.rb +20 -0
- data/db/migrate/20250629212642_create_glancer_audits.rb +19 -0
- data/db/migrate/20250629212643_create_glancer_chats.rb +10 -0
- data/db/migrate/20250629212645_create_glancer_embeddings.rb +17 -0
- data/db/migrate/20250629212647_create_glancer_messages.rb +29 -0
- data/db/migrate/20260513204129_add_user_edited_sql_to_glancer_messages.rb +11 -0
- data/db/migrate/20260513210647_create_glancer_sql_versions.rb +18 -0
- data/db/migrate/20260513210648_add_message_id_to_glancer_audits.rb +8 -0
- data/db/migrate/20260513220000_create_glancer_settings.rb +12 -0
- data/db/migrate/20260514083509_add_llm_model_to_glancer_messages.rb +9 -0
- data/db/migrate/20260523120000_rename_code_columns_in_glancer_messages.rb +8 -0
- data/db/migrate/20260523120001_rename_code_column_in_glancer_audits.rb +7 -0
- data/db/migrate/20260523120002_add_code_type_to_glancer_tables.rb +10 -0
- data/db/migrate/20260523120003_rename_glancer_sql_versions_to_code_versions.rb +8 -0
- data/db/migrate/20260523130000_add_enriched_question_to_glancer_messages.rb +7 -0
- data/db/migrate/20260524100000_add_status_to_glancer_messages.rb +9 -0
- data/lib/generators/glancer/install/install_generator.rb +74 -0
- data/lib/generators/glancer/install/templates/glancer.rb +227 -0
- data/lib/generators/glancer/install/templates/llm_context.glancer.md +51 -0
- data/lib/glancer/async_runner.rb +50 -0
- data/lib/glancer/chart_analyzer.rb +230 -0
- data/lib/glancer/configuration.rb +372 -0
- data/lib/glancer/engine.rb +90 -0
- data/lib/glancer/indexer/context_indexer.rb +58 -0
- data/lib/glancer/indexer/model_indexer.rb +64 -0
- data/lib/glancer/indexer/schema_indexer.rb +171 -0
- data/lib/glancer/indexer.rb +50 -0
- data/lib/glancer/retriever.rb +114 -0
- data/lib/glancer/utils/logger.rb +83 -0
- data/lib/glancer/utils/markdown_helper.rb +56 -0
- data/lib/glancer/utils/result_formatter.rb +25 -0
- data/lib/glancer/utils/table_stats.rb +18 -0
- data/lib/glancer/utils/transaction.rb +59 -0
- data/lib/glancer/version.rb +5 -0
- data/lib/glancer/workflow/ar_executor.rb +104 -0
- data/lib/glancer/workflow/ar_extractor.rb +25 -0
- data/lib/glancer/workflow/ar_prompt_builder.rb +64 -0
- data/lib/glancer/workflow/ar_sanitizer.rb +88 -0
- data/lib/glancer/workflow/builder.rb +129 -0
- data/lib/glancer/workflow/cache.rb +55 -0
- data/lib/glancer/workflow/executor.rb +72 -0
- data/lib/glancer/workflow/llm.rb +123 -0
- data/lib/glancer/workflow/prompt_builder.rb +143 -0
- data/lib/glancer/workflow/query_enricher.rb +117 -0
- data/lib/glancer/workflow/sql_extractor.rb +42 -0
- data/lib/glancer/workflow/sql_sanitizer.rb +42 -0
- data/lib/glancer/workflow/sql_validator.rb +67 -0
- data/lib/glancer/workflow.rb +158 -0
- data/lib/glancer.rb +50 -0
- data/lib/tasks/glancer/tailwind.rake +8 -0
- data/lib/tasks/glancer.rake +99 -0
- data/spec/glancer_spec.rb +62 -0
- data/spec/lib/glancer/async_runner_spec.rb +133 -0
- data/spec/lib/glancer/chart_analyzer_spec.rb +296 -0
- data/spec/lib/glancer/configuration_spec.rb +858 -0
- data/spec/lib/glancer/engine_spec.rb +209 -0
- data/spec/lib/glancer/indexer/context_indexer_spec.rb +96 -0
- data/spec/lib/glancer/indexer/model_indexer_spec.rb +103 -0
- data/spec/lib/glancer/indexer/schema_indexer_spec.rb +382 -0
- data/spec/lib/glancer/indexer_spec.rb +95 -0
- data/spec/lib/glancer/retriever_spec.rb +179 -0
- data/spec/lib/glancer/utils/logger_spec.rb +85 -0
- data/spec/lib/glancer/utils/markdown_helper_spec.rb +92 -0
- data/spec/lib/glancer/utils/result_formatter_spec.rb +73 -0
- data/spec/lib/glancer/utils/table_stats_spec.rb +34 -0
- data/spec/lib/glancer/utils/transaction_spec.rb +73 -0
- data/spec/lib/glancer/workflow/ar_executor_spec.rb +155 -0
- data/spec/lib/glancer/workflow/ar_extractor_spec.rb +50 -0
- data/spec/lib/glancer/workflow/ar_prompt_builder_spec.rb +79 -0
- data/spec/lib/glancer/workflow/ar_sanitizer_spec.rb +175 -0
- data/spec/lib/glancer/workflow/builder_spec.rb +204 -0
- data/spec/lib/glancer/workflow/cache_spec.rb +142 -0
- data/spec/lib/glancer/workflow/executor_spec.rb +149 -0
- data/spec/lib/glancer/workflow/llm_spec.rb +124 -0
- data/spec/lib/glancer/workflow/prompt_builder_spec.rb +196 -0
- data/spec/lib/glancer/workflow/query_enricher_spec.rb +184 -0
- data/spec/lib/glancer/workflow/sql_extractor_spec.rb +82 -0
- data/spec/lib/glancer/workflow/sql_sanitizer_spec.rb +98 -0
- data/spec/lib/glancer/workflow/sql_validator_spec.rb +166 -0
- data/spec/lib/glancer/workflow_spec.rb +308 -0
- data/spec/models/glancer/audit_spec.rb +82 -0
- data/spec/models/glancer/chat_spec.rb +60 -0
- data/spec/models/glancer/code_version_spec.rb +71 -0
- data/spec/models/glancer/embedding_spec.rb +73 -0
- data/spec/models/glancer/message_spec.rb +144 -0
- data/spec/models/glancer/setting_spec.rb +88 -0
- data/spec/models/glancer/sql_version_spec.rb +4 -0
- data/spec/spec_helper.rb +128 -0
- data/spec/support/schema.rb +55 -0
- metadata +255 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
|
|
5
|
+
RSpec.describe Glancer::ChartAnalyzer do
|
|
6
|
+
# ── Helpers ────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
def make_rows(count, **cols)
|
|
9
|
+
Array.new(count) { |i| cols.transform_values { |v| v.respond_to?(:call) ? v.call(i) : v } }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# ── .analyze ──────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
describe ".analyze" do
|
|
15
|
+
context "when data is not suitable" do
|
|
16
|
+
it "returns [] for an empty array" do
|
|
17
|
+
expect(described_class.analyze([])).to eq([])
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
it "returns [] for a single row" do
|
|
21
|
+
expect(described_class.analyze([{ a: 1, b: 2 }])).to eq([])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "returns [] when rows have only one column" do
|
|
25
|
+
expect(described_class.analyze([{ a: 1 }, { a: 2 }])).to eq([])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "returns [] when rows are not hashes" do
|
|
29
|
+
expect(described_class.analyze([[1, 2], [3, 4]])).to eq([])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "returns [] when data is not an Array" do
|
|
33
|
+
expect(described_class.analyze("not an array")).to eq([])
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context "line chart (date + numeric measure)" do
|
|
38
|
+
let(:data) do
|
|
39
|
+
[
|
|
40
|
+
{ "month" => "2024-01", "revenue" => 1000 },
|
|
41
|
+
{ "month" => "2024-02", "revenue" => 2000 },
|
|
42
|
+
{ "month" => "2024-03", "revenue" => 1500 }
|
|
43
|
+
]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "includes a line chart" do
|
|
47
|
+
charts = described_class.analyze(data)
|
|
48
|
+
types = charts.map { |c| c[:type] }
|
|
49
|
+
expect(types).to include("line")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it "populates labels and datasets" do
|
|
53
|
+
line = described_class.analyze(data).find { |c| c[:type] == "line" && !c[:datasets].first[:label].to_s.empty? }
|
|
54
|
+
expect(line).not_to be_nil
|
|
55
|
+
expect(line[:datasets].first[:data]).not_to be_empty
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
context "doughnut chart (label + single numeric, low cardinality)" do
|
|
60
|
+
let(:data) do
|
|
61
|
+
[
|
|
62
|
+
{ "category" => "A", "count" => 10 },
|
|
63
|
+
{ "category" => "B", "count" => 20 },
|
|
64
|
+
{ "category" => "C", "count" => 30 }
|
|
65
|
+
]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "includes a doughnut chart" do
|
|
69
|
+
charts = described_class.analyze(data)
|
|
70
|
+
types = charts.map { |c| c[:type] }
|
|
71
|
+
expect(types).to include("doughnut")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it "sets the correct labels" do
|
|
75
|
+
doughnut = described_class.analyze(data).find { |c| c[:type] == "doughnut" }
|
|
76
|
+
expect(doughnut[:labels]).to contain_exactly("A", "B", "C")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
context "bar chart (label + multiple numerics)" do
|
|
81
|
+
let(:data) do
|
|
82
|
+
[
|
|
83
|
+
{ "region" => "North", "sales" => 100, "returns" => 10 },
|
|
84
|
+
{ "region" => "South", "sales" => 200, "returns" => 20 },
|
|
85
|
+
{ "region" => "East", "sales" => 150, "returns" => 15 }
|
|
86
|
+
]
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it "includes a bar chart" do
|
|
90
|
+
charts = described_class.analyze(data)
|
|
91
|
+
types = charts.map { |c| c[:type] }
|
|
92
|
+
expect(types).to include("bar")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "has multiple datasets" do
|
|
96
|
+
bar = described_class.analyze(data).find { |c| c[:type] == "bar" }
|
|
97
|
+
expect(bar[:datasets].size).to be >= 2
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
context "scatter chart (two numerics, no date/group)" do
|
|
102
|
+
let(:data) do
|
|
103
|
+
Array.new(5) { |i| { "x_val" => i * 1.0, "y_val" => i * 2.0 } }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "includes a scatter chart" do
|
|
107
|
+
charts = described_class.analyze(data)
|
|
108
|
+
types = charts.map { |c| c[:type] }
|
|
109
|
+
expect(types).to include("scatter")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "populates datasets with x/y pairs" do
|
|
113
|
+
scatter = described_class.analyze(data).find { |c| c[:type] == "scatter" }
|
|
114
|
+
expect(scatter[:datasets].first[:data].first).to have_key(:x)
|
|
115
|
+
expect(scatter[:datasets].first[:data].first).to have_key(:y)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
context "multi-series (date + group + metric)" do
|
|
120
|
+
# Need enough rows so low_cardinality? passes:
|
|
121
|
+
# uniq(regions=2) < (rows / 2.0).ceil → 2 < ceil(6/2) = 3 → true ✓
|
|
122
|
+
# Values must vary (uniq > 1) so measure_cols filter passes ✓
|
|
123
|
+
let(:data) do
|
|
124
|
+
months = %w[2024-01 2024-02 2024-03]
|
|
125
|
+
regions = %w[North South]
|
|
126
|
+
months.each_with_index.flat_map do |m, i|
|
|
127
|
+
regions.map { |r| { "month" => m, "region" => r, "sales" => (i + 1) * 100 } }
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it "includes a multi-series line chart" do
|
|
132
|
+
charts = described_class.analyze(data)
|
|
133
|
+
types = charts.map { |c| c[:type] }
|
|
134
|
+
expect(types).to include("line")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it "builds a dataset per region" do
|
|
138
|
+
chart = described_class.analyze(data).find { |c| c[:type] == "line" && c[:datasets].size >= 2 }
|
|
139
|
+
expect(chart).not_to be_nil
|
|
140
|
+
expect(chart[:datasets].size).to eq(2)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it "returns nil for multi-series when group cardinality exceeds MAX_BAR_CATEGORIES" do
|
|
144
|
+
# 61 distinct groups > MAX_BAR_CATEGORIES(60) → build_multi_series returns nil → compacted out
|
|
145
|
+
many_regions = (1..61).map { |i| "region_#{i}" }
|
|
146
|
+
big_data = many_regions.flat_map do |r|
|
|
147
|
+
%w[2024-01 2024-02 2024-03].map { |m| { "month" => m, "region" => r, "sales" => 10 } }
|
|
148
|
+
end
|
|
149
|
+
# low_cardinality requires uniq < rows/2; 61 < 183/2=92 → true (group_col detected)
|
|
150
|
+
charts = described_class.analyze(big_data)
|
|
151
|
+
multi = charts.select { |c| c[:type] == "line" && c[:datasets]&.size.to_i > 10 }
|
|
152
|
+
# build_multi_series returns nil → it gets compacted out
|
|
153
|
+
expect(multi).to be_empty
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
context "multi-series with non-monthly date strings" do
|
|
158
|
+
# Dates like "2024-01-01" are not monthly_period? → raw_dates.sort path (L156)
|
|
159
|
+
# Values must vary so measure_cols passes the uniqueness filter
|
|
160
|
+
let(:data) do
|
|
161
|
+
dates = %w[2024-01-01 2024-02-01 2024-03-01]
|
|
162
|
+
regions = %w[East West]
|
|
163
|
+
dates.each_with_index.flat_map do |d, i|
|
|
164
|
+
regions.map { |r| { "date" => d, "region" => r, "sales" => (i + 1) * 50 } }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
it "still produces a line chart using sorted raw dates" do
|
|
169
|
+
charts = described_class.analyze(data)
|
|
170
|
+
types = charts.map { |c| c[:type] }
|
|
171
|
+
expect(types).to include("line")
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
context "on internal error" do
|
|
176
|
+
it "returns [] instead of raising" do
|
|
177
|
+
bad_data = [{ a: nil, b: nil }, { a: nil, b: nil }]
|
|
178
|
+
expect { described_class.analyze(bad_data) }.not_to raise_error
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
it "returns [] when an unexpected StandardError is raised internally" do
|
|
182
|
+
allow(described_class).to receive(:detect_date_column).and_raise(StandardError, "internal failure")
|
|
183
|
+
data = [{ "a" => "x", "b" => 1 }, { "a" => "y", "b" => 2 }]
|
|
184
|
+
expect(described_class.analyze(data)).to eq([])
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# ── date_string? rescue path ──────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
describe ".date_string? rescue path" do
|
|
192
|
+
it "returns false when match? raises" do
|
|
193
|
+
exploding = Class.new(String) { def match?(*_) = raise(StandardError, "oops") }.new("2024-01")
|
|
194
|
+
expect(described_class.send(:date_string?, exploding)).to be(false)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# ── date_string? (via analyze with date-string data) ─────────────────────
|
|
199
|
+
|
|
200
|
+
describe "date_string? behavior" do
|
|
201
|
+
it "treats YYYY-MM-DD strings as dates" do
|
|
202
|
+
data = Array.new(3) { |i| { "date" => "2024-0#{i + 1}-01", "val" => i } }
|
|
203
|
+
charts = described_class.analyze(data)
|
|
204
|
+
expect(charts).not_to be_empty
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
it "treats YYYY-MM strings as monthly periods" do
|
|
208
|
+
data = [
|
|
209
|
+
{ "period" => "2024-01", "sales" => 100 },
|
|
210
|
+
{ "period" => "2024-02", "sales" => 200 },
|
|
211
|
+
{ "period" => "2024-03", "sales" => 150 }
|
|
212
|
+
]
|
|
213
|
+
charts = described_class.analyze(data)
|
|
214
|
+
line = charts.find { |c| c[:type] == "line" }
|
|
215
|
+
expect(line).not_to be_nil
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# ── fill_monthly_gaps ─────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
describe "monthly gap filling" do
|
|
222
|
+
it "fills missing months between start and end" do
|
|
223
|
+
data = [
|
|
224
|
+
{ "month" => "2024-01", "sales" => 100 },
|
|
225
|
+
{ "month" => "2024-03", "sales" => 300 }
|
|
226
|
+
]
|
|
227
|
+
# The line chart should include 2024-02 with 0 sales
|
|
228
|
+
line = described_class.analyze(data).find { |c| c[:type] == "line" }
|
|
229
|
+
expect(line).not_to be_nil
|
|
230
|
+
expect(line[:labels]).to include("2024-02")
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# ── numeric? helper (exercised through analyze) ───────────────────────────
|
|
235
|
+
|
|
236
|
+
describe "numeric? behavior" do
|
|
237
|
+
it "treats integer rows as numeric" do
|
|
238
|
+
data = [{ "a" => "Label", "b" => 10 }, { "a" => "Other", "b" => 20 }]
|
|
239
|
+
charts = described_class.analyze(data)
|
|
240
|
+
expect(charts).not_to be_empty
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
it "treats float strings as numeric" do
|
|
244
|
+
data = [{ "a" => "X", "b" => "1.5" }, { "a" => "Y", "b" => "2.5" }]
|
|
245
|
+
charts = described_class.analyze(data)
|
|
246
|
+
expect(charts).not_to be_empty
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# ── humanize helper ───────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
describe "humanize behavior" do
|
|
253
|
+
it "converts snake_case column names to Title Case in datasets" do
|
|
254
|
+
data = [
|
|
255
|
+
{ "category" => "A", "total_orders" => 5 },
|
|
256
|
+
{ "category" => "B", "total_orders" => 10 }
|
|
257
|
+
]
|
|
258
|
+
charts = described_class.analyze(data)
|
|
259
|
+
chart = charts.find { |c| c[:datasets] }
|
|
260
|
+
label = chart[:datasets].first[:label]
|
|
261
|
+
expect(label).to match(/Total Orders/i)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# ── id column exclusion ───────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
describe "id column exclusion from measures" do
|
|
268
|
+
it "does not use id columns as metric values" do
|
|
269
|
+
data = [
|
|
270
|
+
{ "user_id" => 1, "category" => "A", "revenue" => 100 },
|
|
271
|
+
{ "user_id" => 2, "category" => "B", "revenue" => 200 }
|
|
272
|
+
]
|
|
273
|
+
charts = described_class.analyze(data)
|
|
274
|
+
# scatter would require 2 measure cols — user_id should be excluded
|
|
275
|
+
scatter = charts.find { |c| c[:type] == "scatter" }
|
|
276
|
+
if scatter
|
|
277
|
+
labels = scatter[:datasets].map { |d| d[:label] }
|
|
278
|
+
expect(labels.join).not_to match(/user.id/i)
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# ── expand_month_range ────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
describe "month range expansion" do
|
|
286
|
+
it "generates all months between two YYYY-MM strings" do
|
|
287
|
+
data = [
|
|
288
|
+
{ "month" => "2023-11", "n" => 1 },
|
|
289
|
+
{ "month" => "2024-01", "n" => 2 }
|
|
290
|
+
]
|
|
291
|
+
line = described_class.analyze(data).find { |c| c[:type] == "line" }
|
|
292
|
+
expect(line).not_to be_nil
|
|
293
|
+
expect(line[:labels]).to include("2023-11", "2023-12", "2024-01")
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
end
|