├── .github └── workflows │ └── release.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── README ├── Lollipop.png └── PureChartLogo.png ├── app └── views │ ├── _bar.html.erb │ ├── _line_plot.html.erb │ ├── _lollipop.html.erb │ ├── _pie.html.erb │ └── _styles.html.erb ├── lib ├── purechart.rb └── purechart │ ├── chart_helper.rb │ └── styles │ ├── default.yml │ ├── futuristic_dark.yml │ ├── futuristic_light.yml │ ├── professional_dark.yml │ └── professional_light.yml └── purechart.gemspec /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | if: contains(github.event.head_commit.message, 'RELEASE') 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | # Setup ruby if a release was created 16 | - uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: 3.0.0 19 | 20 | # Publish 21 | - name: publish gem 22 | run: | 23 | mkdir -p $HOME/.gem 24 | touch $HOME/.gem/credentials 25 | chmod 0600 $HOME/.gem/credentials 26 | printf -- "---\n:rubygems_api_key: ${GEM_HOST_API_KEY}\n" > $HOME/.gem/credentials 27 | gem build *.gemspec 28 | gem push *.gem 29 | env: 30 | # Make sure to update the secret name 31 | # if yours isn't named RUBYGEMS_AUTH_TOKEN 32 | GEM_HOST_API_KEY: "${{secrets.RUBYGEMS_AUTH_TOKEN}}" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.gem -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 George Berdovskiy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | # PureChart 6 | Fully customizable HTML/CSS charts for Ruby on Rails. PureChart serves as an alternative to other charting libraries that extensively use JavaScript and HTML `canvas` elements to render charts, resulting in Rails rendering problems and very limited customization options. 7 | 8 | ## Getting Started 9 | Integrating PureChart into your Rails project is incredibly easy. Simply add `gem 'purechart'` to your Gemfile, run `bundle install` and you should be all set! Now, use our helpers to generate charts in your `html.erb` files and create your own style configurations in either `YML`, `TOML`, or `JSON` format in the `app/purechart` directory of your application. 10 | 11 | **Note -** We're currently working on a documentation website ([docs.purechart.org](https://docs.purechart.org)). Check back once in a while for updates! 12 | 13 | ## Examples 14 | ### Lollipop Chart 15 | #### Controller 16 | ```ruby 17 | class ChartsController < ApplicationController 18 | def index 19 | @data = [ 20 | { 21 | name: "Burger King", 22 | color: "#ff7f50", 23 | value: 1200, 24 | }, 25 | { 26 | name: "McDonalds", 27 | color: "#ff4757", 28 | value: 500, 29 | }, 30 | { 31 | name: "Green Burrito", 32 | color: "#2ed573", 33 | value: 780, 34 | } 35 | ] 36 | 37 | @axes = { 38 | horizontal: "Dollars" 39 | } 40 | end 41 | end 42 | ``` 43 | 44 | #### Optional Style Configuration (`app/purechart/custom_bar.yml`) 45 | ```yml 46 | --- 47 | labels: 48 | font: Inter Tight 49 | ... 50 | ``` 51 | 52 | #### Template 53 | ```erb 54 |
55 | <%= lollipop_chart @data, @axes, "custom_bar" %> 56 |
57 | ``` 58 | 59 |

60 | 61 |

62 | -------------------------------------------------------------------------------- /README/Lollipop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PureChart/purechart/7eab14955b37b9355c072453628388127caa218e/README/Lollipop.png -------------------------------------------------------------------------------- /README/PureChartLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PureChart/purechart/7eab14955b37b9355c072453628388127caa218e/README/PureChartLogo.png -------------------------------------------------------------------------------- /app/views/_bar.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%- data.each do |object| %> 5 |
6 |

<%= object[:name] %>

7 |
8 |

<%= object[:name] %>

9 |

<%= configuration[:symbol] ? configuration[:symbol] : ''%><%= object[:value] %>

10 |
11 |
12 | <% end %> 13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | <%- (1..10).each do |index| %> 39 |
40 |

<%= index * gridlines[:vertical_increment] %>

41 |
42 | <% end %> 43 |
44 |
45 |

<%= configuration[:axes][:horizontal] %><%= configuration[:symbol] ? " (" + configuration[:symbol] + ")" : ''%>

46 |
47 |
48 | 49 | <%= render :partial => '/styles' %> -------------------------------------------------------------------------------- /app/views/_line_plot.html.erb: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 |

<%= configuration[:axes][:vertical] %><%= configuration[:symbol] ? " (" + configuration[:symbol] + ")" : ''%>

9 |
10 |
11 |
12 | <% data.each_with_index do |object, index| %> 13 | <% left_offset = (object[:name].size)+5; %> 14 | <% vertical_position = 100 %> 15 |
16 |
17 |

<%= object[:name] %>

18 |

<%= configuration[:symbol] ? configuration[:symbol] : ''%><%= object[:value] %>

19 |
20 |
21 | <% (object[:length].to_i).times do |circle_index| %> 22 |
23 | 24 | 25 | 26 | 27 |
28 | <% vertical_position -= 10 %> 29 | <% end %> 30 | 31 |
32 | 33 |

<%= object[:name] %>

34 | <% end %> 35 |
36 |
37 | 38 |
39 | 40 | <%= render :partial => '/styles' %> 41 | -------------------------------------------------------------------------------- /app/views/_lollipop.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | <%- data.each do |object| %> 5 |
6 |

<%= object[:name] %>

7 |
8 |
9 |

<%= object[:name] %>

10 |

<%= configuration[:symbol] ? configuration[:symbol] : ''%><%= object[:value] %>

11 |
12 |
13 |
14 | <% end %> 15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | <%- (1..10).each do |index| %> 41 |
42 |

<%= index * gridlines[:vertical_increment] %>

43 |
44 | <% end %> 45 |
46 |
47 |

<%= configuration[:axes][:horizontal] %><%= configuration[:symbol] ? " (" + configuration[:symbol] + ")" : ''%>

48 |
49 |
50 | 51 | <%= render :partial => '/styles' %> -------------------------------------------------------------------------------- /app/views/_pie.html.erb: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | <%- arc_x = 25 %> 5 | <%- arc_y = 150 %> 6 | <%- cumulative_percent = 0 %> 7 | <%- cumulative_angle = -180 %> 8 | <%# Calculate arc coordinates to draw each slice %> 9 | <%- data.each_with_index do |object, index| %> 10 | <%- cumulative_percent += object[:percent_value] %> 11 | 19 | <%- arc_x = 150 - 125 * Math.cos(2 * Math::PI * cumulative_percent) %> 20 | <%- arc_y = 150 - 125 * Math.sin(2 * Math::PI * cumulative_percent) %> 21 | 22 | <%# Render arc highlights for hover effect %> 23 | 29 | <%- cumulative_angle += 360 * object[:percent_value] %> 30 | <% end %> 31 | 32 | <%# Pie chart key %> 33 |
34 | <%- data.each_with_index do |object, index| %> 35 | 36 | 37 | <%= object[:name] %> 38 | 39 | <% end %> 40 |
41 |
42 |
43 | 44 | 85 | 86 | <%= render :partial => '/styles' %> -------------------------------------------------------------------------------- /app/views/_styles.html.erb: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/purechart.rb: -------------------------------------------------------------------------------- 1 | require_relative "purechart/chart_helper" 2 | 3 | if defined?(ActiveSupport.on_load) 4 | ActiveSupport.on_load(:action_view) do 5 | include PureChart::ChartHelpers 6 | puts "Helpers sent." 7 | end 8 | else 9 | puts "Active support not defined" 10 | end 11 | 12 | module PureChart 13 | class << self 14 | end 15 | 16 | class Engine < Rails::Engine; 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/purechart/chart_helper.rb: -------------------------------------------------------------------------------- 1 | module PureChart 2 | module ChartHelpers 3 | def lollipop_chart(data, configuration = { axes: { horizontal: "Value" } }, path="") 4 | # Set default configuration file path 5 | default_config_path = File.join( File.dirname(__FILE__), 'styles/default.yml' ) 6 | 7 | default_config_hash = YAML.load(File.read(default_config_path)) 8 | user_config_hash = {} 9 | 10 | if path == "professional_light" 11 | # TODO - Instead of loading our own by default, try/catch to see if they defined their own 12 | # style using the same name 13 | style_config_path = File.join( File.dirname(__FILE__), 'styles/professional_light.yml' ) 14 | default_config_hash = YAML.load(File.read(style_config_path)) 15 | elsif path == "professional_dark" 16 | style_config_path = File.join( File.dirname(__FILE__), 'styles/professional_dark.yml' ) 17 | default_config_hash = YAML.load(File.read(style_config_path)) 18 | elsif path == "futuristic_light" 19 | style_config_path = File.join( File.dirname(__FILE__), 'styles/futuristic_light.yml' ) 20 | default_config_hash = YAML.load(File.read(style_config_path)) 21 | elsif path == "futuristic_dark" 22 | style_config_path = File.join( File.dirname(__FILE__), 'styles/futuristic_dark.yml' ) 23 | default_config_hash = YAML.load(File.read(style_config_path)) 24 | elsif path != "" 25 | # TODO - Implement better logic 26 | if File.file?("app/purechart/" + path + ".yml") 27 | user_config_hash = YAML.load(File.read("app/purechart/" + path + ".yml")) 28 | elsif File.file?("app/purechart/" + path + ".yaml") 29 | user_config_hash = YAML.load(File.read("app/purechart/" + path + ".yaml")) 30 | elsif File.file?("app/purechart/" + path + ".json") 31 | user_config_hash = JSON.load(File.read("app/purechart/" + path + ".json")) 32 | else 33 | raise "(PureChart) ERROR - Could not locate configuration file '" + path + ".[YML, YAML, JSON]'. Make sure this file exists in your 'app/purechart' directory." 34 | end 35 | end 36 | 37 | # Merge user's configuration with default 38 | style_config = default_config_hash.merge(user_config_hash) 39 | 40 | # Format data for chart generation 41 | largest_value = (data.map { |object| object[:value] }).max() 42 | 43 | data.each do |object| 44 | object[:width] = (Float(object[:value]) / largest_value) * 100 45 | end 46 | 47 | gridlines = { 48 | vertical_lines: 10, 49 | vertical_increment: (Float(largest_value) / 10).ceil 50 | } 51 | 52 | ActionController::Base.render partial: '/lollipop', locals: { 53 | data: data, 54 | gridlines: gridlines, 55 | configuration: configuration, 56 | style: style_config 57 | } 58 | end 59 | 60 | def bar_chart(data, configuration = { axes: { horizontal: "Value" }, corner_radius: "Value" }, path="") 61 | # Set default configuration file path 62 | default_config_path = File.join( File.dirname(__FILE__), 'styles/default.yml' ) 63 | 64 | default_config_hash = YAML.load(File.read(default_config_path)) 65 | user_config_hash = {} 66 | 67 | if path == "professional_light" 68 | # TODO - Instead of loading our own by default, try/catch to see if they defined their own 69 | # style using the same name 70 | style_config_path = File.join( File.dirname(__FILE__), 'styles/professional_light.yml' ) 71 | default_config_hash = YAML.load(File.read(style_config_path)) 72 | elsif path == "professional_dark" 73 | style_config_path = File.join( File.dirname(__FILE__), 'styles/professional_dark.yml' ) 74 | default_config_hash = YAML.load(File.read(style_config_path)) 75 | elsif path != "" 76 | # TODO - Implement better logic 77 | if File.file?("app/purechart/" + path + ".yml") 78 | user_config_hash = YAML.load(File.read("app/purechart/" + path + ".yml")) 79 | elsif File.file?("app/purechart/" + path + ".yaml") 80 | user_config_hash = YAML.load(File.read("app/purechart/" + path + ".yaml")) 81 | elsif File.file?("app/purechart/" + path + ".json") 82 | user_config_hash = JSON.load(File.read("app/purechart/" + path + ".json")) 83 | else 84 | raise "(PureChart) ERROR - Could not locate configuration file '" + path + ".[YML, YAML, JSON]'. Make sure this file exists in your 'app/purechart' directory." 85 | end 86 | end 87 | 88 | # Merge user's configuration with default 89 | style_config = default_config_hash.merge(user_config_hash) 90 | 91 | # Format data for chart generation 92 | largest_value = (data.map { |object| object[:value] }).max() 93 | 94 | data.each do |object| 95 | object[:width] = (Float(object[:value]) / largest_value) * 100 96 | end 97 | 98 | gridlines = { 99 | vertical_lines: 10, 100 | vertical_increment: (Float(largest_value) / 10).ceil 101 | } 102 | 103 | ActionController::Base.render partial: '/bar', locals: { 104 | data: data, 105 | gridlines: gridlines, 106 | configuration: configuration, 107 | style: style_config 108 | } 109 | end 110 | 111 | def column_chart 112 | "
Column chart will be rendered here.
".html_safe 113 | end 114 | 115 | def pie_chart(data) 116 | # check for negative values 117 | has_negative_value = 0 118 | 119 | # Find total value for calculating percentages 120 | total_value = 0 121 | data.each do |object| 122 | total_value += (object[:value]).abs 123 | end 124 | 125 | # Calculate percentages for each data point 126 | data.each do |object| 127 | object[:percent_value] = (Float(object[:value]) / total_value).abs 128 | if object[:value] < 0 129 | object[:is_negative] = 1 130 | has_negative_value = 1 131 | else 132 | object[:is_negative] = 0 133 | end 134 | end 135 | 136 | negative_value = { 137 | negative: has_negative_value 138 | } 139 | 140 | ActionController::Base.render partial: '/pie', locals: { 141 | data: data, 142 | negative_value: negative_value 143 | } 144 | end 145 | 146 | def box_plot(data, configuration = {}, path="") 147 | default_color_config = { 148 | colors: { 149 | fill: "white", 150 | stroke: "black" 151 | }, 152 | width: 1 153 | } 154 | 155 | merged_color_config = default_color_config.merge(configuration) 156 | 157 | fill_color = merged_color_config.dig(:colors, :fill) 158 | stroke_color = merged_color_config.dig(:colors, :stroke) 159 | stroke_width = merged_color_config[:width] 160 | 161 | default_config_path = File.join(File.dirname(__FILE__), 'styles', 'default.yml') 162 | default_config_hash = YAML.load(File.read(default_config_path)) 163 | user_config_hash = {} 164 | 165 | if path == "professional_light" 166 | # TODO - Instead of loading our own by default, try/catch to see if they defined their own 167 | # style using the same name 168 | style_config_path = File.join( File.dirname(__FILE__), 'styles/professional_light.yml' ) 169 | default_config_hash = YAML.load(File.read(style_config_path)) 170 | elsif path == "professional_dark" 171 | style_config_path = File.join( File.dirname(__FILE__), 'styles/professional_dark.yml' ) 172 | default_config_hash = YAML.load(File.read(style_config_path)) 173 | elsif path == "futuristic_light" 174 | style_config_path = File.join( File.dirname(__FILE__), 'styles/futuristic_light.yml' ) 175 | default_config_hash = YAML.load(File.read(style_config_path)) 176 | elsif path == "futuristic_dark" 177 | style_config_path = File.join( File.dirname(__FILE__), 'styles/futuristic_dark.yml' ) 178 | default_config_hash = YAML.load(File.read(style_config_path)) 179 | elsif path == "default" 180 | style_config_path = File.join( File.dirname(__FILE__), 'styles/default.yml' ) 181 | default_config_hash = YAML.load(File.read(style_config_path)) 182 | elsif path != "" 183 | # TODO - Implement better logic 184 | if File.file?("app/purechart/" + path + ".yml") 185 | user_config_hash = YAML.load(File.read("app/purechart/" + path + ".yml")) 186 | elsif File.file?("app/purechart/" + path + ".yaml") 187 | user_config_hash = YAML.load(File.read("app/purechart/" + path + ".yaml")) 188 | elsif File.file?("app/purechart/" + path + ".json") 189 | user_config_hash = JSON.load(File.read("app/purechart/" + path + ".json")) 190 | else 191 | raise "(PureChart) ERROR - Could not locate configuration file '" + path + ".[YML, YAML, JSON]'. Make sure this file exists in your 'app/purechart' directory." 192 | end 193 | end 194 | 195 | # Merge user's configuration with default 196 | style_config = default_config_hash.merge(user_config_hash) 197 | area = '''''' 198 | 199 | s_data = data.map{ |data| data[:value]}.sort 200 | 201 | def find_median(array) 202 | return nil if array.empty? 203 | if array.length.even? 204 | puts array.length 205 | return (array[array.length/2]+array[array.length/2-1])/2.0 206 | else 207 | return array[array.length/2].to_f; 208 | end 209 | end 210 | 211 | def normalize(value,min,max) 212 | return ((value.to_f-(min))/(max.to_f-min)+0.025)*950.0 213 | end 214 | 215 | n = s_data.length 216 | min = s_data[0] 217 | max = s_data[-1] 218 | median = find_median(s_data) 219 | 220 | if s_data.length.even? 221 | lower = find_median s_data[0..n/2-1] 222 | upper = find_median s_data[n/2..-1] 223 | else 224 | lower = find_median s_data[0..n/2-1] 225 | upper = find_median s_data[n/2+1..-1] 226 | end 227 | 228 | iqr = 1.5*(upper - lower) 229 | 230 | # color selection from YML - FIX LATER 231 | # Q1 and Q3 232 | area += "" 234 | area += "" 236 | 237 | def linemaker(start,endp, stroke, stroke_width) 238 | line = "" 239 | line+="" 240 | line+="" 241 | return line 242 | end 243 | 244 | # min wisker + lower outliers 245 | if min >= lower-iqr 246 | area+=linemaker(normalize(lower,min,max),normalize(min,min,max), stroke_color, stroke_width) 247 | else 248 | area+=linemaker(normalize(lower,min,max),(normalize(lower-iqr,min,max)), stroke_color, stroke_width) 249 | i=0 250 | while s_data[i] <= lower-iqr 251 | area+= "" 252 | i+=1 253 | end 254 | end 255 | 256 | # max wisker + upper outliers 257 | if max <= upper+iqr 258 | area+=linemaker(normalize(upper,min,max),normalize(max,min,max), stroke_color, stroke_width) 259 | else 260 | area+=linemaker(normalize(upper,min,max),(normalize(upper+iqr,min,max)), stroke_color, stroke_width) 261 | i=s_data.length-1 262 | while s_data[i] >= upper+iqr 263 | area+= "" 264 | i-=1 265 | end 266 | end 267 | 268 | area+="" 269 | area.html_safe 270 | end 271 | 272 | def line_graph(data) 273 | # If all values are very high, the "adjust factor" will be used 274 | # to make sure they are all evenly spread across the vertical axis 275 | # TODO - Decide what the "adjust factor" should be based on user 276 | # input... also rename it 277 | adjust_factor = 0 278 | 279 | chart = '''''' 280 | 281 | min_val = data.min - adjust_factor 282 | max_val = data.max + adjust_factor 283 | 284 | # Calculate the vertical scaling factor 285 | scale_factor = 500.to_f / (max_val - min_val) 286 | 287 | prev_x = 10 288 | prev_y = -1 289 | 290 | i = 10 291 | data.each do |val| 292 | # Apply the transformation formula to scale the y coordinate 293 | scaled_y = ((val - min_val) * scale_factor) 294 | # Invert the y-axis to match the SVG coordinate system (0 at the top) 295 | inverted_y = 500 - scaled_y 296 | 297 | if prev_y == -1 298 | prev_y = inverted_y 299 | end 300 | 301 | chart += "" 302 | chart += "" 303 | 304 | prev_x = i 305 | prev_y = inverted_y 306 | 307 | i += 480.to_f / data.length 308 | end 309 | 310 | chart += "" 311 | chart.html_safe 312 | end 313 | 314 | def dot_plot(data) 315 | adjust_factor = 0 316 | chart = '''''' 317 | 318 | min_val = [0, data.min - adjust_factor].min 319 | max_val = data.max + adjust_factor 320 | 321 | # Calculate the vertical scaling factor 322 | scale_factor = 480.to_f / (max_val - min_val) 323 | x_scale_factor = 480.to_f / data.length 324 | 325 | # Calculate the number of ticks and the increment based on the data range 326 | data_range = max_val - min_val 327 | num_ticks = [data.length, 10].min 328 | increment = data_range.to_f / num_ticks 329 | 330 | # Draw ticks and grey lines for the y-axis 331 | (0..num_ticks).each do |index| 332 | value = min_val + (index * increment) 333 | y = 500 - (index * (450 / num_ticks)) 334 | 335 | if (value % 1).zero? 336 | formatted_value = value.to_i 337 | else 338 | formatted_value = "%.1f" % value 339 | end 340 | 341 | # Only add label and line if value is not 0 342 | if value != 0 343 | chart += "" 344 | chart += "#{formatted_value}" 345 | end 346 | end 347 | 348 | # dots 349 | data.each_with_index do |val, index| 350 | scaled_y = ((val - min_val) * scale_factor) 351 | inverted_y = 500 - (scaled_y / 48 * 45) 352 | chart += "" 353 | end 354 | 355 | # x and y axis 356 | chart += "" 357 | chart += "" 358 | chart += "" 359 | chart.html_safe 360 | end 361 | 362 | def line_plot(data, configuration = { axes: { vertical: "Value" } }, path="") 363 | # Set default configuration file path 364 | default_config_path = File.join( File.dirname(__FILE__), 'styles/default.yml' ) 365 | 366 | default_config_hash = YAML.load(File.read(default_config_path)) 367 | user_config_hash = {} 368 | 369 | if path == "professional_light" 370 | # TODO - Instead of loading our own by default, try/catch to see if they defined their own 371 | # style using the same name 372 | style_config_path = File.join( File.dirname(__FILE__), 'styles/professional_light.yml' ) 373 | default_config_hash = YAML.load(File.read(style_config_path)) 374 | elsif path == "professional_dark" 375 | style_config_path = File.join( File.dirname(__FILE__), 'styles/professional_dark.yml' ) 376 | default_config_hash = YAML.load(File.read(style_config_path)) 377 | elsif path == "futuristic_light" 378 | style_config_path = File.join( File.dirname(__FILE__), 'styles/futuristic_light.yml' ) 379 | default_config_hash = YAML.load(File.read(style_config_path)) 380 | elsif path == "futuristic_dark" 381 | style_config_path = File.join( File.dirname(__FILE__), 'styles/futuristic_dark.yml' ) 382 | default_config_hash = YAML.load(File.read(style_config_path)) 383 | elsif path != "" 384 | # TODO - Implement better logic 385 | if File.file?("app/purechart/" + path + ".yml") 386 | user_config_hash = YAML.load(File.read("app/purechart/" + path + ".yml")) 387 | elsif File.file?("app/purechart/" + path + ".yaml") 388 | user_config_hash = YAML.load(File.read("app/purechart/" + path + ".yaml")) 389 | elsif File.file?("app/purechart/" + path + ".json") 390 | user_config_hash = JSON.load(File.read("app/purechart/" + path + ".json")) 391 | else 392 | raise "(PureChart) ERROR - Could not locate configuration file '" + path + ".[YML, YAML, JSON]'. Make sure this file exists in your 'app/purechart' directory." 393 | end 394 | end 395 | 396 | # Merge user's configuration with default 397 | style_config = default_config_hash.merge(user_config_hash) 398 | 399 | # Format data for chart generation 400 | largest_value = (data.map { |object| object[:value] }).max() 401 | 402 | data.each do |object| 403 | object[:length] = (Float(object[:value]) / largest_value) * 10 404 | end 405 | 406 | gridlines = { 407 | vertical_lines: data.size 408 | } 409 | 410 | ActionController::Base.render partial: '/line_plot', locals: { 411 | data: data, 412 | gridlines: gridlines, 413 | configuration: configuration, 414 | style: style_config 415 | } 416 | end 417 | end 418 | end 419 | -------------------------------------------------------------------------------- /lib/purechart/styles/default.yml: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3 | font: Inter Tight 4 | weight: 700 5 | color: "#000000" 6 | labels: 7 | font: Inter Tight 8 | weight: 700 9 | color: "#000000" 10 | ticks: 11 | font: Inter Tight 12 | weight: 400 13 | color: "#000000" 14 | axes: 15 | style: "2px solid #000000" 16 | gridlines: 17 | style: "2px dashed #00000033" 18 | colors: 19 | red: '#eb3b5a' 20 | orange: '#fa8231' 21 | yellow: '#f7b731' 22 | green: '#20bf6b' 23 | blue: '#4b7bec' 24 | purple: '#a55eea' 25 | ... -------------------------------------------------------------------------------- /lib/purechart/styles/futuristic_dark.yml: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3 | font: Futura 4 | weight: 700 5 | color: "#FFFFFF" 6 | labels: 7 | font: Futura 8 | weight: 700 9 | color: "#FFFFFF" 10 | ticks: 11 | font: Futura 12 | weight: 400 13 | color: "#FFFFFF" 14 | axes: 15 | style: "2px solid #FFFFFF" 16 | gridlines: 17 | style: "2px dashed #FFFFFF33" 18 | colors: 19 | green: '#90EE90' 20 | red: '#FF6347' 21 | orange: '#FFA500' 22 | blue: '#1E90FF' 23 | yellow: '#FFFF00' 24 | purple: '#BA55D3' 25 | brown: '#A52A2A' 26 | grey: '#808080' 27 | ... 28 | 29 | -------------------------------------------------------------------------------- /lib/purechart/styles/futuristic_light.yml: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3 | font: Futura 4 | weight: 700 5 | color: "#000000" 6 | labels: 7 | font: Futura 8 | weight: 700 9 | color: "#000000" 10 | ticks: 11 | font: Futura 12 | weight: 400 13 | color: "#000000" 14 | axes: 15 | style: "2px solid #000000" 16 | gridlines: 17 | style: "2px dashed #00000033" 18 | colors: 19 | green: '#478279' 20 | red: '#e2458c' 21 | orange: '#f7931b' 22 | blue: '#00c2ff' 23 | yellow: '#ffc107' 24 | purple: '#943ca3' 25 | brown: '#8a5b3f' 26 | grey: '#808080' 27 | ... -------------------------------------------------------------------------------- /lib/purechart/styles/professional_dark.yml: -------------------------------------------------------------------------------- 1 | title: 2 | font: 'Times New Roman' 3 | weight: 800 4 | color: '#CCCCCC' 5 | 6 | labels: 7 | font: 'Times New Roman' 8 | weight: 800 9 | color: '#AAAAAA' 10 | 11 | ticks: 12 | font: 'Times New Roman' 13 | weight: 900 14 | color: '#777777' 15 | 16 | axes: 17 | style: '2px solid #FFFFFF' 18 | 19 | gridlines: 20 | style: '2px dashed #FFFFFF33' 21 | 22 | colors: 23 | muted-blue: '#445577' 24 | muted-green: '#4C5E4F' 25 | muted-pink: '#8D6F6A' 26 | muted-purple: '#725A72' 27 | muted-teal: '#465C5C' 28 | muted-peach: '#DAAF93' 29 | muted-gray: '#909292' 30 | muted-olive: '#7D7F6A' 31 | muted-brown: '#7A6E63' 32 | muted-slate: '#57646C' 33 | muted-lavender: '#8F7FA2' 34 | muted-coral: '#E06B58' 35 | muted-mint: '#539E87' 36 | muted-lilac: '#A687AD' 37 | muted-sand: '#D9C997' 38 | -------------------------------------------------------------------------------- /lib/purechart/styles/professional_light.yml: -------------------------------------------------------------------------------- 1 | title: 2 | font: 'Times New Roman' 3 | weight: 800 4 | color: '#333333' 5 | 6 | labels: 7 | font: 'Times New Roman' 8 | weight: 800 9 | color: '#666666' 10 | 11 | ticks: 12 | font: 'Times New Roman' 13 | weight: 900 14 | color: '#999999' 15 | 16 | axes: 17 | style: '2px solid #333333' 18 | 19 | gridlines: 20 | style: '2px dashed #33333333' 21 | 22 | colors: 23 | muted-blue: '#A7C6E4' 24 | muted-green: '#B5CEBF' 25 | muted-pink: '#F2C9C4' 26 | muted-purple: '#D5C1D2' 27 | muted-teal: '#A4BEC0' 28 | muted-peach: '#F8DCC8' 29 | muted-gray: '#D6D7D7' 30 | muted-olive: '#C2C4AB' 31 | muted-brown: '#C0B8AF' 32 | muted-slate: '#A6B0B9' 33 | muted-lavender: '#D5C9DA' 34 | muted-coral: '#FAC5BA' 35 | muted-mint: '#BEEAD2' 36 | muted-lilac: '#E4D4EB' 37 | muted-sand: '#F6EDD5' 38 | -------------------------------------------------------------------------------- /purechart.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = "purechart" 3 | s.version = "0.0.5" 4 | s.summary = "PureChart" 5 | s.description = "Pure HTML/CSS charts for Ruby on Rails." 6 | s.authors = ["George Berdovskiy"] 7 | s.files = Dir['lib/**/*'] + Dir['app/**/*'] 8 | s.license = "MIT" 9 | end 10 | --------------------------------------------------------------------------------