intia-theme 0.1.56 → 0.1.59

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }