intia-theme 0.1.56 → 0.1.59
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/_includes/head.html +3 -2
- data/_includes/header.html +1 -1
- data/_layouts/default.html +2 -2
- data/_sass/_icons.scss +8 -0
- data/_sass/_layout.scss +292 -422
- data/_sass/_main.scss +2 -0
- data/_sass/_mobile.scss +181 -0
- data/_sass/_radar.scss +317 -0
- data/assets/img/arrow-up.svg +17 -0
- data/assets/img/icons/close-dropdown.svg +13 -0
- data/assets/img/icons/open-dropdown.svg +13 -0
- data/assets/img/logo-horizontal-short.png +0 -0
- data/assets/img/logo-short.png +0 -0
- data/assets/js/main.js +6 -1
- data/assets/js/radar.js +902 -0
- metadata +10 -3
- data/assets/img/logo-black.png +0 -0
data/assets/js/radar.js
ADDED
@@ -0,0 +1,902 @@
|
|
1
|
+
/*
|
2
|
+
all occurring angles are in radian
|
3
|
+
*/
|
4
|
+
function createRadar(config, structure, entries){
|
5
|
+
//#region variable definition #######################################################
|
6
|
+
const
|
7
|
+
radarId = config.radar.id,
|
8
|
+
diameter = config.radar.renderResolution,
|
9
|
+
radius = diameter / 2,
|
10
|
+
sectorThickness = 2 * Math.PI / structure.sectors.length,
|
11
|
+
blipMinSize = config.blip.size;
|
12
|
+
|
13
|
+
const fillingRatio = Math.PI / Math.sqrt(18); // ~0.74
|
14
|
+
|
15
|
+
|
16
|
+
let
|
17
|
+
radarData={}, // object to save all radar data
|
18
|
+
seed = 42, // seed number for reproducible random sequence
|
19
|
+
blipIdCounter = 1, // counter variable to give each blip a unique id
|
20
|
+
onlyOneSectorDisplayed = false;
|
21
|
+
|
22
|
+
//#endregion ########################################################################
|
23
|
+
|
24
|
+
window.onresize = () => {
|
25
|
+
mobileMode = (getSvgDivWidth() < diameter) ? true : false;
|
26
|
+
update();
|
27
|
+
}
|
28
|
+
|
29
|
+
//#region helper function math ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
30
|
+
/*-------------------------------------------------------------------
|
31
|
+
custom random number generator, to make random sequence reproducible
|
32
|
+
source: https://stackoverflow.com/questions/521295
|
33
|
+
-------------------------------------------------------------------*/
|
34
|
+
let random = () => {let x = Math.sin(seed++) * 10000; return x - Math.floor(x);};
|
35
|
+
let random_between = (min, max) => (min+random()*(max-min));
|
36
|
+
let normal_between = (min, max) => (min+(random()+random())*0.5*(max-min));
|
37
|
+
|
38
|
+
let pointByAngleAndRadius = (angle, radius) => ({
|
39
|
+
x: Math.cos(angle) * radius,
|
40
|
+
y: Math.sin(angle) * radius
|
41
|
+
})
|
42
|
+
|
43
|
+
let angleAndRadiusByPoint = (point) => ({
|
44
|
+
angle: angleOfPoint(point),
|
45
|
+
radius: radiusOfPoint(point),
|
46
|
+
})
|
47
|
+
|
48
|
+
let angleOfPoint = (point) => (point.y < 0)
|
49
|
+
? Math.PI*2 + Math.atan2(point.y, point.x)
|
50
|
+
: Math.atan2(point.y, point.x);
|
51
|
+
|
52
|
+
let radiusOfPoint = (point) =>
|
53
|
+
Math.sqrt(point.x * point.x + point.y * point.y);
|
54
|
+
|
55
|
+
let calcOffsetAngle = (radius) =>
|
56
|
+
Math.atan(blipRadiusWithPadding / radius);
|
57
|
+
|
58
|
+
// NEW
|
59
|
+
let occupiedSpaceByBlips = (blipCount) => {
|
60
|
+
let radius = blipMinSize/2 + config.blip.margin;
|
61
|
+
return Math.pow(radius, 2) * Math.PI * blipCount;
|
62
|
+
}
|
63
|
+
|
64
|
+
let calcAngleRatio = (angle) => angle / (2 * Math.PI);
|
65
|
+
|
66
|
+
let blipAreaInSegment = (segment) => {
|
67
|
+
let radii = Math.pow(segment.outerRadius, 2) - Math.pow(segment.innerRadius, 2)
|
68
|
+
return Math.PI * radii * calcAngleRatio(segment.angleSpan) * fillingRatio;
|
69
|
+
}
|
70
|
+
|
71
|
+
let blipMaxRadiusInArea = (blipCount, area) =>
|
72
|
+
Math.sqrt(area / (Math.PI * blipCount));
|
73
|
+
|
74
|
+
let calcSegmentOuterRadius = (innerRadius, angle, blipCount) => {
|
75
|
+
let blipSpace = occupiedSpaceByBlips(blipCount);
|
76
|
+
let angleRatio = calcAngleRatio(angle);
|
77
|
+
let squareRootTerm = blipSpace / (Math.PI * angleRatio * fillingRatio) + Math.pow(innerRadius, 2);
|
78
|
+
return Math.sqrt(squareRootTerm);
|
79
|
+
}
|
80
|
+
|
81
|
+
let calcSegmentAngleSpan = (innerRadius, outerRadius, blipCount) => {
|
82
|
+
let blipSpace = occupiedSpaceByBlips(blipCount);
|
83
|
+
let denominator = (Math.PI * (Math.pow(outerRadius, 2) - Math.pow(innerRadius, 2)) * fillingRatio);
|
84
|
+
return blipSpace / denominator * (2 * Math.PI)
|
85
|
+
}
|
86
|
+
//#endregion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
87
|
+
|
88
|
+
//#region helper function segment borders ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
89
|
+
/* Checks if a value is in interval between min and max.
|
90
|
+
-> If the value is below the interval minimum, the interval minimum is returned.
|
91
|
+
-> If the value is above the interval maximum, the interval maximum is returned.*/
|
92
|
+
let bounded_interval = (value, min, max) => {
|
93
|
+
let low = Math.min(min, max);
|
94
|
+
let high = Math.max(min, max);
|
95
|
+
return Math.min(Math.max(value, low), high);
|
96
|
+
}
|
97
|
+
|
98
|
+
let boundedRadius = (point, minRadius, maxRadius) => ({
|
99
|
+
angle: point.angle,
|
100
|
+
radius: bounded_interval(point.radius, minRadius, maxRadius)
|
101
|
+
})
|
102
|
+
|
103
|
+
let boundedAngle = (point, minAngle, maxAngle) => {
|
104
|
+
let blipPointRadius = radiusOfPoint(point);
|
105
|
+
let offsetAngle = calcOffsetAngle(blipPointRadius);
|
106
|
+
let minOffsetAngle = minAngle + offsetAngle;
|
107
|
+
let maxOffsetAngle = maxAngle - offsetAngle;
|
108
|
+
let blipPointAngle = angleOfPoint(point);
|
109
|
+
let angle = bounded_interval(blipPointAngle, minOffsetAngle, maxOffsetAngle);
|
110
|
+
//if the blip was outside the interval the blip point is recalculated
|
111
|
+
if(angle == minOffsetAngle) return pointByAngleAndRadius(minOffsetAngle, blipPointRadius);
|
112
|
+
if(angle == maxOffsetAngle) return pointByAngleAndRadius(maxOffsetAngle, blipPointRadius);
|
113
|
+
else return point;
|
114
|
+
}
|
115
|
+
|
116
|
+
let segmentFunctions = (segment) => ({
|
117
|
+
clip: (blip) => {
|
118
|
+
let pointInAngleInterval = boundedAngle(blip, segment.startAngle, segment.endAngle);
|
119
|
+
let pointInRadiusInterval = boundedRadius(
|
120
|
+
angleAndRadiusByPoint(pointInAngleInterval),
|
121
|
+
segment.blipMinRadius,
|
122
|
+
segment.blipMaxRadius
|
123
|
+
);
|
124
|
+
blip.x = pointByAngleAndRadius(pointInRadiusInterval.angle, pointInRadiusInterval.radius).x;
|
125
|
+
blip.y = pointByAngleAndRadius(pointInRadiusInterval.angle, pointInRadiusInterval.radius).y;
|
126
|
+
return { x: blip.x, y: blip.y };
|
127
|
+
},
|
128
|
+
random: () => pointByAngleAndRadius(
|
129
|
+
random_between(segment.startAngle, segment.endAngle),
|
130
|
+
normal_between(segment.blipMinRadius, segment.blipMaxRadius))
|
131
|
+
})
|
132
|
+
//#endregion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
133
|
+
|
134
|
+
//#region helper functions svg ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
135
|
+
let getSvgDivWidth = () => {
|
136
|
+
// returns the width of the div tag where the svg is placed in excluding the padding
|
137
|
+
let radarOffsetWidth = radarDiv.select(`.radar`).node().offsetWidth;
|
138
|
+
let padding = parseInt(window.getComputedStyle(radarDiv.select(`.radar`).node()).paddingLeft) * 2;
|
139
|
+
return radarOffsetWidth - padding;
|
140
|
+
}
|
141
|
+
|
142
|
+
let translate = (x, y) => `translate(${x}, ${y})`;
|
143
|
+
|
144
|
+
let arc = (segment) => {
|
145
|
+
const startMaxPoint = pointByAngleAndRadius(segment.startAngle, segment.outerRadius);
|
146
|
+
const startMinPoint = pointByAngleAndRadius(segment.startAngle, segment.innerRadius);
|
147
|
+
const endMaxPoint = pointByAngleAndRadius(segment.endAngle, segment.outerRadius);
|
148
|
+
const endMinPoint = pointByAngleAndRadius(segment.endAngle, segment.innerRadius);
|
149
|
+
return [
|
150
|
+
'M', startMaxPoint.x, startMaxPoint.y,
|
151
|
+
'A', segment.outerRadius, segment.outerRadius, 0, 0, 1, endMaxPoint.x, endMaxPoint.y,
|
152
|
+
'L', endMinPoint.x, endMinPoint.y,
|
153
|
+
'A', segment.innerRadius, segment.innerRadius, 0, 0, 0, startMinPoint.x, startMinPoint.y,
|
154
|
+
'L', startMaxPoint.x, startMaxPoint.y,
|
155
|
+
'Z'
|
156
|
+
].join(' ');
|
157
|
+
}
|
158
|
+
|
159
|
+
let sectorNamePath = (segment) => {
|
160
|
+
const radius = segment.outerRadius;
|
161
|
+
const startPoint = pointByAngleAndRadius(segment.startAngle, radius);
|
162
|
+
const endPoint = pointByAngleAndRadius(segment.endAngle, radius);
|
163
|
+
return [
|
164
|
+
'M', startPoint.x, startPoint.y,
|
165
|
+
'A', radius, radius, 0, 0, 1, endPoint.x, endPoint.y
|
166
|
+
].join(' ');
|
167
|
+
}
|
168
|
+
|
169
|
+
let segmentNamePath = (segment) => {
|
170
|
+
const endMaxPoint = pointByAngleAndRadius(segment.endAngle, segment.outerRadius);
|
171
|
+
const endMinPoint = pointByAngleAndRadius(segment.endAngle, segment.innerRadius);
|
172
|
+
|
173
|
+
if(segment.endAngle > 1.5 * Math.PI && segment.endAngle <= 2 * Math.PI ||
|
174
|
+
segment.endAngle < 0.5 * Math.PI){
|
175
|
+
return [
|
176
|
+
'M', endMinPoint.x, endMinPoint.y,
|
177
|
+
'L', endMaxPoint.x, endMaxPoint.y
|
178
|
+
].join(' ');
|
179
|
+
}
|
180
|
+
return [
|
181
|
+
'M', endMaxPoint.x, endMaxPoint.y,
|
182
|
+
'L', endMinPoint.x, endMinPoint.y
|
183
|
+
].join(' ');
|
184
|
+
}
|
185
|
+
|
186
|
+
let getSectorColorPalette = (colorCode) => {
|
187
|
+
let colorStart, colorEnd, brighterColor;
|
188
|
+
switch (true){
|
189
|
+
case config.sector.useColor && config.segment.colorGradient:
|
190
|
+
brighterColor = d3.hsl(colorCode);
|
191
|
+
brighterColor.l *= config.segment.colorGradientLimit;
|
192
|
+
colorStart = d3.rgb(colorCode);
|
193
|
+
colorEnd = d3.rgb(brighterColor);
|
194
|
+
break;
|
195
|
+
case config.segment.colorGradient:
|
196
|
+
brighterColor = d3.hsl(config.radar.defaultColor);
|
197
|
+
brighterColor.l *= config.segment.colorGradientLimit;
|
198
|
+
colorStart = d3.rgb(config.radar.defaultColor);
|
199
|
+
colorEnd = d3.rgb(brighterColor);
|
200
|
+
break;
|
201
|
+
case config.sector.useColor:
|
202
|
+
colorStart = d3.rgb(colorCode);
|
203
|
+
colorEnd = d3.rgb(colorCode);
|
204
|
+
break;
|
205
|
+
default:
|
206
|
+
colorStart = d3.rgb(config.radar.defaultColor);
|
207
|
+
colorEnd = d3.rgb(config.radar.defaultColor);
|
208
|
+
}
|
209
|
+
return d3.scaleLinear()
|
210
|
+
.domain([0, structure.rings.list.length])
|
211
|
+
.range([colorStart, colorEnd]);
|
212
|
+
}
|
213
|
+
|
214
|
+
let getBlipColor = (blip) =>
|
215
|
+
(blip.stateID >= 0 && blip.stateID < structure.entryStates.list.length)
|
216
|
+
? structure.entryStates.list[blip.stateID].color
|
217
|
+
: config.blip.defaultColor;
|
218
|
+
|
219
|
+
let getBlipRingColor = (blip) => {
|
220
|
+
let color = (blip.stateID >= 0 && blip.stateID < structure.entryStates.list.length)
|
221
|
+
? d3.rgb(structure.entryStates.list[blip.stateID].color)
|
222
|
+
: d3.rgb(config.blip.defaultColor);
|
223
|
+
if(blip.moved != 0) color.opacity = 0.25;
|
224
|
+
return color;
|
225
|
+
}
|
226
|
+
let getBlipMovedIndicator = (blip) => {
|
227
|
+
if(blip.moved != 0){
|
228
|
+
let radius = config.blip.outerCircleRadius;
|
229
|
+
|
230
|
+
let startAngle = (blip.moved > 0)
|
231
|
+
? radarData.sectors[blip.sectorID].startAngle + Math.PI
|
232
|
+
: radarData.sectors[blip.sectorID].startAngle;
|
233
|
+
let endAngle = (blip.moved > 0)
|
234
|
+
? radarData.sectors[blip.sectorID].endAngle + Math.PI
|
235
|
+
: radarData.sectors[blip.sectorID].endAngle;
|
236
|
+
let startPoint = pointByAngleAndRadius(startAngle, radius);
|
237
|
+
let endPoint = pointByAngleAndRadius(endAngle, radius);
|
238
|
+
return [
|
239
|
+
'M', startPoint.x, startPoint.y,
|
240
|
+
'A', radius, radius, 0, 0, 1, endPoint.x, endPoint.y,
|
241
|
+
].join(' ');
|
242
|
+
}
|
243
|
+
return ``;
|
244
|
+
}
|
245
|
+
//#endregion ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
246
|
+
|
247
|
+
//#region preparing radar data ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
248
|
+
|
249
|
+
|
250
|
+
|
251
|
+
|
252
|
+
|
253
|
+
// create data structure ----------------------------------------------------------
|
254
|
+
radarData.rings = structure.rings.list.map((ring, index) => ({
|
255
|
+
...ring,
|
256
|
+
index: index,
|
257
|
+
}));
|
258
|
+
radarData.sectors = structure.sectors.map((sector, index) => ({
|
259
|
+
...sector,
|
260
|
+
id: index,
|
261
|
+
idText: `${radarId}_sector${index}`,
|
262
|
+
color: getSectorColorPalette(sector.color),
|
263
|
+
segments: radarData.rings,
|
264
|
+
}));
|
265
|
+
radarData.sectors.forEach(sector => {
|
266
|
+
sector.segments = sector.segments.map((segment, index) => ({
|
267
|
+
...segment,
|
268
|
+
idText: `${sector.idText}_segment${index}`,
|
269
|
+
color: sector.color(index),
|
270
|
+
blips: entries.filter(entry =>
|
271
|
+
entry.sectorID == sector.id &&
|
272
|
+
entry.ringID == index &&
|
273
|
+
entry.active)
|
274
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
275
|
+
}))
|
276
|
+
});
|
277
|
+
// --------------------------------------------------------------------------------
|
278
|
+
// adding ring radii and ring thickness -----------------------------------------
|
279
|
+
radarData.rings.forEach((ring, index) => {
|
280
|
+
ring.innerRadius = (index == 0) ? 0 : radarData.rings[index - 1].outerRadius;
|
281
|
+
ring.outerRadius = radarData.sectors.reduce((prev, curr) =>
|
282
|
+
Math.max(prev, calcSegmentOuterRadius(ring.innerRadius, sectorThickness, curr.segments[index].blips.length))
|
283
|
+
, 0);
|
284
|
+
ring.radiusThickness = ring.outerRadius - ring.innerRadius;
|
285
|
+
});
|
286
|
+
/* if the last outer radius is larger or smaller than the given radar radius, the rings must be
|
287
|
+
adjusted in such a way that any open space is evenly filled or any space that overlaps is evenly
|
288
|
+
subtracted from all ring thicknesses. */
|
289
|
+
let ringsLastOuterRadius = radarData.rings[radarData.rings.length - 1].outerRadius;
|
290
|
+
let ringThicknessCorrection = (radius - ringsLastOuterRadius) / radarData.rings.length;
|
291
|
+
radarData.rings.forEach((ring, index) => {
|
292
|
+
ring.radiusThickness = ring.radiusThickness + ringThicknessCorrection;
|
293
|
+
ring.innerRadius = (index == 0) ? 0 : radarData.rings[index - 1].outerRadius;
|
294
|
+
ring.outerRadius = ring.innerRadius + ring.radiusThickness;
|
295
|
+
});
|
296
|
+
// update segments
|
297
|
+
radarData.sectors.forEach(sector => {
|
298
|
+
sector.segments = sector.segments.map((segment, index) => ({
|
299
|
+
...segment,
|
300
|
+
radiusThickness: radarData.rings[index].radiusThickness,
|
301
|
+
innerRadius: radarData.rings[index].innerRadius,
|
302
|
+
outerRadius: radarData.rings[index].outerRadius,
|
303
|
+
}))
|
304
|
+
});
|
305
|
+
|
306
|
+
// add needed min angle span for each sector
|
307
|
+
radarData.sectors.forEach((sector) => {
|
308
|
+
sector.angleSpan = sector.segments.reduce((prev, curr) =>
|
309
|
+
Math.max(prev, calcSegmentAngleSpan(curr.innerRadius, curr.outerRadius, curr.blips.length))
|
310
|
+
, 0);
|
311
|
+
});
|
312
|
+
// add up all sector angle spans
|
313
|
+
let sectorAngleSpanSum = radarData.sectors.reduce((prev, curr) => prev + curr.angleSpan, 0);
|
314
|
+
let sectorAngleCorrection = ((Math.PI * 2) - sectorAngleSpanSum) / radarData.sectors.length;
|
315
|
+
radarData.sectors.forEach((sector, index) => {
|
316
|
+
sector.angleSpan = sector.angleSpan + sectorAngleCorrection;
|
317
|
+
sector.startAngle = (index == 0) ? 0 : radarData.sectors[index - 1].endAngle;
|
318
|
+
sector.endAngle = sector.startAngle + sector.angleSpan;
|
319
|
+
});
|
320
|
+
// update segments
|
321
|
+
radarData.sectors.forEach(sector => {
|
322
|
+
sector.segments = sector.segments.map((segment, index) => ({
|
323
|
+
...segment,
|
324
|
+
angleSpan: sector.angleSpan,
|
325
|
+
startAngle: sector.startAngle,
|
326
|
+
endAngle: sector.endAngle,
|
327
|
+
}))
|
328
|
+
})
|
329
|
+
// ------------------------------------------------------------------------------
|
330
|
+
|
331
|
+
|
332
|
+
|
333
|
+
// ------------------------------------------------------------------------------
|
334
|
+
// Blip anpassung
|
335
|
+
let sectorMinBilpRadius = radarData.sectors.reduce((prev, curr) => {
|
336
|
+
let segmentMinBilpRadius = curr.segments.reduce((prev, curr) => {
|
337
|
+
let area = blipAreaInSegment(curr);
|
338
|
+
let radius = blipMaxRadiusInArea(curr.blips.length, area);
|
339
|
+
let size = (radius - config.blip.margin) * 2;
|
340
|
+
return Math.min(size, prev)
|
341
|
+
}, Number.MAX_VALUE)
|
342
|
+
return Math.min(prev, segmentMinBilpRadius)
|
343
|
+
}, Number.MAX_VALUE);
|
344
|
+
|
345
|
+
const
|
346
|
+
blipSize = Math.max(sectorMinBilpRadius, blipMinSize),
|
347
|
+
blipRadiusWithPadding = blipSize / 2 + config.segment.padding;
|
348
|
+
|
349
|
+
config.blip.size = blipSize;
|
350
|
+
|
351
|
+
radarData.sectors.forEach(sector => {
|
352
|
+
sector.segments = sector.segments.map((segment, index) => ({
|
353
|
+
...segment,
|
354
|
+
blipMinRadius: (index == 0)
|
355
|
+
? blipRadiusWithPadding / Math.sin(sector.angleSpan / 2)
|
356
|
+
: segment.innerRadius + blipRadiusWithPadding,
|
357
|
+
blipMaxRadius: segment.outerRadius - blipRadiusWithPadding
|
358
|
+
}))
|
359
|
+
})
|
360
|
+
|
361
|
+
// ------------------------------------------------------------------------------
|
362
|
+
|
363
|
+
|
364
|
+
|
365
|
+
|
366
|
+
|
367
|
+
|
368
|
+
radarData.blips = []; // list of all blips, for a better processing later on
|
369
|
+
radarData.sectors.forEach(sector => sector.segments.forEach(segment => {
|
370
|
+
// give each blip the corresponding segment functions
|
371
|
+
segment.blips = segment.blips.map(blip => ({
|
372
|
+
...blip,
|
373
|
+
idText: `${segment.idText}_blip${blipIdCounter}`,
|
374
|
+
id: blipIdCounter++,
|
375
|
+
focused: false,
|
376
|
+
segmentFunctions: segmentFunctions(segment),
|
377
|
+
}))
|
378
|
+
// save each blip in a list, for better processing later on
|
379
|
+
segment.blips.forEach(blip => radarData.blips.push(blip))
|
380
|
+
}));
|
381
|
+
|
382
|
+
// give each blip the first random position
|
383
|
+
radarData.blips.forEach(blip => {
|
384
|
+
let point = blip.segmentFunctions.random();
|
385
|
+
blip.x = point.x;
|
386
|
+
blip.y = point.y;
|
387
|
+
});
|
388
|
+
|
389
|
+
// add data to the configuration of a blip to create blips later on
|
390
|
+
let fontSize = blipSize * 0.33,
|
391
|
+
blipRadius = blipSize * 0.5,
|
392
|
+
strokeWidth = blipRadius * 0.2,
|
393
|
+
outerCircleRadius = blipRadius ,
|
394
|
+
innerCircleRadius = outerCircleRadius - strokeWidth;
|
395
|
+
config.blip = ({
|
396
|
+
...config.blip,
|
397
|
+
fontSize: fontSize,
|
398
|
+
radius: blipRadius,
|
399
|
+
strokeWidth: strokeWidth,
|
400
|
+
outerCircleRadius: outerCircleRadius,
|
401
|
+
innerCircleRadius: innerCircleRadius
|
402
|
+
});
|
403
|
+
|
404
|
+
structure.entryStates.list = structure.entryStates.list.map((state, index)=>({
|
405
|
+
...state,
|
406
|
+
index: index
|
407
|
+
}));
|
408
|
+
|
409
|
+
|
410
|
+
//#endregion ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
411
|
+
|
412
|
+
//#region create div structure ______________________________________________________
|
413
|
+
let radarDiv = d3.select(`div#${radarId}`).classed(`radarContainer`, true);
|
414
|
+
if(config.radar.showName){
|
415
|
+
radarDiv.append(`div`)
|
416
|
+
.classed(`radarTitle`, true)
|
417
|
+
.text(config.radar.name);
|
418
|
+
}
|
419
|
+
// select sector dropdown
|
420
|
+
radarDiv.append(`div`)
|
421
|
+
.attr(`id`, `${radarId}_selectionDropdown`)
|
422
|
+
.classed(`radarSelection dropdown`, true);
|
423
|
+
radarDiv.append(`div`)
|
424
|
+
.classed(`radar`, true)
|
425
|
+
.attr(`id`, `${radarId}_radarDiv`);
|
426
|
+
/*
|
427
|
+
radarDiv.append(`div`)
|
428
|
+
.classed(`radarBlipLegend`, true);
|
429
|
+
*/
|
430
|
+
//#endregion ________________________________________________________________________
|
431
|
+
|
432
|
+
//#region create radar SVG and radar legend _________________________________________
|
433
|
+
radarDiv.select(`.radar`)
|
434
|
+
.append(`div`)
|
435
|
+
.classed(`radarContent`, true)
|
436
|
+
.append(`svg`)
|
437
|
+
.attr(`id`, `${radarId}_svg`)
|
438
|
+
.classed(`radarSVG`, true)
|
439
|
+
.attr(`preserveAspectRatio`, `xMinYMin meet`)
|
440
|
+
.attr(`viewBox`, `0 0 ${diameter} ${diameter}`);
|
441
|
+
radarDiv.select(`svg#${radarId}_svg`).append(`g`)
|
442
|
+
.attr(`id`, `${radarId}_radarContent`)
|
443
|
+
.attr(`transform`, translate(radius, radius));
|
444
|
+
// append radar legend div
|
445
|
+
radarDiv.select(`.radar`)
|
446
|
+
.append(`div`)
|
447
|
+
.attr(`id`, `${radarId}_radarLegend`)
|
448
|
+
.classed(`radarLegend dropdown`, true)
|
449
|
+
.on(`click`, ()=>
|
450
|
+
document.getElementById(`${radarId}_radarLegend`).classList.toggle(`active`))
|
451
|
+
.text(config.radar.legendDropdownText);
|
452
|
+
//#endregion ________________________________________________________________________
|
453
|
+
|
454
|
+
// can be declared only after the radar svg is appended
|
455
|
+
let mobileMode = (getSvgDivWidth() < diameter) ? true : false;
|
456
|
+
// let mobileMode = (getSvgDivWidth() < 400) ? true : false;
|
457
|
+
|
458
|
+
//#region event fuctions general ****************************************************
|
459
|
+
let update = () => {
|
460
|
+
selectionDropdownContent.select(`.selectionButton`)
|
461
|
+
.style(`display`, (mobileMode) ? `none` : `block`);
|
462
|
+
if(mobileMode && !onlyOneSectorDisplayed){
|
463
|
+
displaySector(radarData.sectors[0]);
|
464
|
+
changeSvgViewbox(radarData.sectors[0].idText);
|
465
|
+
selectionDropdownText.text(radarData.sectors[0].name);
|
466
|
+
}
|
467
|
+
else changeSvgViewbox(`${radarId}_radarContent`);
|
468
|
+
}
|
469
|
+
|
470
|
+
let changeSvgViewbox = (idText) => {
|
471
|
+
onlyOneSectorDisplayed = (idText == `${radarId}_radarContent`) ? false : true;
|
472
|
+
let box = radarDiv.select(`g#${idText}`).node().getBBox()
|
473
|
+
let size = Math.max(box.width, box.height);
|
474
|
+
let x = radius + box.x;
|
475
|
+
let y = radius + box.y;
|
476
|
+
d3.select(`svg#${radarId}_svg`).attr(`viewBox`, `${x} ${y} ${size} ${size}`);
|
477
|
+
}
|
478
|
+
//#endregion ************************************************************************
|
479
|
+
|
480
|
+
//#region event functions sector ****************************************************
|
481
|
+
let displayAllSectors = () => {
|
482
|
+
sectors.style(`display`, `block`);
|
483
|
+
blipLegendSectors.style(`display`, `block`);
|
484
|
+
}
|
485
|
+
let displaySector = (sector) => {
|
486
|
+
sectors.style(`display`, `none`);
|
487
|
+
radarDiv.select(`g#${sector.idText}`).style(`display`, `block`);
|
488
|
+
blipLegendSectors.style(`display`, `none`);
|
489
|
+
radarDiv.select(`div#${sector.idText}_legend`).style(`display`, `block`);
|
490
|
+
}
|
491
|
+
let focusAllSector = () => {
|
492
|
+
sectors.style(`opacity`, 1);
|
493
|
+
blipLegendSectors.style(`opacity`, 1);
|
494
|
+
}
|
495
|
+
let focusSector = (sector) => {
|
496
|
+
if(!onlyOneSectorDisplayed){
|
497
|
+
sectors.style(`opacity`, 0.25);
|
498
|
+
radarDiv.select(`g#${sector.idText}`).style(`opacity`, 1);
|
499
|
+
blipLegendSectors.style(`opacity`, 0.25);
|
500
|
+
radarDiv.select(`div#${sector.idText}_legend`).style(`opacity`, 1);
|
501
|
+
}
|
502
|
+
}
|
503
|
+
//#endregion ************************************************************************
|
504
|
+
|
505
|
+
//#region event functions ring ******************************************************
|
506
|
+
let focusRing = (ring) => {
|
507
|
+
segments.style(`opacity`, 0.25)
|
508
|
+
segments.filter(seg => seg.index == ring.index).style(`opacity`, 1)
|
509
|
+
}
|
510
|
+
let focusAllRings = () => segments.style(`opacity`, 1);
|
511
|
+
//#endregion ************************************************************************
|
512
|
+
|
513
|
+
//#region event functions blip ******************************************************
|
514
|
+
let blipClick = (blip) => {
|
515
|
+
let blipData = radarData.blips.find(data => data.id == blip.id);
|
516
|
+
if (blipData.focused){
|
517
|
+
window.open(blipData.link);
|
518
|
+
}
|
519
|
+
else blipData.focused = true;
|
520
|
+
}
|
521
|
+
|
522
|
+
let focusAllBlips = () => {
|
523
|
+
radarData.blips.forEach(blip => blip.focused = false);
|
524
|
+
blips.style(`opacity`, 1);
|
525
|
+
radarDiv.selectAll(`.blipLegendBlip`).classed(`active`, false);
|
526
|
+
hideBubble();
|
527
|
+
}
|
528
|
+
|
529
|
+
let focusBlip = (blip) => {
|
530
|
+
radarData.blips.find(data => data.id == blip.id).focused = true;
|
531
|
+
blips.filter(data => data.sectorID == blip.sectorID).style(`opacity`, 0.25);
|
532
|
+
radarDiv.select(`g#${blip.idText}`).style(`opacity`, 1);
|
533
|
+
blipLegendBlips.filter(data => data.id == blip.id).classed(`active`, true);
|
534
|
+
showBubble(blip);
|
535
|
+
}
|
536
|
+
|
537
|
+
let focusBlipByState = (state) => {
|
538
|
+
blips.style(`opacity`, 0.25);
|
539
|
+
blips.filter(data => data.stateID == state.index).style(`opacity`, 1);
|
540
|
+
blipLegendBlips.filter(data => data.stateID == state.index).classed(`active`, true);
|
541
|
+
}
|
542
|
+
|
543
|
+
let focusBlipByMovement = (movementValue) => {
|
544
|
+
blips.style(`opacity`, 0.25);
|
545
|
+
if(movementValue > 0) {
|
546
|
+
blips.filter(data => data.moved > 0).style(`opacity`, 1);
|
547
|
+
blipLegendBlips.filter(data => data.moved > 0).classed(`active`, true);
|
548
|
+
}
|
549
|
+
if(movementValue < 0) {
|
550
|
+
blips.filter(data => data.moved < 0).style(`opacity`, 1);
|
551
|
+
blipLegendBlips.filter(data => data.moved < 0).classed(`active`, true);
|
552
|
+
}
|
553
|
+
if(movementValue == 0) {
|
554
|
+
blips.filter(data => data.moved == 0).style(`opacity`, 1);
|
555
|
+
blipLegendBlips.filter(data => data.moved == 0).classed(`active`, true);
|
556
|
+
}
|
557
|
+
}
|
558
|
+
//#endregion ************************************************************************
|
559
|
+
|
560
|
+
//#region event functions bubble ****************************************************
|
561
|
+
let showBubble = (blip) => {
|
562
|
+
bubble.style(`display`, `block`);
|
563
|
+
let text = bubble.select(`text`).text(blip.name);
|
564
|
+
let textBox = text.node().getBBox();
|
565
|
+
bubble.attr('transform', translate(blip.x - textBox.width / 2, blip.y - 19))
|
566
|
+
bubble.select(`rect`)
|
567
|
+
.attr('x', -5)
|
568
|
+
.attr('y', -textBox.height)
|
569
|
+
.attr('width', textBox.width + 10)
|
570
|
+
.attr('height', textBox.height + 4);
|
571
|
+
bubble.select(`path`).attr('transform', translate(textBox.width / 2 - 5, 3));
|
572
|
+
}
|
573
|
+
let hideBubble = () =>
|
574
|
+
radarDiv.select(`g#${radarId}_bubble`).style(`display`, `none`);
|
575
|
+
//#endregion ************************************************************************
|
576
|
+
|
577
|
+
//#region d3-components radar -------------------------------------------------------
|
578
|
+
/* to place text on an svg-path the attribute alignment-baseline has been used,
|
579
|
+
the options of this attribute are well explained here
|
580
|
+
https://vanseodesign.com/web-design/svg-text-baseline-alignment/
|
581
|
+
*/
|
582
|
+
let makeSector = (selection) => {
|
583
|
+
selection
|
584
|
+
.attr(`id`, sector => `${sector.idText}`)
|
585
|
+
.classed(`sector`, true)
|
586
|
+
.on(`mouseover`, sector => focusSector(sector))
|
587
|
+
.on(`mouseout`, focusAllSector)
|
588
|
+
.on(`click`, sector => {
|
589
|
+
displaySector(sector);
|
590
|
+
changeSvgViewbox(sector.idText);
|
591
|
+
});
|
592
|
+
if(config.sector.showName){
|
593
|
+
let name = selection.append(`g`)
|
594
|
+
.attr(`class`, `sectorName`)
|
595
|
+
name.append(`path`)
|
596
|
+
.attr(`id`, sector => `${sector.idText}_name`)
|
597
|
+
.attr(`d`, sector => sectorNamePath(sector.segments[sector.segments.length-1]))
|
598
|
+
.attr(`fill`, `none`);
|
599
|
+
name.append(`text`).append(`textPath`)
|
600
|
+
.attr(`href`, sector => `#${sector.idText}_name`, `http://www.w3.org/1999/xlink`)
|
601
|
+
.attr(`alignment-baseline`, `after-edge`)
|
602
|
+
.attr(`startOffset`, `50%`)
|
603
|
+
.attr(`style`, `text-anchor:middle;`)
|
604
|
+
.text(sector => sector.name);
|
605
|
+
}
|
606
|
+
}
|
607
|
+
|
608
|
+
let makeSegment = (selection) => {
|
609
|
+
selection
|
610
|
+
.attr(`id`, segment => `${segment.idText}`)
|
611
|
+
.classed(`segment`, true)
|
612
|
+
.append(`path`)
|
613
|
+
.classed(`radarLines`, true)
|
614
|
+
.attr(`d`, segment => arc(segment))
|
615
|
+
.attr(`fill`, segment => segment.color);
|
616
|
+
|
617
|
+
if(config.segment.showName){
|
618
|
+
let name = selection.append(`g`)
|
619
|
+
.classed(`segmentName`, true);
|
620
|
+
name.append(`path`)
|
621
|
+
.attr(`id`, segment => `${segment.idText}_namePath`)
|
622
|
+
.attr(`d`, segment => segmentNamePath(segment))
|
623
|
+
.attr(`fill`, `none`);
|
624
|
+
name.append(`text`).append(`textPath`)
|
625
|
+
.attr(`href`, segment => `#${segment.idText}_namePath`, `http://www.w3.org/1999/xlink`)
|
626
|
+
.attr(`alignment-baseline`, segment =>
|
627
|
+
(segment.endAngle > 1.5 * Math.PI && segment.endAngle <= 2 * Math.PI ||
|
628
|
+
segment.endAngle < 0.5 * Math.PI)
|
629
|
+
? `before-edge`
|
630
|
+
: `after-edge`)
|
631
|
+
.attr(`startOffset`,`50%`)
|
632
|
+
.attr(`style`, `text-anchor:middle;`)
|
633
|
+
.text(segment => (config.segment.showNameAsId) ? segment.index : segment.name);
|
634
|
+
}
|
635
|
+
}
|
636
|
+
|
637
|
+
let makeBlip = (selection) => {
|
638
|
+
selection
|
639
|
+
.attr(`id`, data => `${data.idText}`)
|
640
|
+
.classed(`blip`, true)
|
641
|
+
.attr(`transform`, data => translate(data.x, data.y))
|
642
|
+
.on(`click`, data => blipClick(data))
|
643
|
+
.on(`mouseover`, data => focusBlip(data))
|
644
|
+
.on(`mouseout`, data => focusAllBlips(data));
|
645
|
+
// blip outer ring
|
646
|
+
selection.append(`circle`)
|
647
|
+
.attr(`r`, config.blip.outerCircleRadius)
|
648
|
+
.attr(`fill`, `rgba(0, 0, 0, 0)`)
|
649
|
+
.attr(`stroke-width`, config.blip.strokeWidth)
|
650
|
+
.attr(`stroke`, getBlipRingColor);
|
651
|
+
// blip indicater for movement
|
652
|
+
selection.append(`path`)
|
653
|
+
.attr(`d`, getBlipMovedIndicator)
|
654
|
+
.attr(`fill`, `none`)
|
655
|
+
.attr(`stroke-width`, config.blip.strokeWidth)
|
656
|
+
.attr(`stroke`, getBlipColor);
|
657
|
+
// blip innerCircle
|
658
|
+
selection.append('circle')
|
659
|
+
.attr('r', config.blip.innerCircleRadius)
|
660
|
+
.attr('fill', getBlipColor);
|
661
|
+
// blip text
|
662
|
+
selection.append('text')
|
663
|
+
.classed('blipText', true)
|
664
|
+
.attr('y', config.blip.fontSize/3)
|
665
|
+
.attr('text-anchor', 'middle')
|
666
|
+
.style(`font-size`, config.blip.fontSize)
|
667
|
+
.text(data => data.id);
|
668
|
+
}
|
669
|
+
|
670
|
+
let makeBubble = (selection) => {
|
671
|
+
selection
|
672
|
+
.classed(`radarBubble`, true)
|
673
|
+
.attr(`id`, `${radarId}_bubble`)
|
674
|
+
.style(`display`, `none`)
|
675
|
+
let fontSize = config.blip.radius;
|
676
|
+
selection.append('rect')
|
677
|
+
.attr('class', 'background')
|
678
|
+
.attr('rx', 4)
|
679
|
+
.attr('ry', 4);
|
680
|
+
selection.append('text')
|
681
|
+
.attr('class', 'bubbleText')
|
682
|
+
.attr(`y`, -fontSize/9)
|
683
|
+
.style(`font-size`, fontSize);
|
684
|
+
selection.append('path')
|
685
|
+
.attr('class', 'background')
|
686
|
+
.attr('d', 'M 0,0 10,0 5,8 z');
|
687
|
+
}
|
688
|
+
//#endregion ------------------------------------------------------------------------
|
689
|
+
|
690
|
+
//#region d3-components radar legend ------------------------------------------------
|
691
|
+
let makeLegendBlipStates = (selection) => {
|
692
|
+
selection.append(`span`)
|
693
|
+
.classed(`stateColor`, true)
|
694
|
+
.style(`background-color`, data => data.color);
|
695
|
+
selection.append(`span`)
|
696
|
+
.classed(`paddingText`, true)
|
697
|
+
.text(data => data.name);
|
698
|
+
}
|
699
|
+
|
700
|
+
let makeLegendBlipMovement = (selection) => {
|
701
|
+
selection.append(`span`)
|
702
|
+
.classed(`movementIndicator`, true)
|
703
|
+
.classed(`in`, data => data.value > 0)
|
704
|
+
.classed(`out`, data => data.value < 0);
|
705
|
+
selection.append(`span`)
|
706
|
+
.classed(`paddingText`, true)
|
707
|
+
.text(data => data.name);
|
708
|
+
}
|
709
|
+
|
710
|
+
let makeLegendRings = (selection) => {
|
711
|
+
selection.append(`span`)
|
712
|
+
.classed(`text`, true)
|
713
|
+
.text(data => `${data.index}. ${data.name}`);
|
714
|
+
}
|
715
|
+
//#endregion ------------------------------------------------------------------------
|
716
|
+
|
717
|
+
//#region d3-components radar blip legend -------------------------------------------
|
718
|
+
let makeBlipLegendSector = (selection) => {
|
719
|
+
selection
|
720
|
+
.attr(`id`, sector => `${sector.idText}_legend`)
|
721
|
+
.classed(`blipLegendSector card`, true)
|
722
|
+
.on(`click, mouseover`, sector => focusSector(sector))
|
723
|
+
.on(`mouseout`, focusAllSector)
|
724
|
+
.text(sector => sector.name);
|
725
|
+
}
|
726
|
+
|
727
|
+
let makeBlipLegendSegment = (selection) => {
|
728
|
+
selection
|
729
|
+
.attr(`id`, segment => `${segment.idText}_legend`)
|
730
|
+
.classed(`blipLegendSegment subCard`, true)
|
731
|
+
.text(segment => segment.name);
|
732
|
+
}
|
733
|
+
|
734
|
+
let makeBlipLegendBlip = (selection) => {
|
735
|
+
selection
|
736
|
+
.attr(`id`, blip => `${blip.idText}_legend`)
|
737
|
+
.classed(`blipLegendBlip cardItem`, true)
|
738
|
+
.on(`click`, blip => blipClick(blip))
|
739
|
+
.on(`mouseover`, blip => focusBlip(blip))
|
740
|
+
.on(`mouseout`, blip => focusAllBlips(blip))
|
741
|
+
.text(blip => `${blip.id} ${blip.name}`);
|
742
|
+
}
|
743
|
+
//#endregion ------------------------------------------------------------------------
|
744
|
+
|
745
|
+
//#region generate selection ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
746
|
+
let selectionDropdownText = radarDiv.select(`.radarSelection`)
|
747
|
+
.on(`click`, ()=> {
|
748
|
+
document.getElementById(`${radarId}_selectionDropdown`)
|
749
|
+
.classList.toggle(`active`);
|
750
|
+
})
|
751
|
+
.append(`span`)
|
752
|
+
.classed(`dropdownText`, true)
|
753
|
+
.text(config.radar.showAllSectorsText);
|
754
|
+
|
755
|
+
// append a dropdown content div to add the dropdown options
|
756
|
+
let selectionDropdownContent = radarDiv.select(`.radarSelection`)
|
757
|
+
.append(`div`)
|
758
|
+
.classed(`dropdownContent`, true);
|
759
|
+
|
760
|
+
// append the first dropdown option to show the whole radar
|
761
|
+
selectionDropdownContent
|
762
|
+
.append(`div`)
|
763
|
+
.classed(`selectionButton`, true)
|
764
|
+
.style(`display`, (mobileMode) ? `none` : `block`)
|
765
|
+
.text(config.radar.showAllSectorsText)
|
766
|
+
.on(`click`, () => {
|
767
|
+
displayAllSectors();
|
768
|
+
changeSvgViewbox(`${radarId}_radarContent`);
|
769
|
+
selectionDropdownText.text(config.radar.showAllSectorsText);
|
770
|
+
});
|
771
|
+
|
772
|
+
// append a dropdown option for each sector in radar
|
773
|
+
selectionDropdownContent.selectAll(null)
|
774
|
+
.data(radarData.sectors)
|
775
|
+
.enter()
|
776
|
+
.append(`div`)
|
777
|
+
.classed(`selectionButton`, true)
|
778
|
+
.text(sector => sector.name)
|
779
|
+
.on(`click`, sector => {
|
780
|
+
displaySector(sector);
|
781
|
+
changeSvgViewbox(sector.idText);
|
782
|
+
selectionDropdownText.text(sector.name);
|
783
|
+
});
|
784
|
+
//#endregion ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
785
|
+
|
786
|
+
//#region generate radar ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
787
|
+
let sectors = d3.select(`g#${radarId}_radarContent`)
|
788
|
+
.selectAll(`.sector`)
|
789
|
+
.data(radarData.sectors)
|
790
|
+
.enter()
|
791
|
+
.append(`g`)
|
792
|
+
.call(makeSector);
|
793
|
+
|
794
|
+
let segments = sectors.selectAll(`.segment`)
|
795
|
+
.data(sector => sector.segments )
|
796
|
+
.enter()
|
797
|
+
.append(`g`)
|
798
|
+
.call(makeSegment);
|
799
|
+
|
800
|
+
let blips = segments.selectAll(`.blip`)
|
801
|
+
.data(segment => segment.blips)
|
802
|
+
.enter()
|
803
|
+
.append(`g`)
|
804
|
+
.call(makeBlip);
|
805
|
+
|
806
|
+
let bubble = d3.select(`g#${radarId}_radarContent`)
|
807
|
+
.append(`g`)
|
808
|
+
.call(makeBubble);
|
809
|
+
//#endregion ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
810
|
+
|
811
|
+
//#region generate radar legend +++++++++++++++++++++++++++++++++++++++++++++++++++++
|
812
|
+
let radarLegendContainer = radarDiv.select(`.radarLegend`)
|
813
|
+
.append(`div`)
|
814
|
+
.attr(`id`, `${radarId}_radarLegendContainer`)
|
815
|
+
.classed(`dropdownContent`, true);
|
816
|
+
|
817
|
+
// generate entry states legend
|
818
|
+
let entryStatesLegend = radarLegendContainer.append(`div`)
|
819
|
+
.classed(`card`, true)
|
820
|
+
entryStatesLegend.append(`div`)
|
821
|
+
.classed(`cardTitle`, true)
|
822
|
+
.text(structure.entryStates.legendTitle);
|
823
|
+
entryStatesLegend.selectAll(null)
|
824
|
+
.data(structure.entryStates.list)
|
825
|
+
.enter()
|
826
|
+
.append(`div`)
|
827
|
+
.classed(`cardItem`, true)
|
828
|
+
.call(makeLegendBlipStates)
|
829
|
+
.on(`mouseover`, (data)=> focusBlipByState(data))
|
830
|
+
.on(`mouseout`, data => focusAllBlips(data));
|
831
|
+
|
832
|
+
// generate entry movement legend
|
833
|
+
let entryMovementLegend = radarLegendContainer.append(`div`)
|
834
|
+
.classed(`card`, true);
|
835
|
+
entryMovementLegend.append(`div`)
|
836
|
+
.classed(`cardTitle`, true)
|
837
|
+
.text(structure.entryMovement.legendTitle);
|
838
|
+
entryMovementLegend.selectAll(null)
|
839
|
+
.data(structure.entryMovement.list)
|
840
|
+
.enter()
|
841
|
+
.append(`div`)
|
842
|
+
.classed(`cardItem`, true)
|
843
|
+
.call(makeLegendBlipMovement)
|
844
|
+
.on(`mouseover`, (data)=> focusBlipByMovement(data.value))
|
845
|
+
.on(`mouseout`, data => focusAllBlips(data));
|
846
|
+
|
847
|
+
// generate ring legend
|
848
|
+
let ringLegend = radarLegendContainer.append(`div`)
|
849
|
+
.classed(`card`, true);
|
850
|
+
ringLegend.append(`div`)
|
851
|
+
.classed(`cardTitle`, true)
|
852
|
+
.text(structure.rings.legendTitle);
|
853
|
+
ringLegend.selectAll(null)
|
854
|
+
.data(radarData.rings)
|
855
|
+
.enter()
|
856
|
+
.append(`div`)
|
857
|
+
.classed(`cardItem`, true)
|
858
|
+
.call(makeLegendRings)
|
859
|
+
.on(`mouseover`, (data)=> focusRing(data))
|
860
|
+
.on(`mouseout`, ()=> focusAllRings());
|
861
|
+
//#endregion ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
862
|
+
|
863
|
+
//#region generate radar blip legend ++++++++++++++++++++++++++++++++++++++++++++++++
|
864
|
+
let blipLegendSectors = radarDiv.select(`.radarBlipLegend`).selectAll(null)
|
865
|
+
.data(radarData.sectors)
|
866
|
+
.enter()
|
867
|
+
.append(`div`)
|
868
|
+
.call(makeBlipLegendSector);
|
869
|
+
|
870
|
+
let blipLegendSegments = blipLegendSectors.selectAll(null)
|
871
|
+
.data(sector => sector.segments.filter(segment => segment.blips.length != 0))
|
872
|
+
.enter()
|
873
|
+
.append(`div`)
|
874
|
+
.call(makeBlipLegendSegment);
|
875
|
+
|
876
|
+
let blipLegendBlips = blipLegendSegments.selectAll(null)
|
877
|
+
.data(segment => segment.blips)
|
878
|
+
.enter()
|
879
|
+
.append(`div`)
|
880
|
+
.call(makeBlipLegendBlip)
|
881
|
+
//#endregion ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
882
|
+
|
883
|
+
//#region forceSimulation %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
884
|
+
// make sure that blips stay inside their segment
|
885
|
+
let ticked = () => blips.attr(`transform`, (d) => translate(
|
886
|
+
d.segmentFunctions.clip(d).x,
|
887
|
+
d.segmentFunctions.clip(d).y
|
888
|
+
));
|
889
|
+
// distribute blips, while avoiding collisions
|
890
|
+
d3.forceSimulation(radarData.blips)
|
891
|
+
.force(`collision`,
|
892
|
+
d3.forceCollide()
|
893
|
+
.radius(blipSize/2 + config.blip.margin)
|
894
|
+
.strength(0.15))
|
895
|
+
.on(`tick`, ticked);
|
896
|
+
//#endregion %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
897
|
+
|
898
|
+
update();
|
899
|
+
console.log(radarData, config);
|
900
|
+
|
901
|
+
|
902
|
+
}
|