Skip to content

Commit a08ecaf

Browse files
committed
Data update for renderers
1 parent 91a0ad4 commit a08ecaf

File tree

7 files changed

+289
-48
lines changed

7 files changed

+289
-48
lines changed

src/graphs/Renderer.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export class Renderer {
3131
* @returns {d3.Selection} The created SVG element.
3232
*/
3333
createSvg(selector, height = this.height, width = this.width) {
34-
this.graphElementSelector = selector;
3534
const htmlElement = document.querySelector(selector);
3635
if (htmlElement) {
3736
htmlElement.innerHTML = '';

src/graphs/UIControlsRenderer.js

Lines changed: 111 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,16 @@ export class UIControlsRenderer extends Renderer {
135135
* @param {number} noOfDays - The number of days for the reporting range.
136136
* @returns {Array} The computed start and end dates of the reporting range.
137137
*/
138+
138139
computeReportingRange(noOfDays) {
139-
const finalDate = this.data[this.data.length - 1][this.datePropertyName];
140+
// Ensure finalDate is a Date object
141+
const finalDateRaw = this.data[this.data.length - 1][this.datePropertyName];
142+
const finalDate = finalDateRaw instanceof Date ? finalDateRaw : new Date(finalDateRaw);
140143
let endDate = new Date(finalDate);
141144
let startDate = addDaysToDate(finalDate, -Number(noOfDays));
142145
if (this.selectedTimeRange) {
143-
endDate = new Date(this.selectedTimeRange[1]);
144-
startDate = new Date(this.selectedTimeRange[0]);
146+
endDate = this.selectedTimeRange[1] instanceof Date ? new Date(this.selectedTimeRange[1]) : new Date(this.selectedTimeRange[1]);
147+
startDate = this.selectedTimeRange[0] instanceof Date ? new Date(this.selectedTimeRange[0]) : new Date(this.selectedTimeRange[0]);
145148
const diffDays = Number(noOfDays) - calculateDaysBetweenDates(startDate, endDate).roundedDays;
146149
if (diffDays < 0) {
147150
startDate = addDaysToDate(startDate, -Number(diffDays));
@@ -154,8 +157,11 @@ export class UIControlsRenderer extends Renderer {
154157
}
155158
}
156159
}
157-
if (startDate < this.data[0][this.datePropertyName]) {
158-
startDate = this.data[0][this.datePropertyName];
160+
// Ensure startDate and endDate are not before/after data bounds
161+
const firstDateRaw = this.data[0][this.datePropertyName];
162+
const firstDate = firstDateRaw instanceof Date ? firstDateRaw : new Date(firstDateRaw);
163+
if (startDate < firstDate) {
164+
startDate = firstDate;
159165
}
160166
if (endDate < this.x.domain()[1]) {
161167
endDate = this.x.domain()[1];
@@ -173,20 +179,103 @@ export class UIControlsRenderer extends Renderer {
173179
createXAxis(x, timeInterval = this.timeInterval) {
174180
let axis;
175181
switch (timeInterval) {
176-
case 'days':
182+
case 'days': {
183+
const ticks = x.ticks(d3.timeDay.every(1));
177184
axis = d3
178185
.axisBottom(x)
179-
.ticks(d3.timeDay.every(1)) // label every 2 days
186+
.ticks(d3.timeDay.every(1))
180187
.tickFormat((d, i) => {
181-
return i % 2 === 0 ? d3.timeFormat('%b %d')(d) : '';
188+
const dayFormat = d3.timeFormat('%b %d');
189+
const yearFormat = d3.timeFormat('%Y');
190+
if (i === 0) return `${dayFormat(d)} ${yearFormat(d)}`;
191+
return i % 2 === 0 ? dayFormat(d) : '';
182192
});
183193
break;
184-
case 'weeks':
185-
axis = d3.axisBottom(x).ticks(d3.timeWeek);
194+
}
195+
case 'weeks': {
196+
const ticks = x.ticks(d3.timeDay);
197+
// Find the first tick that is a Monday
198+
let firstMonday = -1;
199+
for (let i = 0; i < ticks.length; i++) {
200+
if (ticks[i].getDay() === 1) {
201+
firstMonday = i;
202+
break;
203+
}
204+
}
205+
axis = d3
206+
.axisBottom(x)
207+
.ticks(d3.timeDay)
208+
.tickFormat((d, i) => {
209+
const dayFormat = d3.timeFormat('%b %d');
210+
const yearFormat = d3.timeFormat('%Y');
211+
if (i === firstMonday) return `${dayFormat(d)} ${yearFormat(d)}`;
212+
return d.getDay() === 1 && i > firstMonday ? dayFormat(d) : '';
213+
});
186214
break;
187-
case 'months':
188-
axis = d3.axisBottom(x).ticks(d3.timeMonth);
215+
}
216+
case 'months': {
217+
const ticks = x.ticks(d3.timeWeek);
218+
// Find the first tick that is the first week of a month
219+
let firstMonthWeek = -1;
220+
for (let i = 0; i < ticks.length; i++) {
221+
if (ticks[i].getDate() <= 7) {
222+
firstMonthWeek = i;
223+
break;
224+
}
225+
}
226+
const weeks = d3.timeWeek.range(x.domain()[0], x.domain()[1]);
227+
axis = d3
228+
.axisBottom(x)
229+
.ticks(d3.timeWeek)
230+
.tickFormat((d, i) => {
231+
const monthFormat = d3.timeFormat('%b');
232+
const yearFormat = d3.timeFormat('%Y');
233+
if (i === firstMonthWeek) return `${monthFormat(d)} ${yearFormat(d)}`;
234+
if (d.getDate() <= 7 && i > firstMonthWeek) {
235+
if (i > 0 && d.getFullYear() !== weeks[i - 1].getFullYear()) {
236+
return `${monthFormat(d)} ${yearFormat(d)}`;
237+
}
238+
return monthFormat(d);
239+
}
240+
return '';
241+
});
189242
break;
243+
}
244+
case 'bimonthly': {
245+
const ticks = x.ticks(d3.timeMonth);
246+
// Find the first tick that is the first month
247+
let firstQuarterMonth = -1;
248+
for (let i = 0; i < ticks.length; i++) {
249+
if (ticks[i].getMonth() % 2 === 0) {
250+
firstQuarterMonth = i;
251+
break;
252+
}
253+
}
254+
const months = d3.timeMonth.range(x.domain()[0], x.domain()[1]);
255+
axis = d3
256+
.axisBottom(x)
257+
.ticks(d3.timeMonth)
258+
.tickFormat((d, i) => {
259+
const monthFormat = d3.timeFormat('%b');
260+
const yearFormat = d3.timeFormat('%Y');
261+
if (i === firstQuarterMonth) return `${monthFormat(d)} ${yearFormat(d)}`;
262+
if (d.getMonth() % 2 === 0 && i > firstQuarterMonth) {
263+
// Show year if year changes from previous quarter tick
264+
const prevQuarterIndex = (() => {
265+
for (let j = i - 1; j >= 0; j--) {
266+
if (months[j].getMonth() % 2 === 0) return j;
267+
}
268+
return -1;
269+
})();
270+
if (prevQuarterIndex >= 0 && d.getFullYear() !== months[prevQuarterIndex].getFullYear()) {
271+
return `${monthFormat(d)} ${yearFormat(d)}`;
272+
}
273+
return monthFormat(d);
274+
}
275+
return '';
276+
});
277+
break;
278+
}
190279
default:
191280
return d3.axisBottom(x);
192281
}
@@ -210,6 +299,9 @@ export class UIControlsRenderer extends Renderer {
210299
case 'days':
211300
this.timeInterval = 'weeks';
212301
break;
302+
case 'bimonthly':
303+
this.timeInterval = 'weeks';
304+
break;
213305
default:
214306
this.timeInterval = 'weeks';
215307
}
@@ -220,14 +312,17 @@ export class UIControlsRenderer extends Renderer {
220312
this.eventBus?.emitEvents(`change-time-interval-${this.chartName}`, this.timeInterval);
221313
}
222314

223-
determineTheAppropriateAxisLabels() {
224-
if (this.reportingRangeDays <= 31) {
315+
determineTheAppropriateAxisLabels(noOfDays = this.reportingRangeDays) {
316+
if (noOfDays <= 31) {
225317
return 'days';
226318
}
227-
if (this.reportingRangeDays > 31 && this.reportingRangeDays <= 124) {
319+
if (noOfDays > 31 && noOfDays <= 150) {
228320
return 'weeks';
229321
}
230-
return 'months';
322+
if (noOfDays > 150 && noOfDays <= 750) {
323+
return 'months';
324+
}
325+
return 'bimonthly';
231326
}
232327

233328
/**

src/graphs/cfd/CFDRenderer.js

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -133,15 +133,44 @@ export class CFDRenderer extends UIControlsRenderer {
133133
}
134134

135135
/**
136-
* Clears the CFD graph and brush from the specified DOM elements.
137-
* @param {string} graphElementSelector - Selector of the DOM element to clear the graph.
138-
* @param {string} cfdBrushElementSelector - Selector of the DOM element to clear the brush.
136+
* Updates the renderer's data and re-renders the graph, preserving persistent props.
137+
* @param {Array} newData - The new data to render.
138+
*/
139+
updateData(newData) {
140+
this.data = newData;
141+
const dataDates = this.data.map((d) => new Date(d.date));
142+
const minDate = d3.min(dataDates);
143+
const maxDate = d3.max(dataDates);
144+
145+
if (this.selectedTimeRange) {
146+
let [selectedStart, selectedEnd] = this.selectedTimeRange;
147+
selectedStart = selectedStart < minDate ? minDate : selectedStart;
148+
selectedEnd = selectedEnd > maxDate ? maxDate : selectedEnd;
149+
this.selectedTimeRange = [selectedStart, selectedEnd];
150+
} else {
151+
this.selectedTimeRange = [minDate, maxDate];
152+
}
153+
if (this.graphElementSelector) {
154+
this.clearGraph(this.graphElementSelector, this.brushSelector);
155+
this.renderGraph(this.graphElementSelector);
156+
this.renderBrush(this.brushSelector);
157+
}
158+
}
159+
160+
/**
161+
* Clears the graph and brush SVGs, removes listeners, and handles missing event bus gracefully.
139162
*/
140163
clearGraph(graphElementSelector, cfdBrushElementSelector) {
141-
this.eventBus.removeAllListeners('change-time-range-scatterplot');
142-
this.eventBus.removeAllListeners('scatterplot-mousemove');
143-
this.eventBus.removeAllListeners('scatterplot-mouseleave');
144-
this.eventBus.removeAllListeners('change-time-interval-scatterplot');
164+
if (this.eventBus && typeof this.eventBus.removeAllListeners === 'function') {
165+
this.eventBus.removeAllListeners('change-time-range-scatterplot');
166+
this.eventBus.removeAllListeners('scatterplot-mousemove');
167+
this.eventBus.removeAllListeners('scatterplot-mouseleave');
168+
this.eventBus.removeAllListeners('change-time-interval-scatterplot');
169+
}
170+
// Remove all children from the SVG elements
171+
d3.select(graphElementSelector).selectAll('*').remove();
172+
d3.select(cfdBrushElementSelector).selectAll('*').remove();
173+
145174
this.#drawBrushSvg(cfdBrushElementSelector);
146175
this.#drawSvg(graphElementSelector);
147176
}
@@ -372,7 +401,8 @@ export class CFDRenderer extends UIControlsRenderer {
372401
g.selectAll('text').attr('y', 30).style('fill', 'black');
373402
g.attr('clip-path', `url(#${clipId})`);
374403
} else {
375-
axis = this.createXAxis(x, 'months');
404+
const noOfDataDays = calculateDaysBetweenDates(this.x.domain()[0], this.x.domain()[1]).roundedDays;
405+
axis = this.createXAxis(x, this.determineTheAppropriateAxisLabels(noOfDataDays));
376406
g.call(axis).attr('transform', `translate(0, ${height})`);
377407
}
378408
}

src/graphs/control-chart/ControlRenderer.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ScatterplotRenderer } from '../scatterplot/ScatterplotRenderer.js';
22
import * as d3 from 'd3';
3+
import { addDaysToDate, calculateDaysBetweenDates } from '../../utils/utils.js';
34

45
export class ControlRenderer extends ScatterplotRenderer {
56
color = '#0ea5e9';
@@ -45,7 +46,7 @@ export class ControlRenderer extends ScatterplotRenderer {
4546
}
4647

4748
setActiveProcessSignal(signalType) {
48-
this.hideSignals();
49+
this.activeProcessSignal = signalType;
4950
this.activeProcessSignal = signalType;
5051
this.showActiveSignal();
5152
}
@@ -136,6 +137,7 @@ export class ControlRenderer extends ScatterplotRenderer {
136137
}
137138

138139
renderGraph(graphElementSelector) {
140+
this.graphElementSelector = graphElementSelector;
139141
this.drawSvg(graphElementSelector);
140142
this.drawAxes();
141143
this.drawArea();
@@ -190,24 +192,31 @@ export class ControlRenderer extends ScatterplotRenderer {
190192
}
191193

192194
drawScatterplot(chartArea, data, x, y) {
195+
// Ensure deliveredDate is a Date object
196+
const safeData = data.map((d) => ({
197+
...d,
198+
deliveredDate: d.deliveredDate instanceof Date ? d.deliveredDate : new Date(d.deliveredDate),
199+
}));
193200
chartArea
194201
.selectAll(`.${this.dotClass}`)
195-
.data(data)
202+
.data(safeData)
196203
.enter()
197204
.append('circle')
198205
.attr('class', this.dotClass)
199206
.attr('id', (d) => `control-${d.sourceId}`)
200207
.attr('data-date', (d) => d.deliveredDate)
201208
.attr('r', (d) => {
202-
const overlapping = data.filter((item) => item.deliveredDate.getTime() === d.deliveredDate.getTime() && item.value === d.value);
209+
const overlapping = safeData.filter(
210+
(item) => item.deliveredDate.getTime() === d.deliveredDate.getTime() && item.value === d.value
211+
);
203212
return overlapping.length > 1 ? 7 : 5;
204213
})
205214
.attr('cx', (d) => x(d.deliveredDate))
206215
.attr('cy', (d) => this.applyYScale(y, d.value))
207216
.style('cursor', 'pointer')
208217
.attr('fill', this.color)
209218
.on('click', (event, d) => this.handleMouseClickEvent(event, d));
210-
this.connectDots && this.generateLines(chartArea, data, x, y);
219+
this.connectDots && this.generateLines(chartArea, safeData, x, y);
211220
}
212221

213222
generateLines(chartArea, data, x, y) {

src/graphs/moving-range/MovingRangeRenderer.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export class MovingRangeRenderer extends ScatterplotRenderer {
1818
}
1919

2020
renderGraph(graphElementSelector) {
21+
this.graphElementSelector = graphElementSelector;
2122
this.drawSvg(graphElementSelector);
2223
this.drawAxes();
2324
this.drawArea();
@@ -47,7 +48,7 @@ export class MovingRangeRenderer extends ScatterplotRenderer {
4748
}
4849

4950
setActiveProcessSignal(signalType) {
50-
this.hideSignals();
51+
this.activeProcessSignal = signalType;
5152
this.activeProcessSignal = signalType;
5253
this.showActiveSignal();
5354
}
@@ -74,7 +75,7 @@ export class MovingRangeRenderer extends ScatterplotRenderer {
7475
}
7576

7677
drawLimits() {
77-
// Remove existing limits
78+
// Remove existing limits first
7879
this.svg.selectAll('[id^="line-"], [id^="text-"]').remove();
7980
// Draw new limits
8081
Object.entries(this.limitData).forEach(([limitType, limitValue]) => {
@@ -185,15 +186,20 @@ export class MovingRangeRenderer extends ScatterplotRenderer {
185186
}
186187

187188
drawScatterplot(chartArea, data, x, y) {
189+
// Ensure deliveredDate is a Date object
190+
const safeData = data.map(d => ({
191+
...d,
192+
deliveredDate: d.deliveredDate instanceof Date ? d.deliveredDate : new Date(d.deliveredDate)
193+
}));
188194
chartArea
189195
.selectAll(`.${this.dotClass}`)
190-
.data(data)
196+
.data(safeData)
191197
.enter()
192198
.append('circle')
193199
.attr('class', this.dotClass)
194200
.attr('id', (d) => `mr-${d.fromSourceId}-${d.toSourceId}`)
195201
.attr('r', (d) => {
196-
const overlapping = data.filter((item) => item.deliveredDate.getTime() === d.deliveredDate.getTime() && item.value === d.value);
202+
const overlapping = safeData.filter((item) => item.deliveredDate.getTime() === d.deliveredDate.getTime() && item.value === d.value);
197203
return overlapping.length > 1 ? 7 : 5;
198204
})
199205
.attr('cx', (d) => x(d.deliveredDate))
@@ -209,7 +215,7 @@ export class MovingRangeRenderer extends ScatterplotRenderer {
209215
.y((d) => this.applyYScale(y, d.value));
210216
chartArea
211217
.selectAll('dot-line')
212-
.data([data])
218+
.data([safeData])
213219
.enter()
214220
.append('path')
215221
.attr('class', 'dot-line')
@@ -231,6 +237,10 @@ export class MovingRangeRenderer extends ScatterplotRenderer {
231237
this.drawSignals();
232238
}
233239

240+
clearGraph(graphElementSelector) {
241+
this.svg.select(graphElementSelector).selectAll('*').remove();
242+
}
243+
234244
cleanup() {
235245
this.limitData = {};
236246
this.visibleLimits = {};

0 commit comments

Comments
 (0)