spoom 1.0.1 → 1.0.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -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