blob: ca1a5524489afd077224543bfc50c26921a50c89 [file] [log] [blame]
'use strict';
/**
* Utility functions common to graph plotters.
*/
class GraphUtils {
/**
* Attaches a zoom listener to the chart and rescales the
* axes based on mouse scroll and click events.
* @param {boolean} shouldScale.x
* Set to true if the x axis should be rescaled.
* @param {boolean} shouldScale.y
* Set to true if the y axis should be rescaled.
* @param {Object} chart The chart to draw on.
* @param {Object} chartDimensions
* The width, height and margins of the chart.
* @param {function(Object, Object):} redraw Given the x and y axes scales
* this should redraw the graph based on the new scales.
* @param {Object} axes Provides the axis drawings, scales and generators
* in the format:
* {
* x: {
* generator: Object,
* scale: Object,
* drawing: Object,
* },
* y: {
* generator: Object,
* scale: Object,
* drawing: Object,
* },
* }
* If an axis is set to true in shouldScale it's corresponding information
* must be provided in axes. Otherwise, it may be omitted.
*/
static createZoom(shouldScale, chart, chartDimensions, redraw, axes) {
const transformLinePlot = (xAxisScale, yAxisScale) => {
if (xAxisScale) {
axes.x.generator.scale(xAxisScale);
axes.x.drawing.call(axes.x.generator);
}
if (yAxisScale) {
axes.y.generator.scale(yAxisScale);
axes.y.drawing.call(axes.y.generator);
}
redraw(xAxisScale, yAxisScale);
};
const setUpZoom = (zoom) => {
const onZoom = () => {
const transform = d3.event.transform;
const transformedScaleForYAxis = shouldScale.y ?
transform.rescaleY(axes.y.scale) : undefined;
const transformedScaleForXAxis = shouldScale.x ?
transform.rescaleX(axes.x.scale) : undefined;
transformLinePlot(
transformedScaleForXAxis, transformedScaleForYAxis);
};
zoom.on('zoom', onZoom).scaleExtent([1, Infinity]);
// The following invisible rectangle is there just to catch
// mouse events for zooming. It's not possible to listen on
// the chart itself because it is a g element (which does not
// capture mouse events).
chart.append('rect')
.attr('width', chartDimensions.width)
.attr('height', chartDimensions.height)
.attr('class', 'zoom-listener')
.style('opacity', 0)
.call(zoom);
};
const setUpZoomReset = (zoom) => {
// Gives some padding between the x-axis and the reset button.
const padding = 10;
const resetButton = chart.append('svg')
.attr('width', '10em')
.attr('height', '2em')
.attr('x', `${chartDimensions.width + padding}px`)
.attr('y', `${chartDimensions.height}px`)
.attr('cursor', 'pointer');
// Styling for the button.
resetButton.append('rect')
.attr('rx', '5px')
.attr('ry', '5px')
.attr('width', '100%')
.attr('height', '100%')
.attr('fill', '#1b39a8');
resetButton.append('text')
.text('RESET CHART')
.attr('x', '50%')
.attr('y', '50%')
.attr('fill', 'white')
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'middle');
resetButton
.on('mouseover', () => {
resetButton.attr('opacity', '0.5');
})
.on('mouseout', () => {
resetButton.attr('opacity', '1');
})
.on('click', () => {
resetButton.attr('opacity', '1');
chart.select('.zoom-listener')
.call(zoom.transform, d3.zoomIdentity);
const originalXAxisScale = shouldScale.x ?
axes.x.scale : undefined;
const originalYAxisScale = shouldScale.y ?
axes.y.scale : undefined;
transformLinePlot(
originalXAxisScale, originalYAxisScale);
});
};
const zoom = d3.zoom();
setUpZoom(zoom);
setUpZoomReset(zoom
);
}
/**
* Utility function for converting text which may contain invalid
* characters for CSS classes/selectors into something safe to
* use in class names and retrieve using selectors.
* @return {function(string): string} Retrieves a unique class name
* mapped to the supplied key.
*/
static getClassNameSuffixFactory() {
let nextUniqueClassName = 0;
const keyToClassName = {};
return (key) => {
if (keyToClassName[key] === undefined) {
keyToClassName[key] = nextUniqueClassName++;
}
return keyToClassName[key].toString();
};
}
static useTraceCallback(selection, graph, key, radius, className) {
selection
.on('click', datum => {
graph.interactiveCallbackForSelectedDatum(key, datum.id);
})
.on('mouseover', (datum, datumIndex) => {
const zoomOnHoverScaleFactor = 2;
d3.selectAll(`.${className}`)
.filter((d, i) => i === datumIndex)
.attr('r', radius * zoomOnHoverScaleFactor);
})
.on('mouseout', (datum, datumIndex) => {
d3.selectAll(`.${className}`)
.filter((d, i) => i === datumIndex)
.attr('r', radius);
})
.append('title')
.text('click to view trace');
return selection;
}
}