Class: Shadcn::Chart::Plot
- Inherits:
-
Object
- Object
- Shadcn::Chart::Plot
- Defined in:
- app/components/shadcn/chart/plot.rb
Overview
The arithmetic a cartesian chart needs, and nothing that draws.
This is the part the pie did not have and the reason the todo said the
axis is the work: a bar is a rectangle, but where it goes is a scale, a
nice maximum, a set of ticks and a band per category. A plain object, so
all of that is asserted directly rather than through a browser — the same
trade Calendar::Month makes.
data is a Hash of category to a Hash of series to number, which is what
a group(:month, :platform).sum(:visits) becomes after one each_with_object:
{ "January" => { desktop: 186, mobile: 80 },
"February" => { desktop: 305, mobile: 200 } }
Constant Summary collapse
- SIZE =
{ width: 560, height: 240 }.freeze
- PADDING =
Room for the tick labels on the left and the category labels below.
{ left: 34, right: 8, top: 12, bottom: 24 }.freeze
- TICKS =
4- INSET =
The share of a band left empty, so neighbouring categories do not touch.
0.15
Instance Attribute Summary collapse
-
#data ⇒ Object
readonly
Returns the value of attribute data.
-
#height ⇒ Object
readonly
Returns the value of attribute height.
-
#series ⇒ Object
readonly
Returns the value of attribute series.
-
#stacked ⇒ Object
readonly
Returns the value of attribute stacked.
-
#width ⇒ Object
readonly
Returns the value of attribute width.
Instance Method Summary collapse
-
#band ⇒ Object
The horizontal room one category gets, including the space between.
-
#bars ⇒ Object
One rectangle per series per category.
- #baseline ⇒ Object
- #categories ⇒ Object
-
#initialize(data: {}, series: nil, stacked: false, width: SIZE[:width], height: SIZE[:height]) ⇒ Plot
constructor
A new instance of Plot.
-
#label_every(pixels_per_character: 7) ⇒ Object
Every label would collide long before a year of days fits, so only every
nth is drawn —nfrom the room the longest label needs against the room a band gives it. -
#max ⇒ Object
The tallest thing the chart has to fit: one bar's value, or a whole stack's, rounded up to something a person would choose as a maximum.
- #plot_height ⇒ Object
- #plot_width ⇒ Object
-
#point_x(index) ⇒ Object
Where a series' line goes.
- #points(key) ⇒ Object
- #ticks ⇒ Object
- #x_of(index) ⇒ Object
- #y_of(value) ⇒ Object
Constructor Details
#initialize(data: {}, series: nil, stacked: false, width: SIZE[:width], height: SIZE[:height]) ⇒ Plot
Returns a new instance of Plot.
28 29 30 31 32 33 34 |
# File 'app/components/shadcn/chart/plot.rb', line 28 def initialize(data: {}, series: nil, stacked: false, width: SIZE[:width], height: SIZE[:height]) @data = data.to_h { |category, values| [ category.to_s, values.to_h.transform_keys(&:to_s) ] } @series = (series || @data.values.flat_map(&:keys).uniq).map(&:to_s) @stacked = stacked @width = width @height = height end |
Instance Attribute Details
#data ⇒ Object (readonly)
Returns the value of attribute data.
26 27 28 |
# File 'app/components/shadcn/chart/plot.rb', line 26 def data @data end |
#height ⇒ Object (readonly)
Returns the value of attribute height.
26 27 28 |
# File 'app/components/shadcn/chart/plot.rb', line 26 def height @height end |
#series ⇒ Object (readonly)
Returns the value of attribute series.
26 27 28 |
# File 'app/components/shadcn/chart/plot.rb', line 26 def series @series end |
#stacked ⇒ Object (readonly)
Returns the value of attribute stacked.
26 27 28 |
# File 'app/components/shadcn/chart/plot.rb', line 26 def stacked @stacked end |
#width ⇒ Object (readonly)
Returns the value of attribute width.
26 27 28 |
# File 'app/components/shadcn/chart/plot.rb', line 26 def width @width end |
Instance Method Details
#band ⇒ Object
The horizontal room one category gets, including the space between.
47 |
# File 'app/components/shadcn/chart/plot.rb', line 47 def band = @band ||= (plot_width / [ categories.size, 1 ].max.to_f) |
#bars ⇒ Object
One rectangle per series per category. Grouped side by side, or stacked one on top of the other — which is the same arithmetic with the bottom moved up by what is already there.
62 63 64 65 66 |
# File 'app/components/shadcn/chart/plot.rb', line 62 def data.flat_map.with_index do |(category, values), index| stacked ? (category, values, index) : (category, values, index) end end |
#baseline ⇒ Object
53 |
# File 'app/components/shadcn/chart/plot.rb', line 53 def baseline = y_of(0) |
#categories ⇒ Object
36 |
# File 'app/components/shadcn/chart/plot.rb', line 36 def categories = data.keys |
#label_every(pixels_per_character: 7) ⇒ Object
Every label would collide long before a year of days fits, so only every
nth is drawn — n from the room the longest label needs against the
room a band gives it. Upstream's own charts rotate them instead; a Rails
app is likelier to pass twelve months than a thousand points, so this
stays a skip rather than a rotation.
93 94 95 96 97 |
# File 'app/components/shadcn/chart/plot.rb', line 93 def label_every(pixels_per_character: 7) longest = categories.map { |category| category.length }.max.to_i [ ((longest * pixels_per_character) / band).ceil, 1 ].max end |
#max ⇒ Object
The tallest thing the chart has to fit: one bar's value, or a whole stack's, rounded up to something a person would choose as a maximum.
40 41 42 |
# File 'app/components/shadcn/chart/plot.rb', line 40 def max @max ||= nice(data.values.map { |values| tallest(values) }.max.to_f) end |
#plot_height ⇒ Object
57 |
# File 'app/components/shadcn/chart/plot.rb', line 57 def plot_height = height - PADDING[:top] - PADDING[:bottom] |
#plot_width ⇒ Object
55 |
# File 'app/components/shadcn/chart/plot.rb', line 55 def plot_width = width - PADDING[:left] - PADDING[:right] |
#point_x(index) ⇒ Object
Where a series' line goes. Not the middle of a band: a line's axis is a point scale, so the first reading sits on the left edge and the last on the right, and upstream's own line and area charts touch both. A bar cannot — it needs a band to be wide in — which is why the two shapes place their category labels differently.
73 74 75 76 77 |
# File 'app/components/shadcn/chart/plot.rb', line 73 def point_x(index) return x_of(index) + band / 2 if categories.size < 2 PADDING[:left] + plot_width * index / (categories.size - 1).to_f end |
#points(key) ⇒ Object
79 80 81 82 83 84 85 86 |
# File 'app/components/shadcn/chart/plot.rb', line 79 def points(key) key = key.to_s data.map.with_index do |(category, values), index| { category:, value: values[key], x: point_x(index).round(2), y: y_of(values[key]).round(2) } end end |
#ticks ⇒ Object
44 |
# File 'app/components/shadcn/chart/plot.rb', line 44 def ticks = @ticks ||= (0..TICKS).map { |step| max * step / TICKS } |
#x_of(index) ⇒ Object
49 |
# File 'app/components/shadcn/chart/plot.rb', line 49 def x_of(index) = PADDING[:left] + band * index |
#y_of(value) ⇒ Object
51 |
# File 'app/components/shadcn/chart/plot.rb', line 51 def y_of(value) = PADDING[:top] + (plot_height - plot_height * value.to_f / max) |