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.
@@ -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
+ }