Skip to content

Commit 8fede10

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

File tree

7 files changed

+280
-48
lines changed

7 files changed

+280
-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: 110 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,102 @@ export class UIControlsRenderer extends Renderer {
173179
createXAxis(x, timeInterval = this.timeInterval) {
174180
let axis;
175181
switch (timeInterval) {
176-
case 'days':
182+
case 'days': {
177183
axis = d3
178184
.axisBottom(x)
179-
.ticks(d3.timeDay.every(1)) // label every 2 days
185+
.ticks(d3.timeDay.every(1))
180186
.tickFormat((d, i) => {
181-
return i % 2 === 0 ? d3.timeFormat('%b %d')(d) : '';
187+
const dayFormat = d3.timeFormat('%b %d');
188+
const yearFormat = d3.timeFormat('%Y');
189+
if (i === 0) return `${dayFormat(d)} ${yearFormat(d)}`;
190+
return i % 2 === 0 ? dayFormat(d) : '';
182191
});
183192
break;
184-
case 'weeks':
185-
axis = d3.axisBottom(x).ticks(d3.timeWeek);
193+
}
194+
case 'weeks': {
195+
const ticks = x.ticks(d3.timeDay);
196+
// Find the first tick that is a Monday
197+
let firstMonday = -1;
198+
for (let i = 0; i < ticks.length; i++) {
199+
if (ticks[i].getDay() === 1) {
200+
firstMonday = i;
201+
break;
202+
}
203+
}
204+
axis = d3
205+
.axisBottom(x)
206+
.ticks(d3.timeDay)
207+
.tickFormat((d, i) => {
208+
const dayFormat = d3.timeFormat('%b %d');
209+
const yearFormat = d3.timeFormat('%Y');
210+
if (i === firstMonday) return `${dayFormat(d)} ${yearFormat(d)}`;
211+
return d.getDay() === 1 && i > firstMonday ? dayFormat(d) : '';
212+
});
186213
break;
187-
case 'months':
188-
axis = d3.axisBottom(x).ticks(d3.timeMonth);
214+
}
215+
case 'months': {
216+
const ticks = x.ticks(d3.timeWeek);
217+
// Find the first tick that is the first week of a month
218+
let firstMonthWeek = -1;
219+
for (let i = 0; i < ticks.length; i++) {
220+
if (ticks[i].getDate() <= 7) {
221+
firstMonthWeek = i;
222+
break;
223+
}
224+
}
225+
const weeks = d3.timeWeek.range(x.domain()[0], x.domain()[1]);
226+
axis = d3
227+
.axisBottom(x)
228+
.ticks(d3.timeWeek)
229+
.tickFormat((d, i) => {
230+
const monthFormat = d3.timeFormat('%b');
231+
const yearFormat = d3.timeFormat('%Y');
232+
if (i === firstMonthWeek) return `${monthFormat(d)} ${yearFormat(d)}`;
233+
if (d.getDate() <= 7 && i > firstMonthWeek) {
234+
if (i > 0 && d.getFullYear() !== weeks[i - 1].getFullYear()) {
235+
return `${monthFormat(d)} ${yearFormat(d)}`;
236+
}
237+
return monthFormat(d);
238+
}
239+
return '';
240+
});
189241
break;
242+
}
243+
case 'bimonthly': {
244+
const ticks = x.ticks(d3.timeMonth);
245+
// Find the first tick that is the first month
246+
let firstQuarterMonth = -1;
247+
for (let i = 0; i < ticks.length; i++) {
248+
if (ticks[i].getMonth() % 2 === 0) {
249+
firstQuarterMonth = i;
250+
break;
251+
}
252+
}
253+
const months = d3.timeMonth.range(x.domain()[0], x.domain()[1]);
254+
axis = d3
255+
.axisBottom(x)
256+
.ticks(d3.timeMonth)
257+
.tickFormat((d, i) => {
258+
const monthFormat = d3.timeFormat('%b');
259+
const yearFormat = d3.timeFormat('%Y');
260+
if (i === firstQuarterMonth) return `${monthFormat(d)} ${yearFormat(d)}`;
261+
if (d.getMonth() % 2 === 0 && i > firstQuarterMonth) {
262+
// Show year if year changes from previous quarter tick
263+
const prevQuarterIndex = (() => {
264+
for (let j = i - 1; j >= 0; j--) {
265+
if (months[j].getMonth() % 2 === 0) return j;
266+
}
267+
return -1;
268+
})();
269+
if (prevQuarterIndex >= 0 && d.getFullYear() !== months[prevQuarterIndex].getFullYear()) {
270+
return `${monthFormat(d)} ${yearFormat(d)}`;
271+
}
272+
return monthFormat(d);
273+
}
274+
return '';
275+
});
276+
break;
277+
}
190278
default:
191279
return d3.axisBottom(x);
192280
}
@@ -210,6 +298,9 @@ export class UIControlsRenderer extends Renderer {
210298
case 'days':
211299
this.timeInterval = 'weeks';
212300
break;
301+
case 'bimonthly':
302+
this.timeInterval = 'weeks';
303+
break;
213304
default:
214305
this.timeInterval = 'weeks';
215306
}
@@ -220,14 +311,17 @@ export class UIControlsRenderer extends Renderer {
220311
this.eventBus?.emitEvents(`change-time-interval-${this.chartName}`, this.timeInterval);
221312
}
222313

223-
determineTheAppropriateAxisLabels() {
224-
if (this.reportingRangeDays <= 31) {
314+
determineTheAppropriateAxisLabels(noOfDays = this.reportingRangeDays) {
315+
if (noOfDays <= 31) {
225316
return 'days';
226317
}
227-
if (this.reportingRangeDays > 31 && this.reportingRangeDays <= 124) {
318+
if (noOfDays > 31 && noOfDays <= 150) {
228319
return 'weeks';
229320
}
230-
return 'months';
321+
if (noOfDays > 150 && noOfDays <= 750) {
322+
return 'months';
323+
}
324+
return 'bimonthly';
231325
}
232326

233327
/**

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: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class ControlRenderer extends ScatterplotRenderer {
4545
}
4646

4747
setActiveProcessSignal(signalType) {
48-
this.hideSignals();
48+
this.activeProcessSignal = signalType;
4949
this.activeProcessSignal = signalType;
5050
this.showActiveSignal();
5151
}
@@ -136,6 +136,7 @@ export class ControlRenderer extends ScatterplotRenderer {
136136
}
137137

138138
renderGraph(graphElementSelector) {
139+
this.graphElementSelector = graphElementSelector;
139140
this.drawSvg(graphElementSelector);
140141
this.drawAxes();
141142
this.drawArea();
@@ -190,24 +191,29 @@ export class ControlRenderer extends ScatterplotRenderer {
190191
}
191192

192193
drawScatterplot(chartArea, data, x, y) {
194+
// Ensure deliveredDate is a Date object
195+
const safeData = data.map((d) => ({
196+
...d,
197+
deliveredDate: d.deliveredDate instanceof Date ? d.deliveredDate : new Date(d.deliveredDate),
198+
}));
193199
chartArea
194200
.selectAll(`.${this.dotClass}`)
195-
.data(data)
201+
.data(safeData)
196202
.enter()
197203
.append('circle')
198204
.attr('class', this.dotClass)
199205
.attr('id', (d) => `control-${d.sourceId}`)
200206
.attr('data-date', (d) => d.deliveredDate)
201207
.attr('r', (d) => {
202-
const overlapping = data.filter((item) => item.deliveredDate.getTime() === d.deliveredDate.getTime() && item.value === d.value);
208+
const overlapping = safeData.filter((item) => item.deliveredDate.getTime() === d.deliveredDate.getTime() && item.value === d.value);
203209
return overlapping.length > 1 ? 7 : 5;
204210
})
205211
.attr('cx', (d) => x(d.deliveredDate))
206212
.attr('cy', (d) => this.applyYScale(y, d.value))
207213
.style('cursor', 'pointer')
208214
.attr('fill', this.color)
209215
.on('click', (event, d) => this.handleMouseClickEvent(event, d));
210-
this.connectDots && this.generateLines(chartArea, data, x, y);
216+
this.connectDots && this.generateLines(chartArea, safeData, x, y);
211217
}
212218

213219
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 = {};

src/graphs/pbc/PBCRenderer.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ export class PBCRenderer extends UIControlsRenderer {
8484
*/
8585
syncChartProperties() {
8686
if (!this.controlRenderer || !this.movingRangeRenderer) return;
87-
this.movingRangeRenderer.reportingRangeDays = 30;
88-
this.controlRenderer.reportingRangeDays = 30;
87+
this.movingRangeRenderer.reportingRangeDays = 90;
88+
this.controlRenderer.reportingRangeDays = 90;
8989
this.movingRangeRenderer.timeInterval = 'months';
9090
this.controlRenderer.timeInterval = 'months';
9191
this.movingRangeRenderer.timeScale = 'logarithmic';
@@ -134,6 +134,22 @@ export class PBCRenderer extends UIControlsRenderer {
134134
});
135135
}
136136

137+
/**
138+
* Updates the renderer's data and re-renders the graph, preserving persistent props.
139+
* @param {Array} newControlData - The new control chart data.
140+
* @param {Array} newMovingRangeData - The new moving range chart data.
141+
*/
142+
updateData(newControlData, newMovingRangeData) {
143+
this.controlData = newControlData;
144+
this.movingRangeData = newMovingRangeData;
145+
if (this.controlRenderer && this.movingRangeRenderer) {
146+
this.controlRenderer.updateData(newControlData);
147+
this.movingRangeRenderer.updateData(newMovingRangeData);
148+
this.syncChartProperties();
149+
this.setupBrush(this.brushSelector);
150+
}
151+
}
152+
137153
/**
138154
* Clear both charts
139155
*/

0 commit comments

Comments
 (0)