Skip to content

Commit b1546ee

Browse files
committed
updated EpiCurve to allow label size font and legend font size to be adjustable, added borders, updated color scheme and updated timeline speed options to include 50 and 100.
1 parent 98557ce commit b1546ee

4 files changed

Lines changed: 107 additions & 54 deletions

File tree

src/app/microbe-trace-next-plugin.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@
360360
</div>
361361
<div class="form-group row " title="How much time between ticks of the timeline (ms)?">
362362
<div class="col-4"><label>Timeline Speed: {{ timelineSpeed }} ms</label></div>
363-
<div class="col-8"><input type="range" class="custom-range" min="200" step="100" max="1000" [(ngModel)]="timelineSpeed"></div>
363+
<div class="col-8"><input type="range" class="custom-range" min="0" step="1" [max]="timelineSpeedOptions.length - 1" [ngModel]="timelineSpeedIndex" (ngModelChange)="onTimelineSpeedIndexChanged($event)"></div>
364364
</div>
365365
<div>Timeline works on 2D Network, Map, and Bubble Views.</div>
366366
</div>

src/app/microbe-trace-next-plugin.component.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,9 @@ export class MicrobeTraceNextHomeComponent extends AppComponentBase implements A
163163
SelectedColorLinksByVariable: string = 'origin';
164164

165165
SelectedTimelineVariable: string = 'None';
166+
timelineSpeedOptions: number[] = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000];
166167
timelineSpeed: number = 200;
168+
timelineSpeedIndex: number = this.timelineSpeedOptions.indexOf(this.timelineSpeed);
167169

168170

169171
LinkColorTableTypes: any = [
@@ -1942,6 +1944,15 @@ export class MicrobeTraceNextHomeComponent extends AppComponentBase implements A
19421944

19431945
}
19441946

1947+
public onTimelineSpeedIndexChanged(index: number): void {
1948+
const parsedIndex = Number(index);
1949+
const safeIndex = Number.isFinite(parsedIndex)
1950+
? Math.max(0, Math.min(this.timelineSpeedOptions.length - 1, parsedIndex))
1951+
: this.timelineSpeedIndex;
1952+
this.timelineSpeedIndex = safeIndex;
1953+
this.timelineSpeed = this.timelineSpeedOptions[safeIndex];
1954+
}
1955+
19451956
update(h) {
19461957

19471958
this.handle.attr("cx", this.xAttribute(h));

src/app/visualizationComponents/TimelineComponent/timeline-component.component.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@
8181
<input type="number" class="form-control form-control-sm" min="1" max="4" step="1" [(ngModel)]="tickInterval" (ngModelChange)="onTickIntevalChange()">
8282
</div>
8383
</div>
84+
<div class="form-group row node-label-row" title="Select a font Size for axis labels.">
85+
<div class="col-4 padding-right-5"><label>Label Size</label></div>
86+
<div class="col-8"><input type="range" class="custom-range" min="10" step="2" max="28" [(ngModel)]="labelSize" (ngModelChange)="onLabelSizeChange()"></div>
87+
</div>
8488
<div class="form-group row" title="Show data noncumulatively or cumulatively.">
8589
<div class="col-4">Epi Curve</div>
8690
<div class="col-8">
@@ -104,6 +108,12 @@
104108
<p-selectButton [options]="legendPositionOptions" [(ngModel)]="widgets['epiCurve-legendPosition']" (ngModelChange)="onLegendPositionChange()"></p-selectButton>
105109
</div>
106110
</div>
111+
@if (widgets['epiCurve-legendPosition'] != 'Hide') {
112+
<div class="form-group row node-label-row" title="Select a font size for legend labels.">
113+
<div class="col-4 padding-right-5"><label>Legend Size</label></div>
114+
<div class="col-8"><input type="range" class="custom-range" min="10" step="2" max="28" [(ngModel)]="legendLabelSize" (ngModelChange)="onLegendLabelSizeChange()"></div>
115+
</div>
116+
}
107117
</div>
108118
</div>
109119
</div>

src/app/visualizationComponents/TimelineComponent/timeline-component.component.ts

Lines changed: 85 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ export class TimelineComponent extends BaseComponentDirective implements OnInit,
3838
SelectedDateFieldVariable3;
3939
binSizes = ['Day', 'Week', 'Month', 'Quarter', 'Year']
4040
tickInterval;
41+
labelSize = 12;
42+
legendLabelSize = 15;
4143

4244
graphTypes = ['Single Date Field', 'Multi: Side by Side', 'Multi: Overlay']
4345
selectedGraphType = 'Single Date Field';
@@ -157,7 +159,7 @@ export class TimelineComponent extends BaseComponentDirective implements OnInit,
157159

158160
// colors
159161
if (this.widgets['epiCurve-colors'] == undefined) {
160-
this.widgets['epiCurve-colors'] = ['#f5d742', '#f20fff', '#20ff1a'];
162+
this.widgets['epiCurve-colors'] = ['#C6D8EB','#B79ECC', '#F3BF79'];
161163
}
162164

163165
// stackColorBy field
@@ -185,13 +187,6 @@ export class TimelineComponent extends BaseComponentDirective implements OnInit,
185187

186188
// this.initializeD3Chart();
187189
this.setupEventListeners();
188-
189-
if (this.widgets['epiCurve-legendPosition'] == 'Bottom') {
190-
this.margin.bottom = 100;
191-
} else {
192-
this.margin.bottom = 50;
193-
}
194-
195190
this.refresh();
196191
}
197192

