rubyvis_charts 0.1.3

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