diff --git a/src/graphs/Renderer.js b/src/graphs/Renderer.js
index 4f06585..b25ac5e 100644
--- a/src/graphs/Renderer.js
+++ b/src/graphs/Renderer.js
@@ -7,9 +7,9 @@ import styles from './tooltipStyles.module.css';
export class Renderer {
margin = { top: 30, right: 40, bottom: 70, left: 40 };
width = 1040 - this.margin.left - this.margin.right;
- height = 460 - this.margin.top - this.margin.bottom;
+ height = 380 - this.margin.top - this.margin.bottom;
axisLabelFontSize = 14;
- focusHeight = 120;
+ focusHeight = 90;
gx;
gy;
x;
diff --git a/src/graphs/control-chart/ControlRenderer.js b/src/graphs/control-chart/ControlRenderer.js
index 6bc1ed0..9c59cc9 100644
--- a/src/graphs/control-chart/ControlRenderer.js
+++ b/src/graphs/control-chart/ControlRenderer.js
@@ -3,7 +3,7 @@ import * as d3 from 'd3';
export class ControlRenderer extends ScatterplotRenderer {
color = '#0ea5e9';
- timeScale = 'linear';
+ timeScale = 'logarithmic';
connectDots = false;
constructor(data, avgMovingRangeFunc, chartName, workTicketsURL) {
diff --git a/src/graphs/moving-range/MovingRangeRenderer.js b/src/graphs/moving-range/MovingRangeRenderer.js
index 670c852..a7197e1 100644
--- a/src/graphs/moving-range/MovingRangeRenderer.js
+++ b/src/graphs/moving-range/MovingRangeRenderer.js
@@ -3,7 +3,7 @@ import * as d3 from 'd3';
export class MovingRangeRenderer extends ScatterplotRenderer {
color = '#0ea5e9';
- timeScale = 'linear';
+ timeScale = 'logarithmic';
constructor(data, avgMovingRangeFunc, workTicketsURL, chartName) {
super(data);
diff --git a/src/graphs/pbc/PBCRenderer.js b/src/graphs/pbc/PBCRenderer.js
new file mode 100644
index 0000000..013d595
--- /dev/null
+++ b/src/graphs/pbc/PBCRenderer.js
@@ -0,0 +1,171 @@
+import { UIControlsRenderer } from '../UIControlsRenderer.js';
+
+export class PBCRenderer extends UIControlsRenderer {
+ constructor(controlData, movingRangeData, avgMovingRangeFunc, workTicketsURL, chartName = 'pbc') {
+ super(controlData);
+ this.controlData = controlData;
+ this.movingRangeData = movingRangeData;
+ this.avgMovingRangeFunc = avgMovingRangeFunc;
+ this.workTicketsURL = workTicketsURL;
+ this.chartName = chartName;
+ this.chartType = 'PBC';
+
+ // Child renderers
+ this.controlRenderer = null;
+ this.movingRangeRenderer = null;
+ }
+
+ /**
+ * Initialize child renderers with their respective data
+ */
+ initializeRenderers(ControlRenderer, MovingRangeRenderer) {
+ this.controlRenderer = new ControlRenderer(this.controlData, this.avgMovingRangeFunc, `${this.chartName}-control`, this.workTicketsURL);
+
+ this.movingRangeRenderer = new MovingRangeRenderer(
+ this.movingRangeData, // Moving range calculated data
+ this.avgMovingRangeFunc,
+ this.workTicketsURL,
+ `${this.chartName}-moving-range`
+ );
+ }
+
+ /**
+ * Render both charts
+ */
+ renderGraph(containerSelector) {
+ this.createContainers(containerSelector);
+ // Render control chart
+ this.controlRenderer.renderGraph(`${containerSelector} .control-chart`);
+ // Render moving range chart
+ this.movingRangeRenderer.renderGraph(`${containerSelector} .moving-range-chart`);
+ // Sync baseline dates and time ranges
+ this.syncChartProperties();
+ }
+
+ /**
+ * Create HTML containers for both charts
+ */
+ createContainers(containerSelector) {
+ const container = document.querySelector(containerSelector);
+ if (!container) return;
+
+ container.innerHTML = `
+
+ `;
+ }
+
+ /**
+ * Setup brush - only from control chart
+ */
+ setupBrush(brushSelector) {
+ this.brushSelector = brushSelector;
+ this.controlRenderer.setupBrush(brushSelector);
+ this.syncBrushEvents();
+ this.updateGraph(this.controlRenderer.selectedTimeRange);
+ }
+
+ /**
+ * Sync brush events to update both charts
+ */
+ syncBrushEvents() {
+ // Override control renderer's updateGraph to also update moving range
+ const originalUpdateGraph = this.controlRenderer.updateGraph.bind(this.controlRenderer);
+ this.controlRenderer.updateGraph = (domain) => {
+ // Update control chart
+ originalUpdateGraph(domain);
+
+ // Update moving range chart with the same domain
+ this.movingRangeRenderer.updateGraph(domain);
+ };
+ }
+
+ setTimeScale(timeScale) {
+ this.controlRenderer.timeScale = timeScale;
+ this.movingRangeRenderer.timeScale = timeScale;
+ }
+
+ /**
+ * Sync properties between charts
+ */
+ syncChartProperties() {
+ if (!this.controlRenderer || !this.movingRangeRenderer) return;
+ this.movingRangeRenderer.reportingRangeDays = 30;
+ this.controlRenderer.reportingRangeDays = 30;
+ this.movingRangeRenderer.timeInterval = 'months';
+ this.controlRenderer.timeInterval = 'months';
+ this.movingRangeRenderer.timeScale = 'logarithmic';
+ this.controlRenderer.timeScale = 'logarithmic';
+ }
+
+ /**
+ * Setup event bus for both charts
+ */
+ setupEventBus(eventBus, mouseChartsEvents, timeRangeChartsEvents) {
+ this.controlRenderer?.setupEventBus(eventBus, mouseChartsEvents, timeRangeChartsEvents);
+ this.movingRangeRenderer?.setupEventBus(eventBus, mouseChartsEvents, timeRangeChartsEvents);
+ }
+
+ /**
+ * Setup observations for both charts
+ */
+ setupObservationLogging(observations) {
+ this.controlRenderer?.setupObservationLogging(observations);
+ this.movingRangeRenderer?.setupObservationLogging(observations);
+ }
+
+ setTimeScaleListener(timeScaleSelector) {
+ const selectElement = document.querySelector(timeScaleSelector);
+ if (!selectElement) {
+ console.warn(`Time scale selector not found: ${timeScaleSelector}`);
+ return;
+ }
+ // Single event listener that updates both charts
+ selectElement.addEventListener('change', (event) => {
+ const newTimeScale = event.target.value;
+ // Update both renderers
+ if (this.controlRenderer) {
+ this.controlRenderer.timeScale = newTimeScale;
+ this.controlRenderer.computeYScale();
+ this.controlRenderer.updateGraph(this.controlRenderer.selectedTimeRange);
+ this.controlRenderer.renderBrush(); // Only control renderer renders brush
+ }
+
+ if (this.movingRangeRenderer) {
+ this.movingRangeRenderer.timeScale = newTimeScale;
+ this.movingRangeRenderer.computeYScale();
+ this.movingRangeRenderer.updateGraph(this.controlRenderer.selectedTimeRange);
+ // No renderBrush() for moving range
+ }
+ });
+ }
+
+ /**
+ * Clear both charts
+ */
+ clearGraph(containerSelector, brushSelector) {
+ this.controlRenderer?.clearGraph(`${containerSelector} .control-chart`, brushSelector);
+ this.movingRangeRenderer?.clearGraph(`${containerSelector} .moving-range-chart`, null);
+
+ const container = document.querySelector(containerSelector);
+ if (container) container.innerHTML = '';
+ }
+
+ /**
+ * Update both charts
+ */
+ updateGraph(domain) {
+ this.controlRenderer?.updateGraph(domain);
+ this.movingRangeRenderer?.updateGraph(domain);
+ }
+
+ /**
+ * Cleanup
+ */
+ cleanup() {
+ this.controlRenderer?.cleanupTooltip?.();
+ this.movingRangeRenderer?.cleanupTooltip?.();
+ }
+}
diff --git a/src/graphs/tooltipStyles.module.css b/src/graphs/tooltipStyles.module.css
index 4d42229..58f19f8 100644
--- a/src/graphs/tooltipStyles.module.css
+++ b/src/graphs/tooltipStyles.module.css
@@ -5,7 +5,7 @@
height: max-content;
padding: 8px;
font: 14px sans-serif;
- background: #e2ecfd;
+ background: #b1b1b1;
border: 0;
border-radius: 8px;
z-index: 1;
@@ -44,13 +44,13 @@
/* Specific metric colors */
.cycleTime {
- color: #d57500; /* orange */
+ color: yellow; /* orange */
}
.leadTime {
- color: #0062a5; /* blue */
+ color: indigo; /* blue */
}
.wip {
- color: #dd0030; /* pinkish-red */
+ color: red; /* pinkish-red */
}
diff --git a/src/index.js b/src/index.js
index 240d89d..73d9f12 100644
--- a/src/index.js
+++ b/src/index.js
@@ -9,6 +9,7 @@ import { ControlRenderer } from './graphs/control-chart/ControlRenderer.js';
import { HistogramRenderer } from './graphs/histogram/HistogramRenderer.js';
import { WorkItemAgeGraph } from './graphs/work-item-age/WorkItemAgeGraph.js';
import { WorkItemAgeRenderer } from './graphs/work-item-age/WorkItemAgeRenderer.js';
+import { PBCRenderer } from './graphs/pbc/PBCRenderer.js';
import { eventBus } from './utils/EventBus.js';
import { processServiceData } from './data-processor.js';
import { ObservationLoggingService } from './graphs/ObservationLoggingService.js';
@@ -26,6 +27,7 @@ export {
WorkItemAgeGraph,
WorkItemAgeRenderer,
ObservationLoggingService,
+ PBCRenderer,
eventBus,
processServiceData,
};