@@ -236,7 +231,7 @@ public refresh(): void {
236231

237232
const epiCurve = this.svg.append("g")
238233
.classed("epiCurve-epi-curve", true)
239-
.attr("transform", `translate(${this.margin.left}, 5)`);
234+
.attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
240235

241236
let bins = this.histogram(this.vnodes);
242237

@@ -267,6 +262,7 @@ public refresh(): void {
267262
.attr("width", d => this.x(d.x1) - this.x(d.x0))
268263
.attr("height", d => this.height - this.y(d.length2[ind]))
269264
.attr("fill", this.localColorMap(value) )
265+
.attr("stroke", "black" )
270266

271267
nodeColors.push(this.localColorMap(value));
272268
})
@@ -280,7 +276,8 @@ public refresh(): void {
280276
.attr("transform", d => `translate(${this.x(d.x0)}, ${this.y(d.length)})`)
281277
.attr("width", d => this.x(d.x1) - this.x(d.x0))
282278
.attr("height", d => this.height - this.y(d.length))
283-
.attr("fill", color);
279+
.attr("fill", color)
280+
.attr("stroke", "black");
284281

285282
this.generateLegend(epiCurve, [color] ,[this.SelectedDateFieldVariable])
286283
}
@@ -336,13 +333,12 @@ private refreshMulti(): void {
336333

337334
this.svg = d3.select(this.epiCurveSVGElement.nativeElement)
338335
.attr("width", this.width + this.margin.left + this.margin.right)
339-
.attr("height", this.height + this.margin.top + this.margin.bottom)
340-
.attr("transform", `translate(0, ${this.margin.top})`);
336+
.attr("height", this.height + this.margin.top + this.margin.bottom);
341337

342338

343339
const epiCurve = this.svg.append("g")
344340
.classed("epiCurve-epi-curve", true)
345-
.attr("transform", `translate(${this.margin.left}, 5)`);
341+
.attr("transform", `translate(${this.margin.left}, ${this.margin.top})`);
346342

347343
let maxCount = 0;
348344
let bins = [];
@@ -405,7 +401,8 @@ private refreshMulti(): void {
405401
.attr("transform", d => `translate(${xOffset[ind](d, that)}, ${this.y(d.length)})`)
406402
.attr("width", width)
407403
.attr("height", d => this.height - this.y(d.length))
408-
.attr("fill", colors[ind]);
404+
.attr("fill", colors[ind])
405+
.attr("stroke", "black" );
409406
});
410407
}
411408

@@ -430,6 +427,7 @@ updateLocalColorMap() {
430427
}
431428

