twb 1.0 → 1.0.1
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/lib/twb.rb +3 -1
- data/lib/twb/analysis/calculatedfieldsanalyzer.rb +509 -0
- data/lib/twb/util/graphnode.rb +20 -3
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8aae5b5038ecc0eade737b33b274b8a05dd3cc94
|
4
|
+
data.tar.gz: 161e0430d5558f2151f04be57a79a1ddb9e8c02a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 297a5b3feadd0ad310843a4308883e7a74971ecb200b31450cabd568cba460fbd7b7b1dfb09a401f8f82e3612618310ded0c1c7049217d84f26f4592418ef44b
|
7
|
+
data.tar.gz: 8ae0b5c9b192d3f9796fe7f9bee9cc09cd2b7bd415692903d6861d34d24c39239c0fc230b69b60b0edef7d6b2e842b745ae62d3b1968b0b2b50f8baffde4b9a6
|
data/lib/twb.rb
CHANGED
@@ -34,9 +34,11 @@ require_relative 'twb/util/xraydashboards'
|
|
34
34
|
require_relative 'twb/util/graphnode'
|
35
35
|
require_relative 'twb/util/graphedge'
|
36
36
|
require_relative 'twb/util/graphedges'
|
37
|
+
require_relative 'twb/analysis/calculatedfieldsanalyzer'
|
38
|
+
|
37
39
|
|
38
40
|
# Represents Tableau Workbooks and their contents.
|
39
41
|
#
|
40
42
|
module Twb
|
41
|
-
VERSION = '1.0'
|
43
|
+
VERSION = '1.0.1'
|
42
44
|
end
|
@@ -0,0 +1,509 @@
|
|
1
|
+
# TTC_CalculatedFields.rb - this Ruby script Copyright 2017 Christopher Gerrard
|
2
|
+
#
|
3
|
+
# This program is free software: you can redistribute it and/or modify
|
4
|
+
# it under the terms of the GNU General Public License as published by
|
5
|
+
# the Free Software Foundation, either version 3 of the License, or
|
6
|
+
# (at your option) any later version.
|
7
|
+
#
|
8
|
+
# This program is distributed in the hope that it will be useful,
|
9
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
10
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
11
|
+
# GNU General Public License for more details.
|
12
|
+
#
|
13
|
+
# You should have received a copy of the GNU General Public License
|
14
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
15
|
+
|
16
|
+
require 'nokogiri'
|
17
|
+
require 'twb'
|
18
|
+
require 'set'
|
19
|
+
require 'csv'
|
20
|
+
require 'logger'
|
21
|
+
|
22
|
+
module Twb
|
23
|
+
module Analysis
|
24
|
+
|
25
|
+
class CalculatedFieldsAnalyzer
|
26
|
+
|
27
|
+
attr_reader :calculatedFieldsCount, :formulaFieldsCount
|
28
|
+
|
29
|
+
@@ttlogfile = 'CalculatedFieldsAnalyzer.ttlog'
|
30
|
+
@@gvDotLocation = 'C:\\tech\\graphviz\\Graphviz2.38\\bin\\dot.exe'
|
31
|
+
@@processName = '.CalculatedFields'
|
32
|
+
|
33
|
+
@@calcFieldsCSVFileName = 'TwbCalculatedFields.csv'
|
34
|
+
@@calcFieldsCSVFileHeader = ['Record #',
|
35
|
+
'Workbook', 'Workbook Dir',
|
36
|
+
'Data Source', 'Data Source Caption', 'Data Source Name (tech)',
|
37
|
+
'Field Name', 'Field Caption', 'Field Name (tech)',
|
38
|
+
'Data Source + Field Name (tech)',
|
39
|
+
'Data Type', 'Role', 'Type',
|
40
|
+
'Class',
|
41
|
+
'Scope Isolation',
|
42
|
+
'Formula Length',
|
43
|
+
'Formula Code',
|
44
|
+
'Formula',
|
45
|
+
'Formula Comments',
|
46
|
+
'Formula LOD?'
|
47
|
+
]
|
48
|
+
|
49
|
+
@@formFieldsCSVFileName = 'TwbFormulaFields.csv'
|
50
|
+
@@formFieldsCSVFileHeader = ['Rec #',
|
51
|
+
'Workbook', 'Workbook Dir',
|
52
|
+
'Data Source',
|
53
|
+
'Field - Calculated',
|
54
|
+
'Data Source - Formula (tech)',
|
55
|
+
'Data Source - Formula',
|
56
|
+
'Field - Formula (tech)',
|
57
|
+
'Field - Formula',
|
58
|
+
'Data Source + Field - Calculated',
|
59
|
+
'Table'
|
60
|
+
]
|
61
|
+
|
62
|
+
@techUINames = {}
|
63
|
+
@fieldTables = {}
|
64
|
+
|
65
|
+
|
66
|
+
@@dotHeader = <<DOTHEADER
|
67
|
+
digraph g {
|
68
|
+
graph [rankdir="LR" splines=line];
|
69
|
+
node [shape="box" width="2"];
|
70
|
+
|
71
|
+
DOTHEADER
|
72
|
+
|
73
|
+
def initialize
|
74
|
+
#-- Logging setup --
|
75
|
+
@logger = Logger.new(@@ttlogfile)
|
76
|
+
@logger.level = Logger::DEBUG
|
77
|
+
#-- CSV files setup --
|
78
|
+
@calcFieldsCSVFile = CSV.open(@@calcFieldsCSVFileName,'w')
|
79
|
+
@calcFieldsCSVFile << @@calcFieldsCSVFileHeader
|
80
|
+
# --
|
81
|
+
@formFieldsCSVFile = CSV.open(@@formFieldsCSVFileName ,'w')
|
82
|
+
@formFieldsCSVFile << @@formFieldsCSVFileHeader
|
83
|
+
#-- Counters setup --
|
84
|
+
@twbCount = 0
|
85
|
+
@calculatedFieldsCount = 0
|
86
|
+
@formulaFieldsCount = 0
|
87
|
+
# --
|
88
|
+
@referencedFields = SortedSet.new
|
89
|
+
# --
|
90
|
+
@localEmit = false
|
91
|
+
emit "\n\nLogging activity to: #{File.basename(@@ttlogfile)}"
|
92
|
+
@imageFiles = []
|
93
|
+
end
|
94
|
+
|
95
|
+
def loadFieldTables dataSource
|
96
|
+
emit "FIELD TABLES"
|
97
|
+
@records = CSV.read('C:\Professional\Clients\Incapsulate\Internal Project Monitoring\Project Portfolio v2\Salesforce Fields.csv')
|
98
|
+
@records.each do |rec|
|
99
|
+
emit "-- #{rec}"
|
100
|
+
m = {}
|
101
|
+
m['table'] = rec[1]
|
102
|
+
m['dbFieldName'] = rec[2]
|
103
|
+
@fieldTables[rec[0]] = m
|
104
|
+
end
|
105
|
+
emit "=========="
|
106
|
+
emit @fieldTables
|
107
|
+
emit "=========="
|
108
|
+
emit "FIELD TABLES"
|
109
|
+
end
|
110
|
+
|
111
|
+
def processTWB twbWithDir
|
112
|
+
twb = File.basename(twbWithDir)
|
113
|
+
@twb = Twb::Workbook.new twbWithDir
|
114
|
+
puts " - #{twbWithDir}"
|
115
|
+
emit "- Workbook: #{twbWithDir}"
|
116
|
+
emit " version: #{@twb.version}"
|
117
|
+
return if twbWithDir.end_with? == "Tableau Calculated Fields Analyses.twb"
|
118
|
+
twbDir = File.dirname(File.expand_path(twbWithDir))
|
119
|
+
edges = Set.new
|
120
|
+
# -- processing
|
121
|
+
dss = @twb.datasources
|
122
|
+
twbRootFields = Set.new
|
123
|
+
dss.each do |ds|
|
124
|
+
emit "Datasource: '#{ds.uiname}' -> #{ds.Parameters?}"
|
125
|
+
next if ds.Parameters? # don't process the Parameters data source
|
126
|
+
# it requires special handling, has different XML structure
|
127
|
+
#-- For tracking unreferenced (root) calculated fields = calculatedFields - referencedFields
|
128
|
+
calculatedFields = SortedSet.new
|
129
|
+
referencedFields = SortedSet.new
|
130
|
+
#--
|
131
|
+
dsTechName = ds.name
|
132
|
+
dsCaption = ds.caption
|
133
|
+
dsName = ds.uiname
|
134
|
+
dsID = dsTechName + ':::' + dsName
|
135
|
+
emit "\n\n "
|
136
|
+
emit "======================================================"
|
137
|
+
emit "======================================================"
|
138
|
+
emit "======= DATA SOURCE: #{ds.uiname} ====== "
|
139
|
+
emit "======================================================"
|
140
|
+
emit "======================================================\n\n "
|
141
|
+
dsGraphNode = Twb::Util::Graphnode.new(name: dsName, id: dsID, type: :TwbDataConnection, properties: {workbook: twbWithDir})
|
142
|
+
emit "\t dsgnode: #{dsGraphNode}"
|
143
|
+
fieldUINames = ds.fieldUINames
|
144
|
+
calculationNodes = ds.calculatedFields
|
145
|
+
emit "calculationNodes : nil? '#{calculationNodes.nil?}'" # - len '#{calculationNodes.length}'"
|
146
|
+
calculationNodes.each do |calcNode|
|
147
|
+
calculation = Twb::FieldCalculation.new calcNode
|
148
|
+
emit "HANDLING CALCULATION NODE:"
|
149
|
+
emit calcNode.attributes
|
150
|
+
#-- field names --
|
151
|
+
fldCaption, = calcNode.xpath('../@caption').text
|
152
|
+
fldTechName = calcNode.xpath('../@name').text.gsub(/^\[/,'').gsub(/\]$/,'')
|
153
|
+
fldName = if fldCaption == ''
|
154
|
+
then fldTechName
|
155
|
+
else fldCaption
|
156
|
+
end
|
157
|
+
emit "\t Field : #{fldName}"
|
158
|
+
emit "\t Formula : #{calcNode.attribute('formula')}"
|
159
|
+
dataType = calcNode.xpath('../@datatype').text
|
160
|
+
role = calcNode.xpath('../@role').text
|
161
|
+
type = calcNode.xpath('../@type').text
|
162
|
+
fieldID = fldTechName+'::'+dsName
|
163
|
+
calculatedFields.add fieldID
|
164
|
+
srcGraphNode = Twb::Util::Graphnode.new(name: fldName, id: fieldID, type: :CalculatedField, properties: {:DataSource => dsName})
|
165
|
+
dsFieldEdge = Twb::Util::Graphedge.new(from: dsGraphNode, to: srcGraphNode, relationship: 'contains')
|
166
|
+
edges.add dsFieldEdge
|
167
|
+
hasFormula = calcNode.has_attribute?('formula')
|
168
|
+
if hasFormula
|
169
|
+
formulaText = calcNode.attribute('formula').text
|
170
|
+
emit "\t Formula: #{formulaText}"
|
171
|
+
#-- field attributes --
|
172
|
+
# fldDispLabel = "#{fldName}\n--\n#{formulaText.gsub('"', "'")}" #fldName + '\n--' + formulaText.to_s
|
173
|
+
emit "\t srfnode: #{srcGraphNode} "
|
174
|
+
emit "\t dsfedge: #{dsFieldEdge} "
|
175
|
+
emit " "
|
176
|
+
emit "\tFIELD cap: #{fldCaption} "
|
177
|
+
emit "\t name: #{fldTechName} "
|
178
|
+
emit "\t uiname: #{fldName} "
|
179
|
+
emit "\t------------------------------------------------------------"
|
180
|
+
#-- calculation --
|
181
|
+
formulaFlat = formulaText.gsub(/\r\n/, ' ## ').gsub(/\n/, ' ## ').gsub(/[ ]+/,' ')
|
182
|
+
formulaFlatFlat = formulaFlat.upcase
|
183
|
+
formulaLOD = formulaFlatFlat.include?('{FIXED') || formulaFlatFlat.include?('{INCLUDE') || formulaFlatFlat.include?('{EXCLUDE')
|
184
|
+
formulaLength = formulaText.length
|
185
|
+
emit "\tFORMULA TEXT: #{formulaText} "
|
186
|
+
emit "\t FLAT: #{formulaFlat}"
|
187
|
+
emit "\t------------------------------------------------------------"
|
188
|
+
comments = calculation.comments # getComments( formulaText )
|
189
|
+
calcClass = calculation.class # d.xpath('./@class').text
|
190
|
+
scopeIsolation = calcNode.xpath('./@scope-isolation').text
|
191
|
+
# -- resolved fields: {internal field name => datasource}
|
192
|
+
# -- datasource is only present for fields located in other data sources
|
193
|
+
resolvedFields = calculation.resolvedFields
|
194
|
+
# prepare UI formula, replacing technical field names with their UI forms
|
195
|
+
uiFormula = formulaFlat.gsub(' XX ',' ')
|
196
|
+
resolvedFields.each do |rf|
|
197
|
+
emit "\tRESOLVED FLD: #{rf.inspect}"
|
198
|
+
calcFieldName = rf[:field]
|
199
|
+
if rf[:source].nil?
|
200
|
+
calcFieldRef = "[%s]" % [ calcFieldName ]
|
201
|
+
dispFieldRef = "[%s]" % [ ds.fieldUIName(calcFieldName) ]
|
202
|
+
else
|
203
|
+
remoteDS = @twb.datasource(rf[:source])
|
204
|
+
remoteDSName = remoteDS.uiname
|
205
|
+
remoteDSFld = remoteDS.fieldUIName(calcFieldName)
|
206
|
+
calcFieldRef = "[%s].[%s]" % [ rf[:source], calcFieldName ]
|
207
|
+
dispFieldRef = "[%s].[%s]" % [ remoteDSName, remoteDSFld ]
|
208
|
+
end
|
209
|
+
emit "\tcalcFieldRef: #{calcFieldRef}"
|
210
|
+
emit "\tdispFieldRef: #{dispFieldRef}"
|
211
|
+
uiFormula = uiFormula.gsub(calcFieldRef, dispFieldRef)
|
212
|
+
end
|
213
|
+
emit "\t FLAT: #{formulaFlat}"
|
214
|
+
emit "\t Resolved: #{uiFormula}\n\t--"
|
215
|
+
@calcFieldsCSVFile << [
|
216
|
+
@calculatedFieldsCount += 1,
|
217
|
+
twb, twbDir,
|
218
|
+
dsName, dsCaption, dsTechName,
|
219
|
+
fldName, fldCaption, fldTechName,
|
220
|
+
dsTechName + '::' + fldTechName,
|
221
|
+
dataType, role, type,
|
222
|
+
calcClass, scopeIsolation,
|
223
|
+
formulaLength, formulaFlat, uiFormula,
|
224
|
+
comments,
|
225
|
+
formulaLOD
|
226
|
+
]
|
227
|
+
resolvedFields.each do |rf|
|
228
|
+
emit "\t\t res field : #{rf[:field]} "
|
229
|
+
emit "\t\t res source: #{rf[:source]}"
|
230
|
+
calcFieldName = rf[:field]
|
231
|
+
calcDataSource = rf[:source]
|
232
|
+
localDataSource = rf[:source].nil? # if there isn't a rf[:source] value
|
233
|
+
# the field is from this data source
|
234
|
+
# else the field is from an alien data source (in the same workbook)
|
235
|
+
refDataSource = localDataSource ? ds : @twb.datasource(calcDataSource)
|
236
|
+
dispFieldName = refDataSource.fieldUIName(calcFieldName)
|
237
|
+
calcFieldTable = refDataSource.fieldTable(calcFieldName)
|
238
|
+
emit "\t\t calc field : #{dispFieldName} nil?<#{dispFieldName.nil?}>"
|
239
|
+
emit "\t\t data source: #{refDataSource.uiname}"
|
240
|
+
emit "\t\t table: #{calcFieldTable} nil?<#{calcFieldTable.nil?}>"
|
241
|
+
properties = {'DataSource' => dsName, 'DataSourceReference' => 'local'}
|
242
|
+
if dispFieldName.nil?
|
243
|
+
dispFieldName = "<#{calcFieldName}>::<#{calcDataSource}> UNDEFINED"
|
244
|
+
properties['status'] = 'UNDEFINED'
|
245
|
+
end
|
246
|
+
calcFieldID = "#{calcFieldName}::#{refDataSource.uiname}"
|
247
|
+
if !localDataSource
|
248
|
+
calcFieldID = "#{calcFieldName}:LDS:#{ds.uiname}:RDS:#{refDataSource.uiname}"
|
249
|
+
properties['DataSourceReference'] = 'remote'
|
250
|
+
end
|
251
|
+
calcFieldTable = ds.fieldTable(calcFieldName)
|
252
|
+
calcFieldType = calcFieldTable.nil? ? :CalculatedField : :DatabaseField
|
253
|
+
calcFieldNode = Twb::Util::Graphnode.new(name: dispFieldName, id: calcFieldID, type: calcFieldType, properties: properties)
|
254
|
+
fieldFieldEdge = Twb::Util::Graphedge.new(from: srcGraphNode, to: calcFieldNode, relationship: 'references')
|
255
|
+
edges.add fieldFieldEdge
|
256
|
+
referencedFields.add calcFieldID
|
257
|
+
# @formulaFieldsCount+=1
|
258
|
+
emit "\t\t calcFieldNode: #{calcFieldNode}"
|
259
|
+
emit "\t\t graphEdge: #{fieldFieldEdge}"
|
260
|
+
fldToDsNode = calcFieldNode
|
261
|
+
if !calcFieldTable.nil?
|
262
|
+
tableID = calcFieldTable + ':::' + ds.uiname
|
263
|
+
tableName = "-[#{calcFieldTable}]-"
|
264
|
+
tableNode = Twb::Util::Graphnode.new(name: tableName, id: tableID, type: :DBTable, properties: properties)
|
265
|
+
fieldFieldEdge = Twb::Util::Graphedge.new(from: calcFieldNode, to: tableNode, relationship: 'is a field in')
|
266
|
+
edges.add fieldFieldEdge
|
267
|
+
fldToDsNode = tableNode
|
268
|
+
end
|
269
|
+
if !localDataSource
|
270
|
+
alienDSNode = Twb::Util::Graphnode.new( name: '==>' + refDataSource.uiname,
|
271
|
+
id: "#{ds.uiname}::::=>#{refDataSource.uiname}",
|
272
|
+
type: :DBTable,
|
273
|
+
properties: {'Home Source' => dsName, 'Remote Source' => refDataSource.uiname}
|
274
|
+
)
|
275
|
+
fieldFieldEdge = Twb::Util::Graphedge.new(from: fldToDsNode, to: alienDSNode, relationship: 'In Remote Data Source')
|
276
|
+
edges.add fieldFieldEdge
|
277
|
+
end
|
278
|
+
@formFieldsCSVFile << [ @formulaFieldsCount+=1,
|
279
|
+
twb,
|
280
|
+
twbDir,
|
281
|
+
dsName,
|
282
|
+
fldName,
|
283
|
+
refDataSource.name,
|
284
|
+
refDataSource.uiname,
|
285
|
+
calcFieldName,
|
286
|
+
dispFieldName,
|
287
|
+
dsName + '::' + dispFieldName,
|
288
|
+
'fieldTable'
|
289
|
+
]
|
290
|
+
end
|
291
|
+
end # if hasFormula
|
292
|
+
end # calculationNodes.each
|
293
|
+
dsRootFields = calculatedFields - referencedFields
|
294
|
+
@referencedFields.merge referencedFields
|
295
|
+
#--
|
296
|
+
emit "--\nCalculated Fields\n-----------------"
|
297
|
+
calculatedFields.each { |f| emit f }
|
298
|
+
emit "--\nReferenced Fields\n-----------------"
|
299
|
+
referencedFields.each { |f| emit f }
|
300
|
+
emit "--\nDS Root Fields\n-----------------"
|
301
|
+
dsRootFields.each { |f| emit f }
|
302
|
+
emit "--"
|
303
|
+
# --
|
304
|
+
twbRootFields.merge dsRootFields
|
305
|
+
end # dss.each
|
306
|
+
@twbCount += 1
|
307
|
+
mapTwb twb, edges, twbRootFields
|
308
|
+
graphEdges twb, edges
|
309
|
+
emit "#######################"
|
310
|
+
return @imageFiles
|
311
|
+
end
|
312
|
+
|
313
|
+
|
314
|
+
def mapTwb twb, edges, rootFields
|
315
|
+
dotFile = initDot twb
|
316
|
+
dotFileName = File.basename dotFile
|
317
|
+
dotFile.puts "\n // subgraph cluster_1 {"
|
318
|
+
dotFile.puts " // color= grey;"
|
319
|
+
dotFile.puts ""
|
320
|
+
edgesAsStrings = SortedSet.new
|
321
|
+
# this two step process coalesces the edges into a unique set, avoiding duplicating the dot
|
322
|
+
# file entries, and can be shrunk when graph edges expose the bits necessary for management by Set
|
323
|
+
emit "\n========================\nLoading Edges\n========================\n From DC? Referenced? Edge \n %s %s %s" % ['--------', '-----------', '-'*45]
|
324
|
+
edges.each do |e|
|
325
|
+
# don't want to emit edge which is from a Data Connection to a
|
326
|
+
# Calculated Field which is also referenced by another calculated field
|
327
|
+
isFromDC = e.from.type == :TwbDataConnection
|
328
|
+
isRefField = @referencedFields.include?(e.to.id)
|
329
|
+
edgesAsStrings.add(e.dot) unless isFromDC && isRefField
|
330
|
+
end
|
331
|
+
emit "------------------------\n "
|
332
|
+
edgesAsStrings.each do |es|
|
333
|
+
dotFile.puts " #{es}"
|
334
|
+
emit " #{es}"
|
335
|
+
end
|
336
|
+
emit "========================\n "
|
337
|
+
dotFile.puts ""
|
338
|
+
dotFile.puts " // }"
|
339
|
+
dotFile.puts "\n\n // 4--------------------------------------------------------------------"
|
340
|
+
# "table::JIRA_HARVEST_Correspondence__c::Jira" [label="JIRA_HARVEST_Correspondence__c"]
|
341
|
+
nodes = SortedSet.new
|
342
|
+
edges.each do |e|
|
343
|
+
nodes.add e.from.dotLabel
|
344
|
+
nodes.add e.to.dotLabel
|
345
|
+
end
|
346
|
+
nodes.each do |n|
|
347
|
+
dotFile.puts n
|
348
|
+
end
|
349
|
+
dotFile.puts "\n\n // 5--------------------------------------------------------------------"
|
350
|
+
emitTypes( edges, dotFile )
|
351
|
+
rankRootFields( dotFile, rootFields )
|
352
|
+
closeDot( dotFile, twb )
|
353
|
+
# renderPng(twb.name,dotFileName)
|
354
|
+
# renderPdf(twb.name,dotFileName)
|
355
|
+
renderDot(twb,dotFileName,'pdf')
|
356
|
+
renderDot(twb,dotFileName,'png')
|
357
|
+
renderDot(twb,dotFileName,'svg')
|
358
|
+
emitEdges edges
|
359
|
+
end
|
360
|
+
|
361
|
+
|
362
|
+
def graphEdges twb, edges
|
363
|
+
graphFile = File.new(twb + '.cypher', 'w')
|
364
|
+
# graphFile.puts "OKEY DOKE, graphing away"
|
365
|
+
cypherCode = Set.new
|
366
|
+
edges.each do |edge|
|
367
|
+
cypherCode.add edge.from.cypherCreate
|
368
|
+
cypherCode.add edge.to.cypherCreate
|
369
|
+
cypherCode.add edge.cypherCreate
|
370
|
+
end
|
371
|
+
cypherCode.each do |cc|
|
372
|
+
graphFile.puts cc
|
373
|
+
end
|
374
|
+
graphFile.puts "\nreturn *"
|
375
|
+
graphFile.close unless graphFile.nil?
|
376
|
+
@imageFiles << File.basename(graphFile)
|
377
|
+
end
|
378
|
+
|
379
|
+
def emitEdges edges
|
380
|
+
emit " %-15s %s" % ['type', 'Edge']
|
381
|
+
emit " %-15s %s" % ['-'*15, '-'*35]
|
382
|
+
edges.each do |edge|
|
383
|
+
emit " %-15s %s" % [edge.from.type, edge.from]
|
384
|
+
emit " %-15s %s" % [edge.to.type, edge.to]
|
385
|
+
emit "\n "
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
def emitTypes edges, dotFile
|
390
|
+
typedNodes = {}
|
391
|
+
dotFile.puts "\n\n // 2--------------------------------------------------------------------"
|
392
|
+
edges.each do |edge|
|
393
|
+
emit " EDGE :: #{edge}"
|
394
|
+
loadNodeType typedNodes, edge.from
|
395
|
+
loadNodeType typedNodes, edge.to
|
396
|
+
end
|
397
|
+
typedNodes.each do |type, nodes|
|
398
|
+
emit "+++++++++ typedNodes of '#{type}'' "
|
399
|
+
nodes.each do |node|
|
400
|
+
emit " -n- #{node}"
|
401
|
+
end
|
402
|
+
rankSame(dotFile, type, nodes) unless type == :CalculatedField
|
403
|
+
end
|
404
|
+
# labelTypes dotFile, edges
|
405
|
+
end
|
406
|
+
|
407
|
+
def loadNodeType set, node
|
408
|
+
type = node.type
|
409
|
+
set[type] = Set.new unless set.include? type
|
410
|
+
set[type].add node
|
411
|
+
end
|
412
|
+
|
413
|
+
def rankSame dotFile, type, nodes
|
414
|
+
dotFile.puts "\n // '#{type}' --------------------------------------------------------------------"
|
415
|
+
dotFile.puts "\n {rank=same "
|
416
|
+
# dotFile.puts " \"#{type}\" [shape=\"box3d\" style=\"filled\" ]" unless ''.eql? type # [shape=\"box3d\" style=\"filled\" ]\"" unless label.equal? ''
|
417
|
+
nodes.each do |node|
|
418
|
+
dotFile.puts " \"#{node.id}\""
|
419
|
+
end
|
420
|
+
dotFile.puts " }"
|
421
|
+
end
|
422
|
+
|
423
|
+
def rankRootFields dotFile, dsRootFields
|
424
|
+
dotFile.puts "\n // Unreferenced (root) Calculated Fields -----------------------------------------"
|
425
|
+
dotFile.puts "\n {rank=same "
|
426
|
+
dsRootFields.each do |rf|
|
427
|
+
dotFile.puts " \"#{rf}\""
|
428
|
+
end
|
429
|
+
dotFile.puts " }"
|
430
|
+
end
|
431
|
+
|
432
|
+
|
433
|
+
def labelTypes dotFile, edges
|
434
|
+
fromTos = Set.new
|
435
|
+
edges.each do |edge|
|
436
|
+
# fromTos.add "\"Alien Data Source\" -> \"Alien Data Source\""
|
437
|
+
fromTos.add "\"#{edge.from.type}\""
|
438
|
+
fromTos.add "\"#{edge.to.type}\""
|
439
|
+
end
|
440
|
+
return if fromTos.empty?
|
441
|
+
dotFile.puts "\n // 3--------------------------------------------------------------------"
|
442
|
+
dotFile.puts ' subgraph cluster_0 {'
|
443
|
+
dotFile.puts ' color=white;'
|
444
|
+
dotFile.puts ' node [shape="box3d" style="filled" ];'
|
445
|
+
fromTos.each do |ft|
|
446
|
+
dotFile.puts " #{ft}"
|
447
|
+
end
|
448
|
+
dotFile.puts ' }'
|
449
|
+
end
|
450
|
+
|
451
|
+
|
452
|
+
def emit(local=@localEmit, stuff)
|
453
|
+
#puts "\nstuff.class #{stuff.class} :: #{stuff}" if local
|
454
|
+
if stuff.is_a? String then
|
455
|
+
lines = stuff.split(/\n/)
|
456
|
+
lines.each do |line|
|
457
|
+
@logger.debug "#{@emitPrefix}#{line}"
|
458
|
+
puts "#{@emitPrefix}#{line}" if local
|
459
|
+
end
|
460
|
+
else
|
461
|
+
@logger.debug "#{@emitPrefix}#{stuff}"
|
462
|
+
puts "#{@emitPrefix}#{stuff}" if local
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
|
467
|
+
def initDot twb
|
468
|
+
dotFile = File.open("#{twb}#{@@processName}.dot",'w')
|
469
|
+
dotFile.puts @@dotHeader
|
470
|
+
return dotFile
|
471
|
+
end
|
472
|
+
|
473
|
+
def closeDot dotFile, twb
|
474
|
+
dotFile.puts ' '
|
475
|
+
dotFile.puts '// -------------------------------------------------------------'
|
476
|
+
dotFile.puts ' '
|
477
|
+
dotFile.puts ' subgraph cluster_1 {'
|
478
|
+
# dotFile.puts ' color=white;'
|
479
|
+
dotFile.puts ' style=invis;'
|
480
|
+
# dotFile.puts ' border=0;'
|
481
|
+
dotFile.puts ' node [border=blue];'
|
482
|
+
dotFile.puts ' '
|
483
|
+
dotFile.puts ' "" [style=invis]'
|
484
|
+
dotFile.puts " \"Tableau Tools\\nCalculated Fields Map\\nWorkbook '#{twb}'\\n#{Time.new.ctime}\" [penwidth=0]"
|
485
|
+
# dotFile.puts " \"Tableau Tools Workbook Calculated Fields Map\\n#{Time.new.ctime}\" -> \"\" [style=invis]"
|
486
|
+
dotFile.puts ' '
|
487
|
+
dotFile.puts ' }'
|
488
|
+
dotFile.puts ' '
|
489
|
+
dotFile.puts '}'
|
490
|
+
dotFile.close
|
491
|
+
end
|
492
|
+
|
493
|
+
|
494
|
+
def renderDot twb, dot, format
|
495
|
+
emit "Rendering DOT file\n - #{twb}\n - #{dot}\n - #{format}"
|
496
|
+
imageType = '-T' + format
|
497
|
+
imageFile = twb + @@processName + 'Graph.' + format
|
498
|
+
imageParam = '-o' + imageFile
|
499
|
+
emit "system #{@@gvDotLocation} #{imageType} #{imageParam} #{dot}"
|
500
|
+
system @@gvDotLocation, imageType, imageParam, dot
|
501
|
+
@imageFiles << imageFile
|
502
|
+
return imageFile
|
503
|
+
end
|
504
|
+
|
505
|
+
|
506
|
+
end # class
|
507
|
+
|
508
|
+
end # module Analysis
|
509
|
+
end # module Twb
|
data/lib/twb/util/graphnode.rb
CHANGED
@@ -21,7 +21,23 @@ module Util
|
|
21
21
|
@@stripChars = /[.: %-\-\(\)=]/
|
22
22
|
@@replChar = '_'
|
23
23
|
|
24
|
-
|
24
|
+
@@typeShapes = { :CalculatedField => 'shape=note',
|
25
|
+
:DBTable => 'shape=ellipse',
|
26
|
+
:TwbDataConnection => '',
|
27
|
+
:DatabaseField => ''
|
28
|
+
}
|
29
|
+
|
30
|
+
@@typeColors = { :CalculatedField => 'fillcolor=lightskyblue3',
|
31
|
+
:DBTable => 'fillcolor=steelblue',
|
32
|
+
:TwbDataConnection => 'fillcolor=cornflowerblue',
|
33
|
+
:DatabaseField => 'fillcolor=steelblue',
|
34
|
+
}
|
35
|
+
|
36
|
+
@@typeStyles = { :CalculatedField => 'style=filled',
|
37
|
+
:DBTable => 'style=filled',
|
38
|
+
:TwbDataConnection => 'style=filled',
|
39
|
+
:DatabaseField => 'style=filled'
|
40
|
+
}
|
25
41
|
|
26
42
|
# @name - the visible name
|
27
43
|
# @id - the technical identifier, used to distinquish the node from similarly named nodes
|
@@ -58,8 +74,9 @@ module Util
|
|
58
74
|
|
59
75
|
def dotLabel
|
60
76
|
# "JIRA 1::JIRA 1.csv" [label="JIRA 1.csv"]
|
61
|
-
|
62
|
-
|
77
|
+
# style = @type =~ /Data Source|DB Table|Database Field/ ? "style=filled" : ''
|
78
|
+
# style = @type =~ /Data Source|DB Table|Database Field/ ? "style=filled" : ''
|
79
|
+
"\"%s\" [label=\"%s\" %s %s %s ]" % [id, name, @@typeShapes[@type], 'style=filled', @@typeColors[@type]]
|
63
80
|
end
|
64
81
|
|
65
82
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: twb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chris Gerrard
|
@@ -24,6 +24,7 @@ files:
|
|
24
24
|
- lib/twb.rb
|
25
25
|
- lib/twb/TwbTest.rb
|
26
26
|
- lib/twb/action.rb
|
27
|
+
- lib/twb/analysis/calculatedfieldsanalyzer.rb
|
27
28
|
- lib/twb/apps/X-Ray Dashboards.rb
|
28
29
|
- lib/twb/countNodes.rb
|
29
30
|
- lib/twb/dashboard.rb
|