rubyvis_charts 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +56 -0
- data/LICENSE.txt +21 -0
- data/README.md +176 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/images/AreaTimelineChart.png +0 -0
- data/images/BarTimelineChart.png +0 -0
- data/images/DonutChart.png +0 -0
- data/images/GroupedBarTimelineChart.png +0 -0
- data/images/LineTimelineChart.png +0 -0
- data/images/StackedBarTimelineChart.png +0 -0
- data/lib/rubyvis_charts.rb +12 -0
- data/lib/rubyvis_charts/abstract_chart.rb +49 -0
- data/lib/rubyvis_charts/abstract_timeline_chart.rb +441 -0
- data/lib/rubyvis_charts/area_timeline_chart.rb +41 -0
- data/lib/rubyvis_charts/bar_timeline_chart.rb +44 -0
- data/lib/rubyvis_charts/donut_chart.rb +44 -0
- data/lib/rubyvis_charts/grouped_bar_timeline_chart.rb +51 -0
- data/lib/rubyvis_charts/line_timeline_chart.rb +39 -0
- data/lib/rubyvis_charts/padding.rb +19 -0
- data/lib/rubyvis_charts/stacked_bar_timeline_chart.rb +51 -0
- data/lib/rubyvis_charts/version.rb +3 -0
- data/rubyvis_charts.gemspec +37 -0
- metadata +171 -0
@@ -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
|