twb 1.0.5 → 1.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a568fbc9fa00d25cdc6a703c6f8f57803ff9b430
4
- data.tar.gz: b0accc4559b2ef5fce3acf8f578920765be6346f
3
+ metadata.gz: 44eeddf556df0d517a68f5b4a1af89ae52138f37
4
+ data.tar.gz: 46d6c474f5d6d5b020d7849463733c66bfb724fe
5
5
  SHA512:
6
- metadata.gz: 17d333f0e261725cd3ec6e7361caf2a213cf53c0d62d3acae34de1f1ba3fa1e3d5b8fc5293e92e172ae2e3021569b7f54a14497450978d4ef84cca1d0d226720
7
- data.tar.gz: 2582fddea1388ea6d33a68870facd2e7ca7e305f07083665e065de07d6b7c1c6bf8cc84062f69ff65c235819bbffd28c546c55f211ff9d6de0a9c49038b627d7
6
+ metadata.gz: 6ba98c71c6cd2ccfa8bb6800e20541e90d1e8c9d8da2a420b93a8b8ba5b91912228185fd4a9d6ae20e66ea0da98b62c24b1f24cb9d631fbae9de028742c9fbef
7
+ data.tar.gz: 5d06d43f7367afc011bb3dce3c33c95cbbc7205238e08bdf2f494426e2c6fc90088c6854a55f8a49712bec79a54ed01aacb7358d42e1e0bb50ec5fc68d9b8126
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # TWB - The Tableau Workbook Ruby gem.
2
2
 
3
- This gem models Tableau workbooks and their major components. It's the result to years of writing similar code to access, interpret,
4
- and manage Tableau workbooks that involved writing the same code for loading and navigating workbooks again and again.
3
+ This gem models Tableau workbooks and their major components. It's the result of years of writing similar code to access, interpret,
4
+ and manage Tableau workbooks.
5
5
 
6
6
  ## Philosophy
7
7
 
data/lib/twb.rb CHANGED
@@ -16,7 +16,6 @@
16
16
  require_relative 'twb/dashboard'
17
17
  require_relative 'twb/datasource'
18
18
  require_relative 'twb/docdashboard'
19
- require_relative 'twb/fieldcalculation.rb'
20
19
  require_relative 'twb/localfield'
21
20
  require_relative 'twb/metadatafield'
22
21
  require_relative 'twb/storyboard'
@@ -24,9 +23,11 @@ require_relative 'twb/window'
24
23
  require_relative 'twb/workbook'
25
24
  require_relative 'twb/worksheet'
26
25
  require_relative 'twb/action'
26
+ require_relative 'twb/columnfield.rb'
27
+ require_relative 'twb/calculatedfield'
27
28
  require_relative 'twb/fieldcalculation'
28
- require_relative 'twb/docdashboardimagevert.rb'
29
- require_relative 'twb/docdashboardwebvert.rb'
29
+ require_relative 'twb/docdashboardimagevert'
30
+ require_relative 'twb/docdashboardwebvert'
30
31
  require_relative 'twb/util/twbDashSheetDataDotBuilder'
31
32
  require_relative 'twb/util/dotFileRenderer'
32
33
  require_relative 'twb/util/htmllistcollapsible'
@@ -34,11 +35,14 @@ require_relative 'twb/util/xraydashboards'
34
35
  require_relative 'twb/util/graphnode'
35
36
  require_relative 'twb/util/graphedge'
36
37
  require_relative 'twb/util/graphedges'
37
- require_relative 'twb/analysis/calculatedfieldsanalyzer'
38
-
38
+ require_relative 'twb/analysis/calculatedfields/calculatedfieldsanalyzer'
39
+ require_relative 'twb/analysis/calculatedfields/markdownemitter'
40
+ require_relative 'twb/analysis/calculatedfields/csvemitter'
41
+ require_relative 'twb/analysis/datasources/DataSourceTableFieldsCSVEmitter'
42
+ require_relative 'twb/analysis/Sheets/WorksheetDataStructureCSVEmitter.rb'
39
43
 
40
44
  # Represents Tableau Workbooks and their contents.
41
45
  #
