rack-charts-api 0.1.0
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.
- checksums.yaml +7 -0
- data/lib/rack/charts_api/app.rb +109 -0
- data/lib/rack/charts_api/data_parser.rb +110 -0
- data/lib/rack/charts_api/html_renderer.rb +75 -0
- data/lib/rack/charts_api/png_renderer.rb +61 -0
- data/lib/rack/charts_api/version.rb +5 -0
- data/lib/rack/charts_api/version.rb.erb +5 -0
- data/lib/rack/charts_api.rb +35 -0
- metadata +117 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9d9e73cfb5510f3759cb0427a5e0520731add2d39b679ba3cd982b9b483dfac9
|
|
4
|
+
data.tar.gz: 4cb7889529666731b1943f765c842ecac86eeb66eef5d802ffd3d7e9ea0fd688
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bd5cfb122e6efe0a49d3419e923906956a48c541aeb935933b2b373ada2a248ba6e71bc6c268fabeb99fa1723fa3b5b3785e8cab7c593027dce775df10c6e200
|
|
7
|
+
data.tar.gz: 59a45e4696df7261e46e04196837489537b8d62e1655dd7755e46ad0bb6b7fe91bd1f4cf173901731be41438c73ab8243a233a46986bf4c3b3dfd016448438be
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
module Rack
|
|
2
|
+
class ChartsApi
|
|
3
|
+
# The core Rack application. Can be used standalone or behind the
|
|
4
|
+
# ChartsApi middleware.
|
|
5
|
+
#
|
|
6
|
+
# Response format is determined by the URL extension:
|
|
7
|
+
#
|
|
8
|
+
# GET /charts.png?data=...&w=800&h=400 -> PNG image
|
|
9
|
+
# GET /charts.html?data=... -> HTML page with Chart.js chart
|
|
10
|
+
# GET /charts?data=... -> HTML (default)
|
|
11
|
+
# POST /charts.png (JSON body) -> PNG image
|
|
12
|
+
# POST /charts.html (JSON body) -> HTML page
|
|
13
|
+
#
|
|
14
|
+
class App
|
|
15
|
+
def call(env)
|
|
16
|
+
data, opts = DataParser.parse(env)
|
|
17
|
+
|
|
18
|
+
unless data
|
|
19
|
+
return [
|
|
20
|
+
400,
|
|
21
|
+
{ 'content-type' => 'application/json' },
|
|
22
|
+
['{"error":"Missing or invalid chart data. Pass a `data` param (JSON) or POST a JSON body with a `data` key."}']
|
|
23
|
+
]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
format = detect_format(env)
|
|
27
|
+
|
|
28
|
+
case format
|
|
29
|
+
when :png
|
|
30
|
+
render_png(data, opts)
|
|
31
|
+
else
|
|
32
|
+
render_html(env, data, opts)
|
|
33
|
+
end
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
[
|
|
36
|
+
500,
|
|
37
|
+
{ 'content-type' => 'application/json' },
|
|
38
|
+
[{ error: e.message }.to_json]
|
|
39
|
+
]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def detect_format(env)
|
|
45
|
+
path = env['PATH_INFO'].to_s
|
|
46
|
+
|
|
47
|
+
if path.end_with?('.png')
|
|
48
|
+
:png
|
|
49
|
+
elsif path.end_with?('.html')
|
|
50
|
+
:html
|
|
51
|
+
else
|
|
52
|
+
# Check Accept header
|
|
53
|
+
accept = env['HTTP_ACCEPT'].to_s
|
|
54
|
+
if accept.include?('image/png')
|
|
55
|
+
:png
|
|
56
|
+
else
|
|
57
|
+
:html
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_png(data, opts)
|
|
63
|
+
blob = PngRenderer.render(data, opts)
|
|
64
|
+
[
|
|
65
|
+
200,
|
|
66
|
+
{
|
|
67
|
+
'content-type' => 'image/png',
|
|
68
|
+
'content-length' => blob.bytesize.to_s,
|
|
69
|
+
'content-disposition' => 'inline; filename="chart.png"',
|
|
70
|
+
'cache-control' => 'no-cache'
|
|
71
|
+
},
|
|
72
|
+
[blob]
|
|
73
|
+
]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render_html(env, data, opts)
|
|
77
|
+
# For the HTML renderer, we want the original chartkick-compatible
|
|
78
|
+
# JSON, not the normalised hash. Re-parse to get the raw data.
|
|
79
|
+
raw_data = extract_raw_data(env)
|
|
80
|
+
html = HtmlRenderer.render(raw_data || data, opts)
|
|
81
|
+
[
|
|
82
|
+
200,
|
|
83
|
+
{
|
|
84
|
+
'content-type' => 'text/html; charset=utf-8',
|
|
85
|
+
'content-length' => html.bytesize.to_s,
|
|
86
|
+
'cache-control' => 'no-cache'
|
|
87
|
+
},
|
|
88
|
+
[html]
|
|
89
|
+
]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def extract_raw_data(env)
|
|
93
|
+
request = Rack::Request.new(env)
|
|
94
|
+
|
|
95
|
+
if request.post? && DataParser.json_content?(env)
|
|
96
|
+
body = env['rack.input'].read
|
|
97
|
+
env['rack.input'].rewind
|
|
98
|
+
payload = JSON.parse(body)
|
|
99
|
+
payload['data']
|
|
100
|
+
else
|
|
101
|
+
raw = request.params['data']
|
|
102
|
+
raw ? JSON.parse(raw) : nil
|
|
103
|
+
end
|
|
104
|
+
rescue JSON::ParserError
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
module Rack
|
|
2
|
+
class ChartsApi
|
|
3
|
+
# Extracts chart data and options from either the query string or a JSON
|
|
4
|
+
# request body. Supports chartkick-compatible data formats:
|
|
5
|
+
#
|
|
6
|
+
# Single series hash: {"Jan": 10, "Feb": 20}
|
|
7
|
+
# Array of pairs: [["Jan", 10], ["Feb", 20]]
|
|
8
|
+
# Multi-series array: [{"name": "A", "data": {...}}, ...]
|
|
9
|
+
#
|
|
10
|
+
# Query-string mode (GET with short payloads):
|
|
11
|
+
# ?data=URL-ENCODED-JSON&type=line&w=800&h=400&title=Hello
|
|
12
|
+
#
|
|
13
|
+
# JSON body mode (POST with large payloads):
|
|
14
|
+
# POST body: {"data": ..., "type": "line", "options": {"w": 800}}
|
|
15
|
+
#
|
|
16
|
+
module DataParser
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
# Returns [chart_data, options_hash]
|
|
20
|
+
def parse(env)
|
|
21
|
+
request = Rack::Request.new(env)
|
|
22
|
+
params = request.params # merged GET + POST form params
|
|
23
|
+
|
|
24
|
+
if request.post? && json_content?(env)
|
|
25
|
+
parse_json_body(env, params)
|
|
26
|
+
else
|
|
27
|
+
parse_query(params)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def json_content?(env)
|
|
32
|
+
ct = env['CONTENT_TYPE'].to_s
|
|
33
|
+
ct.include?('application/json') || ct.include?('text/json')
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def parse_json_body(env, params)
|
|
37
|
+
body = env['rack.input'].read
|
|
38
|
+
env['rack.input'].rewind
|
|
39
|
+
payload = JSON.parse(body)
|
|
40
|
+
|
|
41
|
+
chart_data = payload['data']
|
|
42
|
+
opts = extract_options(payload.merge(params))
|
|
43
|
+
[normalize_data(chart_data), opts]
|
|
44
|
+
rescue JSON::ParserError
|
|
45
|
+
[nil, {}]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse_query(params)
|
|
49
|
+
raw = params['data']
|
|
50
|
+
return [nil, {}] unless raw
|
|
51
|
+
|
|
52
|
+
chart_data = begin
|
|
53
|
+
JSON.parse(raw)
|
|
54
|
+
rescue JSON::ParserError
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
[normalize_data(chart_data), extract_options(params)]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Normalise chartkick-format data into a consistent internal format:
|
|
62
|
+
# { "series_name" => [values...], ... }
|
|
63
|
+
#
|
|
64
|
+
# This is used by PngRenderer (Gruff). HtmlRenderer passes the raw
|
|
65
|
+
# JSON straight through to chartkick.js which handles all formats
|
|
66
|
+
# natively.
|
|
67
|
+
def normalize_data(data)
|
|
68
|
+
return nil if data.nil?
|
|
69
|
+
|
|
70
|
+
case data
|
|
71
|
+
when Hash
|
|
72
|
+
# { "Jan" => 10, "Feb" => 20 } -- single series hash
|
|
73
|
+
data
|
|
74
|
+
when Array
|
|
75
|
+
if data.first.is_a?(Hash) && data.first.key?('name')
|
|
76
|
+
# Multi-series: [{"name": "A", "data": [...]}, ...]
|
|
77
|
+
data.each_with_object({}) do |series, h|
|
|
78
|
+
name = series['name'] || 'Series'
|
|
79
|
+
values = series['data']
|
|
80
|
+
h[name] = case values
|
|
81
|
+
when Hash then values.values
|
|
82
|
+
when Array
|
|
83
|
+
values.map { |v| v.is_a?(Array) ? v.last : v }
|
|
84
|
+
else
|
|
85
|
+
Array(values)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
elsif data.first.is_a?(Array)
|
|
89
|
+
# Array of pairs: [["Jan", 10], ["Feb", 20]]
|
|
90
|
+
{ 'Series' => data.map(&:last) }
|
|
91
|
+
else
|
|
92
|
+
# Flat array of numbers
|
|
93
|
+
{ 'Series' => data }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def extract_options(params)
|
|
99
|
+
{
|
|
100
|
+
type: params['type'] || 'line',
|
|
101
|
+
w: (params['w'] || params.dig('options', 'w') || 800).to_i,
|
|
102
|
+
h: (params['h'] || params.dig('options', 'h') || 600).to_i,
|
|
103
|
+
title: params['title'] || params.dig('options', 'title'),
|
|
104
|
+
colors: params['colors'] || params.dig('options', 'colors'),
|
|
105
|
+
labels: params['labels'] || params.dig('options', 'labels')
|
|
106
|
+
}.compact
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'securerandom'
|
|
3
|
+
|
|
4
|
+
module Rack
|
|
5
|
+
class ChartsApi
|
|
6
|
+
module HtmlRenderer
|
|
7
|
+
CHARTKICK_CDN = 'https://unpkg.com/chartkick@5.0.1/dist/chartkick.js'
|
|
8
|
+
CHARTJS_CDN = 'https://unpkg.com/chart.js@4/dist/chart.umd.js'
|
|
9
|
+
DATE_ADAPTER = 'https://unpkg.com/chartjs-adapter-date-fns@3/dist/chartjs-adapter-date-fns.bundle.js'
|
|
10
|
+
|
|
11
|
+
# Map our type names to chartkick JS class names
|
|
12
|
+
JS_CHART_TYPES = {
|
|
13
|
+
'line' => 'LineChart',
|
|
14
|
+
'bar' => 'ColumnChart',
|
|
15
|
+
'column' => 'ColumnChart',
|
|
16
|
+
'pie' => 'PieChart',
|
|
17
|
+
'area' => 'AreaChart',
|
|
18
|
+
'scatter' => 'ScatterChart',
|
|
19
|
+
'side_bar' => 'BarChart',
|
|
20
|
+
'stacked_bar' => 'ColumnChart',
|
|
21
|
+
'dot' => 'LineChart',
|
|
22
|
+
'spider' => 'LineChart'
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
# Renders a standalone HTML page containing a Chart.js chart via
|
|
28
|
+
# chartkick.js. Designed to be loaded in an <iframe>.
|
|
29
|
+
#
|
|
30
|
+
# @param raw_data the original parsed JSON data (any chartkick format)
|
|
31
|
+
# @param opts [Hash] :type, :w, :h, :title, :colors
|
|
32
|
+
# @return [String] HTML document
|
|
33
|
+
def render(raw_data, opts = {})
|
|
34
|
+
chart_id = "chart-#{SecureRandom.hex(4)}"
|
|
35
|
+
js_type = JS_CHART_TYPES.fetch(opts[:type].to_s, 'LineChart')
|
|
36
|
+
width = opts[:w] || 800
|
|
37
|
+
height = opts[:h] || 600
|
|
38
|
+
|
|
39
|
+
js_options = {}
|
|
40
|
+
js_options['title'] = opts[:title] if opts[:title]
|
|
41
|
+
js_options['colors'] = opts[:colors] if opts[:colors]
|
|
42
|
+
js_options['stacked'] = true if opts[:type].to_s == 'stacked_bar'
|
|
43
|
+
|
|
44
|
+
# For the JS side, pass the raw data exactly as chartkick expects.
|
|
45
|
+
# chartkick.js handles Hash, Array-of-pairs, and multi-series natively.
|
|
46
|
+
chart_data_json = raw_data.to_json
|
|
47
|
+
chart_options_json = js_options.to_json
|
|
48
|
+
|
|
49
|
+
<<~HTML
|
|
50
|
+
<!DOCTYPE html>
|
|
51
|
+
<html>
|
|
52
|
+
<head>
|
|
53
|
+
<meta charset="utf-8">
|
|
54
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
55
|
+
<style>
|
|
56
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
57
|
+
body { font-family: system-ui, -apple-system, sans-serif; }
|
|
58
|
+
##{chart_id} { width: #{width}px; height: #{height}px; max-width: 100%; }
|
|
59
|
+
</style>
|
|
60
|
+
<script src="#{CHARTJS_CDN}"></script>
|
|
61
|
+
<script src="#{DATE_ADAPTER}"></script>
|
|
62
|
+
<script src="#{CHARTKICK_CDN}"></script>
|
|
63
|
+
</head>
|
|
64
|
+
<body>
|
|
65
|
+
<div id="#{chart_id}"></div>
|
|
66
|
+
<script>
|
|
67
|
+
new Chartkick.#{js_type}("#{chart_id}", #{chart_data_json}, #{chart_options_json});
|
|
68
|
+
</script>
|
|
69
|
+
</body>
|
|
70
|
+
</html>
|
|
71
|
+
HTML
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require 'gruff'
|
|
2
|
+
|
|
3
|
+
module Rack
|
|
4
|
+
class ChartsApi
|
|
5
|
+
module PngRenderer
|
|
6
|
+
CHART_TYPES = {
|
|
7
|
+
'line' => Gruff::Line,
|
|
8
|
+
'bar' => Gruff::Bar,
|
|
9
|
+
'pie' => Gruff::Pie,
|
|
10
|
+
'area' => Gruff::Area,
|
|
11
|
+
'side_bar' => Gruff::SideBar,
|
|
12
|
+
'stacked_bar' => Gruff::StackedBar,
|
|
13
|
+
'dot' => Gruff::Dot,
|
|
14
|
+
'spider' => Gruff::Spider
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
# Map chartkick type names to gruff equivalents
|
|
18
|
+
TYPE_ALIASES = {
|
|
19
|
+
'column' => 'bar',
|
|
20
|
+
'scatter' => 'line'
|
|
21
|
+
}.freeze
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
# @param data [Hash{String => Array}] normalised series data
|
|
26
|
+
# @param opts [Hash] :w, :h, :type, :title, :labels, :colors
|
|
27
|
+
# @return [String] binary PNG blob
|
|
28
|
+
def render(data, opts = {})
|
|
29
|
+
width = opts[:w] || 800
|
|
30
|
+
height = opts[:h] || 600
|
|
31
|
+
type = resolve_type(opts[:type] || 'line')
|
|
32
|
+
|
|
33
|
+
chart = type.new("#{width}x#{height}")
|
|
34
|
+
chart.title = opts[:title] if opts[:title]
|
|
35
|
+
|
|
36
|
+
if opts[:labels]
|
|
37
|
+
chart.labels = case opts[:labels]
|
|
38
|
+
when Hash then opts[:labels].transform_keys(&:to_i)
|
|
39
|
+
when Array
|
|
40
|
+
opts[:labels].each_with_index.to_h { |l, i| [i, l.to_s] }
|
|
41
|
+
else
|
|
42
|
+
{}
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
colors = opts[:colors]
|
|
47
|
+
data.each_with_index do |(name, points), i|
|
|
48
|
+
color = colors[i] if colors.is_a?(Array)
|
|
49
|
+
chart.data(name.to_s, Array(points), color)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
chart.to_image.to_blob
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def resolve_type(name)
|
|
56
|
+
key = TYPE_ALIASES.fetch(name.to_s.downcase, name.to_s.downcase)
|
|
57
|
+
CHART_TYPES.fetch(key, Gruff::Line)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'rack'
|
|
2
|
+
require 'json'
|
|
3
|
+
|
|
4
|
+
require_relative 'charts_api/version'
|
|
5
|
+
require_relative 'charts_api/data_parser'
|
|
6
|
+
require_relative 'charts_api/png_renderer'
|
|
7
|
+
require_relative 'charts_api/html_renderer'
|
|
8
|
+
require_relative 'charts_api/app'
|
|
9
|
+
|
|
10
|
+
module Rack
|
|
11
|
+
# Rack middleware that intercepts requests to a chart endpoint and returns
|
|
12
|
+
# either a PNG image or an HTML page with an interactive Chart.js chart.
|
|
13
|
+
#
|
|
14
|
+
# # config.ru or Rails application.rb
|
|
15
|
+
# use Rack::ChartsApi
|
|
16
|
+
# use Rack::ChartsApi, path: "/charts" # custom mount path
|
|
17
|
+
#
|
|
18
|
+
class ChartsApi
|
|
19
|
+
DEFAULT_PATH = '/charts'
|
|
20
|
+
|
|
21
|
+
def initialize(app, path: DEFAULT_PATH)
|
|
22
|
+
@app = app
|
|
23
|
+
@path = path.chomp('/')
|
|
24
|
+
@charts_app = App.new
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def call(env)
|
|
28
|
+
if env['PATH_INFO']&.start_with?(@path)
|
|
29
|
+
@charts_app.call(env)
|
|
30
|
+
else
|
|
31
|
+
@app.call(env)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rack-charts-api
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Nathan
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-01 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: gruff
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.23'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.23'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rack
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: rack-test
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '2.0'
|
|
61
|
+
type: :development
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '2.0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: rake
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :development
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
description: A mountable Rack app that accepts chartkick-compatible data via URL params
|
|
83
|
+
or JSON request body and returns either a server-rendered PNG (via Gruff) or an
|
|
84
|
+
HTML page with a Chart.js chart suitable for iframe embedding.
|
|
85
|
+
executables: []
|
|
86
|
+
extensions: []
|
|
87
|
+
extra_rdoc_files: []
|
|
88
|
+
files:
|
|
89
|
+
- lib/rack/charts_api.rb
|
|
90
|
+
- lib/rack/charts_api/app.rb
|
|
91
|
+
- lib/rack/charts_api/data_parser.rb
|
|
92
|
+
- lib/rack/charts_api/html_renderer.rb
|
|
93
|
+
- lib/rack/charts_api/png_renderer.rb
|
|
94
|
+
- lib/rack/charts_api/version.rb
|
|
95
|
+
- lib/rack/charts_api/version.rb.erb
|
|
96
|
+
licenses:
|
|
97
|
+
- MIT
|
|
98
|
+
metadata: {}
|
|
99
|
+
rdoc_options: []
|
|
100
|
+
require_paths:
|
|
101
|
+
- lib
|
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
103
|
+
requirements:
|
|
104
|
+
- - ">="
|
|
105
|
+
- !ruby/object:Gem::Version
|
|
106
|
+
version: '3.0'
|
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - ">="
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '0'
|
|
112
|
+
requirements: []
|
|
113
|
+
rubygems_version: 3.7.2
|
|
114
|
+
specification_version: 4
|
|
115
|
+
summary: Rack middleware that serves PNG and HTML charts from query params or JSON
|
|
116
|
+
body
|
|
117
|
+
test_files: []
|