@rws-framework/ai-tools 0.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.
- package/.bin/add-v.sh +10 -0
- package/.bin/emerge.sh +11 -0
- package/.emerge-vis-output/rws-server/emerge-file_result_dependency_graph.graphml +2067 -0
- package/.emerge-vis-output/rws-server/emerge-filesystem_graph.graphml +1505 -0
- package/.emerge-vis-output/rws-server/emerge-statistics-and-metrics.json +1 -0
- package/.emerge-vis-output/rws-server/emerge-statistics-metrics.txt +1147 -0
- package/.emerge-vis-output/rws-server/html/emerge.html +501 -0
- package/.emerge-vis-output/rws-server/html/jsconfig.json +9 -0
- package/.emerge-vis-output/rws-server/html/resources/css/custom.css +211 -0
- package/.emerge-vis-output/rws-server/html/resources/js/emerge_common.js +45 -0
- package/.emerge-vis-output/rws-server/html/resources/js/emerge_data.js +13 -0
- package/.emerge-vis-output/rws-server/html/resources/js/emerge_git.js +1414 -0
- package/.emerge-vis-output/rws-server/html/resources/js/emerge_graph.js +539 -0
- package/.emerge-vis-output/rws-server/html/resources/js/emerge_heatmap.js +220 -0
- package/.emerge-vis-output/rws-server/html/resources/js/emerge_hull.js +180 -0
- package/.emerge-vis-output/rws-server/html/resources/js/emerge_main.js +1003 -0
- package/.emerge-vis-output/rws-server/html/resources/js/emerge_search.js +71 -0
- package/.emerge-vis-output/rws-server/html/resources/js/emerge_ui.js +199 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-grid.css +4124 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-grid.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-grid.min.css +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-grid.min.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-grid.rtl.css +4123 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-grid.rtl.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-grid.rtl.min.css +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-grid.rtl.min.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-reboot.css +488 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-reboot.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-reboot.min.css +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-reboot.min.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-reboot.rtl.css +485 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-reboot.rtl.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-reboot.rtl.min.css +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-reboot.rtl.min.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-utilities.css +4266 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-utilities.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-utilities.min.css +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-utilities.min.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-utilities.rtl.css +4257 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-utilities.rtl.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-utilities.rtl.min.css +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap-utilities.rtl.min.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap.css +10878 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap.min.css +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap.min.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap.rtl.css +10842 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap.rtl.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap.rtl.min.css +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/css/bootstrap.rtl.min.css.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.bundle.js +7075 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.bundle.js.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.bundle.min.js +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.bundle.min.js.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.esm.js +5202 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.esm.js.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.esm.min.js +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.esm.min.js.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.js +5249 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.js.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.min.js +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/bootstrap/js/bootstrap.min.js.map +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/d3/d3.v7.8.4.min.js +2 -0
- package/.emerge-vis-output/rws-server/html/vendors/d3/d3.v7.min.js +2 -0
- package/.emerge-vis-output/rws-server/html/vendors/dark-mode-switch/css/dark-mode.css +148 -0
- package/.emerge-vis-output/rws-server/html/vendors/dark-mode-switch/js/dark-mode-switch.min.js +1 -0
- package/.emerge-vis-output/rws-server/html/vendors/daterangepicker/daterangepicker.css +410 -0
- package/.emerge-vis-output/rws-server/html/vendors/daterangepicker/daterangepicker.min.js +8 -0
- package/.emerge-vis-output/rws-server/html/vendors/daterangepicker/moment.min.js +7 -0
- package/.emerge-vis-output/rws-server/html/vendors/hull/hull.js +373 -0
- package/.emerge-vis-output/rws-server/html/vendors/jquery/jquery-3.6.0.min.js +2 -0
- package/.emerge-vis-output/rws-server/html/vendors/popper/popper.min.js +6 -0
- package/.emerge-vis-output/rws-server/html/vendors/simpleheat/simpleheat.js +141 -0
- package/.eslintrc.json +53 -0
- package/README.md +862 -0
- package/package.json +49 -0
- package/src/index.ts +22 -0
- package/src/models/convo/ConvoLoader.ts +415 -0
- package/src/models/convo/VectorStore.ts +33 -0
- package/src/models/prompts/_prompt.ts +388 -0
- package/src/services/VectorStoreService.ts +15 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,1414 @@
|
|
|
1
|
+
let changeCouplingMapForDateRange = {}
|
|
2
|
+
|
|
3
|
+
function clickDateRangePickerCancel() {
|
|
4
|
+
console.log("reset date range picker")
|
|
5
|
+
if (includeGitMetrics) {
|
|
6
|
+
initDateRangeUI()
|
|
7
|
+
dateRangePickerFrom = commit_first_date
|
|
8
|
+
dateRangePickerTo = commit_last_date
|
|
9
|
+
initGitMetricsForDateRange()
|
|
10
|
+
showToastDateRangeUpdate()
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function initGitMetricsForDateRange() {
|
|
15
|
+
gitMetricsIndexFrom = commit_dates.indexOf( dateRangePickerFrom );
|
|
16
|
+
gitMetricsIndexTo = commit_dates.lastIndexOf( dateRangePickerTo );
|
|
17
|
+
// console.log("found first index: " + gitMetricsIndexFrom + " for `from` " + dateRangePickerFrom)
|
|
18
|
+
// console.log("found last index: " + gitMetricsIndexTo + " for `to` " + dateRangePickerTo)
|
|
19
|
+
changeCouplingMapForDateRange = calculateCouplingForDateRange()
|
|
20
|
+
// console.log(changeCouplingMapForDateRange)
|
|
21
|
+
// console.log(changeCouplingMapForDateRange)
|
|
22
|
+
addGitMetricToFileNodes()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nodeNamesHaveChangeCoupling(sourceName, targetName) {
|
|
26
|
+
for (const [key, value] of Object.entries(changeCouplingMapForDateRange)) {
|
|
27
|
+
if (sourceName.includes( key ) ) {
|
|
28
|
+
for (const k of changeCouplingMapForDateRange[key]) {
|
|
29
|
+
if (targetName.includes(k)) {
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function calculateCouplingForDateRange() {
|
|
40
|
+
let totalChangeCouplingDict = {}
|
|
41
|
+
|
|
42
|
+
for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) {
|
|
43
|
+
let couplingLinks = commit_metrics[i].links
|
|
44
|
+
|
|
45
|
+
if (couplingLinks.length > 0) {
|
|
46
|
+
for (nextChangeCouplingDict of couplingLinks) {
|
|
47
|
+
|
|
48
|
+
// source -> target
|
|
49
|
+
if ( !(nextChangeCouplingDict.source in totalChangeCouplingDict) ) {
|
|
50
|
+
totalChangeCouplingDict[nextChangeCouplingDict.source] = new Set();
|
|
51
|
+
totalChangeCouplingDict[nextChangeCouplingDict.source].add(nextChangeCouplingDict.target)
|
|
52
|
+
} else {
|
|
53
|
+
totalChangeCouplingDict[nextChangeCouplingDict.source].add(nextChangeCouplingDict.target)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// target -> source
|
|
57
|
+
if ( !(nextChangeCouplingDict.target in totalChangeCouplingDict) ) {
|
|
58
|
+
totalChangeCouplingDict[nextChangeCouplingDict.target] = new Set();
|
|
59
|
+
totalChangeCouplingDict[nextChangeCouplingDict.target].add(nextChangeCouplingDict.source)
|
|
60
|
+
} else {
|
|
61
|
+
totalChangeCouplingDict[nextChangeCouplingDict.target].add(nextChangeCouplingDict.source)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return totalChangeCouplingDict
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function calculateFileChurnForDateRange() {
|
|
70
|
+
let totalFileChurnDict = {}
|
|
71
|
+
|
|
72
|
+
for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) {
|
|
73
|
+
let nextChurnDict = commit_metrics[i].churn
|
|
74
|
+
totalFileChurnDict = mergeDicts(totalFileChurnDict, nextChurnDict)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return totalFileChurnDict
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function calculateSlocForDateRange() {
|
|
81
|
+
let totalSlocDict = {}
|
|
82
|
+
|
|
83
|
+
for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) {
|
|
84
|
+
let nextSlocDict = commit_metrics[i].sloc
|
|
85
|
+
totalSlocDict = mergeDictsToMostCurrentValues(totalSlocDict, nextSlocDict)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return totalSlocDict
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function calculateWhiteSpaceComplexityForDateRange() {
|
|
92
|
+
let totalWhiteSpaceComplexityDict = {}
|
|
93
|
+
|
|
94
|
+
for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) {
|
|
95
|
+
let nextWhiteSpaceComplexityDict = commit_metrics[i].ws_complexity
|
|
96
|
+
totalWhiteSpaceComplexityDict = mergeDictsToMostCurrentValues(totalWhiteSpaceComplexityDict, nextWhiteSpaceComplexityDict)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return totalWhiteSpaceComplexityDict
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function calculateAuthorsForDateRange() {
|
|
103
|
+
let totalFileAuthorsDict = {}
|
|
104
|
+
|
|
105
|
+
for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) {
|
|
106
|
+
let nextFileAuthorsDict = commit_metrics[i].files_author_map
|
|
107
|
+
totalFileAuthorsDict = mergeDicts(totalFileAuthorsDict, nextFileAuthorsDict)
|
|
108
|
+
}
|
|
109
|
+
return totalFileAuthorsDict
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function addGitMetricToFileNodes() {
|
|
113
|
+
if (currentGraphType.includes('file_result_dependency_graph')) {
|
|
114
|
+
let fileChurnMap = calculateFileChurnForDateRange()
|
|
115
|
+
let whiteSpaceComplexityMap = calculateWhiteSpaceComplexityForDateRange()
|
|
116
|
+
let slocMap = calculateSlocForDateRange()
|
|
117
|
+
let authorsMap = calculateAuthorsForDateRange()
|
|
118
|
+
|
|
119
|
+
// console.log(authorsMap)
|
|
120
|
+
// console.log(whiteSpaceComplexityMap)
|
|
121
|
+
// console.log("fileResultPrefix: " + fileResultPrefix)
|
|
122
|
+
|
|
123
|
+
currentGraph.nodes.forEach(function(node, i) {
|
|
124
|
+
|
|
125
|
+
// housekeeping git code churn
|
|
126
|
+
delete node['metric_git_code_churn']
|
|
127
|
+
if (node.hasOwnProperty('metrics')) {
|
|
128
|
+
delete node.metrics['metric_git_code_churn']
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// housekeeping git ws complexity
|
|
132
|
+
delete node['metric_git_ws_complexity']
|
|
133
|
+
if (node.hasOwnProperty('metrics')) {
|
|
134
|
+
delete node.metrics['metric_git_ws_complexity']
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// housekeeping git number of file authors
|
|
138
|
+
delete node['metric_git_number_authors']
|
|
139
|
+
if (node.hasOwnProperty('metrics')) {
|
|
140
|
+
delete node.metrics['metric_git_number_authors']
|
|
141
|
+
}
|
|
142
|
+
delete node['metric_git_main_contrib']
|
|
143
|
+
if (node.hasOwnProperty('metrics')) {
|
|
144
|
+
delete node.metrics['metric_git_main_contrib']
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// housekeeping file contributors
|
|
148
|
+
delete node['metric_git_contributors']
|
|
149
|
+
if (node.hasOwnProperty('metrics')) {
|
|
150
|
+
delete node.metrics['metric_git_contributors']
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// housekeeping git sloc
|
|
154
|
+
delete node['metric_git_sloc']
|
|
155
|
+
if (node.hasOwnProperty('metrics')) {
|
|
156
|
+
delete node.metrics['metric_git_sloc']
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!node.hasOwnProperty('metrics')) {
|
|
160
|
+
node.metrics = {}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let nodeFileName = node.id.split("/").pop();
|
|
164
|
+
|
|
165
|
+
let nodeSearchPath = ""
|
|
166
|
+
if (fileResultPrefix === "") {
|
|
167
|
+
nodeSearchPath = node.id
|
|
168
|
+
} else {
|
|
169
|
+
nodeSearchPath = fileResultPrefix + "/" + node.id
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// add git code churn
|
|
173
|
+
for (const [key, value] of Object.entries(fileChurnMap)) {
|
|
174
|
+
if (nodeSearchPath.includes(key)) {
|
|
175
|
+
node['metric_git_code_churn'] = value
|
|
176
|
+
node.metrics['metric_git_code_churn'] = value
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// add git whitespace complexity
|
|
181
|
+
for (const [key, value] of Object.entries(whiteSpaceComplexityMap)) {
|
|
182
|
+
if (nodeSearchPath.includes(key)) {
|
|
183
|
+
node['metric_git_ws_complexity'] = value
|
|
184
|
+
node.metrics['metric_git_ws_complexity'] = value
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// add git sloc
|
|
189
|
+
for (const [key, value] of Object.entries(slocMap)) {
|
|
190
|
+
if (nodeSearchPath.includes(key)) {
|
|
191
|
+
node['metric_git_sloc'] = value
|
|
192
|
+
node.metrics['metric_git_sloc'] = value
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// add git number authors
|
|
197
|
+
for (const [key, value] of Object.entries(authorsMap)) {
|
|
198
|
+
if (nodeSearchPath.includes(key)) {
|
|
199
|
+
node['metric_git_contributors'] = value
|
|
200
|
+
node.metrics['metric_git_contributors'] = value
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// add all git contributors to file
|
|
205
|
+
for (const [key, value] of Object.entries(authorsMap)) {
|
|
206
|
+
if (nodeSearchPath.includes(key)) {
|
|
207
|
+
node['metric_git_contributors'] = Object.keys(value)
|
|
208
|
+
node.metrics['metric_git_contributors'] = Object.keys(value)
|
|
209
|
+
node['metric_git_number_authors'] = Object.keys(value).length
|
|
210
|
+
node.metrics['metric_git_number_authors'] = Object.keys(value).length
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function mainContributor(obj={}, asc=true) {
|
|
219
|
+
let biggestChurn = 0
|
|
220
|
+
let authorBiggestChurn = ''
|
|
221
|
+
|
|
222
|
+
for (let key in obj) {
|
|
223
|
+
if (obj[key] > biggestChurn) {
|
|
224
|
+
biggestChurn = obj[key]
|
|
225
|
+
authorBiggestChurn = key
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return authorBiggestChurn
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// daterangepicker for git date range
|
|
233
|
+
function initDateRangeUI() {
|
|
234
|
+
$('input[name="daterange"]').daterangepicker({
|
|
235
|
+
"startDate": commit_first_date,
|
|
236
|
+
"endDate": commit_last_date,
|
|
237
|
+
"minDate": commit_first_date,
|
|
238
|
+
"maxDate": commit_last_date,
|
|
239
|
+
|
|
240
|
+
// ranges: {
|
|
241
|
+
// 'Last 3 days': [moment().subtract(2, 'days'), moment()],
|
|
242
|
+
// 'Last 10 days': [moment().subtract(9, 'days'), moment()],
|
|
243
|
+
// 'Last 30 days': [moment().subtract(29, 'days'), moment()],
|
|
244
|
+
// 'Last 60 days': [moment().subtract(59, 'days'), moment()],
|
|
245
|
+
// 'This month': [moment().startOf('month'), moment().endOf('month')],
|
|
246
|
+
// 'Last month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
|
|
247
|
+
// },
|
|
248
|
+
|
|
249
|
+
isInvalidDate: function(date) {
|
|
250
|
+
if ( commit_dates.includes(date.format('DD/MM/YYYY')) ) {
|
|
251
|
+
return false
|
|
252
|
+
} else {
|
|
253
|
+
return true
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
"locale": {
|
|
258
|
+
"format": "DD/MM/YYYY",
|
|
259
|
+
"separator": " - ",
|
|
260
|
+
"applyLabel": "Apply",
|
|
261
|
+
"cancelLabel": "Cancel",
|
|
262
|
+
"fromLabel": "From",
|
|
263
|
+
"toLabel": "To",
|
|
264
|
+
"customRangeLabel": "Custom",
|
|
265
|
+
"weekLabel": "W",
|
|
266
|
+
"daysOfWeek": [
|
|
267
|
+
"Su",
|
|
268
|
+
"Mo",
|
|
269
|
+
"Tu",
|
|
270
|
+
"We",
|
|
271
|
+
"Th",
|
|
272
|
+
"Fr",
|
|
273
|
+
"Sa"
|
|
274
|
+
],
|
|
275
|
+
"monthNames": [
|
|
276
|
+
"January",
|
|
277
|
+
"February",
|
|
278
|
+
"March",
|
|
279
|
+
"April",
|
|
280
|
+
"May",
|
|
281
|
+
"June",
|
|
282
|
+
"July",
|
|
283
|
+
"August",
|
|
284
|
+
"September",
|
|
285
|
+
"October",
|
|
286
|
+
"November",
|
|
287
|
+
"December"
|
|
288
|
+
],
|
|
289
|
+
"firstDay": 1
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
opens: 'left'
|
|
293
|
+
}, function(start, end, label) {
|
|
294
|
+
console.log("A new date selection was made: " + start.format('DD/MM/YYYY') + ' to ' + end.format('DD/MM/YYYY'));
|
|
295
|
+
dateRangePickerFrom = start.format('DD/MM/YYYY')
|
|
296
|
+
dateRangePickerTo = end.format('DD/MM/YYYY')
|
|
297
|
+
|
|
298
|
+
// console.log(dateRangePickerFrom)
|
|
299
|
+
// console.log(dateRangePickerTo)
|
|
300
|
+
|
|
301
|
+
initGitMetricsForDateRange()
|
|
302
|
+
showToastDateRangeUpdate()
|
|
303
|
+
})
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function showToastDateRangeUpdate() {
|
|
307
|
+
const toastLiveExample = document.getElementById('toastDateRangeUpdated')
|
|
308
|
+
const toast = new bootstrap.Toast(toastLiveExample)
|
|
309
|
+
toast.show()
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Copyright 2021 Observable, Inc.
|
|
313
|
+
// Released under the ISC license.
|
|
314
|
+
// https://observablehq.com/@d3/multi-line-chart
|
|
315
|
+
function LineChart(data, {
|
|
316
|
+
x = ([x]) => x, // given d in data, returns the (temporal) x-value
|
|
317
|
+
y = ([, y]) => y, // given d in data, returns the (quantitative) y-value
|
|
318
|
+
z = () => 1, // given d in data, returns the (categorical) z-value
|
|
319
|
+
title, // given d in data, returns the title text
|
|
320
|
+
defined, // for gaps in data
|
|
321
|
+
curve = d3.curveLinear, // method of interpolation between points
|
|
322
|
+
marginTop = 20, // top margin, in pixels
|
|
323
|
+
marginRight = 30, // right margin, in pixels
|
|
324
|
+
marginBottom = 30, // bottom margin, in pixels
|
|
325
|
+
marginLeft = 40, // left margin, in pixels
|
|
326
|
+
width = 640, // outer width, in pixels
|
|
327
|
+
height = 400, // outer height, in pixels
|
|
328
|
+
xType = d3.scaleUtc, // type of x-scale
|
|
329
|
+
xDomain, // [xmin, xmax]
|
|
330
|
+
xRange = [marginLeft, width - marginRight], // [left, right]
|
|
331
|
+
yType = d3.scaleLinear, // type of y-scale
|
|
332
|
+
yDomain, // [ymin, ymax]
|
|
333
|
+
yRange = [height - marginBottom, marginTop], // [bottom, top]
|
|
334
|
+
yFormat, // a format specifier string for the y-axis
|
|
335
|
+
yLabel, // a label for the y-axis
|
|
336
|
+
zDomain, // array of z-values
|
|
337
|
+
color = "currentColor", // stroke color of line, as a constant or a function of *z*
|
|
338
|
+
strokeLinecap, // stroke line cap of line
|
|
339
|
+
strokeLinejoin, // stroke line join of line
|
|
340
|
+
strokeWidth = 1.0, // stroke width of line
|
|
341
|
+
strokeOpacity, // stroke opacity of line
|
|
342
|
+
mixBlendMode = "multiply", // blend mode of lines
|
|
343
|
+
voronoi, // show a Voronoi overlay? (for debugging)
|
|
344
|
+
id
|
|
345
|
+
} = {}) {
|
|
346
|
+
// Compute values.
|
|
347
|
+
|
|
348
|
+
let textColor = "#FFF"
|
|
349
|
+
if (!darkMode) { textColor = "#333333"}
|
|
350
|
+
|
|
351
|
+
const X = d3.map(data, x);
|
|
352
|
+
const Y = d3.map(data, y);
|
|
353
|
+
const Z = d3.map(data, z);
|
|
354
|
+
const O = d3.map(data, d => d);
|
|
355
|
+
if (defined === undefined) defined = (d, i) => !isNaN(X[i]) && !isNaN(Y[i]) ;
|
|
356
|
+
const D = d3.map(data, defined);
|
|
357
|
+
|
|
358
|
+
// Compute default domains, and unique the z-domain.
|
|
359
|
+
if (xDomain === undefined) xDomain = d3.extent(X);
|
|
360
|
+
if (yDomain === undefined) yDomain = [0, d3.max(Y, d => typeof d === "string" ? +d : d)];
|
|
361
|
+
if (zDomain === undefined) zDomain = Z;
|
|
362
|
+
zDomain = new d3.InternSet(zDomain);
|
|
363
|
+
|
|
364
|
+
// Omit any data not present in the z-domain.
|
|
365
|
+
const I = d3.range(X.length).filter(i => zDomain.has(Z[i]));
|
|
366
|
+
|
|
367
|
+
// Construct scales and axes.
|
|
368
|
+
const xScale = xType(xDomain, xRange);
|
|
369
|
+
const yScale = yType(yDomain, yRange);
|
|
370
|
+
const xAxis = d3.axisBottom(xScale).ticks(width / 80).tickSizeOuter(0);
|
|
371
|
+
const yAxis = d3.axisLeft(yScale).ticks(height / 60, yFormat);
|
|
372
|
+
|
|
373
|
+
// Compute titles.
|
|
374
|
+
const T = title === undefined ? Z : title === null ? null : d3.map(data, title);
|
|
375
|
+
|
|
376
|
+
// Construct a line generator.
|
|
377
|
+
const line = d3.line()
|
|
378
|
+
.defined(i => D[i])
|
|
379
|
+
.curve(curve)
|
|
380
|
+
.x(i => xScale(X[i]))
|
|
381
|
+
.y(i => yScale(Y[i]));
|
|
382
|
+
|
|
383
|
+
const svg = d3.create("svg")
|
|
384
|
+
.attr("id", id)
|
|
385
|
+
.attr("width", width)
|
|
386
|
+
.attr("height", height)
|
|
387
|
+
.attr("viewBox", [0, 0, width, height])
|
|
388
|
+
.attr("style", "max-width: 100%; height: auto; height: intrinsic;")
|
|
389
|
+
.style("-webkit-tap-highlight-color", "transparent")
|
|
390
|
+
.on("pointerenter", pointerentered)
|
|
391
|
+
.on("pointermove", pointermoved)
|
|
392
|
+
.on("pointerleave", pointerleft)
|
|
393
|
+
.on("touchstart", event => event.preventDefault());
|
|
394
|
+
|
|
395
|
+
// An optional Voronoi display (for fun).
|
|
396
|
+
if (voronoi) svg.append("path")
|
|
397
|
+
.attr("fill", "none")
|
|
398
|
+
.attr("stroke", "#ccc")
|
|
399
|
+
.attr("d", d3.Delaunay
|
|
400
|
+
.from(I, i => xScale(X[i]), i => yScale(Y[i]))
|
|
401
|
+
.voronoi([0, 0, width, height])
|
|
402
|
+
.render());
|
|
403
|
+
|
|
404
|
+
svg.append("g")
|
|
405
|
+
.attr("transform", `translate(0,${height - marginBottom})`)
|
|
406
|
+
.call(xAxis);
|
|
407
|
+
|
|
408
|
+
svg.append("g")
|
|
409
|
+
.attr("transform", `translate(${marginLeft},0)`)
|
|
410
|
+
.call(yAxis)
|
|
411
|
+
.call(g => g.select(".domain").remove())
|
|
412
|
+
.call(voronoi ? () => {} : g => g.selectAll(".tick line").clone()
|
|
413
|
+
.attr("x2", width - marginLeft - marginRight)
|
|
414
|
+
.attr("stroke-opacity", 0.1))
|
|
415
|
+
.call(g => g.append("text")
|
|
416
|
+
.attr("x", -marginLeft)
|
|
417
|
+
.attr("y", 10)
|
|
418
|
+
.attr("fill", "currentColor")
|
|
419
|
+
.attr("text-anchor", "start")
|
|
420
|
+
.text(yLabel));
|
|
421
|
+
|
|
422
|
+
const path = svg.append("g")
|
|
423
|
+
.attr("fill", "none")
|
|
424
|
+
.attr("stroke", typeof color === "string" ? color : null)
|
|
425
|
+
.attr("stroke-linecap", strokeLinecap)
|
|
426
|
+
.attr("stroke-linejoin", strokeLinejoin)
|
|
427
|
+
.attr("stroke-width", strokeWidth)
|
|
428
|
+
.attr("stroke-opacity", strokeOpacity)
|
|
429
|
+
.selectAll("path")
|
|
430
|
+
.data(d3.group(I, i => Z[i]))
|
|
431
|
+
.join("path")
|
|
432
|
+
//.style("mix-blend-mode", mixBlendMode)
|
|
433
|
+
.attr("stroke", typeof color === "function" ? ([z]) => color(z) : null)
|
|
434
|
+
.attr("d", ([, I]) => line(I));
|
|
435
|
+
|
|
436
|
+
const dot = svg.append("g")
|
|
437
|
+
.attr("display", "none")
|
|
438
|
+
.attr("fill", "red");
|
|
439
|
+
|
|
440
|
+
dot.append("circle")
|
|
441
|
+
.attr("r", 2.5);
|
|
442
|
+
|
|
443
|
+
dot.append("text")
|
|
444
|
+
.attr("font-family", "sans-serif")
|
|
445
|
+
.attr("font-size", 10)
|
|
446
|
+
.attr("text-anchor", "middle")
|
|
447
|
+
.attr("fill", textColor)
|
|
448
|
+
.attr("y", -8);
|
|
449
|
+
|
|
450
|
+
function pointermoved(event) {
|
|
451
|
+
let strokeColor = "#323232"
|
|
452
|
+
if (!darkMode) { strokeColor = "#828282"}
|
|
453
|
+
|
|
454
|
+
const [xm, ym] = d3.pointer(event);
|
|
455
|
+
const i = d3.least(I, i => Math.hypot(xScale(X[i]) - xm, yScale(Y[i]) - ym)); // closest point
|
|
456
|
+
path.style("stroke", ([z]) => Z[i] === z ? null : strokeColor).filter(([z]) => Z[i] === z).raise();
|
|
457
|
+
dot.attr("transform", `translate(${xScale(X[i])},${yScale(Y[i])})`);
|
|
458
|
+
if (T) dot.select("text").text(T[i]);
|
|
459
|
+
svg.property("value", O[i]).dispatch("input", {bubbles: true});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function pointerentered() {
|
|
463
|
+
path.style("mix-blend-mode", null).style("stroke", "lightyellow");
|
|
464
|
+
dot.attr("display", null);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function pointerleft() {
|
|
468
|
+
//path.style("mix-blend-mode", mixBlendMode).style("stroke", null);
|
|
469
|
+
path.style("stroke", "lightsteelblue")
|
|
470
|
+
dot.attr("display", "none");
|
|
471
|
+
svg.node().value = null;
|
|
472
|
+
svg.dispatch("input", {bubbles: true});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return Object.assign(svg.node(), {value: null});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function generateTimeSeriesChart() {
|
|
479
|
+
|
|
480
|
+
let timeSeriesComplexityTotal = {}
|
|
481
|
+
let timeSeriesSlocTotal = {}
|
|
482
|
+
let timeSeriesChurnTotal = {}
|
|
483
|
+
let timeSeriesComplexity = []
|
|
484
|
+
let timeSeriesSloc = []
|
|
485
|
+
let timeSeriesChurn = []
|
|
486
|
+
|
|
487
|
+
for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) {
|
|
488
|
+
|
|
489
|
+
// prepare complexity data for chart
|
|
490
|
+
for (const [key, value] of Object.entries(commit_metrics[i].ws_complexity)) {
|
|
491
|
+
|
|
492
|
+
let filter = true
|
|
493
|
+
if (Object.keys(selectedNodesMap).length > 0) {
|
|
494
|
+
for (const [selectedNode, v] of Object.entries(selectedNodesMap)) {
|
|
495
|
+
if (selectedNode.includes(key.toLowerCase())) {
|
|
496
|
+
filter = false
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
filter = false
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (filter == true) {
|
|
504
|
+
continue
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
for (const [file, complexity] of Object.entries(timeSeriesComplexityTotal)) {
|
|
508
|
+
if (file !== key) {
|
|
509
|
+
timeSeriesComplexity.push(
|
|
510
|
+
{
|
|
511
|
+
'filepath' : file,
|
|
512
|
+
'wscomplexity' : complexity,
|
|
513
|
+
'date': commit_metrics[i].exact_date.replace(/_/g, "-")
|
|
514
|
+
}
|
|
515
|
+
)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
timeSeriesComplexityTotal[key] = value
|
|
520
|
+
|
|
521
|
+
let timeSeriesComplexityEntry = {
|
|
522
|
+
'filepath' : key,
|
|
523
|
+
'wscomplexity' : value,
|
|
524
|
+
'date': commit_metrics[i].exact_date.replace(/_/g, "-") // TODO ...
|
|
525
|
+
}
|
|
526
|
+
timeSeriesComplexity.push(timeSeriesComplexityEntry)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// prepare sloc data for chart
|
|
530
|
+
for (const [key, value] of Object.entries(commit_metrics[i].sloc)) {
|
|
531
|
+
|
|
532
|
+
let filter = true
|
|
533
|
+
if (Object.keys(selectedNodesMap).length > 0) {
|
|
534
|
+
for (const [selectedNode, v] of Object.entries(selectedNodesMap)) {
|
|
535
|
+
if (selectedNode.includes(key.toLowerCase())) {
|
|
536
|
+
filter = false
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
} else {
|
|
540
|
+
filter = false
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (filter == true) {
|
|
544
|
+
continue
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
for (const [file, sloc] of Object.entries(timeSeriesSlocTotal)) {
|
|
548
|
+
if (file !== key) {
|
|
549
|
+
timeSeriesSloc.push(
|
|
550
|
+
{
|
|
551
|
+
'filepath' : file,
|
|
552
|
+
'sloc' : sloc,
|
|
553
|
+
'date': commit_metrics[i].exact_date.replace(/_/g, "-")
|
|
554
|
+
}
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
timeSeriesSlocTotal[key] = value
|
|
560
|
+
|
|
561
|
+
let timeSeriesSlocEntry = {
|
|
562
|
+
'filepath' : key,
|
|
563
|
+
'sloc' : value,
|
|
564
|
+
'date': commit_metrics[i].exact_date.replace(/_/g, "-") // TODO ...
|
|
565
|
+
}
|
|
566
|
+
timeSeriesSloc.push(timeSeriesSlocEntry)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
// prepare churn data for chart
|
|
571
|
+
for (const [key, value] of Object.entries(commit_metrics[i].churn)) {
|
|
572
|
+
let filter = true
|
|
573
|
+
if (Object.keys(selectedNodesMap).length > 0) {
|
|
574
|
+
for (const [selectedNode, v] of Object.entries(selectedNodesMap)) {
|
|
575
|
+
if (selectedNode.includes(key.toLowerCase())) {
|
|
576
|
+
filter = false
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
filter = false
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (filter == true) {
|
|
584
|
+
continue
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
for (const [file, churn] of Object.entries(timeSeriesChurnTotal)) {
|
|
588
|
+
if (file !== key) {
|
|
589
|
+
timeSeriesChurn.push(
|
|
590
|
+
{
|
|
591
|
+
'filepath' : file,
|
|
592
|
+
'churn' : churn,
|
|
593
|
+
'date': commit_metrics[i].exact_date.replace(/_/g, "-")
|
|
594
|
+
}
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
timeSeriesChurnTotal[key] = value
|
|
600
|
+
|
|
601
|
+
let timeSeriesChurnEntry = {
|
|
602
|
+
'filepath' : key,
|
|
603
|
+
'churn' : value,
|
|
604
|
+
'date': commit_metrics[i].exact_date.replace(/_/g, "-") // TODO ...
|
|
605
|
+
}
|
|
606
|
+
timeSeriesChurn.push(timeSeriesChurnEntry)
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
let complexityChart = LineChart(timeSeriesComplexity, {
|
|
611
|
+
x: d => Date.parse(d.date),
|
|
612
|
+
y: d => d.wscomplexity,
|
|
613
|
+
z: d => d.filepath,
|
|
614
|
+
yLabel: "Whitespace Complexity",
|
|
615
|
+
width: 1000,
|
|
616
|
+
height: 300,
|
|
617
|
+
color: "lightsteelblue",
|
|
618
|
+
voronoi: false,
|
|
619
|
+
id: "timeSeriesComplexityChart"
|
|
620
|
+
})
|
|
621
|
+
|
|
622
|
+
let slocChart = LineChart(timeSeriesSloc, {
|
|
623
|
+
x: d => Date.parse(d.date),
|
|
624
|
+
y: d => d.sloc,
|
|
625
|
+
z: d => d.filepath,
|
|
626
|
+
yLabel: "SLOC",
|
|
627
|
+
width: 1000,
|
|
628
|
+
height: 300,
|
|
629
|
+
color: "lightsteelblue",
|
|
630
|
+
voronoi: false,
|
|
631
|
+
id: "timeSeriesSlocChart"
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
let churnChart = LineChart(timeSeriesChurn, {
|
|
635
|
+
x: d => Date.parse(d.date),
|
|
636
|
+
y: d => d.churn,
|
|
637
|
+
z: d => d.filepath,
|
|
638
|
+
yLabel: "Code churn",
|
|
639
|
+
width: 1000,
|
|
640
|
+
height: 300,
|
|
641
|
+
color: "lightsteelblue",
|
|
642
|
+
voronoi: false,
|
|
643
|
+
id: "timeSeriesChurnChart"
|
|
644
|
+
})
|
|
645
|
+
|
|
646
|
+
document.getElementById("my_dataviz").appendChild(complexityChart);
|
|
647
|
+
document.getElementById("time_series_sloc").appendChild(slocChart);
|
|
648
|
+
document.getElementById("my_dataviz2").appendChild(churnChart);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function generateChangeCouplingChart() {
|
|
652
|
+
|
|
653
|
+
let flows = []
|
|
654
|
+
let locations = []
|
|
655
|
+
let locationId = 0
|
|
656
|
+
|
|
657
|
+
let locationColorMap = {}
|
|
658
|
+
|
|
659
|
+
for (let i = gitMetricsIndexFrom; i < gitMetricsIndexTo; i++) {
|
|
660
|
+
|
|
661
|
+
// prepare change coupling data for chart
|
|
662
|
+
if (commit_metrics[i].links.length > 0) {
|
|
663
|
+
|
|
664
|
+
for (link of commit_metrics[i].links) {
|
|
665
|
+
|
|
666
|
+
const matchingSourceKey = link.source
|
|
667
|
+
const matchingTargetKey = link.target
|
|
668
|
+
|
|
669
|
+
let filter = true
|
|
670
|
+
if (Object.keys(selectedNodesMap).length > 0) {
|
|
671
|
+
for (const [selectedNode, v] of Object.entries(selectedNodesMap)) {
|
|
672
|
+
if ( selectedNode.includes(matchingSourceKey.toLowerCase()) || selectedNode.includes(matchingTargetKey.toLowerCase()) ) {
|
|
673
|
+
filter = false
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
filter = false
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
if (filter == true) {
|
|
681
|
+
continue
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if ( !(locations.find(e => e.name === matchingSourceKey)) ) {
|
|
685
|
+
|
|
686
|
+
if ( !(matchingSourceKey in locationColorMap) ) {
|
|
687
|
+
let randomColor = "#000000".replace(/0/g,function(){return (~~(Math.random()*16)).toString(16);});
|
|
688
|
+
locationColorMap[matchingSourceKey] = randomColor
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
let location = {
|
|
692
|
+
'id': locationId,
|
|
693
|
+
'name': matchingSourceKey,
|
|
694
|
+
'color': locationColorMap[matchingSourceKey]
|
|
695
|
+
}
|
|
696
|
+
locations.push(location)
|
|
697
|
+
|
|
698
|
+
let flow = {
|
|
699
|
+
'from' : locationId,
|
|
700
|
+
'to' : locationId,
|
|
701
|
+
'quantity': 0
|
|
702
|
+
|
|
703
|
+
}
|
|
704
|
+
flows.push(flow)
|
|
705
|
+
locationId += 1
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
if ( !(locations.find(e => e.name === matchingTargetKey)) ) {
|
|
709
|
+
|
|
710
|
+
if ( !(matchingTargetKey in locationColorMap) ) {
|
|
711
|
+
|
|
712
|
+
// find the corresponding node color
|
|
713
|
+
for (const [key, value] of Object.entries(nodeColorMap)) {
|
|
714
|
+
if (key.includes(matchingTargetKey)) {
|
|
715
|
+
locationColorMap[matchingTargetKey] = value
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// if (matchingTargetKey in nodeColorMap) {
|
|
720
|
+
// locationColorMap[matchingTargetKey] = nodeColorMap[matchingTargetKey]
|
|
721
|
+
// } else {
|
|
722
|
+
// console.log("should not happen")
|
|
723
|
+
// locationColorMap[matchingTargetKey] = '#FFFFFF' // "#000000".replace(/0/g,function(){return (~~(Math.random()*16)).toString(16);});
|
|
724
|
+
// }
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
let location = {
|
|
728
|
+
'id': locationId,
|
|
729
|
+
'name': matchingTargetKey,
|
|
730
|
+
'color': locationColorMap[matchingTargetKey]
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
locations.push(location)
|
|
734
|
+
let flow = {
|
|
735
|
+
'from' : locationId,
|
|
736
|
+
'to' : locationId,
|
|
737
|
+
'quantity': 0
|
|
738
|
+
|
|
739
|
+
}
|
|
740
|
+
flows.push(flow)
|
|
741
|
+
locationId += 1
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const iSource = locations.findIndex(e => e.name === matchingSourceKey);
|
|
745
|
+
const iTarget = locations.findIndex(e => e.name === matchingTargetKey);
|
|
746
|
+
|
|
747
|
+
if (iSource > -1 && iTarget > -1) {
|
|
748
|
+
let flow = {
|
|
749
|
+
'from' : locations[iSource].id,
|
|
750
|
+
'to' : locations[iTarget].id,
|
|
751
|
+
'quantity': 15
|
|
752
|
+
}
|
|
753
|
+
flows.push(flow)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
for (let n = 0; n < locationId; n++) {
|
|
763
|
+
for (let m = 0; m < locationId; m++) {
|
|
764
|
+
if ( !(flows.find(e => e.from === n && e.to === m )) ) {
|
|
765
|
+
let flow = {
|
|
766
|
+
'from' : n,
|
|
767
|
+
'to' : m,
|
|
768
|
+
'quantity': 0
|
|
769
|
+
}
|
|
770
|
+
flows.push(flow)
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// Borrowed from this great blog entry:
|
|
776
|
+
// https://blog.noser.com/d3-js-chord-diagramm-teil-2-benutzerdefinierte-sortierung-und-kurvenformen/
|
|
777
|
+
|
|
778
|
+
var matrix = [];
|
|
779
|
+
|
|
780
|
+
//Map list of data to matrix
|
|
781
|
+
flows.forEach(function (flow) {
|
|
782
|
+
|
|
783
|
+
//Initialize sub-array if not yet exists
|
|
784
|
+
if (!matrix[flow.to]) {
|
|
785
|
+
matrix[flow.to] = [];
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
matrix[flow.to][flow.from] = flow.quantity;
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
/*//////////////////////////////////////////////////////////
|
|
792
|
+
/////////////// Initiate Chord Diagram /////////////////////
|
|
793
|
+
//////////////////////////////////////////////////////////*/
|
|
794
|
+
let size = 900;
|
|
795
|
+
let dr = 40; //radial translation for group names
|
|
796
|
+
let dx = 20; //horizontal translation for group names
|
|
797
|
+
let margin = { top: 0, right: 50, bottom: 50, left: 50 };
|
|
798
|
+
let chordWidth = (size + 200) - margin.left - margin.right;
|
|
799
|
+
let chordHeight = size - margin.top - margin.bottom;
|
|
800
|
+
let innerRadius = Math.min(chordWidth, chordHeight) * .39;
|
|
801
|
+
let outerRadius = innerRadius * 1.08;
|
|
802
|
+
|
|
803
|
+
let root = d3.select("#change_coupling_chord_diagram");
|
|
804
|
+
|
|
805
|
+
//Generate tooltip already, but keep it invisible for now.
|
|
806
|
+
var toolTip = root.append("div")
|
|
807
|
+
.classed("tooltip", true)
|
|
808
|
+
.style("opacity", 0)
|
|
809
|
+
.style("position", "absolute")
|
|
810
|
+
.style("text-align", "center")
|
|
811
|
+
.style("padding", "6px")
|
|
812
|
+
.style("font", "10px sans-serif")
|
|
813
|
+
.style("color", "black")
|
|
814
|
+
.style("background", "silver")
|
|
815
|
+
.style("border", "1px solid gray")
|
|
816
|
+
.style("border-radius", "8px")
|
|
817
|
+
.style("pointer-events", "none");
|
|
818
|
+
|
|
819
|
+
var focusedChordGroupIndex = null;
|
|
820
|
+
|
|
821
|
+
/*Initiate the SVG*/
|
|
822
|
+
//D3.js v3!
|
|
823
|
+
var svg = root.append("svg:svg")
|
|
824
|
+
.attr("width", chordWidth + margin.left + margin.right)
|
|
825
|
+
.attr("height", chordHeight + margin.top + margin.bottom)
|
|
826
|
+
.attr("id", "svg_change_coupling_chord_diagram");
|
|
827
|
+
|
|
828
|
+
var container = svg.append("g")
|
|
829
|
+
.attr("transform", "translate(" +
|
|
830
|
+
(margin.left + chordWidth / 2) + "," +
|
|
831
|
+
(margin.top + chordHeight / 2) + ")");
|
|
832
|
+
|
|
833
|
+
var chord = customChordLayout()
|
|
834
|
+
.padding(0.04)
|
|
835
|
+
.sortSubgroups(d3.descending) /*sort the chords inside an arc from high to low*/
|
|
836
|
+
.sortChords(d3.ascending) /*which chord should be shown on top when chords cross. Now the largest chord is at the top*/
|
|
837
|
+
.matrix(matrix);
|
|
838
|
+
|
|
839
|
+
/*//////////////////////////////////////////////////////////
|
|
840
|
+
////////////////// Draw outer Arcs /////////////////////////
|
|
841
|
+
//////////////////////////////////////////////////////////*/
|
|
842
|
+
var arc = d3.arc()
|
|
843
|
+
.innerRadius(innerRadius)
|
|
844
|
+
.outerRadius(outerRadius);
|
|
845
|
+
|
|
846
|
+
var g = container.selectAll("g.group")
|
|
847
|
+
.data(chord.groups)
|
|
848
|
+
.enter()
|
|
849
|
+
.append("svg:g")
|
|
850
|
+
.attr("class", function (d) { return "group group-" + locations[d.index].id; });
|
|
851
|
+
|
|
852
|
+
g.append("svg:path")
|
|
853
|
+
.attr("d", arc)
|
|
854
|
+
.style("fill", function (d) {
|
|
855
|
+
return locations[d.index].color;
|
|
856
|
+
})
|
|
857
|
+
.style("stroke", function (d) {
|
|
858
|
+
return d3.rgb(locations[d.index].color).brighter();
|
|
859
|
+
})
|
|
860
|
+
.on("click", function (event, d) { highlightChords(d.index) }) // .on("click", function (d) { highlightChords(d.index) })
|
|
861
|
+
.on("mouseover", function(event, i) {
|
|
862
|
+
showArcToolTip(event, i);
|
|
863
|
+
})
|
|
864
|
+
.on("mouseout", function(d) { hideToolTip() });
|
|
865
|
+
|
|
866
|
+
/*//////////////////////////////////////////////////////////
|
|
867
|
+
//////////////// Initiate inner chords /////////////////////
|
|
868
|
+
//////////////////////////////////////////////////////////*/
|
|
869
|
+
var chords = container.selectAll("path.chord")
|
|
870
|
+
.data(chord.chords)
|
|
871
|
+
.enter()
|
|
872
|
+
.append("svg:path")
|
|
873
|
+
.attr("class", function (d) {
|
|
874
|
+
return "chord chord-source-" + d.source.index + " chord-target-" + d.target.index;
|
|
875
|
+
})
|
|
876
|
+
.attr("d", customChordPathGenerator().radius(innerRadius))
|
|
877
|
+
//Change the fill to reference the unique gradient ID
|
|
878
|
+
//of the source-target combination
|
|
879
|
+
.style("fill", function (d) {
|
|
880
|
+
return "url(#chordGradient-" + d.source.index + "-" + d.target.index + ")";
|
|
881
|
+
})
|
|
882
|
+
.style("stroke", function (d) {
|
|
883
|
+
return "url(#chordGradient-" + d.source.index + "-" + d.target.index + ")";
|
|
884
|
+
})
|
|
885
|
+
.style("fill-opacity", "0.7")
|
|
886
|
+
.on("mouseover", function(event, i) {
|
|
887
|
+
if (focusedChordGroupIndex === null ||
|
|
888
|
+
i.source.index === focusedChordGroupIndex ||
|
|
889
|
+
i.target.index === focusedChordGroupIndex) {
|
|
890
|
+
if (focusedChordGroupIndex === null) {
|
|
891
|
+
d3.selectAll(".chord")
|
|
892
|
+
.style("fill-opacity", 0.2)
|
|
893
|
+
.style("stroke-opacity", 0.2);
|
|
894
|
+
d3.select(this).style("fill-opacity", 1);
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
d3.selectAll(".chord.chord-source-" + focusedChordGroupIndex + ", " +
|
|
898
|
+
".chord.chord-target-" + focusedChordGroupIndex)
|
|
899
|
+
.style("fill-opacity", 0.2)
|
|
900
|
+
.style("stroke-opacity", 0.2);
|
|
901
|
+
d3.select(this).style("fill-opacity", 1);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
showChordToolTip(event, i);
|
|
905
|
+
}
|
|
906
|
+
})
|
|
907
|
+
.on("mouseout", function(d) {
|
|
908
|
+
if (focusedChordGroupIndex === null) {
|
|
909
|
+
d3.selectAll(".chord")
|
|
910
|
+
.style("fill-opacity", 0.7)
|
|
911
|
+
.style("stroke-opacity", 1);
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
d3.selectAll(".chord.chord-source-" + focusedChordGroupIndex + ", " +
|
|
915
|
+
".chord.chord-target-" + focusedChordGroupIndex)
|
|
916
|
+
.style("fill-opacity", 0.7)
|
|
917
|
+
.style("stroke-opacity", 1);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
hideToolTip();
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
//Cf https://www.visualcinnamon.com/2016/06/orientation-gradient-d3-chord-diagram
|
|
924
|
+
//Create a gradient definition for each chord
|
|
925
|
+
var grads = svg.append("defs").selectAll("linearGradient")
|
|
926
|
+
.data(chord.chords)
|
|
927
|
+
.enter().append("linearGradient")
|
|
928
|
+
//Create a unique gradient id per chord: e.g. "chordGradient-0-4"
|
|
929
|
+
.attr("id", function (d) {
|
|
930
|
+
return "chordGradient-" + d.source.index + "-" + d.target.index;
|
|
931
|
+
})
|
|
932
|
+
//Instead of the object bounding box, use the entire SVG for setting locations
|
|
933
|
+
//in pixel locations instead of percentages (which is more typical)
|
|
934
|
+
.attr("gradientUnits", "userSpaceOnUse")
|
|
935
|
+
//The full mathematical formula to find the x and y locations
|
|
936
|
+
.attr("x1", function (d, i) {
|
|
937
|
+
return innerRadius * Math.cos((d.source.endAngle - d.source.startAngle) / 2 +
|
|
938
|
+
d.source.startAngle - Math.PI / 2);
|
|
939
|
+
})
|
|
940
|
+
.attr("y1", function (d, i) {
|
|
941
|
+
return innerRadius * Math.sin((d.source.endAngle - d.source.startAngle) / 2 +
|
|
942
|
+
d.source.startAngle - Math.PI / 2);
|
|
943
|
+
})
|
|
944
|
+
.attr("x2", function (d, i) {
|
|
945
|
+
return innerRadius * Math.cos((d.target.endAngle - d.target.startAngle) / 2 +
|
|
946
|
+
d.target.startAngle - Math.PI / 2);
|
|
947
|
+
})
|
|
948
|
+
.attr("y2", function (d, i) {
|
|
949
|
+
return innerRadius * Math.sin((d.target.endAngle - d.target.startAngle) / 2 +
|
|
950
|
+
d.target.startAngle - Math.PI / 2);
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
//Set the starting color (at 0%)
|
|
954
|
+
grads.append("stop")
|
|
955
|
+
.attr("offset", "0%")
|
|
956
|
+
.attr("stop-color", function (d) { return locations[d.source.index].color; });
|
|
957
|
+
|
|
958
|
+
//Set the ending color (at 100%)
|
|
959
|
+
grads.append("stop")
|
|
960
|
+
.attr("offset", "100%")
|
|
961
|
+
.attr("stop-color", function (d) { return locations[d.target.index].color; });
|
|
962
|
+
|
|
963
|
+
|
|
964
|
+
/*//////////////////////////////////////////////////////////
|
|
965
|
+
////////////////// Initiate Ticks //////////////////////////
|
|
966
|
+
//////////////////////////////////////////////////////////*/
|
|
967
|
+
var ticks = g.append("svg:g")
|
|
968
|
+
.selectAll("g.ticks")
|
|
969
|
+
.data(groupTicks)
|
|
970
|
+
.enter().append("svg:g")
|
|
971
|
+
.attr("transform", function (d) {
|
|
972
|
+
return "rotate(" + (d.angle * 180 / Math.PI - 90) + ")"
|
|
973
|
+
+ "translate(" + outerRadius + 40 + ",0)";
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
/*Append the tick around the arcs*/
|
|
977
|
+
ticks.append("svg:line")
|
|
978
|
+
.attr("x1", 1)
|
|
979
|
+
.attr("y1", 0)
|
|
980
|
+
.attr("x2", 6)
|
|
981
|
+
.attr("y2", 0)
|
|
982
|
+
.attr("class", "ticks")
|
|
983
|
+
.style("stroke", "#FFF")
|
|
984
|
+
.style("stroke-width", "1.5px");
|
|
985
|
+
|
|
986
|
+
let labelColor = "#FFF"
|
|
987
|
+
if (!darkMode) { labelColor = "#333333"}
|
|
988
|
+
|
|
989
|
+
/*Add the labels for the ticks*/
|
|
990
|
+
ticks.append("svg:text")
|
|
991
|
+
.attr("class", "tickLabels")
|
|
992
|
+
.attr("x", 12)
|
|
993
|
+
.attr("dy", ".35em")
|
|
994
|
+
.style("font-size", "10px")
|
|
995
|
+
.style("font-family", "sans-serif")
|
|
996
|
+
.attr("fill", labelColor)
|
|
997
|
+
.attr("transform", function (d) {
|
|
998
|
+
return d.angle > Math.PI ? "rotate(180)translate(-25)" : null;
|
|
999
|
+
})
|
|
1000
|
+
.style("text-anchor", function (d) {
|
|
1001
|
+
return d.angle > Math.PI ? "end" : null;
|
|
1002
|
+
})
|
|
1003
|
+
//.text(function (d) { return d.label; });
|
|
1004
|
+
|
|
1005
|
+
/*//////////////////////////////////////////////////////////
|
|
1006
|
+
////////////////// Initiate Names //////////////////////////
|
|
1007
|
+
//////////////////////////////////////////////////////////*/
|
|
1008
|
+
g.append("svg:text")
|
|
1009
|
+
.each(function (d) { d.angle = (d.startAngle + d.endAngle) / 2; })
|
|
1010
|
+
.attr("dy", ".35em")
|
|
1011
|
+
.attr("class", "titles")
|
|
1012
|
+
.style("font-size", "10px")
|
|
1013
|
+
.style("font-family", "sans-serif")
|
|
1014
|
+
.attr("fill", labelColor)
|
|
1015
|
+
.attr("text-anchor", function (d) {
|
|
1016
|
+
return d.angle > Math.PI ? "end" : null;
|
|
1017
|
+
})
|
|
1018
|
+
.attr("transform", function (d) {
|
|
1019
|
+
var r = outerRadius + dr;
|
|
1020
|
+
var angle = d.angle + ((3 *Math.PI) / 2);
|
|
1021
|
+
var x = r * Math.cos(angle);
|
|
1022
|
+
var y = r * Math.sin(angle);
|
|
1023
|
+
|
|
1024
|
+
if (d.angle > Math.PI) {
|
|
1025
|
+
x -= dx;
|
|
1026
|
+
}
|
|
1027
|
+
else {
|
|
1028
|
+
x += dx;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
return "translate(" + x + ", " + y + ")";
|
|
1032
|
+
})
|
|
1033
|
+
.text(function (d, i) {
|
|
1034
|
+
if (locations[i].name.includes("/")) {
|
|
1035
|
+
return locations[i].name.substring(locations[i].name.lastIndexOf('/') + 1)
|
|
1036
|
+
} else {
|
|
1037
|
+
return locations[i].name
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
/*Lines from labels to arcs*/
|
|
1042
|
+
/*part in radial direction*/
|
|
1043
|
+
this.g.append("line")
|
|
1044
|
+
.attr("x1", function (d) {
|
|
1045
|
+
return outerRadius * Math.cos(d.angle + ((3 * Math.PI) / 2));
|
|
1046
|
+
})
|
|
1047
|
+
.attr("y1", function (d) {
|
|
1048
|
+
return outerRadius * Math.sin(d.angle + ((3 * Math.PI) / 2));
|
|
1049
|
+
})
|
|
1050
|
+
.attr("x2", function (d) {
|
|
1051
|
+
return (outerRadius + dr) * Math.cos(d.angle + ((3 * Math.PI) / 2));
|
|
1052
|
+
})
|
|
1053
|
+
.attr("y2", function (d) {
|
|
1054
|
+
return (outerRadius + dr) * Math.sin(d.angle + ((3 * Math.PI) / 2));
|
|
1055
|
+
})
|
|
1056
|
+
.style("stroke", "#FFF")
|
|
1057
|
+
.style("stroke-width", "0.5px");
|
|
1058
|
+
|
|
1059
|
+
/*horizontal part*/
|
|
1060
|
+
this.g.append("line")
|
|
1061
|
+
.attr("x1", function (d) {
|
|
1062
|
+
return (outerRadius + dr) * Math.cos(d.angle + ((3 * Math.PI) / 2));
|
|
1063
|
+
})
|
|
1064
|
+
.attr("y1", function (d) {
|
|
1065
|
+
return (outerRadius + dr) * Math.sin(d.angle + ((3 * Math.PI) / 2));
|
|
1066
|
+
})
|
|
1067
|
+
.attr("x2", function (d) {
|
|
1068
|
+
var x = (outerRadius + dr) * Math.cos(d.angle + ((3 * Math.PI) / 2));
|
|
1069
|
+
if (d.angle > Math.PI) {
|
|
1070
|
+
x -= dx - 5;
|
|
1071
|
+
}
|
|
1072
|
+
else {
|
|
1073
|
+
x += dx - 5;
|
|
1074
|
+
}
|
|
1075
|
+
return x;
|
|
1076
|
+
})
|
|
1077
|
+
.attr("y2", function (d) {
|
|
1078
|
+
return (outerRadius + dr) * Math.sin(d.angle + ((3 * Math.PI) / 2));
|
|
1079
|
+
})
|
|
1080
|
+
.style("stroke", "#FFF")
|
|
1081
|
+
.style("stroke-width", "0.5px");
|
|
1082
|
+
|
|
1083
|
+
/*//////////////////////////////////////////////////////////
|
|
1084
|
+
////////////////// Extra Functions /////////////////////////
|
|
1085
|
+
//////////////////////////////////////////////////////////*/
|
|
1086
|
+
|
|
1087
|
+
/*Returns an array of tick angles and labels, given a group*/
|
|
1088
|
+
function groupTicks(d) {
|
|
1089
|
+
var anglePerPerson = (d.endAngle - d.startAngle) / d.value;
|
|
1090
|
+
return d3.range(0, d.value, 100).map(function (v, i) {
|
|
1091
|
+
return {
|
|
1092
|
+
angle: v * anglePerPerson + d.startAngle,
|
|
1093
|
+
label: i % 5 ? null : v //Each 5th tick has a label
|
|
1094
|
+
};
|
|
1095
|
+
});
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
//Hides all chords except the chords connecting to the subgroup /
|
|
1099
|
+
//location of the given index.
|
|
1100
|
+
function highlightChords(index) {
|
|
1101
|
+
//If this subgroup is already highlighted, toggle all chords back on.
|
|
1102
|
+
if (focusedChordGroupIndex === index) {
|
|
1103
|
+
showAllChords();
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
hideAllChords();
|
|
1108
|
+
|
|
1109
|
+
//Show only the ones with source or target == index
|
|
1110
|
+
d3.selectAll(".chord-source-" + index + ", .chord-target-" + index)
|
|
1111
|
+
.transition().duration(500)
|
|
1112
|
+
.style("fill-opacity", "0.7")
|
|
1113
|
+
.style("stroke-opacity", "1");
|
|
1114
|
+
|
|
1115
|
+
focusedChordGroupIndex = index;
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
function showAllChords() {
|
|
1119
|
+
svg.selectAll("path.chord")
|
|
1120
|
+
.transition().duration(500)
|
|
1121
|
+
.style("fill-opacity", "0.7")
|
|
1122
|
+
.style("stroke-opacity", "1");
|
|
1123
|
+
|
|
1124
|
+
focusedChordGroupIndex = null;
|
|
1125
|
+
};
|
|
1126
|
+
|
|
1127
|
+
function hideAllChords() {
|
|
1128
|
+
svg.selectAll("path.chord")
|
|
1129
|
+
.transition().duration(500)
|
|
1130
|
+
.style("fill-opacity", "0")
|
|
1131
|
+
.style("stroke-opacity", "0");
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
function showChordToolTip(event, chord) {
|
|
1135
|
+
var prompt = "";
|
|
1136
|
+
|
|
1137
|
+
// if (chord.source.index !== chord.target.index) {
|
|
1138
|
+
// prompt += chord.source.value + " Kunden gingen von " +
|
|
1139
|
+
// locations[chord.target.index].name + " nach " +
|
|
1140
|
+
// locations[chord.source.index].name + ".";
|
|
1141
|
+
// prompt += "<br>";
|
|
1142
|
+
// prompt += chord.target.value + " Kunden gingen von " +
|
|
1143
|
+
// locations[chord.source.index].name + " nach " +
|
|
1144
|
+
// locations[chord.target.index].name + ".";
|
|
1145
|
+
// }
|
|
1146
|
+
// else {
|
|
1147
|
+
// prompt += chord.source.value + " Kunden blieben in " +
|
|
1148
|
+
// locations[chord.source.index].name + ".";
|
|
1149
|
+
// }
|
|
1150
|
+
|
|
1151
|
+
prompt += locations[chord.target.index].name + "<br>" + "... changed together with ... " + "<br>" + locations[chord.source.index].name + ".";
|
|
1152
|
+
|
|
1153
|
+
const[x, y] = d3.pointer(event);
|
|
1154
|
+
|
|
1155
|
+
toolTip
|
|
1156
|
+
.style("opacity", 1)
|
|
1157
|
+
.style("font-size", "10px")
|
|
1158
|
+
.html(prompt)
|
|
1159
|
+
.style("left", x - toolTip.node().getBoundingClientRect().width / 32 + "px") // .style("left", d3.event.pageX - toolTip.node().getBoundingClientRect().width / 2 + "px")
|
|
1160
|
+
.style("top", y + 300 + "px"); // .style("top", (d3.event.pageY - 50) + "px");
|
|
1161
|
+
};
|
|
1162
|
+
|
|
1163
|
+
function showArcToolTip(event, arc) {
|
|
1164
|
+
const[x, y] = d3.pointer(event);
|
|
1165
|
+
// console.log(locations)
|
|
1166
|
+
// console.log(arc)
|
|
1167
|
+
|
|
1168
|
+
var prompt = locations[arc.index].name + "."; //Math.round(arc.value)
|
|
1169
|
+
|
|
1170
|
+
toolTip
|
|
1171
|
+
.style("opacity", 1)
|
|
1172
|
+
.html(prompt)
|
|
1173
|
+
.style("left", x + toolTip.node().getBoundingClientRect().width + "px")
|
|
1174
|
+
.style("top", y + 300 + "px");
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1177
|
+
function hideToolTip() {
|
|
1178
|
+
toolTip.style("opacity", 0);
|
|
1179
|
+
};
|
|
1180
|
+
|
|
1181
|
+
////////////////////////////////////////////////////////////
|
|
1182
|
+
//////////// Custom Chord Layout Function //////////////////
|
|
1183
|
+
/////// Places the Chords in the visually best order ///////
|
|
1184
|
+
///////////////// to reduce overlap ////////////////////////
|
|
1185
|
+
////////////////////////////////////////////////////////////
|
|
1186
|
+
//////// Slightly adjusted by Nadieh Bremer ////////////////
|
|
1187
|
+
//////////////// VisualCinnamon.com ////////////////////////
|
|
1188
|
+
////////////////////////////////////////////////////////////
|
|
1189
|
+
////// Original from the d3.layout.chord() function ////////
|
|
1190
|
+
///////////////// from the d3.js library ///////////////////
|
|
1191
|
+
//////////////// Created by Mike Bostock ///////////////////
|
|
1192
|
+
////////////////////////////////////////////////////////////
|
|
1193
|
+
function customChordLayout() {
|
|
1194
|
+
var ε = 1e-6, ε2 = ε * ε, π = Math.PI, τ = 2 * π, τε = τ - ε, halfπ = π / 2, d3_radians = π / 180, d3_degrees = 180 / π;
|
|
1195
|
+
var chord = {}, chords, groups, matrix, n, padding = 0, sortGroups, sortSubgroups, sortChords;
|
|
1196
|
+
function relayout() {
|
|
1197
|
+
var subgroups = {}, groupSums = [], groupIndex = d3.range(n), subgroupIndex = [], k, x, x0, i, j;
|
|
1198
|
+
var numSeq;
|
|
1199
|
+
chords = [];
|
|
1200
|
+
groups = [];
|
|
1201
|
+
k = 0, i = -1;
|
|
1202
|
+
|
|
1203
|
+
while (++i < n) {
|
|
1204
|
+
x = 0, j = -1, numSeq = [];
|
|
1205
|
+
while (++j < n) {
|
|
1206
|
+
x += matrix[i][j];
|
|
1207
|
+
}
|
|
1208
|
+
groupSums.push(x);
|
|
1209
|
+
//////////////////////////////////////
|
|
1210
|
+
////////////// New part //////////////
|
|
1211
|
+
//////////////////////////////////////
|
|
1212
|
+
for (var m = 0; m < n; m++) {
|
|
1213
|
+
numSeq[m] = (n + (i - 1) - m) % n;
|
|
1214
|
+
}
|
|
1215
|
+
subgroupIndex.push(numSeq);
|
|
1216
|
+
//////////////////////////////////////
|
|
1217
|
+
////////// End new part /////////////
|
|
1218
|
+
//////////////////////////////////////
|
|
1219
|
+
k += x;
|
|
1220
|
+
}//while
|
|
1221
|
+
|
|
1222
|
+
k = (τ - padding * n) / k;
|
|
1223
|
+
x = 0, i = -1;
|
|
1224
|
+
while (++i < n) {
|
|
1225
|
+
x0 = x, j = -1;
|
|
1226
|
+
while (++j < n) {
|
|
1227
|
+
var di = groupIndex[i], dj = subgroupIndex[di][j], v = matrix[di][dj], a0 = x, a1 = x += v * k;
|
|
1228
|
+
subgroups[di + "-" + dj] = {
|
|
1229
|
+
index: di,
|
|
1230
|
+
subindex: dj,
|
|
1231
|
+
startAngle: a0,
|
|
1232
|
+
endAngle: a1,
|
|
1233
|
+
value: v
|
|
1234
|
+
};
|
|
1235
|
+
}//while
|
|
1236
|
+
|
|
1237
|
+
groups[di] = {
|
|
1238
|
+
index: di,
|
|
1239
|
+
startAngle: x0,
|
|
1240
|
+
endAngle: x,
|
|
1241
|
+
value: (x - x0) / k
|
|
1242
|
+
};
|
|
1243
|
+
x += padding;
|
|
1244
|
+
}//while
|
|
1245
|
+
|
|
1246
|
+
i = -1;
|
|
1247
|
+
while (++i < n) {
|
|
1248
|
+
j = i - 1;
|
|
1249
|
+
while (++j < n) {
|
|
1250
|
+
var source = subgroups[i + "-" + j], target = subgroups[j + "-" + i];
|
|
1251
|
+
if (source.value || target.value) {
|
|
1252
|
+
chords.push(source.value < target.value ? {
|
|
1253
|
+
source: target,
|
|
1254
|
+
target: source
|
|
1255
|
+
} : {
|
|
1256
|
+
source: source,
|
|
1257
|
+
target: target
|
|
1258
|
+
});
|
|
1259
|
+
}//if
|
|
1260
|
+
}//while
|
|
1261
|
+
}//while
|
|
1262
|
+
if (sortChords) resort();
|
|
1263
|
+
}//function relayout
|
|
1264
|
+
|
|
1265
|
+
function resort() {
|
|
1266
|
+
chords.sort(function (a, b) {
|
|
1267
|
+
return sortChords((a.source.value + a.target.value) / 2, (b.source.value + b.target.value) / 2);
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
chord.matrix = function (x) {
|
|
1271
|
+
if (!arguments.length) return matrix;
|
|
1272
|
+
n = (matrix = x) && matrix.length;
|
|
1273
|
+
chords = groups = null;
|
|
1274
|
+
return chord;
|
|
1275
|
+
};
|
|
1276
|
+
chord.padding = function (x) {
|
|
1277
|
+
if (!arguments.length) return padding;
|
|
1278
|
+
padding = x;
|
|
1279
|
+
chords = groups = null;
|
|
1280
|
+
return chord;
|
|
1281
|
+
};
|
|
1282
|
+
chord.sortGroups = function (x) {
|
|
1283
|
+
if (!arguments.length) return sortGroups;
|
|
1284
|
+
sortGroups = x;
|
|
1285
|
+
chords = groups = null;
|
|
1286
|
+
return chord;
|
|
1287
|
+
};
|
|
1288
|
+
chord.sortSubgroups = function (x) {
|
|
1289
|
+
if (!arguments.length) return sortSubgroups;
|
|
1290
|
+
sortSubgroups = x;
|
|
1291
|
+
chords = null;
|
|
1292
|
+
return chord;
|
|
1293
|
+
};
|
|
1294
|
+
chord.sortChords = function (x) {
|
|
1295
|
+
if (!arguments.length) return sortChords;
|
|
1296
|
+
sortChords = x;
|
|
1297
|
+
if (chords) resort();
|
|
1298
|
+
return chord;
|
|
1299
|
+
};
|
|
1300
|
+
chord.chords = function () {
|
|
1301
|
+
if (!chords) relayout();
|
|
1302
|
+
return chords;
|
|
1303
|
+
};
|
|
1304
|
+
chord.groups = function () {
|
|
1305
|
+
if (!groups) relayout();
|
|
1306
|
+
return groups;
|
|
1307
|
+
};
|
|
1308
|
+
return chord;
|
|
1309
|
+
};
|
|
1310
|
+
|
|
1311
|
+
////////////////////////////////////////////////////////////
|
|
1312
|
+
//////////// Custom Chord Path Generator ///////////////////
|
|
1313
|
+
///////// Uses cubic bezier curves with quadratic //////////
|
|
1314
|
+
/////// spread of control points to minimise overlap ///////
|
|
1315
|
+
////////////////// of adjacent chords. /////////////////////
|
|
1316
|
+
////////////////////////////////////////////////////////////
|
|
1317
|
+
/////// Original from the d3.svg.chord() function //////////
|
|
1318
|
+
///////////////// from the d3.js library ///////////////////
|
|
1319
|
+
//////////////// Created by Mike Bostock ///////////////////
|
|
1320
|
+
////////////////////////////////////////////////////////////
|
|
1321
|
+
function customChordPathGenerator() {
|
|
1322
|
+
var source = function(d) { return d.source; };
|
|
1323
|
+
var target = function(d) { return d.target; };
|
|
1324
|
+
var radius = function(d) { return d.radius; };
|
|
1325
|
+
var startAngle = function(d) { return d.startAngle; };
|
|
1326
|
+
var endAngle = function(d) { return d.endAngle; };
|
|
1327
|
+
|
|
1328
|
+
function chord(d, i) {
|
|
1329
|
+
var s = subgroup(this, source, d, i),
|
|
1330
|
+
t = subgroup(this, target, d, i);
|
|
1331
|
+
|
|
1332
|
+
var path = "M" + s.p0
|
|
1333
|
+
+ arc(s.r, s.p1, s.a1 - s.a0) + (equals(s, t)
|
|
1334
|
+
? curve(s.r, s.p1, s.a1, s.r, s.p0, s.a0)
|
|
1335
|
+
: curve(s.r, s.p1, s.a1, t.r, t.p0, t.a0)
|
|
1336
|
+
+ arc(t.r, t.p1, t.a1 - t.a0)
|
|
1337
|
+
+ curve(t.r, t.p1, t.a1, s.r, s.p0, s.a0))
|
|
1338
|
+
+ "Z";
|
|
1339
|
+
|
|
1340
|
+
return path;
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
function subgroup(self, f, d, i) {
|
|
1344
|
+
var subgroup = f.call(self, d, i),
|
|
1345
|
+
r = radius.call(self, subgroup, i),
|
|
1346
|
+
a0 = startAngle.call(self, subgroup, i) - (Math.PI / 2),
|
|
1347
|
+
a1 = endAngle.call(self, subgroup, i) - (Math.PI / 2);
|
|
1348
|
+
|
|
1349
|
+
return {
|
|
1350
|
+
r: r,
|
|
1351
|
+
a0: a0,
|
|
1352
|
+
a1: a1,
|
|
1353
|
+
p0: [r * Math.cos(a0), r * Math.sin(a0)],
|
|
1354
|
+
p1: [r * Math.cos(a1), r * Math.sin(a1)]
|
|
1355
|
+
};
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function equals(a, b) {
|
|
1359
|
+
return a.a0 == b.a0 && a.a1 == b.a1;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function arc(r, p, a) {
|
|
1363
|
+
return "A" + r + "," + r + " 0 " + +(a > Math.PI) + ",1 " + p;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function curve(r0, p0, a0, r1, p1, a1) {
|
|
1367
|
+
var deltaAngle = Math.abs(mod((a1 - a0 + Math.PI), (2 * Math.PI)) - Math.PI);
|
|
1368
|
+
var radialControlPointScale = Math.pow((Math.PI - deltaAngle) / Math.PI, 2) * 0.9;
|
|
1369
|
+
var controlPoint1 = [p0[0] * radialControlPointScale, p0[1] * radialControlPointScale];
|
|
1370
|
+
var controlPoint2 = [p1[0] * radialControlPointScale, p1[1] * radialControlPointScale];
|
|
1371
|
+
var cubicBezierSvg = "C " + controlPoint1[0] + " " + controlPoint1[1] + ", " +
|
|
1372
|
+
controlPoint2[0] + " " + controlPoint2[1] + ", " +
|
|
1373
|
+
p1[0] + " " + p1[1];
|
|
1374
|
+
return cubicBezierSvg;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function mod(a, n) {
|
|
1378
|
+
return (a % n + n) % n;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
chord.radius = function(v) {
|
|
1382
|
+
if (!arguments.length) return radius;
|
|
1383
|
+
radius = typeof v === "function" ? v : function() { return v; };
|
|
1384
|
+
return chord;
|
|
1385
|
+
};
|
|
1386
|
+
|
|
1387
|
+
chord.source = function(v) {
|
|
1388
|
+
if (!arguments.length) return source;
|
|
1389
|
+
source = typeof v === "function" ? v : function() { return v; };
|
|
1390
|
+
return chord;
|
|
1391
|
+
};
|
|
1392
|
+
|
|
1393
|
+
chord.target = function(v) {
|
|
1394
|
+
if (!arguments.length) return target;
|
|
1395
|
+
target = typeof v === "function" ? v : function() { return v; };
|
|
1396
|
+
return chord;
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
chord.startAngle = function(v) {
|
|
1400
|
+
if (!arguments.length) return startAngle;
|
|
1401
|
+
startAngle = typeof v === "function" ? v : function() { return v; };
|
|
1402
|
+
return chord;
|
|
1403
|
+
};
|
|
1404
|
+
|
|
1405
|
+
chord.endAngle = function(v) {
|
|
1406
|
+
if (!arguments.length) return endAngle;
|
|
1407
|
+
endAngle = typeof v === "function" ? v : function() { return v; };
|
|
1408
|
+
return chord;
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
return chord;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
}
|