diff --git a/examples/example.js b/examples/example.js index d399be0..6411bc1 100644 --- a/examples/example.js +++ b/examples/example.js @@ -26,6 +26,7 @@ if (!data || data.length === 0) { renderGraphs(data, serviceId); } +// eslint-disable-next-line no-unused-vars function renderCfdGraph(data, controlsElementSelector, loadConfigInputSelector, resetConfigInputSelector) { //The cfd area chart and brush window elements css selectors const cfdGraphElementSelector = "#cfd-area-div"; diff --git a/src/graphs/Renderer.js b/src/graphs/Renderer.js index cb51b7c..4f06585 100644 --- a/src/graphs/Renderer.js +++ b/src/graphs/Renderer.js @@ -1,4 +1,5 @@ import * as d3 from 'd3'; +import styles from './tooltipStyles.module.css'; /** * Represents a generic graphs renderer @@ -18,6 +19,8 @@ export class Renderer { constructor(data) { this.data = data; + this.tooltip = null; + this.tooltipTimeout = null; } /** @@ -143,4 +146,95 @@ export class Renderer { updateGraph(domain) { throw new Error('Method not implemented. It must be implemented in subclasses!'); } + + /** + * Shows the tooltip with provided event data. + * @param {Object} event - The event data for the tooltip. + */ + showTooltip(event) { + this.clearTooltipTimeout(); + !this.tooltip && this.createTooltip(); + this.clearTooltipContent(); + this.positionTooltip(event.tooltipLeft, event.tooltipTop); + this.populateTooltip(event); + this.tooltipTimeout = setTimeout(() => { + this.hideTooltip(); + }, 10000); + + this.tooltip.on('mouseleave', () => this.setupMouseLeaveHandler()); + } + + /** + * Populates the tooltip's content with event data: ticket id and observation body + * @param {Object} event - The event data for the tooltip. + */ + // eslint-disable-next-line no-unused-vars + populateTooltip(event) { + throw new Error('populateTooltip() must be implemented by child class'); + } + + /** + * Hides the tooltip. + */ + hideTooltip() { + this.clearTooltipTimeout(); // Clear the timeout when manually hiding + this.tooltip?.transition().duration(100).style('opacity', 0).style('pointer-events', 'none'); + } + + clearTooltipTimeout() { + if (this.tooltipTimeout) { + clearTimeout(this.tooltipTimeout); + this.tooltipTimeout = null; + } + } + + cleanupTooltip() { + this.clearTooltipTimeout(); + this.hideTooltip(); + if (this.tooltip) { + this.tooltip.remove(); + this.tooltip = null; + } + } + + /** + * Creates a tooltip for the chart used for the observation logging. + */ + createTooltip() { + this.tooltip = d3.select('body').append('div').attr('class', styles.chartTooltip).attr('id', 's-tooltip').style('opacity', 0); + } + + /** + * Positions the tooltip on the page. + * @param {number} left - The left position for the tooltip. + * @param {number} top - The top position for the tooltip. + */ + positionTooltip(left, top) { + this.tooltip.transition().duration(100).style('opacity', 0.9).style('pointer-events', 'auto'); + this.tooltip.style('left', left + 'px').style('top', top + 'px'); + } + + /** + * Clears the content of the tooltip. + */ + clearTooltipContent() { + this.tooltip.selectAll('*').remove(); + } + + setupMouseLeaveHandler(retries = 10) { + const svgNode = this.svg?.node(); + if (!svgNode || !svgNode.parentNode) { + if (retries > 0) { + setTimeout(() => this.setupMouseLeaveHandler(retries - 1), 100); + } else { + console.error('SVG parentNode is not available after retries.'); + } + return; + } + d3.select(svgNode.parentNode).on('mouseleave', (event) => { + if (event.relatedTarget !== this.tooltip?.node()) { + this.hideTooltip(); + } + }); + } } diff --git a/src/graphs/cfd/CFDRenderer.js b/src/graphs/cfd/CFDRenderer.js index c89ce9c..9551251 100644 --- a/src/graphs/cfd/CFDRenderer.js +++ b/src/graphs/cfd/CFDRenderer.js @@ -75,7 +75,7 @@ export class CFDRenderer extends UIControlsRenderer { if (this.eventBus && Array.isArray(mouseChartsEvents)) { mouseChartsEvents.forEach((chart) => { this.eventBus?.addEventListener(`${chart}-mousemove`, (event) => this.#handleMouseEvent(event, `${chart}-mousemove`)); - this.eventBus?.addEventListener(`${chart}-mouseleave`, () => this.hideTooltipAndMovingLine()); + this.eventBus?.addEventListener(`${chart}-mouseleave`, () => this.hideTooltip()); }); } } @@ -432,15 +432,14 @@ export class CFDRenderer extends UIControlsRenderer { /** * Shows the tooltip and the moving line at a specific position * @param {Object} event - The event object containing details: coordinates for the tooltip and line. - * @private */ - #showTooltipAndMovingLine(event) { - !this.tooltip && this.#createTooltipAndMovingLine(event.lineX, event.lineY); + showTooltip(event) { + !this.tooltip && this.createTooltip(event.lineX, event.lineY); let { tooltipWidth, tooltipTop } = this.computeTooltipWidthAndTop(event); - this.#clearTooltipAndMovingLine(event.lineX, event.lineY); - this.#positionTooltip(event.tooltipLeft, tooltipTop, tooltipWidth); - this.#populateTooltip(event); + this.clearTooltipContent(event.lineX, event.lineY); + this.positionTooltip(event.tooltipLeft, tooltipTop, tooltipWidth); + this.populateTooltip(event); } computeTooltipWidthAndTop(event) { @@ -482,7 +481,7 @@ export class CFDRenderer extends UIControlsRenderer { /** * Hides the tooltip and the moving line on the chart. */ - hideTooltipAndMovingLine() { + hideTooltip() { if (this.tooltip) { this.tooltip.transition().duration(100).style('opacity', 0).style('pointer-events', 'none'); this.cfdLine.transition().duration(100).style('display', 'none'); @@ -490,11 +489,22 @@ export class CFDRenderer extends UIControlsRenderer { } } + cleanupTooltip() { + this.hideTooltip(); + if (this.tooltip) { + this.tooltip.remove(); + this.tooltip = null; + } + if (this.cfdLine) { + this.cfdLine.remove(); + this.cfdLine = null; + } + } + /** * Creates a tooltip and a moving line for the chart used for the metrics and observation logging. - * @private */ - #createTooltipAndMovingLine(x, y) { + createTooltip(x, y) { this.tooltip = d3.select('body').append('div').attr('class', styles.chartTooltip).attr('id', 'c-tooltip').style('opacity', 0); this.cfdLine = this.chartArea ?.append('line') @@ -509,22 +519,28 @@ export class CFDRenderer extends UIControlsRenderer { /** * Positions the tooltip on the page. - * @private * @param {number} left - The left position for the tooltip. * @param {number} top - The top position for the tooltip. * @param {number} width - The width for the tooltip. */ - #positionTooltip(left, top, width) { + positionTooltip(left, top, width) { this.tooltip?.transition().duration(100).style('opacity', 0.9).style('pointer-events', 'auto'); this.tooltip?.style('left', left - width + 'px').style('top', top + 'px'); } + /** + * Clears the content of the tooltip and the moving line. + */ + clearTooltipContent(x, y) { + this.cfdLine?.attr('stroke', 'black').attr('y1', 0).attr('y2', y).attr('x1', x).attr('x2', x).style('display', null); + this.tooltip?.selectAll('*').remove(); + } + /** * Populates the tooltip's content with event data: data, metrics and observation body - * @private * @param {Object} event - The event data for the tooltip. */ - #populateTooltip(event) { + populateTooltip(event) { this.tooltip?.append('p').text(formatDateToLocalString(event.date)).attr('class', styles.tooltipDate); const gridContainer = this.tooltip?.append('div').attr('class', styles.tooltipGrid); @@ -566,15 +582,6 @@ export class CFDRenderer extends UIControlsRenderer { } } - /** - * Clears the content of the tooltip and the moving line. - * @private - */ - #clearTooltipAndMovingLine(x, y) { - this.cfdLine?.attr('stroke', 'black').attr('y1', 0).attr('y2', y).attr('x1', x).attr('x2', x).style('display', null); - this.tooltip?.selectAll('*').remove(); - } - //endregion //region Metrics @@ -632,7 +639,7 @@ export class CFDRenderer extends UIControlsRenderer { observationBody: observation?.body, observationId: observation?.id, }; - this.#showTooltipAndMovingLine(data); + this.showTooltip(data); eventName.includes('click') && this.eventBus?.emitEvents(eventName, data); } } @@ -642,7 +649,7 @@ export class CFDRenderer extends UIControlsRenderer { * @private */ #setupMouseLeaveHandler() { - this.chartArea?.on('mouseleave', () => this.hideTooltipAndMovingLine()); + this.chartArea?.on('mouseleave', () => this.hideTooltip()); } /** diff --git a/src/graphs/control-chart/ControlRenderer.js b/src/graphs/control-chart/ControlRenderer.js index f15e572..6bc1ed0 100644 --- a/src/graphs/control-chart/ControlRenderer.js +++ b/src/graphs/control-chart/ControlRenderer.js @@ -51,7 +51,6 @@ export class ControlRenderer extends ScatterplotRenderer { } populateTooltip(event) { - console.log('populateTooltip', event); this.tooltip .style('pointer-events', 'auto') .style('opacity', 0.9) diff --git a/src/graphs/scatterplot/ScatterplotRenderer.js b/src/graphs/scatterplot/ScatterplotRenderer.js index 782fbe0..39d44ce 100644 --- a/src/graphs/scatterplot/ScatterplotRenderer.js +++ b/src/graphs/scatterplot/ScatterplotRenderer.js @@ -1,6 +1,5 @@ import { calculateDaysBetweenDates } from '../../utils/utils.js'; import { UIControlsRenderer } from '../UIControlsRenderer.js'; -import styles from '../tooltipStyles.module.css'; import * as d3 from 'd3'; @@ -487,32 +486,6 @@ export class ScatterplotRenderer extends UIControlsRenderer { //region Tooltip - /** - * Shows the tooltip with provided event data. - * @param {Object} event - The event data for the tooltip. - */ - showTooltip(event) { - !this.tooltip && this.#createTooltip(); - this.#clearTooltipContent(); - this.#positionTooltip(event.tooltipLeft, event.tooltipTop); - this.populateTooltip(event); - } - - /** - * Hides the tooltip. - */ - hideTooltip() { - this.tooltip?.transition().duration(100).style('opacity', 0).style('pointer-events', 'none'); - } - - /** - * Creates a tooltip for the chart used for the observation logging. - * @private - */ - #createTooltip() { - this.tooltip = d3.select('body').append('div').attr('class', styles.chartTooltip).attr('id', 's-tooltip').style('opacity', 0); - } - /** * Populates the tooltip's content with event data: ticket id and observation body * @private @@ -532,26 +505,6 @@ export class ScatterplotRenderer extends UIControlsRenderer { }); event.observationBody && this.tooltip.append('p').text('Observation: ' + event.observationBody); } - - /** - * Positions the tooltip on the page. - * @private - * @param {number} left - The left position for the tooltip. - * @param {number} top - The top position for the tooltip. - */ - #positionTooltip(left, top) { - this.tooltip.transition().duration(100).style('opacity', 0.9).style('pointer-events', 'auto'); - this.tooltip.style('left', left + 'px').style('top', top + 'px'); - } - - /** - * Clears the content of the tooltip. - * @private - */ - #clearTooltipContent() { - this.tooltip.selectAll('*').remove(); - } - //endregion //region Metrics @@ -615,7 +568,6 @@ export class ScatterplotRenderer extends UIControlsRenderer { } d3.select(svgNode.parentNode).on('mouseleave', (event) => { if (event.relatedTarget !== this.tooltip?.node()) { - console.log('setup mouse leave to hide tooltip'); this.hideTooltip(); } }); diff --git a/src/graphs/work-item-age/WorkItemAgeRenderer.js b/src/graphs/work-item-age/WorkItemAgeRenderer.js index 37c53ac..e4a9128 100644 --- a/src/graphs/work-item-age/WorkItemAgeRenderer.js +++ b/src/graphs/work-item-age/WorkItemAgeRenderer.js @@ -1,5 +1,4 @@ import * as d3 from 'd3'; -import styles from '../tooltipStyles.module.css'; import { Renderer } from '../Renderer.js'; export class WorkItemAgeRenderer extends Renderer { @@ -194,30 +193,6 @@ export class WorkItemAgeRenderer extends Renderer { gy.call(d3.axisLeft(y)).selectAll('text').attr('class', 'axis-label'); } - showTooltip(event) { - !this.tooltip && this.#createTooltip(); - this.#clearTooltipContent(); - this.#positionTooltip(event.tooltipLeft, event.tooltipTop); - this.populateTooltip(event); - this.tooltip.on('mouseleave', () => this.setupMouseLeaveHandler()); - } - - /** - * Hides the tooltip. - */ - hideTooltip() { - console.log('hide tooltip'); - this.tooltip?.transition().duration(100).style('opacity', 0).style('pointer-events', 'none'); - } - - /** - * Creates a tooltip for the chart used for the observation logging. - * @private - */ - #createTooltip() { - this.tooltip = d3.select('body').append('div').attr('class', styles.chartTooltip).attr('id', 's-tooltip').style('opacity', 0); - } - /** * Populates the tooltip's content with event data: ticket id and observation body * @private @@ -240,25 +215,6 @@ export class WorkItemAgeRenderer extends Renderer { }); } - /** - * Positions the tooltip on the page. - * @private - * @param {number} left - The left position for the tooltip. - * @param {number} top - The top position for the tooltip. - */ - #positionTooltip(left, top) { - this.tooltip.transition().duration(100).style('opacity', 0.9).style('pointer-events', 'auto'); - this.tooltip.style('left', left + 'px').style('top', top + 'px'); - } - - /** - * Clears the content of the tooltip. - * @private - */ - #clearTooltipContent() { - this.tooltip.selectAll('*').remove(); - } - handleMouseClickEvent(event, d) { const observationsData = []; d.items.forEach((item) => { @@ -279,23 +235,6 @@ export class WorkItemAgeRenderer extends Renderer { this.showTooltip(data); } - setupMouseLeaveHandler(retries = 10) { - const svgNode = this.svg?.node(); - if (!svgNode || !svgNode.parentNode) { - if (retries > 0) { - setTimeout(() => this.setupMouseLeaveHandler(retries - 1), 100); - } else { - console.error('SVG parentNode is not available after retries.'); - } - return; - } - d3.select(svgNode.parentNode).on('mouseleave', (event) => { - if (event.relatedTarget !== this.tooltip?.node()) { - this.hideTooltip(); - } - }); - } - setupObservationLogging(observations) { if (observations?.data?.length > 0) { this.observations = observations; @@ -344,10 +283,7 @@ export class WorkItemAgeRenderer extends Renderer { } drawPercentileLines(data, y) { - console.log('drawPercentileLines'); const dataSortedByAge = [...data].sort((a, b) => a.age - b.age); - console.log('dataSortedByAge'); - console.table(dataSortedByAge); const percentile1 = this.computePercentileLine(dataSortedByAge, 0.5); const percentile2 = this.computePercentileLine(dataSortedByAge, 0.75); const percentile3 = this.computePercentileLine(dataSortedByAge, 0.85);