spoom 1.0.1 → 1.0.6

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.
@@ -0,0 +1,175 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "base"
5
+
6
+ module Spoom
7
+ module Coverage
8
+ module D3
9
+ class Pie < Base
10
+ extend T::Sig
11
+ extend T::Helpers
12
+
13
+ abstract!
14
+
15
+ sig { params(id: String, title: String, data: T.untyped).void }
16
+ def initialize(id, title, data)
17
+ super(id, data)
18
+ @title = title
19
+ end
20
+
21
+ sig { returns(String) }
22
+ def self.header_style
23
+ <<~CSS
24
+ .pie .title {
25
+ font: 18px Arial, sans-serif;
26
+ font-weight: bold;
27
+ fill: #212529;
28
+ text-anchor: middle;
29
+ pointer-events: none;
30
+ }
31
+
32
+ .pie .arc {
33
+ stroke: #fff;
34
+ stroke-width: 2px;
35
+ }
36
+ CSS
37
+ end
38
+
39
+ sig { returns(String) }
40
+ def self.header_script
41
+ <<~JS
42
+ function tooltipPie(d, title, kind, sum) {
43
+ moveTooltip(d)
44
+ .html("<b>" + title + "</b><br><br>"
45
+ + "<b>" + d.data.value + "</b> " + kind + "<br>"
46
+ + "<b>" + toPercent(d.data.value, sum) + "</b>%")
47
+ }
48
+ JS
49
+ end
50
+
51
+ sig { override.returns(String) }
52
+ def script
53
+ <<~JS
54
+ #{tooltip}
55
+
56
+ var json_#{id} = #{@data.to_json};
57
+ var pie_#{id} = d3.pie().value((d) => d.value);
58
+ var data_#{id} = pie_#{id}(d3.entries(json_#{id}));
59
+ var sum_#{id} = d3.sum(data_#{id}, (d) => d.data.value);
60
+ var title_#{id} = #{@title.to_json};
61
+
62
+ function draw_#{id}() {
63
+ var pieSize_#{id} = document.getElementById("#{id}").clientWidth - 10;
64
+
65
+ var arcGenerator_#{id} = d3.arc()
66
+ .innerRadius(pieSize_#{id} / 4)
67
+ .outerRadius(pieSize_#{id} / 2);
68
+
69
+ d3.select("##{id}").selectAll("*").remove()
70
+
71
+ var svg_#{id} = d3.select("##{id}")
72
+ .attr("width", pieSize_#{id})
73
+ .attr("height", pieSize_#{id})
74
+ .attr("class", "pie")
75
+ .append("g")
76
+ .attr("transform", "translate(" + pieSize_#{id} / 2 + "," + pieSize_#{id} / 2 + ")");
77
+
78
+ svg_#{id}.selectAll("arcs")
79
+ .data(data_#{id})
80
+ .enter()
81
+ .append('path')
82
+ .attr("class", "arc")
83
+ .attr('fill', (d) => strictnessColor(d.data.key))
84
+ .attr('d', arcGenerator_#{id})
85
+ .on("mouseover", (d) => tooltip.style("opacity", 1))
86
+ .on("mousemove", tooltip_#{id})
87
+ .on("mouseleave", (d) => tooltip.style("opacity", 0));
88
+
89
+ svg_#{id}.selectAll("labels")
90
+ .data(data_#{id})
91
+ .enter()
92
+ .append('text')
93
+ .attr("class", "label")
94
+ .attr("transform", (d) => "translate(" + arcGenerator_#{id}.centroid(d) + ")")
95
+ .filter(d => (d.endAngle - d.startAngle) > 0.25)
96
+ .append("tspan")
97
+ .attr("x", 0)
98
+ .attr("y", -3)
99
+ .text((d) => d.data.value)
100
+ .append("tspan")
101
+ .attr("class", "small")
102
+ .attr("x", 0)
103
+ .attr("y", 13)
104
+ .text((d) => toPercent(d.data.value, sum_#{id}) + "%");
105
+
106
+ svg_#{id}
107
+ .append("text")
108
+ .attr("class", "title")
109
+ .append("tspan")
110
+ .attr("y", 7)
111
+ .text(title_#{id});
112
+ }
113
+
114
+ draw_#{id}();
115
+ window.addEventListener("resize", draw_#{id});
116
+ JS
117
+ end
118
+
119
+ class Sigils < Pie
120
+ extend T::Sig
121
+
122
+ sig { params(id: String, title: String, snapshot: Snapshot).void }
123
+ def initialize(id, title, snapshot)
124
+ super(id, title, snapshot.sigils.select { |_k, v| v })
125
+ end
126
+
127
+ sig { override.returns(String) }
128
+ def tooltip
129
+ <<~JS
130
+ function tooltip_#{id}(d) {
131
+ tooltipPie(d, "typed: " + d.data.key, "files", sum_#{id});
132
+ }
133
+ JS
134
+ end
135
+ end
136
+
137
+ class Calls < Pie
138
+ extend T::Sig
139
+
140
+ sig { params(id: String, title: String, snapshot: Snapshot).void }
141
+ def initialize(id, title, snapshot)
142
+ super(id, title, { true: snapshot.calls_typed, false: snapshot.calls_untyped })
143
+ end
144
+
145
+ sig { override.returns(String) }
146
+ def tooltip
147
+ <<~JS
148
+ function tooltip_#{id}(d) {
149
+ tooltipPie(d, d.data.key == "true" ? " checked" : " unchecked", "calls", sum_#{id})
150
+ }
151
+ JS
152
+ end
153
+ end
154
+
155
+ class Sigs < Pie
156
+ extend T::Sig
157
+
158
+ sig { params(id: String, title: String, snapshot: Snapshot).void }
159
+ def initialize(id, title, snapshot)
160
+ super(id, title, { true: snapshot.methods_with_sig, false: snapshot.methods_without_sig })
161
+ end
162
+
163
+ sig { override.returns(String) }
164
+ def tooltip
165
+ <<~JS
166
+ function tooltip_#{id}(d) {
167
+ tooltipPie(d, (d.data.key == "true" ? " with" : " without") + " a signature", "methods", sum_#{id})
168
+ }
169
+ JS
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,486 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "base"
5
+
6
+ module Spoom
7
+ module Coverage
8
+ module D3
9
+ class Timeline < Base
10
+ extend T::Sig
11
+ extend T::Helpers
12
+
13
+ abstract!
14
+
15
+ sig { params(id: String, data: T.untyped, keys: T::Array[String]).void }
16
+ def initialize(id, data, keys)
17
+ super(id, data)
18
+ @keys = keys
19
+ end
20
+
21
+ sig { returns(String) }
22
+ def self.header_style
23
+ <<~CSS
24
+ .domain {
25
+ stroke: transparent;
26
+ }
27
+
28
+ .grid line {
29
+ stroke: #ccc;
30
+ }
31
+
32
+ .axis text {
33
+ font: 12px Arial, sans-serif;
34
+ fill: #333;
35
+ text-anchor: right;
36
+ pointer-events: none;
37
+ }
38
+
39
+ .inverted .grid line {
40
+ stroke: #777;
41
+ }
42
+
43
+ .inverted .axis text {
44
+ fill: #fff;
45
+ }
46
+
47
+ .inverted .axis line {
48
+ stroke: #fff;
49
+ }
50
+ CSS
51
+ end
52
+
53
+ sig { returns(String) }
54
+ def self.header_script
55
+ <<~JS
56
+ var parseVersion = function(version) {
57
+ if (!version) {
58
+ return null;
59
+ }
60
+ return parseFloat(version.replaceAll("0.", ""));
61
+ }
62
+
63
+ function tooltipTimeline(d, kind) {
64
+ moveTooltip(d)
65
+ .html("commit <b>" + d.data.commit + "</b><br>"
66
+ + d3.timeFormat("%y/%m/%d")(parseDate(d.data.timestamp)) + "<br><br>"
67
+ + "<b>typed: " + d.key + "</b><br><br>"
68
+ + "<b>" + (d.data.values[d.key] ? d.data.values[d.key] : 0) + "</b> " + kind +"<br>"
69
+ + "<b>" + toPercent(d.data.values[d.key] ? d.data.values[d.key] : 0, d.data.total) + "%")
70
+ }
71
+ JS
72
+ end
73
+
74
+ sig { override.returns(String) }
75
+ def script
76
+ <<~HTML
77
+ #{tooltip}
78
+
79
+ var data_#{id} = #{@data.to_json};
80
+
81
+ function draw_#{id}() {
82
+ var width_#{id} = document.getElementById("#{id}").clientWidth;
83
+ var height_#{id} = 200;
84
+
85
+ d3.select("##{id}").selectAll("*").remove()
86
+
87
+ var svg_#{id} = d3.select("##{id}")
88
+ .attr("width", width_#{id})
89
+ .attr("height", height_#{id})
90
+
91
+ #{plot}
92
+ }
93
+
94
+ draw_#{id}();
95
+ window.addEventListener("resize", draw_#{id});
96
+ HTML
97
+ end
98
+
99
+ sig { abstract.returns(String) }
100
+ def plot; end
101
+
102
+ sig { returns(String) }
103
+ def x_scale
104
+ <<~HTML
105
+ var xScale_#{id} = d3.scaleTime()
106
+ .range([0, width_#{id}])
107
+ .domain(d3.extent(data_#{id}, (d) => parseDate(d.timestamp)));
108
+
109
+ svg_#{id}.append("g")
110
+ .attr("class", "grid")
111
+ .attr("transform", "translate(0," + height_#{id} + ")")
112
+ .call(d3.axisBottom(xScale_#{id})
113
+ .tickFormat("")
114
+ .tickSize(-height_#{id}))
115
+ HTML
116
+ end
117
+
118
+ sig { returns(String) }
119
+ def x_ticks
120
+ <<~HTML
121
+ svg_#{id}.append("g")
122
+ .attr("class", "axis x")
123
+ .attr("transform", "translate(0," + height_#{id} + ")")
124
+ .call(d3.axisBottom(xScale_#{id})
125
+ .tickFormat(d3.timeFormat("%y/%m/%d"))
126
+ .tickPadding(-15)
127
+ .tickSize(-3));
128
+ HTML
129
+ end
130
+
131
+ sig { params(min: String, max: String, ticks: String).returns(String) }
132
+ def y_scale(min:, max:, ticks:)
133
+ <<~HTML
134
+ var yScale_#{id} = d3.scaleLinear()
135
+ .range([height_#{id}, 0])
136
+ .domain([#{min}, #{max}]);
137
+
138
+ svg_#{id}.append("g")
139
+ .attr("class", "grid")
140
+ .call(d3.axisLeft(yScale_#{id})
141
+ .#{ticks}
142
+ .tickFormat("")
143
+ .tickSize(-width_#{id}))
144
+ HTML
145
+ end
146
+
147
+ sig { params(ticks: String, format: String, padding: Integer).returns(String) }
148
+ def y_ticks(ticks:, format:, padding:)
149
+ <<~HTML
150
+ svg_#{id}.append("g")
151
+ .attr("class", "axis y")
152
+ .call(d3.axisLeft(yScale_#{id})
153
+ .#{ticks}
154
+ .tickSize(-3)
155
+ .tickFormat((d) => #{format})
156
+ .tickPadding(-#{padding}))
157
+ HTML
158
+ end
159
+
160
+ sig { params(y: String, color: String, curve: String).returns(String) }
161
+ def area(y:, color: "#ccc", curve: "curveCatmullRom.alpha(1)")
162
+ <<~HTML
163
+ svg_#{id}.append("path")
164
+ .datum(data_#{id}.filter((d) => #{y}))
165
+ .attr("class", "area")
166
+ .attr("d", d3.area()
167
+ .defined((d) => #{y})
168
+ .x((d) => xScale_#{id}(parseDate(d.timestamp)))
169
+ .y0(yScale_#{id}(0))
170
+ .y1((d) => yScale_#{id}(#{y}))
171
+ .curve(d3.#{curve}))
172
+ .attr("fill", "#{color}")
173
+ .attr("fill-opacity", 0.5)
174
+ HTML
175
+ end
176
+
177
+ sig { params(y: String, color: String, curve: String).returns(String) }
178
+ def line(y:, color: "#ccc", curve: "curveCatmullRom.alpha(1)")
179
+ <<~HTML
180
+ svg_#{id}.append("path")
181
+ .datum(data_#{id}.filter((d) => #{y}))
182
+ .attr("class", "line")
183
+ .attr("d", d3.line()
184
+ .x((d) => xScale_#{id}(parseDate(d.timestamp)))
185
+ .y((d) => yScale_#{id}(#{y}))
186
+ .curve(d3.#{curve}))
187
+ .attr("stroke", "#{color}")
188
+ .attr("stroke-width", 3)
189
+ .attr("fill", "transparent")
190
+ HTML
191
+ end
192
+
193
+ sig { params(y: String).returns(String) }
194
+ def points(y:)
195
+ <<~HTML
196
+ svg_#{id}.selectAll("circle")
197
+ .data(data_#{id})
198
+ .enter()
199
+ .append("circle")
200
+ .attr("class", "dot")
201
+ .attr("r", 3)
202
+ .attr("cx", (d) => xScale_#{id}(parseDate(d.timestamp)))
203
+ .attr("cy", (d, i) => yScale_#{id}(#{y}))
204
+ .attr("fill", "#aaa")
205
+ .on("mouseover", (d) => tooltip.style("opacity", 1))
206
+ .on("mousemove", tooltip_#{id})
207
+ .on("mouseleave", (d) => tooltip.style("opacity", 0));
208
+ HTML
209
+ end
210
+
211
+ class Versions < Timeline
212
+ extend T::Sig
213
+
214
+ sig { params(id: String, snapshots: T::Array[Snapshot]).void }
215
+ def initialize(id, snapshots)
216
+ data = snapshots.map do |snapshot|
217
+ {
218
+ timestamp: snapshot.commit_timestamp,
219
+ commit: snapshot.commit_sha,
220
+ static: snapshot.version_static,
221
+ runtime: snapshot.version_runtime,
222
+ }
223
+ end
224
+ super(id, data, [])
225
+ end
226
+
227
+ sig { override.returns(String) }
228
+ def tooltip
229
+ <<~JS
230
+ function tooltip_#{id}(d) {
231
+ moveTooltip(d)
232
+ .html("commit <b>" + d.commit + "</b><br>"
233
+ + d3.timeFormat("%y/%m/%d")(parseDate(d.timestamp)) + "<br><br>"
234
+ + "static: v<b>" + d.static + "</b><br>"
235
+ + "runtime: v<b>" + d.runtime + "</b><br><br>"
236
+ + "versions from<br>Gemfile.lock")
237
+ }
238
+ JS
239
+ end
240
+
241
+ sig { override.returns(String) }
242
+ def plot
243
+ <<~JS
244
+ #{x_scale}
245
+ #{y_scale(
246
+ min: "d3.min([d3.min(data_#{id}, (d) => parseVersion(d.static)),
247
+ d3.min(data_#{id}, (d) => parseVersion(d.runtime))]) - 0.01",
248
+ max: "d3.max([d3.max(data_#{id}, (d) => parseVersion(d.static)),
249
+ d3.max(data_#{id}, (d) => parseVersion(d.runtime))]) + 0.01",
250
+ ticks: 'ticks(8)'
251
+ )}
252
+ #{line(y: 'parseVersion(d.runtime)', color: '#e83e8c', curve: 'curveStepAfter')}
253
+ #{line(y: 'parseVersion(d.static)', color: '#007bff', curve: 'curveStepAfter')}
254
+ #{points(y: 'parseVersion(d.static)')}
255
+ #{x_ticks}
256
+ #{y_ticks(ticks: 'ticks(4)', format: "'v0.' + d.toFixed(2)", padding: 50)}
257
+ JS
258
+ end
259
+ end
260
+
261
+ class Runtimes < Timeline
262
+ extend T::Sig
263
+
264
+ sig { params(id: String, snapshots: T::Array[Snapshot]).void }
265
+ def initialize(id, snapshots)
266
+ data = snapshots.map do |snapshot|
267
+ {
268
+ timestamp: snapshot.commit_timestamp,
269
+ commit: snapshot.commit_sha,
270
+ runtime: snapshot.duration.to_f / 1000.0 / 1000.0,
271
+ }
272
+ end
273
+ super(id, data, [])
274
+ end
275
+
276
+ sig { override.returns(String) }
277
+ def tooltip
278
+ <<~JS
279
+ function tooltip_#{id}(d) {
280
+ moveTooltip(d)
281
+ .html("commit <b>" + d.commit + "</b><br>"
282
+ + d3.timeFormat("%y/%m/%d")(parseDate(d.timestamp)) + "<br><br>"
283
+ + "<b>" + d.runtime + "</b>s<br><br>"
284
+ + "(sorbet user + system time)")
285
+ }
286
+ JS
287
+ end
288
+
289
+ sig { override.returns(String) }
290
+ def plot
291
+ <<~JS
292
+ #{x_scale}
293
+ #{y_scale(
294
+ min: '0',
295
+ max: "d3.max(data_#{id}, (d) => d.runtime)",
296
+ ticks: 'ticks(10)'
297
+ )}
298
+ #{area(y: 'd.runtime')}
299
+ #{line(y: 'd.runtime')}
300
+ #{points(y: 'd.runtime')}
301
+ #{x_ticks}
302
+ #{y_ticks(ticks: 'ticks(5)', format: 'd.toFixed(2) + "s"', padding: 40)}
303
+ .call(g => g.selectAll(".tick:first-of-type text").remove())
304
+ JS
305
+ end
306
+ end
307
+
308
+ class Stacked < Timeline
309
+ extend T::Sig
310
+ extend T::Helpers
311
+
312
+ abstract!
313
+
314
+ sig { override.returns(String) }
315
+ def script
316
+ <<~JS
317
+ #{tooltip}
318
+
319
+ var data_#{id} = #{@data.to_json};
320
+ var keys_#{id} = #{T.unsafe(@keys).to_json};
321
+
322
+ var stack_#{id} = d3.stack()
323
+ .keys(keys_#{id})
324
+ .value((d, key) => toPercent(d.values[key], d.total));
325
+
326
+ var layers_#{id} = stack_#{id}(data_#{id});
327
+
328
+ var points_#{id} = []
329
+ layers_#{id}.forEach(function(d) {
330
+ d.forEach(function(p) {
331
+ p.key = d.key
332
+ points_#{id}.push(p);
333
+ });
334
+ })
335
+
336
+ function draw_#{id}() {
337
+ var width_#{id} = document.getElementById("#{id}").clientWidth;
338
+ var height_#{id} = 200;
339
+
340
+ d3.select("##{id}").selectAll("*").remove()
341
+
342
+ var svg_#{id} = d3.select("##{id}")
343
+ .attr("class", "inverted")
344
+ .attr("width", width_#{id})
345
+ .attr("height", height_#{id});
346
+
347
+ #{plot}
348
+ }
349
+
350
+ draw_#{id}();
351
+ window.addEventListener("resize", draw_#{id});
352
+ JS
353
+ end
354
+
355
+ sig { override.returns(String) }
356
+ def plot
357
+ <<~JS
358
+ #{x_scale}
359
+ #{y_scale(min: '0', max: '100', ticks: 'tickValues([0, 25, 50, 75, 100])')}
360
+ #{line(y: 'd.data.timestamp')}
361
+ #{x_ticks}
362
+ #{y_ticks(ticks: 'tickValues([25, 50, 75])', format: "d + '%'", padding: 30)}
363
+ JS
364
+ end
365
+
366
+ sig { override.params(y: String, color: String, curve: String).returns(String) }
367
+ def line(y:, color: 'strictnessColor(d.key)', curve: 'curveCatmullRom.alpha(1)')
368
+ <<~JS
369
+ var area_#{id} = d3.area()
370
+ .x((d) => xScale_#{id}(parseDate(#{y})))
371
+ .y0((d) => yScale_#{id}(d[0]))
372
+ .y1((d) => yScale_#{id}(d[1]))
373
+ .curve(d3.#{curve});
374
+
375
+ var layer = svg_#{id}.selectAll(".layer")
376
+ .data(layers_#{id})
377
+ .enter().append("g")
378
+ .attr("class", "layer")
379
+ .attr("fill", (d, i) => #{color})
380
+
381
+ layer.append("path")
382
+ .attr("class", "area")
383
+ .attr("d", area_#{id})
384
+ .attr("fill", (d) => strictnessColor(d.key))
385
+ .attr("fill-opacity", 0.9)
386
+
387
+ svg_#{id}.selectAll("circle")
388
+ .data(points_#{id})
389
+ .enter()
390
+ .append("circle")
391
+ .attr("class", "dot")
392
+ .attr("r", 2)
393
+ .attr("cx", (d) => xScale_#{id}(parseDate(#{y})))
394
+ .attr("cy", (d, i) => yScale_#{id}(d[1]))
395
+ .attr("fill", "#fff")
396
+ .on("mouseover", (d) => tooltip.style("opacity", 1))
397
+ .on("mousemove", tooltip_#{id})
398
+ .on("mouseleave", (d) => tooltip.style("opacity", 0));
399
+ JS
400
+ end
401
+ end
402
+
403
+ class Sigils < Stacked
404
+ extend T::Sig
405
+
406
+ sig { params(id: String, snapshots: T::Array[Snapshot]).void }
407
+ def initialize(id, snapshots)
408
+ keys = Snapshot::STRICTNESSES
409
+ data = snapshots.map do |snapshot|
410
+ {
411
+ timestamp: snapshot.commit_timestamp,
412
+ commit: snapshot.commit_sha,
413
+ total: snapshot.files,
414
+ values: snapshot.sigils,
415
+ }
416
+ end
417
+ super(id, data, keys)
418
+ end
419
+
420
+ sig { override.returns(String) }
421
+ def tooltip
422
+ <<~JS
423
+ function tooltip_#{id}(d) {
424
+ tooltipTimeline(d, "files");
425
+ }
426
+ JS
427
+ end
428
+ end
429
+
430
+ class Calls < Stacked
431
+ extend T::Sig
432
+
433
+ sig { params(id: String, snapshots: T::Array[Snapshot]).void }
434
+ def initialize(id, snapshots)
435
+ keys = ['false', 'true']
436
+ data = snapshots.map do |snapshot|
437
+ {
438
+ timestamp: snapshot.commit_timestamp,
439
+ commit: snapshot.commit_sha,
440
+ total: snapshot.calls_typed + snapshot.calls_untyped,
441
+ values: { true: snapshot.calls_typed, false: snapshot.calls_untyped },
442
+ }
443
+ end
444
+ super(id, data, keys)
445
+ end
446
+
447
+ sig { override.returns(String) }
448
+ def tooltip
449
+ <<~JS
450
+ function tooltip_#{id}(d) {
451
+ tooltipTimeline(d, "calls");
452
+ }
453
+ JS
454
+ end
455
+ end
456
+
457
+ class Sigs < Stacked
458
+ extend T::Sig
459
+
460
+ sig { params(id: String, snapshots: T::Array[Snapshot]).void }
461
+ def initialize(id, snapshots)
462
+ keys = ['false', 'true']
463
+ data = snapshots.map do |snapshot|
464
+ {
465
+ timestamp: snapshot.commit_timestamp,
466
+ commit: snapshot.commit_sha,
467
+ total: snapshot.methods_with_sig + snapshot.methods_without_sig,
468
+ values: { true: snapshot.methods_with_sig, false: snapshot.methods_without_sig },
469
+ }
470
+ end
471
+ super(id, data, keys)
472
+ end
473
+
474
+ sig { override.returns(String) }
475
+ def tooltip
476
+ <<~JS
477
+ function tooltip_#{id}(d) {
478
+ tooltipTimeline(d, "methods");
479
+ }
480
+ JS
481
+ end
482
+ end
483
+ end
484
+ end
485
+ end
486
+ end