flex-cartesian 1.3.1 → 2.0.1.beta
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 +4 -4
- data/CHANGELOG.md +10 -1
- data/LICENSE +6 -0
- data/README.md +133 -439
- data/lib/analyzer.rb +48 -0
- data/lib/analyzers/morris.rb +268 -0
- data/lib/flex-cartesian/flex-cartesian-analyzer.rb +16 -0
- data/lib/flex-cartesian/flex-cartesian-core.rb +624 -0
- data/lib/flex-cartesian/flex-cartesian-deprecations.rb +13 -0
- data/lib/flex-cartesian/flex-cartesian-io.rb +192 -0
- data/lib/flex-cartesian/flex-cartesian-utilities.rb +126 -0
- data/lib/flex-cartesian.rb +13 -336
- data/lib/version.rb +3 -0
- data/lib/visualization/html.rb +217 -0
- metadata +49 -26
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
module FlexCartesianVisualization
|
|
2
|
+
require "csv"
|
|
3
|
+
require "json"
|
|
4
|
+
require "tempfile"
|
|
5
|
+
|
|
6
|
+
def visualize(format: :html, x:, y:, func:, output: nil, text: :dark, show_legend: false, show_z_title: true, show_grid: true, equal_axes: true, start_at_zero: true, show_plot_title: false, bg_color: 'transparent', font_color: nil, grid_color: nil, colorscale: 'Bluered')
|
|
7
|
+
raise "X-axis of visualization cannot be empty" unless x
|
|
8
|
+
|
|
9
|
+
funcs = Array(func).map(&:to_s)
|
|
10
|
+
raise "Functions of visualization cannot be empty" if funcs.empty?
|
|
11
|
+
|
|
12
|
+
# if colors aren't specified, we'll pick them based on theme
|
|
13
|
+
actual_font_color = font_color || (text == :dark ? '#333333' : '#edf5ff')
|
|
14
|
+
actual_grid_color = grid_color || (text == :dark ? 'rgba(0,0,0,0.15)' : 'rgba(255,255,255,0.15)')
|
|
15
|
+
|
|
16
|
+
case format
|
|
17
|
+
when :html
|
|
18
|
+
generate_html(
|
|
19
|
+
x: x.to_s,
|
|
20
|
+
y: y.to_s,
|
|
21
|
+
func: funcs,
|
|
22
|
+
output: output,
|
|
23
|
+
show_legend: show_legend,
|
|
24
|
+
show_z_title: show_z_title,
|
|
25
|
+
show_grid: show_grid,
|
|
26
|
+
equal_axes: equal_axes,
|
|
27
|
+
start_at_zero: start_at_zero,
|
|
28
|
+
show_plot_title: show_plot_title,
|
|
29
|
+
bg_color: bg_color,
|
|
30
|
+
font_color: actual_font_color,
|
|
31
|
+
grid_color: actual_grid_color,
|
|
32
|
+
colorscale: colorscale
|
|
33
|
+
)
|
|
34
|
+
else
|
|
35
|
+
raise "Incorrect visualize format #{format}"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def generate_html(x:, y: nil, func:, output:, show_legend:, show_z_title:, show_grid:, equal_axes:, start_at_zero:, show_plot_title:, bg_color:, font_color:, grid_color:, colorscale:)
|
|
40
|
+
temp_file = Tempfile.new
|
|
41
|
+
output(format: :csv, file: temp_file)
|
|
42
|
+
table = CSV.read(temp_file, headers: true, col_sep: ";")
|
|
43
|
+
temp_file.unlink
|
|
44
|
+
|
|
45
|
+
headers = table.headers.map { |h| h&.strip }
|
|
46
|
+
|
|
47
|
+
unless headers.include?(x)
|
|
48
|
+
raise ArgumentError, "Column '#{x}' not found in CSV. Available columns: #{headers.inspect}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
unless headers.include?(y)
|
|
52
|
+
raise ArgumentError, "Column '#{y}' not found in CSV. Available columns: #{headers.inspect}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
func.each do |f|
|
|
56
|
+
unless headers.include?(f)
|
|
57
|
+
raise ArgumentError, "Column '#{f}' not found in CSV. Available columns: #{headers.inspect}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
normalized_rows = table.map do |row|
|
|
62
|
+
h = {}
|
|
63
|
+
table.headers.each do |original_header|
|
|
64
|
+
normalized_header = original_header&.strip
|
|
65
|
+
h[normalized_header] = row[original_header]&.strip
|
|
66
|
+
end
|
|
67
|
+
h
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
x_values = normalized_rows.map { |r| numeric_or_string(r[x]) }.compact.uniq.sort_by { |v| sortable_key(v) }
|
|
71
|
+
y_values = normalized_rows.map { |r| numeric_or_string(r[y]) }.compact.uniq.sort_by { |v| sortable_key(v) }
|
|
72
|
+
|
|
73
|
+
x_index = x_values.each_with_index.to_h
|
|
74
|
+
y_index = y_values.each_with_index.to_h
|
|
75
|
+
|
|
76
|
+
z_matrices = {}
|
|
77
|
+
func.each do |f|
|
|
78
|
+
z_matrices[f] = Array.new(y_values.size) { Array.new(x_values.size, nil) }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
normalized_rows.each do |row|
|
|
82
|
+
val_x = numeric_or_string(row[x])
|
|
83
|
+
val_y = numeric_or_string(row[y])
|
|
84
|
+
|
|
85
|
+
next if val_x.nil? || val_y.nil?
|
|
86
|
+
|
|
87
|
+
yi = y_index[val_y]
|
|
88
|
+
xi = x_index[val_x]
|
|
89
|
+
|
|
90
|
+
func.each do |f|
|
|
91
|
+
val_z = numeric_or_string(row[f])
|
|
92
|
+
z_matrices[f][yi][xi] = val_z unless val_z.nil?
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
plotly_data = func.map.with_index do |f, index|
|
|
97
|
+
{
|
|
98
|
+
type: "surface",
|
|
99
|
+
name: f,
|
|
100
|
+
x: x_values,
|
|
101
|
+
y: y_values,
|
|
102
|
+
z: z_matrices[f],
|
|
103
|
+
opacity: 0.85,
|
|
104
|
+
hovertemplate: "#{x}: %{x}<br>#{y}: %{y}<br>#{f}: %{z}<extra></extra>",
|
|
105
|
+
connectgaps: false,
|
|
106
|
+
showscale: index == 0 ? show_legend : false,
|
|
107
|
+
colorscale: colorscale,
|
|
108
|
+
contours: {
|
|
109
|
+
x: { show: show_grid, color: '#ffffff', width: 1 }, # 'color' is hardcoded to white - TODO: make it tunable
|
|
110
|
+
y: { show: show_grid, color: '#ffffff', width: 1 }
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
combined_func_names = func.join(", ")
|
|
116
|
+
zaxis_title_js = show_z_title ? "title: #{JSON.generate(combined_func_names)}," : "title: '',"
|
|
117
|
+
grid_flag = show_grid ? 'true' : 'false'
|
|
118
|
+
aspect_mode_js = equal_axes ? "aspectmode: 'cube'," : "aspectmode: 'auto',"
|
|
119
|
+
range_mode_js = start_at_zero ? "rangemode: 'tozero'," : ""
|
|
120
|
+
plot_title = show_plot_title ? "title: #{JSON.generate("#{combined_func_names} (#{x}, #{y})")}," : "title: '',"
|
|
121
|
+
|
|
122
|
+
plotly_bg = bg_color == 'transparent' ? 'rgba(0,0,0,0)' : bg_color
|
|
123
|
+
|
|
124
|
+
html = <<~HTML
|
|
125
|
+
<!DOCTYPE html>
|
|
126
|
+
<html>
|
|
127
|
+
<head>
|
|
128
|
+
<meta charset="UTF-8">
|
|
129
|
+
<title>Surface Plot</title>
|
|
130
|
+
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
|
|
131
|
+
<style>
|
|
132
|
+
html, body {
|
|
133
|
+
background-color: #{bg_color} !important;
|
|
134
|
+
margin: 0;
|
|
135
|
+
padding: 0;
|
|
136
|
+
width: 100%;
|
|
137
|
+
height: 100%;
|
|
138
|
+
font-family: Arial, sans-serif;
|
|
139
|
+
}
|
|
140
|
+
#chart {
|
|
141
|
+
width: 100vw;
|
|
142
|
+
height: 100vh;
|
|
143
|
+
}
|
|
144
|
+
</style>
|
|
145
|
+
</head>
|
|
146
|
+
<body>
|
|
147
|
+
<div id="chart"></div>
|
|
148
|
+
<script>
|
|
149
|
+
const data = #{JSON.generate(plotly_data)};
|
|
150
|
+
|
|
151
|
+
const layout = {
|
|
152
|
+
#{plot_title}
|
|
153
|
+
paper_bgcolor: '#{plotly_bg}',
|
|
154
|
+
plot_bgcolor: '#{plotly_bg}',
|
|
155
|
+
font: { color: '#{font_color}' }, // Применяется ко всему тексту (заголовок, названия осей, значения)
|
|
156
|
+
scene: {
|
|
157
|
+
#{aspect_mode_js}
|
|
158
|
+
xaxis: {
|
|
159
|
+
title: #{JSON.generate(x)},
|
|
160
|
+
showgrid: #{grid_flag},
|
|
161
|
+
zeroline: #{grid_flag},
|
|
162
|
+
gridcolor: '#{grid_color}', // Цвет линий сетки
|
|
163
|
+
zerolinecolor: '#{grid_color}', // Цвет нулевой линии
|
|
164
|
+
linecolor: '#{grid_color}', // Цвет линии самой оси
|
|
165
|
+
#{range_mode_js}
|
|
166
|
+
},
|
|
167
|
+
yaxis: {
|
|
168
|
+
title: #{JSON.generate(y)},
|
|
169
|
+
showgrid: #{grid_flag},
|
|
170
|
+
zeroline: #{grid_flag},
|
|
171
|
+
gridcolor: '#{grid_color}',
|
|
172
|
+
zerolinecolor: '#{grid_color}',
|
|
173
|
+
linecolor: '#{grid_color}',
|
|
174
|
+
#{range_mode_js}
|
|
175
|
+
},
|
|
176
|
+
zaxis: {
|
|
177
|
+
#{zaxis_title_js}
|
|
178
|
+
showgrid: #{grid_flag},
|
|
179
|
+
zeroline: #{grid_flag},
|
|
180
|
+
gridcolor: '#{grid_color}',
|
|
181
|
+
zerolinecolor: '#{grid_color}',
|
|
182
|
+
linecolor: '#{grid_color}',
|
|
183
|
+
#{range_mode_js}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
margin: { l: 0, r: 0, b: 0, t: 50 }
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
Plotly.newPlot("chart", data, layout, { responsive: true });
|
|
190
|
+
</script>
|
|
191
|
+
</body>
|
|
192
|
+
</html>
|
|
193
|
+
HTML
|
|
194
|
+
|
|
195
|
+
output ? File.write(output, html) : STDOUT.write(html)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def numeric_or_string(value)
|
|
199
|
+
return nil if value.nil?
|
|
200
|
+
|
|
201
|
+
s = value.to_s.strip
|
|
202
|
+
return nil if s.empty?
|
|
203
|
+
|
|
204
|
+
Integer(s)
|
|
205
|
+
rescue ArgumentError
|
|
206
|
+
begin
|
|
207
|
+
Float(s)
|
|
208
|
+
rescue ArgumentError
|
|
209
|
+
s
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def sortable_key(value)
|
|
214
|
+
value.is_a?(Numeric) ? [0, value] : [1, value.to_s]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flex-cartesian
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 2.0.1.beta
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yury Rassokhin
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: colorize
|
|
@@ -25,7 +24,7 @@ dependencies:
|
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
25
|
version: '0.8'
|
|
27
26
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
27
|
+
name: progressbar
|
|
29
28
|
requirement: !ruby/object:Gem::Requirement
|
|
30
29
|
requirements:
|
|
31
30
|
- - "~>"
|
|
@@ -39,51 +38,68 @@ dependencies:
|
|
|
39
38
|
- !ruby/object:Gem::Version
|
|
40
39
|
version: '1.13'
|
|
41
40
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
41
|
+
name: method_source
|
|
43
42
|
requirement: !ruby/object:Gem::Requirement
|
|
44
43
|
requirements:
|
|
45
44
|
- - "~>"
|
|
46
45
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '1.
|
|
46
|
+
version: '1.0'
|
|
48
47
|
type: :runtime
|
|
49
48
|
prerelease: false
|
|
50
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
50
|
requirements:
|
|
52
51
|
- - "~>"
|
|
53
52
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '1.
|
|
53
|
+
version: '1.0'
|
|
55
54
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
55
|
+
name: csv
|
|
57
56
|
requirement: !ruby/object:Gem::Requirement
|
|
58
57
|
requirements:
|
|
59
|
-
- - "
|
|
58
|
+
- - ">="
|
|
60
59
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
60
|
+
version: '0'
|
|
62
61
|
type: :runtime
|
|
63
62
|
prerelease: false
|
|
64
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
64
|
requirements:
|
|
66
|
-
- - "
|
|
65
|
+
- - ">="
|
|
67
66
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
67
|
+
version: '0'
|
|
69
68
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
69
|
+
name: roo
|
|
71
70
|
requirement: !ruby/object:Gem::Requirement
|
|
72
71
|
requirements:
|
|
73
|
-
- - "
|
|
72
|
+
- - ">="
|
|
74
73
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '
|
|
74
|
+
version: '0'
|
|
76
75
|
type: :runtime
|
|
77
76
|
prerelease: false
|
|
78
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
78
|
requirements:
|
|
80
|
-
- - "
|
|
79
|
+
- - ">="
|
|
81
80
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: ast
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
description: 'A Ruby DSL for operations on Cartesian multidimensional spaces. Features
|
|
97
|
+
user-defined functions, space conditions, dimensionality-agnostic and dimensionality-aware
|
|
98
|
+
iterators; named dimensions; dynamic adding/removing dimensions; connection to data
|
|
99
|
+
sources; tabular output; lazy/eager evaluation; progress bar; import from JSON/YAML/CSV/XLSX;
|
|
100
|
+
export to Markdown/CSV; DoE with Parametric Behaviour Blueprinting (PBB) to create
|
|
101
|
+
blueprints of real systems; and interactive HTML heatmaps to model and optimize
|
|
102
|
+
real systems effortlessly. Code examples: https://github.com/Yuri-Rassokhin/flex-cartesian/tree/main/examples'
|
|
87
103
|
email:
|
|
88
104
|
- yuri.rassokhin@gmail.com
|
|
89
105
|
executables: []
|
|
@@ -94,13 +110,21 @@ files:
|
|
|
94
110
|
- Gemfile
|
|
95
111
|
- LICENSE
|
|
96
112
|
- README.md
|
|
113
|
+
- lib/analyzer.rb
|
|
114
|
+
- lib/analyzers/morris.rb
|
|
97
115
|
- lib/flex-cartesian.rb
|
|
116
|
+
- lib/flex-cartesian/flex-cartesian-analyzer.rb
|
|
117
|
+
- lib/flex-cartesian/flex-cartesian-core.rb
|
|
118
|
+
- lib/flex-cartesian/flex-cartesian-deprecations.rb
|
|
119
|
+
- lib/flex-cartesian/flex-cartesian-io.rb
|
|
120
|
+
- lib/flex-cartesian/flex-cartesian-utilities.rb
|
|
121
|
+
- lib/version.rb
|
|
122
|
+
- lib/visualization/html.rb
|
|
98
123
|
homepage: https://github.com/Yuri-Rassokhin/flex-cartesian
|
|
99
124
|
licenses:
|
|
100
|
-
- GPL-3.0
|
|
125
|
+
- GPL-3.0-only
|
|
101
126
|
metadata:
|
|
102
127
|
source_code_uri: https://github.com/Yuri-Rassokhin/flex-cartesian
|
|
103
|
-
post_install_message:
|
|
104
128
|
rdoc_options: []
|
|
105
129
|
require_paths:
|
|
106
130
|
- lib
|
|
@@ -115,8 +139,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
115
139
|
- !ruby/object:Gem::Version
|
|
116
140
|
version: '0'
|
|
117
141
|
requirements: []
|
|
118
|
-
rubygems_version:
|
|
119
|
-
signing_key:
|
|
142
|
+
rubygems_version: 4.0.15
|
|
120
143
|
specification_version: 4
|
|
121
|
-
summary:
|
|
144
|
+
summary: Parametric system analysis as operations on Cartesian product for Ruby
|
|
122
145
|
test_files: []
|