erd_map 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,298 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErdMap
4
+ class GraphRenderer
5
+ extend Forwardable
6
+ def_delegators :@graph_renderer, :node_renderer
7
+
8
+ attr_reader :graph_renderer
9
+
10
+ VISIBLE = 1.0
11
+ TRANSLUCENT = 0.01
12
+ HIGHLIGHT_NODE_COLOR = "black"
13
+ HIGHLIGHT_EDGE_COLOR = "orange"
14
+ HIGHLIGHT_TEXT_COLOR = "white"
15
+ BASIC_COLOR = "darkslategray"
16
+ EMPTHASIS_NODE_SIZE = 80
17
+
18
+ def renderers
19
+ [circle_renderer, rect_renderer]
20
+ end
21
+
22
+ def cardinality_label
23
+ bokeh_models.LabelSet.new(
24
+ x: "x",
25
+ y: "y",
26
+ text: "text",
27
+ source: cardinality_data_source,
28
+ text_font_size: "12pt",
29
+ text_color: "text_color",
30
+ text_alpha: { field: "alpha" },
31
+ )
32
+ end
33
+
34
+ def js_args(plot)
35
+ {
36
+ graphRenderer: graph_renderer,
37
+ rectRenderer: rect_renderer,
38
+ circleRenderer: circle_renderer,
39
+ layoutProvider: layout_provider,
40
+ cardinalityDataSource: cardinality_data_source,
41
+ connectionsData: graph.connections.to_json,
42
+ layoutsByChunkData: graph.layouts_by_chunk.to_json,
43
+ chunkedNodesData: graph.chunked_nodes.to_json,
44
+ nodeWithCommunityIndexData: graph.node_with_community_index.to_json,
45
+ searchBox: plot.button_set[:search_box],
46
+ selectingNodeLabel: plot.button_set[:selecting_node_label],
47
+ zoomModeToggle: plot.button_set[:zoom_mode_toggle],
48
+ tapModeToggle: plot.button_set[:tap_mode_toggle],
49
+ displayTitleModeToggle: plot.button_set[:display_title_mode_toggle],
50
+ nodeLabels: {
51
+ titleModelLabel: title_model_label,
52
+ foreignModelLabel: foreign_model_label,
53
+ foreignColumnsLabel: foreign_columns_label,
54
+ },
55
+ plot: plot.plot,
56
+ VISIBLE: VISIBLE,
57
+ TRANSLUCENT: TRANSLUCENT,
58
+ HIGHLIGHT_NODE_COLOR: HIGHLIGHT_NODE_COLOR,
59
+ HIGHLIGHT_EDGE_COLOR: HIGHLIGHT_EDGE_COLOR,
60
+ HIGHLIGHT_TEXT_COLOR: HIGHLIGHT_TEXT_COLOR,
61
+ BASIC_COLOR: BASIC_COLOR,
62
+ EMPTHASIS_NODE_SIZE: EMPTHASIS_NODE_SIZE,
63
+ }
64
+ end
65
+
66
+ private
67
+
68
+ attr_reader :bokeh_models
69
+ attr_reader :graph
70
+
71
+ def initialize(graph)
72
+ import_modules = ErdMap.py_call_modules.imported_modules
73
+ @bokeh_models = import_modules[:bokeh_models]
74
+ @graph = graph
75
+ @graph_renderer = circle_renderer
76
+ end
77
+
78
+ def node_data_source
79
+ nodes_x, nodes_y = graph.node_names.map { |node| graph.initial_layout[node] ? graph.initial_layout[node] : graph.whole_layout[node] }.transpose
80
+ nodes_alpha = graph.node_names.map { |node| graph.initial_layout[node] ? VISIBLE : TRANSLUCENT }
81
+
82
+ columns_label = []
83
+ title_label = []
84
+ rect_heights = []
85
+ graph.node_names.map do |node_name|
86
+ title_text = format_text([node_name], title: true)
87
+ columns_text = [*title_text.scan("\n"), "\n", format_text(graph.association_columns[node_name])].join
88
+ columns_label << columns_text
89
+ title_label << [title_text, "\n", *columns_text.scan("\n")].join
90
+
91
+ padding = 36
92
+ line_count = columns_text.scan("\n").size + 1
93
+ rect_heights << line_count * 20 + padding
94
+ end
95
+
96
+ bokeh_models.ColumnDataSource.new(
97
+ data: {
98
+ index: graph.node_names,
99
+ alpha: nodes_alpha,
100
+ x: nodes_x,
101
+ y: nodes_y,
102
+ radius: graph.node_radius,
103
+ original_radius: graph.node_radius,
104
+ rect_height: rect_heights,
105
+ title_label: title_label,
106
+ columns_label: columns_label,
107
+ fill_color: graph.node_colors,
108
+ circle_original_color: graph.node_colors,
109
+ rect_original_color: graph.node_names.map { "white" },
110
+ text_color: graph.node_names.map { BASIC_COLOR },
111
+ text_outline_color: graph.node_names.map { nil },
112
+ }
113
+ )
114
+ end
115
+
116
+ def format_text(columns, title: false)
117
+ max_chars_size = title ? 18 : 20
118
+ columns.flat_map { |column| column.scan(/(\w{1,#{max_chars_size}})/) }.join("\n")
119
+ end
120
+
121
+ def circle_renderer
122
+ @circle_renderer ||= bokeh_models.GraphRenderer.new(
123
+ layout_provider: layout_provider,
124
+ visible: true,
125
+ ).tap do |renderer|
126
+ renderer.node_renderer.data_source = node_data_source
127
+ renderer.node_renderer.glyph = circle_glyph
128
+ renderer.node_renderer.selection_glyph = renderer.node_renderer.glyph
129
+ renderer.node_renderer.nonselection_glyph = renderer.node_renderer.glyph
130
+ renderer.edge_renderer.data_source = edge_data_source
131
+ renderer.edge_renderer.glyph = bokeh_models.MultiLine.new(
132
+ line_color: { field: "line_color" },
133
+ line_alpha: { field: "alpha" },
134
+ line_width: 1,
135
+ )
136
+ end
137
+ end
138
+
139
+ def rect_renderer
140
+ @rect_renderer ||= bokeh_models.GraphRenderer.new(
141
+ layout_provider: layout_provider,
142
+ visible: false,
143
+ ).tap do |renderer|
144
+ renderer.node_renderer.data_source = node_data_source
145
+ renderer.node_renderer.glyph = rect_glyph
146
+ renderer.node_renderer.selection_glyph = renderer.node_renderer.glyph
147
+ renderer.node_renderer.nonselection_glyph = renderer.node_renderer.glyph
148
+ renderer.edge_renderer.data_source = edge_data_source
149
+ renderer.edge_renderer.glyph = bokeh_models.MultiLine.new(
150
+ line_color: { field: "line_color" },
151
+ line_alpha: { field: "alpha" },
152
+ line_width: 1,
153
+ )
154
+ end
155
+ end
156
+
157
+ def circle_glyph
158
+ bokeh_models.Circle.new(
159
+ radius: "radius",
160
+ radius_units: "screen",
161
+ fill_color: { field: "fill_color" },
162
+ fill_alpha: { field: "alpha" },
163
+ line_alpha: { field: "alpha" },
164
+ )
165
+ end
166
+
167
+ def rect_glyph
168
+ bokeh_models.Rect.new(
169
+ width: 150,
170
+ height: { field: "rect_height" },
171
+ width_units: "screen",
172
+ height_units: "screen",
173
+ fill_color: { field: "fill_color" },
174
+ fill_alpha: { field: "alpha" },
175
+ line_color: BASIC_COLOR,
176
+ line_alpha: { field: "alpha" },
177
+ )
178
+ end
179
+
180
+ def title_model_label
181
+ bokeh_models.LabelSet.new(
182
+ x: "x",
183
+ y: "y",
184
+ text: "index",
185
+ source: graph_renderer.node_renderer.data_source,
186
+ text_font_size: "12pt",
187
+ text_color: { field: "text_color" },
188
+ text_outline_color: { field: "text_outline_color" },
189
+ text_align: "center",
190
+ text_baseline: "middle",
191
+ text_alpha: { field: "alpha" },
192
+ visible: true,
193
+ )
194
+ end
195
+
196
+ def foreign_model_label
197
+ bokeh_models.LabelSet.new(
198
+ x: "x",
199
+ y: "y",
200
+ text: { field: "title_label" },
201
+ source: graph_renderer.node_renderer.data_source,
202
+ text_font_size: "10pt",
203
+ text_font_style: "bold",
204
+ text_color: { field: "text_color" },
205
+ text_outline_color: { field: "text_outline_color" },
206
+ text_align: "center",
207
+ text_baseline: "middle",
208
+ text_alpha: { field: "alpha" },
209
+ # visible: false,
210
+ )
211
+ end
212
+
213
+ def foreign_columns_label
214
+ bokeh_models.LabelSet.new(
215
+ x: "x",
216
+ y: "y",
217
+ text: { field: "columns_label" },
218
+ source: graph_renderer.node_renderer.data_source,
219
+ text_font_size: "10pt",
220
+ text_font_style: "normal",
221
+ text_color: { field: "text_color" },
222
+ text_outline_color: { field: "text_outline_color" },
223
+ text_align: "center",
224
+ text_baseline: "middle",
225
+ text_alpha: { field: "alpha" },
226
+ # visible: false,
227
+ )
228
+ end
229
+
230
+ def edge_data_source
231
+ edge_start, edge_end = graph.edges.map { |edge| [edge[0], edge[1]] }.transpose
232
+ edges_alpha = graph.edges.map { |edge| graph.initial_nodes.include?(edge[0]) && graph.initial_nodes.include?(edge[1]) ? VISIBLE : TRANSLUCENT }
233
+ bokeh_models.ColumnDataSource.new(
234
+ data: {
235
+ start: edge_start,
236
+ end: edge_end,
237
+ alpha: edges_alpha,
238
+ line_color: graph.edges.map { BASIC_COLOR },
239
+ }
240
+ )
241
+ end
242
+
243
+ def cardinality_data_source
244
+ return @cardinality_data_source if @cardinality_data_source
245
+
246
+ @cardinality_data_source = bokeh_models.ColumnDataSource.new(
247
+ data: {
248
+ x: [],
249
+ y: [],
250
+ source: [],
251
+ target: [],
252
+ text: [],
253
+ alpha: [],
254
+ text_color: [],
255
+ }
256
+ )
257
+
258
+ graph.edges.each do |(source_node, target_node)|
259
+ next if source_node == target_node
260
+
261
+ label_alpha = graph.initial_nodes.include?(source_node) && graph.initial_nodes.include?(target_node) ?
262
+ VISIBLE : 0
263
+
264
+ x_offset = 0.2
265
+ y_offset = 0.3
266
+ source_x, source_y = graph.initial_layout[source_node] || graph.whole_layout[source_node]
267
+ target_x, target_y = graph.initial_layout[target_node] || graph.whole_layout[target_node]
268
+ vector_x = target_x - source_x
269
+ vector_y = target_y - source_y
270
+ length = Math.sqrt(vector_x**2 + vector_y**2)
271
+
272
+ @cardinality_data_source.data[:x] << source_x + (vector_x / length) * x_offset
273
+ @cardinality_data_source.data[:y] << source_y + (vector_y / length) * y_offset
274
+ @cardinality_data_source.data[:source] << source_node
275
+ @cardinality_data_source.data[:target] << target_node
276
+ @cardinality_data_source.data[:text] << "1"
277
+ @cardinality_data_source.data[:alpha] << label_alpha
278
+
279
+ @cardinality_data_source.data[:x] << target_x - (vector_x / length) * x_offset
280
+ @cardinality_data_source.data[:y] << target_y - (vector_y / length) * y_offset
281
+ @cardinality_data_source.data[:source] << source_node
282
+ @cardinality_data_source.data[:target] << target_node
283
+ @cardinality_data_source.data[:text] << "n" # FIXME: Show "1" when has_one association
284
+ @cardinality_data_source.data[:alpha] << label_alpha
285
+ end
286
+ @cardinality_data_source.data[:text_color] = Array.new(@cardinality_data_source.data[:x].to_a.size) { BASIC_COLOR }
287
+ @cardinality_data_source
288
+ end
289
+
290
+ def layout_provider
291
+ return @layout_provider if @layout_provider
292
+
293
+ nodes_x, nodes_y = graph.node_names.map { |node| graph.initial_layout[node] ? graph.initial_layout[node] : graph.whole_layout[node] }.transpose
294
+ graph_layout = graph.node_names.zip(nodes_x, nodes_y).map { |node, x, y| [node, [x, y]] }.to_h
295
+ @layout_provider = bokeh_models.StaticLayoutProvider.new(graph_layout: graph_layout)
296
+ end
297
+ end
298
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErdMap
4
+ class MapBuilder
5
+ def execute
6
+ import_modules
7
+ @graph = ErdMap::Graph.new
8
+ @graph_renderer = ErdMap::GraphRenderer.new(@graph)
9
+ save(build_layout)
10
+ end
11
+
12
+ private
13
+
14
+ attr_reader :bokeh_io, :bokeh_models, :bokeh_plotting
15
+ attr_reader :graph, :graph_renderer
16
+
17
+ def import_modules
18
+ import_modules = ErdMap.py_call_modules.imported_modules
19
+ @bokeh_io = import_modules[:bokeh_io]
20
+ @bokeh_models = import_modules[:bokeh_models]
21
+ @bokeh_plotting = import_modules[:bokeh_plotting]
22
+ end
23
+
24
+ def build_layout
25
+ plot = Plot.new(graph)
26
+ graph_renderer.renderers.each do |renderer|
27
+ plot.renderers.append(renderer)
28
+ renderer.node_renderer.data_source.selected.js_on_change("indices", toggle_tapped)
29
+ end
30
+ plot.add_layout(graph_renderer.cardinality_label)
31
+ bokeh_io.curdoc.js_on_event("document_ready", setup_graph_manager(plot))
32
+
33
+ bokeh_models.Column.new(
34
+ children: [
35
+ bokeh_models.Row.new(
36
+ children: [
37
+ plot.button_set[:left_spacer],
38
+ plot.button_set[:selecting_node_label],
39
+ plot.button_set[:search_box],
40
+ plot.button_set[:zoom_mode_toggle],
41
+ plot.button_set[:tap_mode_toggle],
42
+ plot.button_set[:display_title_mode_toggle],
43
+ plot.button_set[:re_layout_button],
44
+ plot.button_set[:zoom_in_button],
45
+ plot.button_set[:zoom_out_button],
46
+ plot.button_set[:re_compute_button],
47
+ plot.button_set[:right_spacer],
48
+ ],
49
+ sizing_mode: "stretch_width",
50
+ ),
51
+ plot.plot,
52
+ ],
53
+ sizing_mode: "stretch_both",
54
+ )
55
+ end
56
+
57
+ def save(layout)
58
+ tmp_dir = Rails.root.join("tmp", "erd_map")
59
+ FileUtils.makedirs(tmp_dir) unless Dir.exist?(tmp_dir)
60
+ output_path = File.join(tmp_dir, "map.html")
61
+
62
+ bokeh_io.output_file(output_path)
63
+ bokeh_io.save(layout)
64
+ puts output_path
65
+ end
66
+
67
+ def setup_graph_manager(plot)
68
+ bokeh_models.CustomJS.new(
69
+ args: graph_renderer.js_args(plot),
70
+ code: <<~JS
71
+ #{graph_manager}
72
+ window.graphManager = new GraphManager({
73
+ graphRenderer,
74
+ rectRenderer,
75
+ circleRenderer,
76
+ layoutProvider,
77
+ connectionsData,
78
+ layoutsByChunkData,
79
+ chunkedNodesData,
80
+ nodeWithCommunityIndexData,
81
+ selectingNodeLabel,
82
+ searchBox,
83
+ zoomModeToggle,
84
+ tapModeToggle,
85
+ displayTitleModeToggle,
86
+ nodeLabels,
87
+ plot,
88
+ windowObj: window,
89
+ })
90
+ JS
91
+ )
92
+ end
93
+
94
+ def toggle_tapped
95
+ bokeh_models.CustomJS.new(
96
+ code: <<~JS
97
+ window.graphManager.cbObj = cb_obj
98
+ window.graphManager.toggleTapped()
99
+ JS
100
+ )
101
+ end
102
+
103
+ def graph_manager
104
+ @graph_manager ||= File.read(__dir__ + "/graph_manager.js")
105
+ end
106
+
107
+ class << self
108
+ def build
109
+ Rails.logger.info "build start"
110
+ new.execute
111
+ Rails.logger.info "build completed"
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ErdMap
4
+ class Plot
5
+ extend Forwardable
6
+ def_delegators :@plot, :renderers, :add_layout
7
+
8
+ def plot
9
+ return @plot if @plot
10
+
11
+ padding_ratio = 0.1
12
+ x_min, x_max, y_min, y_max = graph.initial_layout.values.transpose.map(&:minmax).flatten
13
+ x_padding, y_padding = [(x_max - x_min) * padding_ratio, (y_max - y_min) * padding_ratio]
14
+ @plot = bokeh_models.Plot.new(
15
+ sizing_mode: "stretch_both",
16
+ x_range: bokeh_models.Range1d.new(start: x_min - x_padding, end: x_max + x_padding),
17
+ y_range: bokeh_models.Range1d.new(start: y_min - y_padding, end: y_max + y_padding),
18
+ tools: [
19
+ wheel_zoom_tool = bokeh_models.WheelZoomTool.new,
20
+ bokeh_models.ResetTool.new,
21
+ bokeh_models.PanTool.new,
22
+ bokeh_models.TapTool.new,
23
+ ],
24
+ ).tap do |plot|
25
+ plot.toolbar.active_scroll = wheel_zoom_tool
26
+ end
27
+ end
28
+
29
+ def button_set
30
+ @button_set ||= ButtonSet.new
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :graph
36
+
37
+ def initialize(graph)
38
+ @graph = graph
39
+ register_callback
40
+ end
41
+
42
+ def register_callback
43
+ plot.x_range.js_on_change("start", custom_js("triggerZoom"))
44
+ plot.x_range.js_on_change("end", custom_js("triggerZoom"))
45
+ plot.js_on_event("mousemove", custom_js("toggleHovered"))
46
+ plot.js_on_event("mousemove", bokeh_models.CustomJS.new(code: save_mouse_position))
47
+ plot.js_on_event("reset", custom_js("resetPlot"))
48
+ end
49
+
50
+ def bokeh_models
51
+ @bokeh_models ||= ErdMap.py_call_modules.imported_modules[:bokeh_models]
52
+ end
53
+
54
+ def custom_js(function_name)
55
+ bokeh_models.CustomJS.new(
56
+ code: <<~JS
57
+ window.graphManager.cbObj = cb_obj
58
+ window.graphManager.#{function_name}()
59
+ JS
60
+ )
61
+ end
62
+
63
+ def save_mouse_position
64
+ <<~JS
65
+ if (window.saveMousePosition !== undefined) { clearTimeout(window.saveMousePosition) }
66
+ window.saveMousePosition = setTimeout(function() {
67
+ window.lastMouseX = cb_obj.x
68
+ window.lastMouseY = cb_obj.y
69
+ }, 100)
70
+ JS
71
+ end
72
+
73
+ class ButtonSet
74
+ extend Forwardable
75
+ def_delegators :@button_set, :[]
76
+
77
+ private
78
+
79
+ def initialize
80
+ @button_set = {
81
+ left_spacer: left_spacer,
82
+ selecting_node_label: selecting_node_label,
83
+ search_box: search_box,
84
+ zoom_mode_toggle: zoom_mode_toggle,
85
+ tap_mode_toggle: tap_mode_toggle,
86
+ display_title_mode_toggle: display_title_mode_toggle,
87
+ re_layout_button: re_layout_button,
88
+ zoom_in_button: zoom_in_button,
89
+ zoom_out_button: zoom_out_button,
90
+ re_compute_button: re_compute_button,
91
+ right_spacer: right_spacer,
92
+ }
93
+ end
94
+
95
+ def left_spacer
96
+ bokeh_models.Spacer.new(width: 0, sizing_mode: "stretch_width")
97
+ end
98
+
99
+ def right_spacer
100
+ bokeh_models.Spacer.new(width: 30, sizing_mode: "fixed")
101
+ end
102
+
103
+ def selecting_node_label
104
+ bokeh_models.Div.new(
105
+ text: "",
106
+ height: 28,
107
+ styles: { display: :flex, align_items: :center },
108
+ )
109
+ end
110
+
111
+ def search_box
112
+ bokeh_models.TextInput.new(placeholder: "🔍 Search model", width: 200).tap do |input|
113
+ input.js_on_change("value", custom_js("searchNodes"))
114
+ end
115
+ end
116
+
117
+ def zoom_mode_toggle
118
+ bokeh_models.Button.new(label: "Wheel mode: fix", button_type: "default").tap do |button|
119
+ button.js_on_click(custom_js("toggleZoomMode"))
120
+ end
121
+ end
122
+
123
+ def tap_mode_toggle
124
+ bokeh_models.Button.new(label: "Tap mode: association", button_type: "default").tap do |button|
125
+ button.js_on_click(custom_js("toggleTapMode"))
126
+ end
127
+ end
128
+
129
+ def display_title_mode_toggle
130
+ bokeh_models.Button.new(label: "Display mode: title", button_type: "default").tap do |button|
131
+ button.js_on_click(custom_js("toggleDisplayTitleMode"))
132
+ end
133
+ end
134
+
135
+ def re_layout_button
136
+ bokeh_models.Button.new(label: "Re-Layout", button_type: "default").tap do |button|
137
+ button.js_on_click(custom_js("reLayout"))
138
+ end
139
+ end
140
+
141
+ def zoom_in_button
142
+ bokeh_models.Button.new(label: "Zoom In", button_type: "primary").tap do |button|
143
+ button.js_on_click(custom_js("zoomIn",))
144
+ end
145
+ end
146
+
147
+ def zoom_out_button
148
+ bokeh_models.Button.new(label: "Zoom Out", button_type: "success").tap do |button|
149
+ button.js_on_click(custom_js("zoomOut"))
150
+ end
151
+ end
152
+
153
+ def re_compute_button
154
+ bokeh_models.Button.new(label: "Re-Compute", button_type: "default").tap do |button|
155
+ button.js_on_click(
156
+ bokeh_models.CustomJS.new(
157
+ args: { button: button },
158
+ code: <<~JS
159
+ button.disabled = true
160
+ button.label = "Computing ..."
161
+
162
+ fetch("/erd_map", { method: "PUT" })
163
+ .then(response => {
164
+ if (response.ok) { return response.text() }
165
+ else { return response.json().then(json => { throw new Error(json.message) }) }
166
+ })
167
+ .then(data => { window.location.reload() })
168
+ .catch(error => {
169
+ alert(error.message)
170
+ console.error(error)
171
+
172
+ button.disabled = false
173
+ button.label = "Re-Compute"
174
+ })
175
+ JS
176
+ )
177
+ )
178
+ end
179
+ end
180
+
181
+ def bokeh_models
182
+ @bokeh_models ||= ErdMap.py_call_modules.imported_modules[:bokeh_models]
183
+ end
184
+
185
+ def custom_js(function_name)
186
+ bokeh_models.CustomJS.new(
187
+ code: <<~JS
188
+ window.graphManager.cbObj = cb_obj
189
+ window.graphManager.#{function_name}()
190
+ JS
191
+ )
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pycall"
4
+
5
+ module ErdMap
6
+ class PyCallModules
7
+ def initialize
8
+ @nx = PyCall.import_module("networkx")
9
+ @bokeh_io = PyCall.import_module("bokeh.io")
10
+ @bokeh_models = PyCall.import_module("bokeh.models")
11
+ @bokeh_plotting = PyCall.import_module("bokeh.plotting")
12
+ @networkx_community = PyCall.import_module("networkx.algorithms.community")
13
+ end
14
+
15
+ def imported_modules
16
+ {
17
+ nx: @nx,
18
+ bokeh_io: @bokeh_io,
19
+ bokeh_models: @bokeh_models,
20
+ bokeh_plotting: @bokeh_plotting,
21
+ networkx_community: @networkx_community,
22
+ }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module ErdMap
2
+ VERSION = "0.1.0"
3
+ end
data/lib/erd_map.rb ADDED
@@ -0,0 +1,19 @@
1
+ require "erd_map/version"
2
+ require "erd_map/engine"
3
+ require "erd_map/py_call_modules"
4
+ require "erd_map/graph"
5
+ require "erd_map/graph_renderer"
6
+ require "erd_map/map_builder"
7
+ require "erd_map/plot"
8
+
9
+ module ErdMap
10
+ class << self
11
+ def py_call_modules
12
+ @py_call_modules
13
+ end
14
+
15
+ def py_call_modules=(py_call_modules)
16
+ @py_call_modules = py_call_modules
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ desc "Compute erd_map"
4
+ task erd_map: :environment do
5
+ puts "Map computing start."
6
+ ErdMap::MapBuilder.build
7
+ puts "Map computing completed."
8
+ end