42
46
  module Twb
43
- VERSION = '1.0.5'
47
+ VERSION = '1.9.1'
44
48
  end
@@ -0,0 +1,154 @@
1
+ # calculatedfieldsanalyzer.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 'twb'
17
+
18
+ module Twb
19
+ module Analysis
20
+ module CalculatedFields
21
+
22
+ class CSVEmitter
23
+
24
+ attr_reader :calcFieldsType, :calcFieldsHeader, :calcFieldsFile, :calcFieldsRecords, :calcFieldsCount
25
+ attr_reader :refFieldsType, :refFieldsHeader, :refFieldsFile, :refFieldsRecords, :refFieldsCount
26
+ attr_reader :dictionary, :csvFiles
27
+
28
+ def initialize
29
+ @calcFieldsContents = 'Calculated Fields with Formulas'
30
+ @calcFieldsType = 'CalculatedFieldsWithFormulas'
31
+ @calcFieldsHeader = ['Record #',
32
+ 'Workbook', 'Workbook Dir',
33
+ 'Data Source Name (tech)', 'Data Source Caption', 'Data Source Name',
34
+ 'Field Name (tech)', 'Field Caption', 'Field Name',
35
+ 'Data Source + Field Name (tech)',
36
+ 'Formula (flat)',
37
+ 'Formula Length',
38
+ 'Formula Table Calc?',
39
+ 'Formula LOD?',
40
+ 'Formula LOD Code Position'
41
+ ]
42
+
43
+ @refFieldsContents = 'Fields referenced in Calculated Fields Formulae'
44
+ @refFieldsType = 'CalculatedFieldsReferencedFields'
45
+ @refFieldsHeader = ['Record #',
46
+ 'Workbook', 'Workbook Dir',
47
+ 'Data Source Name (tech)', 'Data Source Caption', 'Data Source Name',
48
+ 'Field Name (tech)', 'Field Caption', 'Field Name',
49
+ 'Referenced Field',
50
+ 'Referenced DataSource',
51
+ 'Referenced DataSource Location'
52
+ ]
53
+
54
+
55
+ @dictionary = [
56
+ { :contents => @calcFieldsContents,
57
+ :fileType => @calcFieldsType,
58
+ :fileHeader => @calcFieldsHeader
59
+ },
60
+ { :contents => @refFieldsContents,
61
+ :fileType => @calcFieldsType,
62
+ :fileHeader => @calcFieldsHeader
63
+ }
64
+ ]
65
+ end
66
+
67
+ # def dictionary
68
+ # @dictionary
69
+ # end
70
+
71
+ # def calcFieldsType
72
+ # @calcFieldsType
73
+ # end
74
+
75
+ # def calcFieldsHeader
76
+ # @calcFieldsHeader
77
+ # end
78
+
79
+ # def refFieldsType
80
+ # @refFieldsType
81
+ # end
82
+
83
+ # def calcFieldsHeader
84
+ # @refFieldsHeader
85
+ # end
86
+
87
+ def processTwb twbFileName
88
+ twb = File.basename(twbFileName)
89
+ @twb = Twb::Workbook.new twb
90
+ # --
91
+ @calcFieldsFile = "#{twb}.#{@calcFieldsType}.csv"
92
+ @csvCalcFile = CSV.open(@calcFieldsFile,'w')
93
+ @csvCalcFile << @calcFieldsHeader
94
+ @calcFieldsCount = 0
95
+ @calcFieldsRecords = []
96
+ # --
97
+ @refFieldsFile = "#{twb}.#{@refFieldsType}.csv" # twb + '.CalcReferenceFields.csv'
98
+ @csvRefFile = CSV.open(@refFieldsFile,'w')
99
+ @csvRefFile << @refFieldsHeader
100
+ @refFieldsCount = 0
101
+ @refFieldsRecords = []
102
+ # --
103
+ @csvFiles = [
104
+ { :contents => @calcFieldsContents,
105
+ :name => @calcFieldsFile,
106
+ :records => @calcFieldsRecords
107
+ },
108
+ { :contents => @refFieldsContents,
109
+ :name => @refFieldsFile,
110
+ :records => @refFieldsRecords
111
+ }
112
+ ]
113
+ # --
114
+ dss = @twb.datasources
115
+ dss.each do |ds|
116
+ calcFields = ds.calculatedFieldsMap.sort_by { |fldName,calc| fldName }
117
+ calcFields.each do |fldname, field|
118
+ formRecord = [ @calcFieldsCount += 1,
119
+ @twb.name, @twb.dir,
120
+ ds.name, ds.caption, ds.uiname,
121
+ field.name, field.caption, field.uiname,
122
+ "#{ds.name}.#{field.name}",
123
+ field.calculation.formulaFlatResolved,
124
+ field.calculation.formula.length,
125
+ field.calculation.is_tableCalc,
126
+ field.calculation.is_lod,
127
+ field.calculation.lodCodePos
128
+ ]
129
+ @calcFieldsRecords.push formRecord
130
+ @csvCalcFile << formRecord
131
+ #--
132
+ field.calculation.calcFields.each do |cf|
133
+ refRecord = [ @refFieldsCount += 1,
134
+ @twb.name, @twb.dir,
135
+ ds.name, ds.caption, ds.uiname,
136
+ field.name, field.caption, field.uiname,
137
+ cf.uiName,
138
+ cf.dataSource,
139
+ cf.dataSourceRef,
140
+ ]
141
+ @refFieldsRecords.push refRecord
142
+ @csvRefFile << refRecord
143
+ end
144
+ end
145
+ end
146
+ @csvCalcFile.close unless @csvCalcFile.nil?
147
+ @csvRefFile.close unless @csvRefFile.nil?
148
+ end
149
+
150
+ end # class CSVEmitter
151
+
152
+ end # nodule CalculatedFields
153
+ end # module Analysis
154
+ end # module Twb
@@ -0,0 +1,527 @@
1
+ # calculatedfieldsanalyzer.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, :dataFiles
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
+ @csvCalculatedFields = []
75
+ @csvFormulaFields = []
76
+ # @testFile = File.open('testCSV.csv','w')
77
+ # @testFile.puts @@calcFieldsCSVFileHeader.inspect
78
+ #-- Logging setup --
79
+ @logger = Logger.new(@@ttlogfile)
80
+ @logger.level = Logger::DEBUG
81
+ #-- Counters setup --
82
+ @twbCount = 0
83
+ @calculatedFieldsCount = 0
84
+ @formulaFieldsCount = 0
85
+ # --
86
+ @referencedFields = SortedSet.new
87
+ # --
88
+ @dataFiles = {'TwbCalculatedFields.csv' => 'Calculated Fields & their Formulas', 'TwbFormulaFields.csv' => 'Fields referenced in Formulas'}
89
+ # --
90
+ @localEmit = false
91
+ emit "\n\nLogging activity to: #{File.basename(@@ttlogfile)}"
92
+ @imageFiles = []
93
+ end
94
+
95
+ def processTWB twbWithDir
96
+ twb = File.basename(twbWithDir)
97
+ @twb = Twb::Workbook.new twbWithDir
98
+ emit "- Workbook: #{twbWithDir}"
99
+ emit " version: #{@twb.version}"
100
+ return if twbWithDir.end_with? == "Tableau Calculated Fields Analyses.twb"
101
+ twbDir = File.dirname(File.expand_path(twbWithDir))
102
+ edges = Set.new
103
+ # -- processing
104
+ dss = @twb.datasources
105
+ puts " # data sources: #{dss.length}"
106
+ twbRootFields = Set.new
107
+ @twbFields = {}
108
+ dss.each do |ds|
109
+ puts "\t\t - #{ds.uiname} \t\t #{ds.calculatedFields.length}"
110
+ next if ds.Parameters? # don't process the Parameters data source
111
+ ds.calculatedFields.each do |calcField|
112
+ emit "HANDLING CALCULATED FIELD:: #{calcField}"
113
+ emit " :: #{calcField.calculation.formula}"
114
+ emit " :: #{calcField.calculation.formulaResolved}"
115
+ dsTechName = ds.name
116
+ dsCaption = ds.caption
117
+ dsName = ds.uiname
118
+ dsID = dsTechName + ':::' + dsName
119
+ emit "\n\n "
120
+ emit "======================================================"
121
+ emit "======================================================"
122
+ emit "======= DATA SOURCE: #{ds.uiname} ====== "
123
+ emit "======================================================"
124
+ emit "======================================================\n\n "
125
+ dsGraphNode = Twb::Util::Graphnode.new(name: dsName, id: dsID, type: :TwbDataConnection, properties: {workbook: twbWithDir})
126
+ emit "\t dsgnode: #{dsGraphNode}"
127
+ fieldUINames = ds.fieldUINames
128
+ emit "ds.calculatedFields :: nil? #{ds.calculatedFields.nil?}"
129
+ end
130
+ processDataSource ds
131
+ end
132
+ end
133
+
134
+ def processDataSource ds
135
+ emit "Datasource: '#{ds.uiname}' -> #{ds.Parameters?}"
136
+ dsFields = {}
137
+ @twbFields[ds.uiname] = dsFields
138
+ # next if ds.Parameters? # don't process the Parameters data source
139
+ # it requires special handling, has different XML structure
140
+ #-- For tracking unreferenced (root) calculated fields = calculatedFields - referencedFields
141
+ calculatedFields = SortedSet.new
142
+ referencedFields = SortedSet.new
143
+ #--
144
+ dsTechName = ds.name
145
+ dsCaption = ds.caption
146
+ dsName = ds.uiname
147
+ dsID = dsTechName + ':::' + dsName
148
+ emit "\n\n "
149
+ emit "======================================================"
150
+ emit "======================================================"
151
+ emit "======= DATA SOURCE: #{ds.uiname} ====== "
152
+ emit "======================================================"
153
+ emit "======================================================\n\n "
154
+ dsGraphNode = Twb::Util::Graphnode.new(name: dsName, id: dsID, type: :TwbDataConnection, properties: {workbook: twbWithDir})
155
+ emit "\t dsgnode: #{dsGraphNode}"
156
+ fieldUINames = ds.fieldUINames
157
+ emit "ds.calculatedFields :: nil? #{ds.calculatedFields.nil?}"
158
+ ds.calculatedFields.each do |calcField|
159
+ emit "HANDLING CALCULATED FIELD:: #{calcField}"
160
+ emit '--'
161
+ fldCaption, = calcField.caption
162
+ fldTechName = calcField.name
163
+ fldName = calcField.uiname
164
+ dataType = calcField.datatype
165
+ role = calcField.role
166
+ type = calcField.type
167
+ fieldID = fldTechName+'::'+dsName
168
+ calculatedFields.add fieldID
169
+ #--
170
+ dsFields[fldName] = calcField
171
+ srcGraphNode = Twb::Util::Graphnode.new(name: fldName, id: fieldID, type: :CalculatedField, properties: {:DataSource => dsName})
172
+ dsFieldEdge = Twb::Util::Graphedge.new(from: dsGraphNode, to: srcGraphNode, relationship: 'contains')
173
+ #--
174
+ emit "\t srfnode: #{srcGraphNode} "
175
+ emit "\t dsfedge: #{dsFieldEdge} "
176
+ edges.add dsFieldEdge
177
+ calculation = calcField.calculation
178
+ emit "calculation: f? %8s -> %s " % [calculation.has_formula, calculation.formula ]
179
+ if calculation.has_formula
180
+ formulaFlat = calculation.formulaFlat
181
+ uiFormula = formulaFlat.gsub(' XX ',' ')
182
+ formulaLOD = formulaFlat.upcase =~ /^[ ]*{[ ]*(INCLUDE|FIXED|EXCLUDE)/
183
+ resolvedFields = calculation.resolvedFields
184
+ resolvedFields.each do |rf|
185
+ emit "\tRESOLVED FLD: #{rf.inspect}"
186
+ calcFieldName = rf[:field]
187
+ if rf[:source].nil?
188
+ calcFieldRef = "[%s]" % [ calcFieldName ]
189
+ dispFieldRef = "[%s]" % [ ds.fieldUIName(calcFieldName) ]
190
+ else
191
+ remoteDS = @twb.datasource(rf[:source])
192
+ if remoteDS.nil?
193
+ calcFieldRef = "[DS_NOT_FOUND].[%s]" % [ calcFieldName ]
194
+ dispFieldRef = calcFieldRef
195
+ else
196
+ remoteDSName = remoteDS.uiname
197
+ remoteDSFld = remoteDS.fieldUIName(calcFieldName)
198
+ calcFieldRef = "[%s].[%s]" % [ rf[:source], calcFieldName ]
199
+ dispFieldRef = "[%s].[%s]" % [ remoteDSName, remoteDSFld ]
200
+ end # remoteDS.nil?
201
+ end # rf[:source].nil?
202
+ emit "\tcalcFieldRef: #{calcFieldRef}"
203
+ emit "\tdispFieldRef: #{dispFieldRef}"
204
+ uiFormula = uiFormula.gsub(calcFieldRef, dispFieldRef)
205
+ end # resolvedFields.each
206
+ @csvCalculatedFields.push [
207
+ @calculatedFieldsCount += 1,
208
+ twb,
209
+ twbDir,
210
+ dsName,
211
+ dsCaption,
212
+ dsTechName,
213
+ fldName,
214
+ fldCaption,
215
+ fldTechName,
216
+ dsTechName + '::' + fldTechName,
217
+ dataType,
218
+ role,
219
+ type,
220
+ calculation.class,
221
+ calculation.scopeIsolation,
222
+ calculation.formulaFlat.length,
223
+ calculation.formulaFlat,
224
+ uiFormula[0...200],
225
+ calculation.comments,
226
+ !formulaLOD.nil?
227
+ ]
228
+ resolvedFields.each do |rf|
229
+ emit "\t\t res field : #{rf[:field]} "
230
+ emit "\t\t res source: #{rf[:source]}"
231
+ calcFieldName = rf[:field]
232
+ calcDataSource = rf[:source]
233
+ localDataSource = rf[:source].nil? # if there isn't a rf[:source] value
234
+ # the field is from this data source
235
+ # else the field is from an alien data source (in the same workbook)
236
+ refDataSource = localDataSource ? ds : @twb.datasource(calcDataSource) # data source may not be in Workbook
237
+ refDataSourceName = !refDataSource.nil? ? refDataSource.uiname : calcDataSource + '\n***DATA CONNECTION NOT IN WORKBOOK***'
238
+ if !refDataSource.nil?
239
+ dispFieldName = refDataSource.fieldUIName(calcFieldName)
240
+ calcFieldTable = refDataSource.fieldTable(calcFieldName)
241
+ else
242
+ dispFieldName = calcFieldName
243
+ calcFieldTable = calcFieldName
244
+ end
245
+ emit "\t\t calc field : #{dispFieldName} nil?<#{dispFieldName.nil?}>"
246
+ emit "\t\t data source: #{refDataSourceName}"
247
+ emit "\t\t table: #{calcFieldTable} nil?<#{calcFieldTable.nil?}>"
248
+ properties = {'DataSource' => dsName, 'DataSourceReference' => 'local'}
249
+ if dispFieldName.nil?
250
+ dispFieldName = "<#{calcFieldName}>::<#{calcDataSource}> UNDEFINED"
251
+ properties['status'] = 'UNDEFINED'
252
+ end
253
+ calcFieldID = "#{calcFieldName}::#{refDataSourceName}"
254
+ if !localDataSource
255
+ calcFieldID = "#{calcFieldName}:LDS:#{ds.uiname}:RDS:#{refDataSourceName}"
256
+ properties['DataSourceReference'] = 'remote'
257
+ end
258
+ calcFieldTable = ds.fieldTable(calcFieldName)
259
+ calcFieldType = calcFieldTable.nil? ? :CalculatedField : :DatabaseField
260
+ calcFieldNode = Twb::Util::Graphnode.new(name: dispFieldName, id: calcFieldID, type: calcFieldType, properties: properties)
261
+ fieldFieldEdge = Twb::Util::Graphedge.new(from: srcGraphNode, to: calcFieldNode, relationship: 'references')
262
+ edges.add fieldFieldEdge
263
+ referencedFields.add calcFieldID
264
+ emit "\t\t calcFieldNode: #{calcFieldNode}"
265
+ emit "\t\t graphEdge: #{fieldFieldEdge}"
266
+ fldToDsNode = calcFieldNode
267
+ if !calcFieldTable.nil?
268
+ tableID = calcFieldTable + ':::' + ds.uiname
269
+ tableName = "-[#{calcFieldTable}]-"
270
+ tableNode = Twb::Util::Graphnode.new(name: tableName, id: tableID, type: :DBTable, properties: properties)
271
+ fieldFieldEdge = Twb::Util::Graphedge.new(from: calcFieldNode, to: tableNode, relationship: 'is a field in')
272
+ edges.add fieldFieldEdge
273
+ fldToDsNode = tableNode
274
+ end
275
+ if !localDataSource
276
+ alienDSNode = Twb::Util::Graphnode.new( name: '==>' + refDataSourceName,
277
+ id: "#{ds.uiname}::::=>#{refDataSourceName}",
278
+ type: :DBTable,
279
+ properties: {'Home Source' => dsName, 'Remote Source' => refDataSourceName}
280
+ )
281
+ fieldFieldEdge = Twb::Util::Graphedge.new(from: fldToDsNode, to: alienDSNode, relationship: 'In Remote Data Source')
282
+ edges.add fieldFieldEdge
283
+ end
284
+
285
+ # @csvFormulaFields.push [ @formulaFieldsCount+=1,
286
+
287
+
288
+ end # resolvedFields.each do
289
+ end # if calculation.has_formula
290
+ end # ds.calculatedFields.each
291
+
292
+ dsRootFields = calculatedFields - referencedFields
293
+ @referencedFields.merge referencedFields
294
+ #--
295
+ emit "--\nCalculated Fields\n-----------------"
296
+ calculatedFields.each { |f| emit f }
297
+ emit "--\nReferenced Fields\n-----------------"
298
+ referencedFields.each { |f| emit f }
299
+ emit "--\nDS Root Fields\n-----------------"
300
+ dsRootFields.each { |f| emit f }
301
+ emit "--"
302
+ # --
303
+ twbRootFields.merge dsRootFields
304
+ @twbCount += 1
305
+ mapTwb twb, edges, twbRootFields
306
+ graphEdges twb, edges
307
+ emit "#######################"
308
+ #--
309
+ csvCF = File.open(@@calcFieldsCSVFileName, 'w')
310
+ csvCF.puts @@calcFieldsCSVFileHeader.to_csv
311
+ @csvCalculatedFields.each do |r|
312
+ csvCF.puts r.to_csv
313
+ end
314
+ csvCF.close
315
+ #--
316
+ csvFF = File.open(@@formFieldsCSVFileName, 'w')
317
+ csvFF.puts @@formFieldsCSVFileHeader.to_csv
318
+ @csvFormulaFields.each do |r|
319
+ csvFF.puts r.to_csv
320
+ end
321
+ csvFF.close
322
+ #--
323
+ return @imageFiles
324
+ end # def processDataSource
325
+
326
+ def emitCalcfield calcField
327
+ emit "\t FIELD cap :: #{calcField.caption} "
328
+ emit "\t tname:: #{calcField.name}"
329
+ emit "\t uiname:: #{calcField.uiname}"
330
+ emit "\t formula:: #{calculation.formulaFlat}"
331
+ end
332
+
333
+ def mapTwb twb, edges, rootFields
334
+ dotFile = initDot twb
335
+ dotFileName = File.basename dotFile
336
+ dotFile.puts "\n // subgraph cluster_1 {"
337
+ dotFile.puts " // color= grey;"
338
+ dotFile.puts ""
339
+ edgesAsStrings = SortedSet.new
340
+ # this two step process coalesces the edges into a unique set, avoiding duplicating the dot
341
+ # file entries, and can be shrunk when graph edges expose the bits necessary for management by Set
342
+ emit "\n========================\nLoading Edges\n========================\n From DC? Referenced? Edge \n %s %s %s" % ['--------', '-----------', '-'*45]
343
+ edges.each do |e|
344
+ # don't want to emit edge which is from a Data Connection to a
345
+ # Calculated Field which is also referenced by another calculated field
346
+ isFromDC = e.from.type == :TwbDataConnection
347
+ isRefField = @referencedFields.include?(e.to.id)
348
+ edgesAsStrings.add(e.dot) unless isFromDC && isRefField
349
+ end
350
+ emit "------------------------\n "
351
+ edgesAsStrings.each do |es|
352
+ dotFile.puts " #{es}"
353
+ emit " #{es}"
354
+ end
355
+ emit "========================\n "
356
+ dotFile.puts ""
357
+ dotFile.puts " // }"
358
+ dotFile.puts "\n\n // 4--------------------------------------------------------------------"
359
+ # "table::JIRA_HARVEST_Correspondence__c::Jira" [label="JIRA_HARVEST_Correspondence__c"]
360
+ nodes = SortedSet.new
361
+ edges.each do |e|
362
+ nodes.add e.from.dotLabel
363
+ nodes.add e.to.dotLabel
364
+ end
365
+ nodes.each do |n|
366
+ dotFile.puts n
367
+ end
368
+ dotFile.puts "\n\n // 5--------------------------------------------------------------------"
369
+ emitTypes( edges, dotFile )
370
+ rankRootFields( dotFile, rootFields )
371
+ closeDot( dotFile, twb )
372
+ # renderPng(twb.name,dotFileName)
373
+ # renderPdf(twb.name,dotFileName)
374
+ renderDot(twb,dotFileName,'pdf')
375
+ renderDot(twb,dotFileName,'png')
376
+ renderDot(twb,dotFileName,'svg')
377
+ emitEdges edges
378
+ end
379
+
380
+
381
+ def graphEdges twb, edges
382
+ graphFile = File.new(twb + '.cypher', 'w')
383
+ # graphFile.puts "OKEY DOKE, graphing away"
384
+ cypherCode = Set.new
385
+ edges.each do |edge|
386
+ cypherCode.add edge.from.cypherCreate
387
+ cypherCode.add edge.to.cypherCreate
388
+ cypherCode.add edge.cypherCreate
389
+ end
390
+ cypherCode.each do |cc|
391
+ graphFile.puts cc
392
+ end
393
+ graphFile.puts "\nreturn *"
394
+ graphFile.close unless graphFile.nil?
395
+ @imageFiles << File.basename(graphFile)
396
+ end
397
+
398
+ def emitEdges edges
399
+ emit " %-15s %s" % ['type', 'Edge']
400
+ emit " %-15s %s" % ['-'*15, '-'*35]
401
+ edges.each do |edge|
402
+ emit " %-15s %s" % [edge.from.type, edge.from]
403
+ emit " %-15s %s" % [edge.to.type, edge.to]
404
+ emit "\n "
405
+ end
406
+ end
407
+
408
+ def emitTypes edges, dotFile
409
+ typedNodes = {}
410
+ dotFile.puts "\n\n // 2--------------------------------------------------------------------"
411
+ edges.each do |edge|
412
+ emit " EDGE :: #{edge}"
413
+ loadNodeType typedNodes, edge.from
414
+ loadNodeType typedNodes, edge.to
415
+ end
416
+ typedNodes.each do |type, nodes|
417
+ emit "+++++++++ typedNodes of '#{type}'' "
418
+ nodes.each do |node|
419
+ emit " -n- #{node}"
420
+ end
421
+ rankSame(dotFile, type, nodes) unless type == :CalculatedField
422
+ end
423
+ # labelTypes dotFile, edges
424
+ end
425
+
426
+ def loadNodeType set, node
427
+ type = node.type
428
+ set[type] = Set.new unless set.include? type
429
+ set[type].add node
430
+ end
431
+
432
+ def rankSame dotFile, type, nodes
433
+ dotFile.puts "\n // '#{type}' --------------------------------------------------------------------"
434
+ dotFile.puts "\n {rank=same "
435
+ # dotFile.puts " \"#{type}\" [shape=\"box3d\" style=\"filled\" ]" unless ''.eql? type # [shape=\"box3d\" style=\"filled\" ]\"" unless label.equal? ''
436
+ nodes.each do |node|
437
+ dotFile.puts " \"#{node.id}\""
438
+ end
439
+ dotFile.puts " }"
440
+ end
441
+
442
+ def rankRootFields dotFile, dsRootFields
443
+ dotFile.puts "\n // Unreferenced (root) Calculated Fields -----------------------------------------"
444
+ dotFile.puts "\n {rank=same "
445
+ dsRootFields.each do |rf|
446
+ dotFile.puts " \"#{rf}\""
447
+ end
448
+ dotFile.puts " }"
449
+ end
450
+
451
+
452
+ def labelTypes dotFile, edges
453
+ fromTos = Set.new
454
+ edges.each do |edge|
455
+ # fromTos.add "\"Alien Data Source\" -> \"Alien Data Source\""
456
+ fromTos.add "\"#{edge.from.type}\""
457
+ fromTos.add "\"#{edge.to.type}\""
458
+ end
459
+ return if fromTos.empty?
460
+ dotFile.puts "\n // 3--------------------------------------------------------------------"
461
+ dotFile.puts ' subgraph cluster_0 {'
462
+ dotFile.puts ' color=white;'
463
+ dotFile.puts ' node [shape="box3d" style="filled" ];'
464
+ fromTos.each do |ft|
465
+ dotFile.puts " #{ft}"
466
+ end
467
+ dotFile.puts ' }'
468
+ end
469
+
470
+
471
+ def emit(local=@localEmit, stuff)
472
+ # puts "\nstuff.class #{stuff.class} :: #{stuff}" if local
473
+ if stuff.is_a? String then
474
+ lines = stuff.split(/\n/)
475
+ lines.each do |line|
476
+ @logger.debug "#{@emitPrefix}#{line}"
477
+ puts "#{@emitPrefix}#{line}" if local
478
+ end
479
+ else
480
+ @logger.debug "#{@emitPrefix}#{stuff}"
481
+ puts "#{@emitPrefix}#{stuff}" if local
482
+ end
483
+ end
484
+
485
+
486
+ def initDot twb
487
+ dotFile = File.open("#{twb}#{@@processName}.dot",'w')
488
+ dotFile.puts @@dotHeader
489
+ return dotFile
490
+ end
491
+
492
+ def closeDot dotFile, twb
493
+ dotFile.puts ' '
494
+ dotFile.puts '// -------------------------------------------------------------'
495
+ dotFile.puts ' '
496
+ dotFile.puts ' subgraph cluster_1 {'
497
+ # dotFile.puts ' color=white;'
498
+ dotFile.puts ' style=invis;'
499
+ # dotFile.puts ' border=0;'
500
+ dotFile.puts ' node [border=blue];'
501
+ dotFile.puts ' '
502
+ dotFile.puts ' "" [style=invis]'
503
+ dotFile.puts " \"Tableau Tools\\nCalculated Fields Map\\nWorkbook '#{twb}'\\n#{Time.new.ctime}\" [penwidth=0]"
504
+ # dotFile.puts " \"Tableau Tools Workbook Calculated Fields Map\\n#{Time.new.ctime}\" -> \"\" [style=invis]"
505
+ dotFile.puts ' '
506
+ dotFile.puts ' }'
507
+ dotFile.puts ' '
508
+ dotFile.puts '}'
509
+ dotFile.close
510
+ end
511
+
512
+
513
+ def renderDot twb, dot, format
514
+ emit "Rendering DOT file\n - #{twb}\n - #{dot}\n - #{format}"
515
+ imageType = '-T' + format
516
+ imageFile = twb + @@processName + 'Graph.' + format
517
+ imageParam = '-o' + imageFile
518
+ emit "system #{@@gvDotLocation} #{imageType} #{imageParam} #{dot}"
519
+ # system @@gvDotLocation, imageType, imageParam, dot
520
+ @imageFiles << imageFile
521
+ return imageFile
522
+ end
523
+
524
+ end # class
525
+
526
+ end # module Analysis
527
+ end # module Twb