432429
updateSizes() {
430+
this.updateBottomMargin();
433431
const wrapper = $(this.epiCurveElement.nativeElement).parent();
434432
$('#epiCurve').height(wrapper.height() - 50);
435433
this.width = wrapper.width() - this.margin.left - this.margin.right;
@@ -438,6 +436,16 @@ updateSizes() {
438436
this.middle = this.height / 2;
439437
}
440438

439+
private updateBottomMargin() {
440+
const baseBottomMargin = this.widgets['epiCurve-legendPosition'] == 'Bottom' ? 100 : 50;
441+
const labelSizePadding = Math.max(0, this.labelSize - 12) * 2;
442+
const legendSizePadding = this.widgets['epiCurve-legendPosition'] == 'Bottom' ? Math.max(0, this.legendLabelSize - 15) * 2 : 0;
443+
this.margin.bottom = baseBottomMargin + labelSizePadding + legendSizePadding;
444+
this.margin.top = Math.max(8, Math.round(this.labelSize * 0.75));
445+
this.margin.left = Math.max(45, Math.round(this.labelSize * 3.2));
446+
this.margin.right = Math.max(20, Math.round(this.labelSize * 2))+10;
447+
}
448+
441449
getTimes(fields) {
442450
let times = [];
443451
this.vnodes = JSON.parse(JSON.stringify(this.commonService.session.data.nodes));
@@ -458,38 +466,56 @@ getTimes(fields) {
458466
}
459467
return times;
460468
}
469+
onLabelSizeChange() {
470+
this.refresh();
471+
}
461472

462473
updateAxes() {
463-
let [xAxis, xLabelOffset] = this.configureXAxisSettings();
474+
const xAxis = this.configureXAxisSettings();
475+
const yDomainMax = Math.max(0, Math.ceil(this.y.domain()[1] as number));
476+
const yTickStep = Math.max(1, Math.ceil(yDomainMax / 10));
477+
const yTickValues = d3.range(0, yDomainMax + 1, yTickStep);
478+
if (yTickValues[yTickValues.length - 1] !== yDomainMax) {
479+
yTickValues.push(yDomainMax);
480+
}
481+
const yAxis = d3.axisLeft(this.y)
482+
.tickValues(yTickValues)
483+
.tickFormat((d: number) => `${Math.round(d)}`);
464484

465-
this.svg.append("g")
485+
const xLabelY = this.height + this.margin.top + Math.max(40, Math.round(this.labelSize * 2.3));
486+
const yTickOffset = -Math.max(9, Math.round(this.labelSize * 0.9));
487+
488+
const xAxisGroup = this.svg.append("g")
466489
.attr("class", "axis axis--x")
467-
.attr("transform", `translate(${this.margin.left}, ${this.height+5})`)
490+
.attr("transform", `translate(${this.margin.left}, ${this.height + this.margin.top})`)
468491
.call(xAxis)
469-
.attr("text-anchor", "center")
470-
.selectAll("text")
471-
.attr("x", xLabelOffset);
492+
.attr("text-anchor", "middle")
493+
.attr("font-size", this.labelSize);
494+
xAxisGroup.selectAll("text").attr("text-anchor", "middle");
472495

473496
this.svg.append("g")
474497
.attr("class", "axis axis--y")
475-
.attr("transform", `translate(${this.margin.left}, 5)`)
476-
.call(d3.axisLeft(this.y))
477-
.attr("text-anchor", null)
498+
.attr("transform", `translate(${this.margin.left}, ${this.margin.top})`)
499+
.call(yAxis)
500+
.attr("font-size", this.labelSize)
478501
.selectAll("text")
479-
.attr("x", -20);
502+
.attr("text-anchor", "end")
503+
.attr("x", yTickOffset);
480504

481505
this.svg.append("text")
482506
.attr("class", "x label")
483-
.attr("text-anchor", "center")
484-
.attr("x", this.width/2)
485-
.attr("y", this.height + 40)
507+
.attr("text-anchor", "middle")
508+
.attr("font-size", this.labelSize)
509+
.attr("x", this.margin.left + this.width / 2)
510+
.attr("y", xLabelY)
486511
.text(`Date (${this.widgets['epiCurve-binSize']=='Day'? 'Dai': this.widgets['epiCurve-binSize']}ly Bins)`);
487512

488513
this.svg.append("text")
489514
.attr("class", "y label")
490-
.attr("text-anchor", "center")
491-
.attr("y", 15)
492-
.attr("x", -this.middle-this.margin.top-this.margin.bottom)
515+
.attr("text-anchor", "middle")
516+
.attr("font-size", this.labelSize)
517+
.attr("y", Math.max(14, Math.round(this.labelSize * 0.95)))
518+
.attr("x", -(this.margin.top + this.height / 2))
493519
.attr("transform", "rotate(-90)")
494520
.text("Number of Cases");
495521

@@ -523,54 +549,62 @@ updateAxes() {
523549
}
524550

525551
generateLegend(epiCurve, colors, fieldNames) {
552+
const legendFontSize = Math.max(6, Number(this.legendLabelSize || 15));
553+
const legendFontSizePx = `${legendFontSize}px`;
554+
const markerRadius = Math.max(4, Math.round(legendFontSize * 0.35));
555+
const markerTextGap = Math.max(8, Math.round(legendFontSize * 0.65));
556+
const legendRowHeight = Math.max(22, Math.round(legendFontSize * 1.9));
557+
const legendCharWidth = Math.max(5.5, legendFontSize * 0.55);
558+
const legendItemGap = Math.max(24, Math.round(legendFontSize * 1.8));
559+
526560
let xOffset = 50; // default for left position
527561
if (this.widgets['epiCurve-legendPosition'] == 'Hide') {
528562
return;
529563
} else if (this.widgets['epiCurve-legendPosition'] == 'Bottom') {
530564
let prevLength = 0;
531565
let rowCount = 0;
532-
let y = this.height + 50;
566+
let y = this.height + this.margin.bottom - (Math.max(legendRowHeight, Math.round(this.labelSize * 1.5)) + 14);
533567
if (this.selectedGraphType=='Single Date Field' && this.widgets['epiCurve-stackColorBy'] == 'Node Color' && this.commonService.session.style.widgets['node-color-variable'] != 'None') {
534568
let field = this.commonService.capitalize(this.commonService.session.style.widgets['node-color-variable']);
535-
epiCurve.append("text").attr("x", 70).attr("y", y).text(field + ': ').style("font-size", "15px").attr("alignment-baseline","middle")
569+
epiCurve.append("text").attr("x", 70).attr("y", y).text(field + ': ').style("font-size", legendFontSizePx).attr("alignment-baseline","middle")
536570
prevLength += field.length + 3;
537571
} else if (this.selectedGraphType=='Single Date Field' && this.widgets['epiCurve-stackColorBy'] != 'Node Color' && this.widgets['epiCurve-stackColorBy'] != 'None') {
538-
epiCurve.append("text").attr("x", 70).attr("y", y).text(this.widgets['epiCurve-stackColorBy'] + ': ').style("font-size", "15px").attr("alignment-baseline","middle")
572+
epiCurve.append("text").attr("x", 70).attr("y", y).text(this.widgets['epiCurve-stackColorBy'] + ': ').style("font-size", legendFontSizePx).attr("alignment-baseline","middle")
539573
prevLength += this.widgets['epiCurve-stackColorBy'].length + 3;
540574
}
541575
fieldNames.forEach((name, i) => {
542576
// this first section calculates the location for each item/name in the legend
543577
let nLength = name==null ? 7: name.toString().length
544-
let baseX = 70+40*(rowCount) + prevLength*7.5;
545-
if (baseX+24+nLength*3 > this.width-70) {
578+
let baseX = 70 + legendItemGap * rowCount + prevLength * legendCharWidth;
579+
if (baseX + markerRadius * 2 + markerTextGap + nLength * legendCharWidth > this.width - 70) {
546580
rowCount = 0;
547-
y += 30;
581+
y -= legendRowHeight;
548582
prevLength = 0;
549583
baseX = 70;
550584
}
551585

552-
epiCurve.append("circle").attr("cx", baseX).attr("cy", y).attr("r", 6).style("fill", colors[i])
553-
epiCurve.append("text").attr("x", baseX+10).attr("y", y).text(this.commonService.capitalize(name==null? '(Empty)': name.toString())).style("font-size", "15px").attr("alignment-baseline","middle")
586+
epiCurve.append("circle").attr("cx", baseX).attr("cy", y).attr("r", markerRadius).style("fill", colors[i])
587+
epiCurve.append("text").attr("x", baseX + markerRadius + markerTextGap).attr("y", y).text(this.commonService.capitalize(name==null? '(Empty)': name.toString())).style("font-size", legendFontSizePx).attr("alignment-baseline","middle")
554588

555589
prevLength += nLength;
556590
rowCount += 1;
557591
})
558592
return;
559593
} else if (this.widgets['epiCurve-legendPosition'] == 'Right') {
560-
xOffset = this.width - 120;
594+
xOffset = this.width - Math.max(120, Math.round(legendFontSize * 8));
561595
}
562596
let count = 0;
563597
if (this.selectedGraphType=='Single Date Field' && this.widgets['epiCurve-stackColorBy'] == 'Node Color' && this.commonService.session.style.widgets['node-color-variable'] != 'None') {
564598
let field = this.commonService.capitalize(this.commonService.session.style.widgets['node-color-variable']);
565-
epiCurve.append("text").attr("x", xOffset).attr("y", 30).text(field + ': ').style("font-size", "15px").attr("alignment-baseline","middle")
599+
epiCurve.append("text").attr("x", xOffset).attr("y", legendRowHeight).text(field + ': ').style("font-size", legendFontSizePx).attr("alignment-baseline","middle")
566600
count += 1;
567601
} else if (this.selectedGraphType=='Single Date Field' && this.widgets['epiCurve-stackColorBy'] != 'Node Color' && this.widgets['epiCurve-stackColorBy'] != 'None') {
568-
epiCurve.append("text").attr("x", xOffset).attr("y", 30).text(this.widgets['epiCurve-stackColorBy'] + ': ').style("font-size", "15px").attr("alignment-baseline","middle")
602+
epiCurve.append("text").attr("x", xOffset).attr("y", legendRowHeight).text(this.widgets['epiCurve-stackColorBy'] + ': ').style("font-size", legendFontSizePx).attr("alignment-baseline","middle")
569603
count += 1;
570604
}
571605
fieldNames.forEach((name, i) => {
572-
epiCurve.append("circle").attr("cx",xOffset).attr("cy",30*(count+1)).attr("r", 6).style("fill", colors[i])
573-
epiCurve.append("text").attr("x", xOffset+20).attr("y", 30*(count+1)).text(this.commonService.capitalize(name==null? '(Empty)': name.toString())).style("font-size", "15px").attr("alignment-baseline","middle")
606+
epiCurve.append("circle").attr("cx", xOffset).attr("cy", legendRowHeight * (count + 1)).attr("r", markerRadius).style("fill", colors[i])
607+
epiCurve.append("text").attr("x", xOffset + markerRadius + markerTextGap).attr("y", legendRowHeight * (count + 1)).text(this.commonService.capitalize(name==null? '(Empty)': name.toString())).style("font-size", legendFontSizePx).attr("alignment-baseline","middle")
574608
count += 1;
575609
})
576610
}
@@ -719,17 +753,15 @@ updateBins(bins, colorVariable='None', nodeColorKeys=undefined) {
719753

720754
/**
721755
*
722-
* @return xAxis which is used to determine the interval and label for xAxis ticks and xLabelOffet which determine how much to shift each label
756+
* @return xAxis which is used to determine the interval and label for xAxis ticks
723757
*/
724758
configureXAxisSettings() {
725759
let xAxis;
726760
let numberOfDays = d3.timeDay.count(this.timeDomainStart, this.timeDomainEnd);
727-
let xLabelOffset = -10;
728761
if (this.widgets['epiCurve-binSize'] == 'Year') {
729762
xAxis = d3.axisBottom(this.x).ticks(d3.timeYear).tickFormat(d3.timeFormat("%Y"));
730763
} else if (numberOfDays<366) {
731764
xAxis = d3.axisBottom(this.x).ticks(d3.timeMonth.every(this.tickInterval)).tickFormat(d3.timeFormat("%b %Y"))
732-
xLabelOffset = -25;
733765
} else if (this.widgets['epiCurve-binSize'] == 'Quarter') {
734766
xAxis = d3.axisBottom(this.x)
735767
.ticks(d3.timeMonth.every(this.tickInterval < 3 ? this.tickInterval * 3 : 12))
@@ -739,7 +771,7 @@ configureXAxisSettings() {
739771
.ticks(d3.timeMonth.every(this.tickInterval))
740772
.tickFormat((d: Date) => d <= d3.timeYear(d) ? d.getFullYear().toString() : null);
741773
}
742-
return [xAxis, xLabelOffset]
774+
return xAxis;
743775
}
744776

745777
goldenLayoutComponentResize() {
@@ -817,11 +849,11 @@ onNodeColorChanged() {
817849
}
818850

819851
onLegendPositionChange() {
820-
if (this.widgets['epiCurve-legendPosition'] == 'Bottom') {
821-
this.margin.bottom = 100;
822-
} else {
823-
this.margin.bottom = 50;
824-
}
852+
this.refresh();
853+
}
854+
855+
onLegendLabelSizeChange() {
856+
this.legendLabelSize = Number(this.legendLabelSize);
825857
this.refresh();
826858
}
827859

@@ -1092,4 +1124,4 @@ onFilterDataChange() {
10921124

10931125
export namespace TimelineComponent {
10941126
export const componentTypeName = 'Epi Curve';
1095-
}
1127+
}

0 commit comments

Comments
 (0)