rubyvis_charts 0.1.3

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,441 @@
1
+ module RubyvisCharts
2
+ class AbstractTimelineChart < AbstractChart
3
+ module DefaultArguments
4
+ Y_SCALE_MAX = nil
5
+ NUMBERS_FORMATTER = ->(number) { number.to_s }
6
+ NUMBERS_COLOR = '#000000'.freeze
7
+ NUMBERS_FONT = '10px sans-serif'.freeze
8
+ TITLE_TEXT = nil
9
+ TITLE_COLOR = '#000000'.freeze
10
+ TITLE_FONT = '10px sans-serif'.freeze
11
+ DATES_FORMATTER = ->(timestamp) { Time.at(timestamp).day.to_s }
12
+ DATES_COLOR = '#000000'.freeze
13
+ DATES_FONT = '10px sans-serif'.freeze
14
+ MARKS = [].freeze
15
+ MARKS_FORMATTER = ->(string) { string }
16
+ MARKS_FONT = '10px sans-serif'.freeze
17
+ MARKS_COLOR = '#000000'.freeze
18
+ RULES_COLOR = '#dfdfdf'.freeze
19
+ RULES_COUNT = 5
20
+ WEEKEND_BAR_COLOR = '#f2f2f2'.freeze
21
+ TIMELINE_WIDTH_RATIO = 0.9
22
+ DATES_HEIGHT_RATIO = 1.0 / 9.0
23
+ MARKS_HEIGHT_RATIO = 0
24
+ LEGEND_TITLES = [].freeze
25
+ LEGEND_COLORS = [].freeze
26
+ LEGEND_TEXT_COLOR = '#000000'.freeze
27
+ LEGEND_FONT = '10px sans-serif'.freeze
28
+ LEGEND_SHAPE = 'square'
29
+ LEGEND_CHARS = []
30
+ THRESHOLD_NUMBER = nil
31
+ THRESHOLD_COLOR = '#0e74eb'
32
+ THRESHOLD_WIDTH = 2
33
+ THRESHOLD_CAPTION = nil
34
+ end
35
+
36
+ EXTRA_WEEKEND_BARS_WIDTH = 0.2
37
+ TITLE_TOP_INDENT = -15
38
+
39
+ attr_reader :dates, :marks,
40
+ :y_scale_max, :numbers_formatter, :numbers_color, :numbers_font,
41
+ :title_text, :title_color, :title_font,
42
+ :dates_formatter, :dates_color, :dates_font,
43
+ :marks_color, :marks_formatter, :marks_font,
44
+ :rules_color, :rules_count,
45
+ :weekend_bar_color,
46
+ :timeline_width_ratio, :dates_height_ratio, :marks_height_ratio,
47
+ :legend_titles, :legend_colors, :legend_text_color, :legend_font, :legend_shape, :legend_chars,
48
+ :custom_legend_offset,
49
+ :threshold_number, :threshold_color, :threshold_width, :threshold_caption,
50
+ :layer_numbers, :layer_title, :layer_dates, :layer_marks, :layer_timeline, :layer_legend
51
+
52
+ def initialize(
53
+ dates:,
54
+ marks: DefaultArguments::MARKS,
55
+ y_scale_max: DefaultArguments::Y_SCALE_MAX,
56
+ numbers_formatter: DefaultArguments::NUMBERS_FORMATTER,
57
+ numbers_color: DefaultArguments::NUMBERS_COLOR,
58
+ numbers_font: DefaultArguments::NUMBERS_FONT,
59
+ title_text: DefaultArguments::TITLE_TEXT,
60
+ title_color: DefaultArguments::TITLE_COLOR,
61
+ title_font: DefaultArguments::TITLE_FONT,
62
+ dates_formatter: DefaultArguments::DATES_FORMATTER,
63
+ dates_color: DefaultArguments::DATES_COLOR,
64
+ dates_font: DefaultArguments::DATES_FONT,
65
+ marks_color: DefaultArguments::MARKS_COLOR,
66
+ marks_font: DefaultArguments::MARKS_FONT,
67
+ marks_formatter: DefaultArguments::MARKS_FORMATTER,
68
+ rules_color: DefaultArguments::RULES_COLOR,
69
+ rules_count: DefaultArguments::RULES_COUNT,
70
+ weekend_bar_color: DefaultArguments::WEEKEND_BAR_COLOR,
71
+ timeline_width_ratio: DefaultArguments::TIMELINE_WIDTH_RATIO,
72
+ dates_height_ratio: DefaultArguments::DATES_HEIGHT_RATIO,
73
+ marks_height_ratio: DefaultArguments::MARKS_HEIGHT_RATIO,
74
+ legend_titles: DefaultArguments::LEGEND_TITLES,
75
+ legend_colors: DefaultArguments::LEGEND_COLORS,
76
+ legend_text_color: DefaultArguments::LEGEND_TEXT_COLOR,
77
+ legend_font: DefaultArguments::LEGEND_FONT,
78
+ legend_shape: DefaultArguments::LEGEND_SHAPE,
79
+ legend_chars: DefaultArguments::LEGEND_CHARS,
80
+ custom_legend_offset: nil,
81
+ threshold_number: DefaultArguments::THRESHOLD_NUMBER,
82
+ threshold_color: DefaultArguments::THRESHOLD_COLOR,
83
+ threshold_width: DefaultArguments::THRESHOLD_WIDTH,
84
+ threshold_caption: DefaultArguments::THRESHOLD_CAPTION,
85
+ **other
86
+ )
87
+ super(other)
88
+
89
+ @dates = dates
90
+ @marks = marks
91
+ @y_scale_max = y_scale_max
92
+ @numbers_formatter = numbers_formatter
93
+ @numbers_color = numbers_color
94
+ @numbers_font = numbers_font
95
+ @title_text = title_text
96
+ @title_color = title_color
97
+ @title_font = title_font
98
+ @dates_formatter = dates_formatter
99
+ @dates_color = dates_color
100
+ @marks_color = marks_color
101
+ @marks_formatter = marks_formatter
102
+ @marks_font = marks_font
103
+ @rules_color = rules_color
104
+ @rules_count = rules_count
105
+ @weekend_bar_color = weekend_bar_color
106
+ @timeline_width_ratio = timeline_width_ratio
107
+ @dates_height_ratio = dates_height_ratio
108
+ @marks_height_ratio = marks_height_ratio
109
+ @legend_titles = legend_titles
110
+ @legend_colors = legend_colors
111
+ @legend_text_color = legend_text_color
112
+ @legend_font = legend_font
113
+ @legend_shape = legend_shape
114
+ @legend_chars = legend_chars
115
+ @dates_font = dates_font
116
+ @custom_legend_offset = custom_legend_offset
117
+ @threshold_number = threshold_number
118
+ @threshold_color = threshold_color
119
+ @threshold_width = threshold_width
120
+ @threshold_caption = threshold_caption
121
+
122
+ initialize_layers!
123
+
124
+ initialize_weekend_bars!
125
+ initialize_numbers!
126
+ initialize_rules!
127
+ initialize_title!
128
+ initialize_dates!
129
+ initialize_marks!
130
+ initialize_threshold!
131
+ initialize_legend!
132
+ end
133
+
134
+ def numbers_width
135
+ inner_box_width - timeline_width
136
+ end
137
+
138
+ def numbers_height
139
+ timeline_height
140
+ end
141
+
142
+ def title_width
143
+ numbers_width
144
+ end
145
+
146
+ def title_height
147
+ dates_height
148
+ end
149
+
150
+ def dates_width
151
+ timeline_width
152
+ end
153
+
154
+ def dates_height
155
+ inner_box_height * dates_height_ratio
156
+ end
157
+
158
+ def marks_height
159
+ inner_box_height * marks_height_ratio
160
+ end
161
+
162
+ def marks_width
163
+ timeline_width
164
+ end
165
+
166
+ def timeline_width
167
+ inner_box_width * timeline_width_ratio
168
+ end
169
+
170
+ def timeline_height
171
+ inner_box_height - dates_height - legend_height - marks_height
172
+ end
173
+
174
+ def legend_width
175
+ timeline_width
176
+ end
177
+
178
+ def legend_height
179
+ legend_titles.any? ? dates_height : 0
180
+ end
181
+
182
+ protected
183
+
184
+ def values_max
185
+ @values_max ||= values.flatten.max
186
+ end
187
+
188
+ def values_max_count
189
+ @values_max_count ||= values.map(&:length).max
190
+ end
191
+
192
+ def numbers_max
193
+ y_scale_max || values_max
194
+ end
195
+
196
+ def numbers_range
197
+ @numbers_range ||= if need_extra_tick?
198
+ Rubyvis::Scale.linear(0, numbers_range_ticks.last + numbers_range_ticks[1])
199
+ .range(0, timeline_height)
200
+ else
201
+ numbers_scaled_heights
202
+ end
203
+ end
204
+
205
+ def numbers_scaled_heights
206
+ @numbers_scaled_heights ||= Rubyvis::Scale.linear(0, numbers_max).range(0, timeline_height)
207
+ end
208
+
209
+ def numbers_range_ticks
210
+ @numbers_range_ticks ||= numbers_scaled_heights.ticks(rules_count)
211
+ end
212
+
213
+ private
214
+
215
+ def initialize_layers!
216
+ @layer_title = create_layer(width: title_width, height: title_height, left: 0, top: 0)
217
+
218
+ @layer_marks = create_layer(width: marks_width, height: marks_height, top: 0, right: 0)
219
+ @layer_numbers = create_layer(width: numbers_width, height: numbers_height, top: marks_height, left: 0)
220
+ @layer_timeline = create_layer(width: timeline_width, height: timeline_height, top: marks_height, right: 0)
221
+ @layer_dates = create_layer(width: dates_width, height: dates_height, top: marks_height + timeline_height, right: 0)
222
+ @layer_legend = create_layer(width: legend_width, height: legend_height, bottom: 0, right: 0)
223
+ end
224
+
225
+ def create_layer(width:, height:, top: nil, right: nil, bottom: nil, left: nil)
226
+ parent_layer.panel
227
+ .width(width)
228
+ .height(height)
229
+ .top(top)
230
+ .right(right)
231
+ .bottom(bottom)
232
+ .left(left)
233
+ end
234
+
235
+ def initialize_weekend_bars!
236
+ chart = self
237
+
238
+ bar_left_indent = -> { index * chart.send(:weekend_bars_range).range_band }
239
+ fill_style_colors = ->(timestamp) { chart.send(:weekend_bars_colors, timestamp) }
240
+
241
+ layer_timeline.add(Rubyvis::Bar)
242
+ .data(dates)
243
+ .width(weekend_bars_range.range_band + EXTRA_WEEKEND_BARS_WIDTH)
244
+ .height(timeline_height + dates_height)
245
+ .left(bar_left_indent)
246
+ .bottom(-dates_height)
247
+ .fillStyle(fill_style_colors)
248
+ end
249
+
250
+ def initialize_numbers!
251
+ numbers_rules = layer_numbers.rule
252
+ .data(numbers_ticks)
253
+ .right(0)
254
+ .width(0)
255
+ .bottom(numbers_range)
256
+
257
+ numbers_rules.add(Rubyvis::Label)
258
+ .text(numbers_formatter)
259
+ .textAlign('right')
260
+ .textBaseline('middle')
261
+ .font(numbers_font)
262
+ .textStyle(numbers_color)
263
+ end
264
+
265
+ def initialize_rules!
266
+ layer_timeline.rule
267
+ .data(rules_ticks)
268
+ .left(0)
269
+ .right(0)
270
+ .bottom(rules_range)
271
+ .strokeStyle(rules_color)
272
+ end
273
+
274
+ def initialize_title!
275
+ return if title_text.nil?
276
+
277
+ layer_title.add(Rubyvis::Label)
278
+ .text(title_text)
279
+ .top(TITLE_TOP_INDENT)
280
+ .left(0)
281
+ .textBaseline('middle')
282
+ .font(title_font)
283
+ .textStyle(title_color)
284
+ end
285
+
286
+ def initialize_dates!
287
+ chart = self
288
+
289
+ label_left_indent = -> { index * chart.send(:dates_range).range_band }
290
+
291
+ dates_panels = layer_dates.panel
292
+ .data(dates)
293
+ .width(dates_range.range_band)
294
+ .left(label_left_indent)
295
+
296
+ dates_panels.add(Rubyvis::Label)
297
+ .text(dates_formatter)
298
+ .textAlign('center')
299
+ .textBaseline('middle')
300
+ .font(dates_font)
301
+ .textStyle(dates_color)
302
+ end
303
+
304
+ def initialize_marks!
305
+ return if marks.empty?
306
+
307
+ chart = self
308
+
309
+ label_left_indent = -> { index * chart.send(:marks_range).range_band }
310
+
311
+ marks_panels = layer_marks.panel
312
+ .data(marks)
313
+ .width(marks_range.range_band)
314
+ .left(label_left_indent)
315
+
316
+ marks_panels.add(Rubyvis::Label)
317
+ .text(marks_formatter)
318
+ .textAlign('center')
319
+ .textBaseline('middle')
320
+ .font(marks_font)
321
+ .textStyle(marks_color)
322
+ end
323
+
324
+ def initialize_threshold!
325
+ return if threshold_number.nil?
326
+
327
+ scaled_threshold = threshold_number * timeline_height / numbers_ticks.last
328
+
329
+ threshold_rule = layer_timeline.rule
330
+ .left(0)
331
+ .right(0)
332
+ .bottom(scaled_threshold)
333
+ .strokeStyle(threshold_color)
334
+ .lineWidth(threshold_width)
335
+
336
+ threshold_rule.add(Rubyvis::Label)
337
+ .left(timeline_width)
338
+ .text(threshold_caption)
339
+ .textAlign('left')
340
+ .textBaseline('middle')
341
+ .font(numbers_font)
342
+ .textStyle(numbers_color)
343
+ end
344
+
345
+ def initialize_legend!
346
+ return if legend_titles.empty?
347
+
348
+ chart = self
349
+
350
+ legend_left_indent = -> { index * chart.send(:legend_range).range_band }
351
+ legend_text = -> { chart.send(:legend_titles)[self.parent.index] }
352
+ legend_color = -> { chart.send(:legend_colors)[self.parent.index] }
353
+
354
+ legend_panels = layer_legend.panel
355
+ .data(legend_titles)
356
+ .top(10)
357
+ .width(legend_range.range_band)
358
+ .left(legend_left_indent)
359
+
360
+ if legend_chars.blank?
361
+ legend_panels.add(Rubyvis::Dot)
362
+ .shape(legend_shape)
363
+ .left(5)
364
+ .fillStyle(legend_color)
365
+ .strokeStyle(legend_color)
366
+ else
367
+ legend_char = -> { chart.send(:legend_chars)[self.parent.index] }
368
+
369
+ legend_panels.add(Rubyvis::Label)
370
+ .text(legend_char)
371
+ .left(-3)
372
+ .textAlign('left')
373
+ .textStyle(legend_color)
374
+ .textBaseline('middle')
375
+ .font(legend_font)
376
+ end
377
+
378
+ legend_panels.add(Rubyvis::Label)
379
+ .text(legend_text)
380
+ .left(10)
381
+ .textStyle(legend_text_color)
382
+ .textBaseline('middle')
383
+ .font(legend_font)
384
+ end
385
+
386
+ def numbers_ticks
387
+ ticks = numbers_range_ticks.length
388
+ ticks += 1 if need_extra_tick?
389
+ numbers_range.ticks(ticks)
390
+ end
391
+
392
+ def rules_range
393
+ numbers_range
394
+ end
395
+
396
+ def rules_ticks
397
+ numbers_ticks
398
+ end
399
+
400
+ def weekend_bars_range
401
+ dates_range
402
+ end
403
+
404
+ def graph_width
405
+ @graph_width ||= Rubyvis::Scale.linear(0, dates.length).range(0, timeline_width)
406
+ end
407
+
408
+ def dates_range
409
+ @dates_range ||= Rubyvis::Scale.ordinal(Rubyvis.range(dates.length)).split_banded(0, dates_width)
410
+ end
411
+
412
+ def marks_range
413
+ dates_range
414
+ end
415
+
416
+ def legend_range
417
+ @legend_range ||= Rubyvis::Scale.ordinal(Rubyvis.range(legend_titles.length)).split_banded(0, custom_legend_width)
418
+ end
419
+
420
+ def custom_legend_width
421
+ custom_legend_offset ? legend_width - custom_legend_offset : legend_width
422
+ end
423
+
424
+ def bars_heights
425
+ @bars_heights ||= numbers_range
426
+ end
427
+
428
+ def need_extra_tick?
429
+ numbers_max > numbers_range_ticks.last
430
+ end
431
+
432
+ def weekend_bars_colors(timestamp)
433
+ time = Time.at(timestamp)
434
+ weekend_bar_color if time.sunday? || time.saturday?
435
+ end
436
+
437
+ def bars_colors_iterator(index, height, colors)
438
+ colors[index % colors.length] if height.nonzero?
439
+ end
440
+ end
441
+ end
@@ -0,0 +1,41 @@
1
+ module RubyvisCharts
2
+ class AreaTimelineChart < AbstractTimelineChart
3
+ module DefaultArguments
4
+ AREAS_COLORS = %w[#4d79da #31d49e].freeze
5
+ end
6
+
7
+ LONG_MONTH_PADDING = 20
8
+ LONG_MONTH_DAYS = 31
9
+
10
+ attr_reader :areas_colors
11
+
12
+ def initialize(
13
+ areas_colors: DefaultArguments::AREAS_COLORS,
14
+ **other
15
+ )
16
+ super(other)
17
+
18
+ @areas_colors = areas_colors
19
+
20
+ initialize_areas!
21
+ end
22
+
23
+ private
24
+
25
+ def initialize_areas!
26
+ chart = self
27
+
28
+ area_left_offset = -> { chart.send(:graph_width).scale(self.index) + chart.send(:graph_width).scale(1)/2 }
29
+ height = ->(d) { chart.send(:bars_heights).scale(d) }
30
+
31
+ values.each_with_index do |area, index|
32
+ @layer_timeline.add(Rubyvis::Area)
33
+ .data(area)
34
+ .bottom(0)
35
+ .left(area_left_offset)
36
+ .height(height)
37
+ .fillStyle(areas_colors[index])
38
+ end
39
+ end
40
+ end
41
+ end