grapht 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rspec +3 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +267 -0
- data/Rakefile +1 -0
- data/bin/grapht +17 -0
- data/example-data/bar_data.json +5 -0
- data/example-data/donut_data.json +5 -0
- data/example-data/grapht_performance_line_data.json +7 -0
- data/example-data/line_data.json +16 -0
- data/grapht.gemspec +24 -0
- data/lib/graph-definitions/bar-horizontal.js +90 -0
- data/lib/graph-definitions/bar-vertical.js +89 -0
- data/lib/graph-definitions/donut.js +47 -0
- data/lib/graph-definitions/line.js +111 -0
- data/lib/grapht.rb +6 -0
- data/lib/grapht/shell.rb +18 -0
- data/lib/grapht/type.rb +5 -0
- data/lib/grapht/version.rb +3 -0
- data/script/grapht.coffee +107 -0
- data/spec/lib/grapht/shell_spec.rb +79 -0
- data/spec/spec_helper.rb +84 -0
- data/spec/support/rspec-prof.rb +8 -0
- data/vendor/d3.min.js +5 -0
- data/vendor/json2.js +489 -0
- metadata +121 -0
@@ -0,0 +1,89 @@
|
|
1
|
+
function(data) {
|
2
|
+
var margin = { top: 50, right: 20, bottom: 30, left: 40 },
|
3
|
+
width = 400,
|
4
|
+
height = 400,
|
5
|
+
svgWidth = width + margin.right + margin.left,
|
6
|
+
svgHeight = height + margin.top + margin.bottom;
|
7
|
+
|
8
|
+
var svg = d3.select("body").append("svg")
|
9
|
+
.style("fill", "none")
|
10
|
+
.attr("width", svgWidth)
|
11
|
+
.attr("height", svgHeight)
|
12
|
+
.attr('viewBox', "0 -10 " +svgWidth+ " " +svgHeight+ "")
|
13
|
+
.append('g')
|
14
|
+
.attr('transform', 'translate(' +margin.left+ ',' +margin.top+ ')')
|
15
|
+
.style("fill", "none");
|
16
|
+
|
17
|
+
// Axis Scales
|
18
|
+
var x = d3.scale
|
19
|
+
.ordinal()
|
20
|
+
.domain(data.map(function(d) { return d.name; }))
|
21
|
+
.rangeRoundBands([0, width], 0.2);
|
22
|
+
|
23
|
+
var y = d3.scale
|
24
|
+
.linear()
|
25
|
+
.domain([0, d3.max(data, function(d) { return d.value; })])
|
26
|
+
.range([height, 0])
|
27
|
+
.nice();
|
28
|
+
|
29
|
+
// Axes
|
30
|
+
var xAxis = d3.svg
|
31
|
+
.axis()
|
32
|
+
.scale(x)
|
33
|
+
.orient('bottom')
|
34
|
+
.ticks(data.length);
|
35
|
+
|
36
|
+
var yAxis = d3.svg
|
37
|
+
.axis()
|
38
|
+
.scale(y)
|
39
|
+
.orient('left')
|
40
|
+
.tickSize(width);
|
41
|
+
|
42
|
+
svg.selectAll('.bar')
|
43
|
+
.data(data)
|
44
|
+
.enter().append('rect')
|
45
|
+
.attr('class', 'bar')
|
46
|
+
.style('fill', '#428bca')
|
47
|
+
.attr('opacity', '0.3')
|
48
|
+
.attr('y', function(d) { return y(d.value); })
|
49
|
+
.attr('x', function(d) { return x(d.name); })
|
50
|
+
.attr('height', function(d) { return height - y(d.value); })
|
51
|
+
.attr('width', x.rangeBand());
|
52
|
+
|
53
|
+
svg.selectAll('.text')
|
54
|
+
.attr('font-family', 'Arial')
|
55
|
+
.selectAll('label')
|
56
|
+
.attr('fill-opacity', '0.3')
|
57
|
+
|
58
|
+
var xAxisElement = svg.append('g')
|
59
|
+
.attr('class', 'x axis')
|
60
|
+
.attr('transform', "translate(0," +height+ ")")
|
61
|
+
.call(xAxis);
|
62
|
+
|
63
|
+
var yAxisElement = svg.append('g')
|
64
|
+
.attr('class', 'y axis')
|
65
|
+
.attr("transform", "translate(" +width+ ",0)")
|
66
|
+
.call(yAxis);
|
67
|
+
|
68
|
+
xAxisElement.selectAll('line')
|
69
|
+
.style('fill', 'none')
|
70
|
+
.attr('stroke', 'black')
|
71
|
+
.attr('stroke-width', '1px')
|
72
|
+
.attr('stroke-opacity', 0.1)
|
73
|
+
.attr('shape-rendering', 'geometricPrecision');
|
74
|
+
|
75
|
+
xAxisElement.selectAll('text')
|
76
|
+
.style('fill', '#333333')
|
77
|
+
.attr('font-family', 'Arial');
|
78
|
+
|
79
|
+
yAxisElement.selectAll('line')
|
80
|
+
.style('fill', 'none')
|
81
|
+
.attr('stroke', 'black')
|
82
|
+
.attr('stroke-width', '1px')
|
83
|
+
.attr('stroke-opacity', 0.1)
|
84
|
+
.attr('shape-rendering', 'geometricPrecision');
|
85
|
+
|
86
|
+
yAxisElement.selectAll('text')
|
87
|
+
.style('fill', '#333333')
|
88
|
+
.attr('font-family', 'Arial');
|
89
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
function(data) {
|
2
|
+
var margin = { top: 20, right: 20, bottom: 10, left: 10 },
|
3
|
+
width = 400,
|
4
|
+
height = 400,
|
5
|
+
svgWidth = width + margin.right + margin.left,
|
6
|
+
svgHeight = height + margin.top + margin.bottom,
|
7
|
+
radius = Math.min(width, height) / 2;
|
8
|
+
|
9
|
+
var svg = d3.select("body").append("svg")
|
10
|
+
.style("fill", "none")
|
11
|
+
.attr("width", svgWidth)
|
12
|
+
.attr("height", svgHeight)
|
13
|
+
.attr('viewBox', "0 -10 " +svgWidth+ " " +svgHeight+ "")
|
14
|
+
.append('g')
|
15
|
+
.attr('transform', 'translate(' +(width/2)+ ',' +(height/2)+ ')')
|
16
|
+
.style("fill", "none");
|
17
|
+
|
18
|
+
var color = d3.scale
|
19
|
+
.ordinal()
|
20
|
+
.range(["#428bca", "#5cb85c", "#5bc0de", "#f0ad4e", "#d9534f"]);
|
21
|
+
|
22
|
+
var arc = d3.svg.arc()
|
23
|
+
.outerRadius(radius - 10)
|
24
|
+
.innerRadius(radius - 70);
|
25
|
+
|
26
|
+
var pie = d3.layout.pie()
|
27
|
+
.sort(null)
|
28
|
+
.value(function(d) { return d.value; });
|
29
|
+
|
30
|
+
var arcElement = svg.selectAll(".arc")
|
31
|
+
.data(pie(data))
|
32
|
+
.enter().append("g")
|
33
|
+
.attr("class", "arc");
|
34
|
+
|
35
|
+
arcElement.append("path")
|
36
|
+
.attr("d", arc)
|
37
|
+
.style("fill", function(d) { return color(d.data.name); });
|
38
|
+
|
39
|
+
arcElement.append("text")
|
40
|
+
.attr("transform", function(d) { return "translate(" + arc.centroid(d) + ")"; })
|
41
|
+
.attr("dy", ".35em")
|
42
|
+
.style('fill', '#FFFFFF')
|
43
|
+
.attr('font-family', 'Arial')
|
44
|
+
.attr('font-weight', 'bold')
|
45
|
+
.style("text-anchor", "middle")
|
46
|
+
.text(function(d) { return d.data.name; });
|
47
|
+
}
|
@@ -0,0 +1,111 @@
|
|
1
|
+
function(data) {
|
2
|
+
var fns = {
|
3
|
+
extentOf: function(data, axis) {
|
4
|
+
var values = d3.values(data),
|
5
|
+
flatVals = Array.prototype.concat.apply([], values),
|
6
|
+
axisVals = flatVals.map(function(o) { return o[axis]; });
|
7
|
+
|
8
|
+
return d3.extent(axisVals);
|
9
|
+
}
|
10
|
+
};
|
11
|
+
|
12
|
+
var margin = { top: 40, right: 80, bottom: 40, left: 40 },
|
13
|
+
width = 400,
|
14
|
+
height = 400,
|
15
|
+
svgWidth = width + margin.right + margin.left,
|
16
|
+
svgHeight = height + margin.top + margin.bottom;
|
17
|
+
|
18
|
+
var svg = d3.select("body").append("svg")
|
19
|
+
.style("fill", "none")
|
20
|
+
.attr("width", svgWidth)
|
21
|
+
.attr("height", svgHeight)
|
22
|
+
.attr('viewBox', "0 -10 " +svgWidth+ " " +svgHeight+ "")
|
23
|
+
.append('g')
|
24
|
+
.attr('transform', 'translate(' +margin.left+ ',' +margin.top+ ')')
|
25
|
+
.style("fill", "none");
|
26
|
+
|
27
|
+
|
28
|
+
var x = d3.scale
|
29
|
+
.linear()
|
30
|
+
.range([0, width])
|
31
|
+
.domain(fns.extentOf(data, 'x')),
|
32
|
+
y = d3.scale
|
33
|
+
.linear()
|
34
|
+
.range([height, 0])
|
35
|
+
.domain(fns.extentOf(data, 'y')),
|
36
|
+
color = d3.scale
|
37
|
+
.ordinal()
|
38
|
+
.range(["#428bca", "#5cb85c", "#5bc0de", "#f0ad4e", "#d9534f"]);
|
39
|
+
|
40
|
+
var xAxis = d3.svg
|
41
|
+
.axis()
|
42
|
+
.scale(x)
|
43
|
+
.orient('bottom')
|
44
|
+
.tickSize(-height),
|
45
|
+
yAxis = d3.svg
|
46
|
+
.axis()
|
47
|
+
.scale(y)
|
48
|
+
.orient('left')
|
49
|
+
.tickSize(width),
|
50
|
+
line = d3.svg
|
51
|
+
.line()
|
52
|
+
.interpolate('basis')
|
53
|
+
.x(function(d) { return x(d.x); })
|
54
|
+
.y(function(d) { return y(d.y); });
|
55
|
+
|
56
|
+
var xAxisElement = svg.append('g')
|
57
|
+
.attr('class', 'x axis')
|
58
|
+
.attr('transform', "translate(0," +height+ ")")
|
59
|
+
.call(xAxis);
|
60
|
+
|
61
|
+
var yAxisElement = svg.append('g')
|
62
|
+
.attr('class', 'y axis')
|
63
|
+
.attr("transform", "translate(" +width+ ",0)")
|
64
|
+
.call(yAxis);
|
65
|
+
|
66
|
+
var lineElements = svg.selectAll('.line')
|
67
|
+
.data(d3.entries(data))
|
68
|
+
.enter().append('g')
|
69
|
+
.attr('class', 'line');
|
70
|
+
|
71
|
+
lineElements.append('path')
|
72
|
+
.attr('class', 'line')
|
73
|
+
.attr('stroke-width', '2px')
|
74
|
+
.attr('opacity', '0.3')
|
75
|
+
.attr('d', function(d) { return line(d.value) })
|
76
|
+
.attr('stroke', function(d) { return color(d.key); });
|
77
|
+
|
78
|
+
lineElements.append('text')
|
79
|
+
.datum(function(d) { return { name: d.key, value: d.value[d.value.length - 1] }; })
|
80
|
+
.attr('transform', function(d) { return "translate(" +x(d.value.x)+ "," +y(d.value.y)+ ")"; })
|
81
|
+
.attr('x', 3)
|
82
|
+
.attr('dy', '.35em')
|
83
|
+
.style('fill', '#333333')
|
84
|
+
.attr('font-family', 'Arial')
|
85
|
+
.text(function(d) { return d.name; });
|
86
|
+
|
87
|
+
// axis styling
|
88
|
+
xAxisElement.selectAll('line')
|
89
|
+
.style('fill', 'none')
|
90
|
+
.attr('stroke', 'black')
|
91
|
+
.attr('stroke-width', '1px')
|
92
|
+
.attr('stroke-opacity', 0.1)
|
93
|
+
.attr('shape-rendering', 'geometricPrecision');
|
94
|
+
|
95
|
+
xAxisElement.selectAll('text')
|
96
|
+
.style('fill', '#333333')
|
97
|
+
.attr('font-family', 'Arial')
|
98
|
+
.attr('dy', '20px');
|
99
|
+
|
100
|
+
yAxisElement.selectAll('line')
|
101
|
+
.style('fill', 'none')
|
102
|
+
.attr('stroke', 'black')
|
103
|
+
.attr('stroke-width', '1px')
|
104
|
+
.attr('stroke-opacity', 0.1)
|
105
|
+
.attr('shape-rendering', 'geometricPrecision');
|
106
|
+
|
107
|
+
yAxisElement.selectAll('text')
|
108
|
+
.style('fill', '#333333')
|
109
|
+
.attr('font-family', 'Arial')
|
110
|
+
.attr('dx', '-10px');
|
111
|
+
}
|
data/lib/grapht.rb
ADDED
data/lib/grapht/shell.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
module Grapht
|
4
|
+
module Shell
|
5
|
+
class Error < StandardError; end
|
6
|
+
|
7
|
+
CMD = File.join(Grapht::ROOT, 'bin/grapht')
|
8
|
+
ALLOWED_OPTIONS = %w(-f)
|
9
|
+
|
10
|
+
def self.exec(type, json_data, options={})
|
11
|
+
options = *options.select { |k,v| ALLOWED_OPTIONS.include? k }.flatten
|
12
|
+
|
13
|
+
out, err, status = Open3.capture3 CMD, type, *options, stdin_data: json_data
|
14
|
+
raise Grapht::Shell::Error, err unless status.success?
|
15
|
+
out
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/grapht/type.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
page = require('webpage').create()
|
2
|
+
system = require('system')
|
3
|
+
fs = require('fs')
|
4
|
+
thisFile = system.args[0]
|
5
|
+
graphType = system.args[1]
|
6
|
+
graphFormat = false
|
7
|
+
scriptPath = fs.absolute(thisFile)
|
8
|
+
.replace(/[\w\s\-\.]+?\.(js|coffee)$/i, '')
|
9
|
+
vendorPath = "#{scriptPath}../vendor/"
|
10
|
+
defsPath = "#{scriptPath}../lib/graph-definitions/"
|
11
|
+
userDefsPath = system.env['EXT_GRAPHT_DEFINITIONS_HOME']
|
12
|
+
dependencies = ['d3.min.js', 'json2.js']
|
13
|
+
niceDirPathRX = /\/$|$/
|
14
|
+
|
15
|
+
# -----------------------------------------------------------------------------
|
16
|
+
# Helper Functions
|
17
|
+
# -----------------------------------------------------------------------------
|
18
|
+
|
19
|
+
fns =
|
20
|
+
# Logs the supplied message, and optional trace to STDERR and exits the process
|
21
|
+
# with an exit code of 1.
|
22
|
+
logError: (message, trace) ->
|
23
|
+
fs.write '/dev/stderr', "<ERROR> #{message}\n"
|
24
|
+
|
25
|
+
if trace
|
26
|
+
traceString = trace.map (t) -> "\t#{t.file}: on line: #{t.line}"
|
27
|
+
fs.write '/dev/stderr', traceString.join('\n')
|
28
|
+
|
29
|
+
phantom.exit(1)
|
30
|
+
|
31
|
+
# Searchs for a valid graph definition in the supplied definition paths. If
|
32
|
+
# no definition is found, we log an error to STDERR and exit with an exitcode
|
33
|
+
# of 1.
|
34
|
+
findDef: (type, defPaths...) ->
|
35
|
+
for dir in defPaths when dir?
|
36
|
+
dir = dir.replace(niceDirPathRX, '/')
|
37
|
+
path = "#{dir}#{type}.js"
|
38
|
+
return path if fs.exists(path)
|
39
|
+
|
40
|
+
@logError "No graph definition could be found for '#{type}'"
|
41
|
+
|
42
|
+
loadDef: (def) -> fs.read(def)
|
43
|
+
|
44
|
+
# Wraps the supplied graph definition in a function that executes the definition
|
45
|
+
# and returns the resulting content of the document body. This function is intended
|
46
|
+
# to minimize boiler-plate in graph definitions, and reduce the likelihood of user
|
47
|
+
# error.
|
48
|
+
wrapDef: (def) ->
|
49
|
+
"function() {
|
50
|
+
(#{def}).apply(this, arguments);
|
51
|
+
return document.body.innerHTML;
|
52
|
+
}"
|
53
|
+
|
54
|
+
getOptions: ->
|
55
|
+
optionsIn = system.args[2..]
|
56
|
+
optionsOut = {}
|
57
|
+
slice = 2
|
58
|
+
|
59
|
+
for i in [0...optionsIn.length] by slice
|
60
|
+
[key, value] = optionsIn[i...i+slice]
|
61
|
+
optionsOut[key] = value
|
62
|
+
|
63
|
+
optionsOut
|
64
|
+
|
65
|
+
getFormat: ->
|
66
|
+
options = @getOptions()
|
67
|
+
options['-f'] || options['--format']
|
68
|
+
|
69
|
+
readDataIn: ->
|
70
|
+
try
|
71
|
+
if fs.size('/dev/stdin') == 0
|
72
|
+
fns.logError('No graph data was received!')
|
73
|
+
else
|
74
|
+
fs.read('/dev/stdin')
|
75
|
+
|
76
|
+
catch err
|
77
|
+
@logError err
|
78
|
+
|
79
|
+
|
80
|
+
# -----------------------------------------------------------------------------
|
81
|
+
# Core Graph Generation Logic
|
82
|
+
# -----------------------------------------------------------------------------
|
83
|
+
|
84
|
+
# Configure the page context.
|
85
|
+
page.libraryPath = vendorPath
|
86
|
+
page.onError = fns.logError
|
87
|
+
dependencies.forEach (dp) -> page.injectJs(dp) || Helper.logError "could not load #{dp}!"
|
88
|
+
|
89
|
+
# load and evaluate the graph definition within the context of the JSON, supplied
|
90
|
+
# via STDIN.
|
91
|
+
graphData = fns.readDataIn()
|
92
|
+
graphDef = fns.wrapDef fns.loadDef fns.findDef(graphType, userDefsPath, defsPath)
|
93
|
+
graphFormat = fns.getFormat()
|
94
|
+
parsedData = try
|
95
|
+
JSON.parse(graphData)
|
96
|
+
catch err
|
97
|
+
fns.logError(err)
|
98
|
+
|
99
|
+
page.content = content = page.evaluate(graphDef, parsedData)
|
100
|
+
|
101
|
+
# Write resulting content to STDOUT and exit.
|
102
|
+
if graphFormat
|
103
|
+
page.render '/dev/stdout', { format: graphFormat, quality: 100 }
|
104
|
+
else
|
105
|
+
fs.write '/dev/stdout', "#{content}\n"
|
106
|
+
|
107
|
+
phantom.exit()
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Grapht::Shell do
|
4
|
+
describe '.exec' do
|
5
|
+
let(:type) { Grapht::Type::BAR_HORIZONTAL }
|
6
|
+
let(:format) { "" }
|
7
|
+
let(:json_data) {
|
8
|
+
<<-json
|
9
|
+
[
|
10
|
+
{ "name": "foo", "value": 20 },
|
11
|
+
{ "name": "bar", "value": 40 },
|
12
|
+
{ "name": "baz", "value": 35 }
|
13
|
+
]
|
14
|
+
json
|
15
|
+
}
|
16
|
+
|
17
|
+
subject { Grapht::Shell.exec type, json_data, '-f' => format }
|
18
|
+
|
19
|
+
context 'when given a known type' do
|
20
|
+
it { should match(/^<svg/) }
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'when given an unknown type' do
|
24
|
+
let(:type) { 'some-invalid-graph-type' }
|
25
|
+
|
26
|
+
it "should raise a Grapht::Shell:Error" do
|
27
|
+
expect { subject }.to raise_error(Grapht::Shell::Error, /No graph definition could be found/)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context 'when well-formed data is provided' do
|
32
|
+
it { should match(/^<svg/) }
|
33
|
+
end
|
34
|
+
|
35
|
+
context 'when malformed data is provided' do
|
36
|
+
let(:json_data) { "{} <this is not JSON>" }
|
37
|
+
|
38
|
+
it "should raise a Grapht::Shell::Error" do
|
39
|
+
expect { subject }.to raise_error(Grapht::Shell::Error, /Unable to parse JSON string/)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'when no data is provided' do
|
44
|
+
let(:json_data) { "" }
|
45
|
+
|
46
|
+
it "should raise a Grapht::Shell::Error" do
|
47
|
+
expect { subject }.to raise_error(Grapht::Shell::Error, /No graph data was received/)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'when no format is provided' do
|
52
|
+
it { should match(/^<svg/) }
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'when an unknown format is provided' do
|
56
|
+
let(:format) { 'some-crazy-invalid-format' }
|
57
|
+
it { should be_empty }
|
58
|
+
end
|
59
|
+
|
60
|
+
context "when a binary format is provided" do
|
61
|
+
before { subject.force_encoding('binary') }
|
62
|
+
|
63
|
+
context "when a format of 'png' is provided" do
|
64
|
+
let(:format) { 'png' }
|
65
|
+
it { should start_with("\x89\x50\x4E\x47\x0D\x0A\x1A\x0A".force_encoding('binary')) }
|
66
|
+
end
|
67
|
+
|
68
|
+
context "when a format of 'jpg' is provided" do
|
69
|
+
let(:format) { 'jpg' }
|
70
|
+
it { should start_with("\xFF\xD8\xFF\xE0\x00\x10JFIF\x00".force_encoding('binary')) }
|
71
|
+
end
|
72
|
+
|
73
|
+
context "when a format of 'gif' is provided" do
|
74
|
+
let(:format) { 'gif' }
|
75
|
+
it { should start_with("\x47\x49\x46\x38\x37\x61".force_encoding('binary')) }
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|