diff --git a/examples/example.js b/examples/example.js index 9321bcb..2b64b6c 100644 --- a/examples/example.js +++ b/examples/example.js @@ -101,17 +101,17 @@ function renderScatterplotAndHistogramGraphs(data, reportingRangeDays, controlsE //Moving range chart const movingRangeGraph = new MovingRangeGraph(filteredLeadTimeDataSet); const movingRangeGraphDataSet = movingRangeGraph.computeDataSet(); - const movingRangeRenderer = new MovingRangeRenderer(movingRangeGraphDataSet); + const avgMovingRange = movingRangeGraph.getAvgMovingRange() + const movingRangeRenderer = new MovingRangeRenderer(movingRangeGraphDataSet, avgMovingRange); movingRangeRenderer.renderGraph(movingRangeGraphElementSelector); movingRangeRenderer.reportingRangeDays = reportingRangeDays; movingRangeRenderer.setupEventBus(eventBus) document.querySelector(movingRangeBrushElementSelector) && movingRangeRenderer.setupBrush(movingRangeBrushElementSelector); - movingRangeRenderer.setupXAxisControl() //Control chart - const controlRenderer = new ControlRenderer(filteredLeadTimeDataSet, movingRangeRenderer.getAvgMovingRange()); + const controlRenderer = new ControlRenderer(filteredLeadTimeDataSet, avgMovingRange); controlRenderer.renderGraph(controlGraphElementSelector); controlRenderer.reportingRangeDays = reportingRangeDays; controlRenderer.setupEventBus(eventBus) diff --git a/src/graphs/UIControlsRenderer.js b/src/graphs/UIControlsRenderer.js index 4d18987..5df7958 100644 --- a/src/graphs/UIControlsRenderer.js +++ b/src/graphs/UIControlsRenderer.js @@ -171,6 +171,7 @@ export default class UIControlsRenderer extends Renderer { } else { this.timeInterval = this.determineTheAppropriateAxisLabels(); } + this.eventBus?.emitEvents(`change-time-interval-${chart}`, this.timeInterval); } diff --git a/src/graphs/cfd/CFDRenderer.js b/src/graphs/cfd/CFDRenderer.js index 147a4bb..81b715b 100644 --- a/src/graphs/cfd/CFDRenderer.js +++ b/src/graphs/cfd/CFDRenderer.js @@ -66,7 +66,7 @@ class CFDRenderer extends UIControlsRenderer { this.eventBus?.addEventListener('scatterplot-mouseleave', () => this.hideTooltipAndMovingLine()); this.eventBus?.addEventListener('change-time-interval-scatterplot', (timeInterval) => { this.timeInterval = timeInterval; - this.drawXAxis(this.gx, this.x.copy().domain(this.selectedTimeRange), this.height, true); + this.drawXAxis(this.gx, this.x?.copy().domain(this.selectedTimeRange), this.height, true); }); } @@ -128,9 +128,12 @@ class CFDRenderer extends UIControlsRenderer { * @param {string} cfdBrushElementSelector - Selector of the DOM element to clear the brush. */ clearGraph(graphElementSelector, cfdBrushElementSelector) { + this.eventBus.removeAllListeners('change-time-range-scatterplot'); + this.eventBus.removeAllListeners('scatterplot-mousemove'); + this.eventBus.removeAllListeners('scatterplot-mouseleave'); + this.eventBus.removeAllListeners('change-time-interval-scatterplot'); this.#drawBrushSvg(cfdBrushElementSelector); this.#drawSvg(graphElementSelector); - this.#drawAxes(); } /** @@ -368,7 +371,7 @@ class CFDRenderer extends UIControlsRenderer { * @param {Object} observations - Observations data for the renderer. */ setupObservationLogging(observations) { - if (observations) { + if (observations.length>0) { this.displayObservationMarkers(observations); this.enableMetrics(); } @@ -390,12 +393,13 @@ class CFDRenderer extends UIControlsRenderer { * @private */ #createObservationMarkers(observations) { + console.log(observations) const triangleHeight = 16; const triangleBase = 11; const trianglePath = `M${-triangleBase / 2},0 L${triangleBase / 2},0 L0,-${triangleHeight} Z`; this.chartArea .selectAll('observations') - .data(observations.data.filter((d) => d.chart_type === 'CFD')) + .data(observations?.data?.filter((d) => d.chart_type === 'CFD')) .join('path') .attr('class', 'observation-marker') .attr('d', trianglePath) @@ -719,6 +723,7 @@ class CFDRenderer extends UIControlsRenderer { } if (averageCycleTime) { + console.log("avg cylcle", averageCycleTime) this.#drawHorizontalMetricLine( cycleTimeDateBefore, currentDate, diff --git a/src/graphs/control-chart/ControlRenderer.js b/src/graphs/control-chart/ControlRenderer.js index 5a7d33e..cc024cb 100644 --- a/src/graphs/control-chart/ControlRenderer.js +++ b/src/graphs/control-chart/ControlRenderer.js @@ -1,8 +1,10 @@ import ScatterplotRenderer from '../scatterplot/ScatterplotRenderer.js'; +import * as d3 from 'd3'; class ControlRenderer extends ScatterplotRenderer { color = '#0ea5e9'; timeScale = 'linear'; + connectDots = false; constructor(data, avgMovingRange) { super(data); @@ -21,10 +23,17 @@ class ControlRenderer extends ScatterplotRenderer { renderGraph(graphElementSelector) { this.drawSvg(graphElementSelector); this.drawAxes(); - this.drawArea(); this.avgLeadTime = this.getAvgLeadTime(); this.topLimit = Math.ceil(this.avgLeadTime + this.avgMovingRange * 2.66); + this.bottomLimit = Math.ceil(this.avgLeadTime - this.avgMovingRange * 2.66); + const maxY = this.y.domain()[1] > this.topLimit ? this.y.domain()[1] : this.topLimit + 2; + let minY = this.y.domain()[0]; + if (this.bottomLimit > 0) { + minY = this.y.domain()[0] < this.bottomLimit ? this.y.domain()[0] : this.bottomLimit - 2; + } + this.y.domain([minY, maxY]); + this.drawArea(); this.drawHorizontalLine(this.y, this.topLimit, 'purple', 'top'); this.drawHorizontalLine(this.y, this.avgLeadTime, 'orange', 'center'); this.bottomLimit > 0 && this.drawHorizontalLine(this.y, this.bottomLimit, 'purple', 'bottom'); @@ -45,14 +54,41 @@ class ControlRenderer extends ScatterplotRenderer { .style('cursor', 'pointer') .attr('fill', this.color) .on('click', (event, d) => this.handleMouseClickEvent(event, d)); + this.connectDots && this.generateLines(chartArea, data, x, y); } getAvgLeadTime() { return Math.ceil(this.data.reduce((acc, curr) => acc + curr.leadTime, 0) / this.data.length); } + generateLines(chartArea, data, x, y) { + // Define the line generator + const line = d3 + .line() + .x((d) => x(d.deliveredDate)) + .y((d) => y(d.leadTime)); + chartArea + .selectAll('dot-line') + .data([data]) + .enter() + .append('path') + .attr('class', 'dot-line') + .attr('id', (d) => `line-${d.ticketId}`) + .attr('d', line) + .attr('stroke', 'black') + .attr('stroke-width', 2) + .attr('fill', 'none'); + } + updateGraph(domain) { this.updateChartArea(domain); + if (this.connectDots) { + const line = d3 + .line() + .x((d) => this.currentXScale(d.deliveredDate)) + .y((d) => this.currentYScale(d.leadTime)); + this.chartArea.selectAll('.dot-line').attr('d', line); + } this.drawHorizontalLine(this.currentYScale, this.topLimit, 'purple', 'top'); this.drawHorizontalLine(this.currentYScale, this.avgLeadTime, 'orange', 'center'); this.bottomLimit > 0 && this.drawHorizontalLine(this.currentYScale, this.bottomLimit, 'purple', 'bottom'); diff --git a/src/graphs/moving-range/MovingRangeGraph.js b/src/graphs/moving-range/MovingRangeGraph.js index f7d615b..74709af 100644 --- a/src/graphs/moving-range/MovingRangeGraph.js +++ b/src/graphs/moving-range/MovingRangeGraph.js @@ -1,5 +1,7 @@ import * as d3 from 'd3'; + class MovingRangeGraph { + dataSet = []; constructor(data) { this.data = data; } @@ -16,20 +18,26 @@ class MovingRangeGraph { // Sort the groupedArray by date to ensure correct ordering for difference calculation groupedArray.sort((a, b) => new Date(a.date) - new Date(b.date)); // Step 3: Calculate absolute differences - const avgLeadTimes = []; + this.dataSet = []; for (let i = 1; i < groupedArray.length; i++) { const prev = groupedArray[i - 1]; const current = groupedArray[i]; const difference = Math.abs(current.value - prev.value); - avgLeadTimes.push({ + this.dataSet .push({ fromDate: new Date(prev.date), deliveredDate: new Date(current.date), leadTime: difference, }); } + return this.dataSet; + } - return avgLeadTimes; + getAvgMovingRange() { + if (!this.dataSet) { + throw new Error("Data set not computed. Call computeDataSet() first."); + } + return Math.ceil(this.dataSet.reduce((acc, curr) => acc + curr.leadTime, 0) / this.dataSet.length); } } diff --git a/src/graphs/moving-range/MovingRangeRenderer.js b/src/graphs/moving-range/MovingRangeRenderer.js index a251f49..cec8124 100644 --- a/src/graphs/moving-range/MovingRangeRenderer.js +++ b/src/graphs/moving-range/MovingRangeRenderer.js @@ -5,8 +5,9 @@ class MovingRangeRenderer extends ScatterplotRenderer { color = '#0ea5e9'; timeScale = 'linear'; - constructor(data) { + constructor(data, avgMovingRange) { super(data); + this.avgMovingRange = avgMovingRange; this.chartName = 'moving-range'; this.chartType = 'MOVING_RANGE'; this.dotClass = 'moving-range-dot'; @@ -20,8 +21,10 @@ class MovingRangeRenderer extends ScatterplotRenderer { renderGraph(graphElementSelector) { this.drawSvg(graphElementSelector); this.drawAxes(); + this.topLimit = this.avgMovingRange; + const maxY = this.y.domain()[1] > this.topLimit ? this.y.domain()[1] : this.topLimit + 2; + this.y.domain([this.y.domain()[0], maxY]); this.drawArea(); - this.topLimit = this.getAvgMovingRange(); this.drawHorizontalLine(this.y, this.topLimit, 'orange', 'mid'); } @@ -62,11 +65,8 @@ class MovingRangeRenderer extends ScatterplotRenderer { .x((d) => this.currentXScale(d.deliveredDate)) .y((d) => this.currentYScale(d.leadTime)); this.chartArea.selectAll('.dot-line').attr('d', line); - this.drawHorizontalLine(this.currentYScale, this.getAvgMovingRange(), 'orange', 'mid'); + this.drawHorizontalLine(this.currentYScale, this.avgMovingRange, 'orange', 'mid'); } - getAvgMovingRange() { - return Math.ceil(this.data.reduce((acc, curr) => acc + curr.leadTime, 0) / this.data.length); - } } export default MovingRangeRenderer; diff --git a/src/graphs/scatterplot/ScatterplotRenderer.js b/src/graphs/scatterplot/ScatterplotRenderer.js index 40dd5fe..60985c9 100644 --- a/src/graphs/scatterplot/ScatterplotRenderer.js +++ b/src/graphs/scatterplot/ScatterplotRenderer.js @@ -51,7 +51,7 @@ class ScatterplotRenderer extends UIControlsRenderer { this.eventBus?.addEventListener('change-time-range-cfd', this.updateBrushSelection.bind(this)); this.eventBus?.addEventListener('change-time-interval-cfd', (timeInterval) => { this.timeInterval = timeInterval; - this.drawXAxis(this.gx, this.x.copy().domain(this.selectedTimeRange), this.height, true); + this.drawXAxis(this.gx, this.x?.copy().domain(this.selectedTimeRange), this.height, true); }); } @@ -110,9 +110,10 @@ class ScatterplotRenderer extends UIControlsRenderer { * @param {string} brushElementSelector - The selector of the brush element to clear. */ clearGraph(graphElementSelector, brushElementSelector) { + this.eventBus.removeAllListeners('change-time-interval-cfd'); + this.eventBus.removeAllListeners('change-time-range-cfd'); this.drawBrushSvg(brushElementSelector); this.drawSvg(graphElementSelector); - this.drawAxes(); } /** @@ -241,7 +242,13 @@ class ScatterplotRenderer extends UIControlsRenderer { } computeXScale() { - const xDomain = d3.extent(this.data, (d) => d.deliveredDate); + const bufferDays = 2; + const xExtent = d3.extent(this.data, (d) => d.deliveredDate); + const minDate = new Date(xExtent[0]); + const maxDate = new Date(xExtent[1]); + minDate.setDate(minDate.getDate() - bufferDays); + maxDate.setDate(maxDate.getDate() + bufferDays); + const xDomain = [minDate, maxDate]; this.x = this.computeTimeScale(xDomain, [0, this.width]); } @@ -311,7 +318,7 @@ class ScatterplotRenderer extends UIControlsRenderer { * @param {Object} observations - Observations data for the renderer. */ setupObservationLogging(observations) { - if (observations) { + if (observations.length>0) { this.displayObservationMarkers(observations); this.enableMetrics(); } @@ -346,7 +353,7 @@ class ScatterplotRenderer extends UIControlsRenderer { .selectAll('ring') .data( this.data.filter((d) => - this.observations.data.some((o) => o.work_item.toString() === d.ticketId.toString() && o.chart_type === this.chartType) + this.observations?.data?.some((o) => o.work_item.toString() === d.ticketId.toString() && o.chart_type === this.chartType) ) ) .enter() diff --git a/src/graphs/scatterplot/SimpleScatterplotRenderer.js b/src/graphs/scatterplot/SimpleScatterplotRenderer.js index 2a8c2ca..af22bcd 100644 --- a/src/graphs/scatterplot/SimpleScatterplotRenderer.js +++ b/src/graphs/scatterplot/SimpleScatterplotRenderer.js @@ -87,6 +87,7 @@ class SimpleScatterplotRenderer extends ScatterplotRenderer { this.timeScale = event.target.value; this.computeYScale(); this.updateGraph(this.selectedTimeRange); + this.renderBrush() }); } } diff --git a/src/utils/EventBus.js b/src/utils/EventBus.js index 10a21c2..6d6dfed 100644 --- a/src/utils/EventBus.js +++ b/src/utils/EventBus.js @@ -12,7 +12,20 @@ class EventBus { emitEvents(eventName, params) { if (!this.eventTopics[eventName] || this.eventTopics[eventName].length < 1) return; - this.eventTopics[eventName].forEach((listener) => listener(params ? params : {})); + this.eventTopics[eventName].forEach((listener) => { + try { + listener(params ? params : {}); + } catch (error) { + console.error("Error in listener for event", eventName, ":", error); + } + }); } + + removeAllListeners(eventName) { + if (this.eventTopics[eventName]) { + this.eventTopics[eventName] = []; + } + } + } export const eventBus = new EventBus();