Skip to content

Commit

Permalink
#233 added tootips and zoom, minor bugfix
Browse files Browse the repository at this point in the history
  • Loading branch information
maerzman committed Aug 5, 2024
1 parent 936d4d9 commit a2fed6f
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,19 @@
pointer-events: none;
text-shadow: -1px 0 white, 0 1px white, 1px 0 white, 0 -1px white;
font-weight: 800;
}

.tooltip {
text-align : center;
max-width : 350px;
max-height : 100px;
padding : 8px;
border : none;
border-radius : 8px;
font : 12px sans-serif;
background : black;
color : white;
pointer-events: none;
display: inline;
position: absolute;
}
199 changes: 171 additions & 28 deletions binocular-frontend/src/components/BubbleChart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,52 @@ import React from 'react';
import * as d3 from 'd3';
import * as baseStyles from './bubbleChart.module.scss';
import _ from 'lodash';
import BubbleToolTip, { ToolTipData } from './tooltip';

interface Props {
data: Bubble[];
paddings: { top: number; left: number; bottom: number; right: number };
paddings: Padding;
showXAxis: boolean;
showYAxis: boolean;
}

interface Padding {
top: number;
left: number;
bottom: number;
right: number;
}

interface State {
simulationData: Bubble[];
data: Bubble[];
tooltipX: number;
tooltipY: number;
tooltipVisible: boolean;
tooltipData: ToolTipData[];
}

export interface Bubble {
x: number;
y: number;
originalX: number;
originalY: number;
size: number;
color: string;
xLabel?: string;
yLabel?: string;
data: ToolTipData[];
}

interface Scales {
x: any;
y: any;
radius: any;
}

interface WindowSpecs {
height: number;
width: number;
paddings: Padding;
}

// TODO: refactor to remove duplicate code from ScalableBaseChart
Expand All @@ -31,16 +61,17 @@ export default class BubbleChart extends React.Component<Props, State> {
super(props);
this.styles = Object.freeze(Object.assign({}, baseStyles, styles));
this.state = {
simulationData: _.cloneDeep(props.data),
data: props.data,
tooltipX: 0,
tooltipY: 0,
tooltipVisible: false,
tooltipData: [],
};
window.addEventListener('resize', this.handleResize);
}

handleResize = () => {
this.setState({
simulationData: _.cloneDeep(this.state.data),
});
this.reassignOriginalDataPointValues();
this.updateElement();
};

Expand All @@ -53,25 +84,37 @@ export default class BubbleChart extends React.Component<Props, State> {
}

componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<State>) {
if (prevProps.data !== this.props.data || prevState.data !== this.state.data) {
if (prevProps.data !== this.props.data) {
this.setState({
data: this.props.data,
simulationData: _.cloneDeep(this.props.data),
});
this.updateElement();
}

if (prevState.data !== this.state.data) {
this.reassignOriginalDataPointValues();
this.updateElement();
}
}

async updateElement() {
this.visualizeData();
const { data } = this.state;
this.visualizeData(data);
}

reassignOriginalDataPointValues() {
this.state.data.forEach((d) => {
d.x = d.originalX;
d.y = d.originalY;
});
}

/**
* calculates x dims based on paddings and min/max values
* @returns the x dims for the chart
*/
getXDims() {
const extent = d3.extent(this.state.simulationData, (d) => d.x);
getXDims(): [number, number] {
const extent = d3.extent(this.state.data, (d) => d.x);
const padding = (extent[1]! - extent[0]!) * 0.1;
return [extent[0]! - padding, extent[1]! + padding];
}
Expand All @@ -80,8 +123,8 @@ export default class BubbleChart extends React.Component<Props, State> {
* calculates y dims based on paddings and min/max values
* @returns the y dims for the chart
*/
getYDims() {
const extent = d3.extent(this.state.simulationData, (d) => d.y);
getYDims(): [number, number] {
const extent = d3.extent(this.state.data, (d) => d.y);
const padding = (extent[1]! - extent[0]!) * 0.1;
return [extent[0]! - padding, extent[1]! + padding];
}
Expand All @@ -90,19 +133,29 @@ export default class BubbleChart extends React.Component<Props, State> {
* calculates radius dims
* @returns the radius dims for the chart
*/
getRadiusDims() {
return [0, d3.max(this.state.simulationData, (d: any) => d.size)];
getRadiusDims(): [number, number] {
return [0, d3.max(this.state.data, (d: any) => d.size)];
}

createScales(xDims, xRange, yDims, yRange, radiusDims, radiusRange) {
/**
* return the scales for the chart
* @param xDims domain of x values
* @param xRange range of x values
* @param yDims domain of y values
* @param yRange rango of y values
* @param radiusDims domain of the radius of the bubble
* @param radiusRange range of the radius of the bubble
* @returns d3 scales for x y and radius
*/
createScales(xDims, xRange, yDims, yRange, radiusDims, radiusRange): Scales {
const x = d3.scaleLinear().domain(xDims).range(xRange);
const y = d3.scaleLinear().domain(yDims).range(yRange);
const radius = d3.scaleSqrt().domain(radiusDims).range(radiusRange);

return { x, y, radius };
}

getDimsAndPaddings(svg: any) {
getDimsAndPaddings(svg: any): WindowSpecs {
const paddings = this.props.paddings || { left: 0, right: 0, top: 0, bottom: 0 };
const node = !svg || typeof svg.node !== 'function' ? { getBoundingClientRec: () => ({}) } : svg.node();
const clientRect = node ? node.getBoundingClientRect() : {};
Expand All @@ -112,23 +165,49 @@ export default class BubbleChart extends React.Component<Props, State> {
return { width, height, paddings };
}

/**
* fills the bubble chart group in the svg with the bubbles
* @param chartGroup the group where the chart is placed
* @param scales the scales for the chart
* @returns the group of the bubble chart
*/
createBubbleChart(chartGroup: any, scales) {
const bubbleChart = chartGroup
.append('g')
.selectAll('circle')
.data(this.state.simulationData)
.data(this.state.data)
.enter()
.append('circle')
.attr('cx', (d) => scales.x(d.x))
.attr('cy', (d) => scales.y(d.y))
.attr('r', (d) => scales.radius(d.size))
.attr('fill', (d) => d.color)
.classed('bubble', true);
.classed('bubble', true)
.on('mouseover', (event, d: Bubble) => {
this.setState({
tooltipX: event.layerX,
tooltipY: event.layerY,
tooltipVisible: true,
tooltipData: d.data,
});
})
.on('mouseout', () => {
this.setState({
tooltipData: [],
tooltipVisible: false,
});
});

return bubbleChart;
}

drawChart(svg, scales, height, width, paddings) {
/**
* @param svg the svg for the chart
* @param scales the scales of the chart
* @param height the height of the window
* @param paddings the paddings of the chart
*/
getChart(svg, scales, height, paddings) {
const chartGroup = svg.append('g');
const bubbleChart = this.createBubbleChart(chartGroup.append('g'), scales);

Expand All @@ -140,29 +219,80 @@ export default class BubbleChart extends React.Component<Props, State> {
return { chartGroup, axes, bubbleChart };
}

createXAxis(brushBubble, scales, height, paddings) {
return brushBubble
/**
* creates the x axis for the chart
* @param chartGroup the group of the d3 element where the chart is
* @param scales the scales for the chart
* @param height the height of the window
* @param paddings the paddings for the chart
* @returns the x axis for the chart
*/
createXAxis(chartGroup, scales, height: number, paddings: Padding) {
if (!this.props.showXAxis) return;

return chartGroup
.append('g')
.attr('class', this.styles.axis)
.attr('transform', `translate(0,${height - paddings.bottom})`)
.call(d3.axisBottom(scales.x));
}

createYAxis(brushBubble, scales, paddings) {
return brushBubble
/**
* creates the x axis for the chart
* @param chartGroup the group of the d3 element where the chart is
* @param scales the scales for the chart
* @param paddings the paddings for the chart
* @returns the y axis for the chart
*/
createYAxis(chartGroup, scales, paddings: Padding) {
if (!this.props.showYAxis) return;

return chartGroup
.append('g')
.attr('class', this.styles.axis)
.attr('transform', `translate(${paddings.left},0)`)
.call(d3.axisLeft(scales.y));
}

visualizeData() {
const { simulationData: data } = this.state;
createBrush(svg, data, width, heigth) {
const component = this;
const brush = d3
.brushX()
.extent([
[0, 0],
[width, heigth],
])
.on('end', selectionEnd);

function selectionEnd(event) {
const selection = event.selection;
if (!selection) return;
const [min, max] = selection;

const selectedData = data.filter((d) => d.x >= min && d.x <= max);
component.setState({
data: selectedData,
});
}

svg.append('g').attr('class', 'brush').call(brush);
}

/**
* visualizes the bubbles array via a bubble chart
*/
visualizeData(data): void {
if (!data) {
return;
}

const svg = d3.select(this.svgRef!);
svg.on('dblclick', () => {
this.setState({
data: this.props.data,
});
});

const { width, height, paddings } = this.getDimsAndPaddings(svg);

const scales = this.createScales(
Expand All @@ -176,8 +306,20 @@ export default class BubbleChart extends React.Component<Props, State> {

svg.selectAll('*').remove();

const { bubbleChart } = this.drawChart(svg, scales, height, width, paddings);
this.createBrush(svg, data, width, height);

const { bubbleChart } = this.getChart(svg, scales, height, paddings);

this.simulateData(data, scales, bubbleChart);
}

/**
* transforms the data using d3 simulation to avoid collision and allign toward its supposed coordinates
* @param data array that contains the bubbles to be drawn
* @param scales scales of the chart
* @param bubbleChart group containing the bubble chart
*/
simulateData(data: Bubble[], scales, bubbleChart) {
d3.forceSimulation(data)
.force(
'x',
Expand All @@ -189,7 +331,7 @@ export default class BubbleChart extends React.Component<Props, State> {
)
.force(
'collision',
d3.forceCollide((d) => scales.radius(d.size) + 1),
d3.forceCollide((d: Bubble) => scales.radius(d.size) + 1),
)
.on('tick', () => bubbleChart.attr('cx', (d) => d.x).attr('cy', (d) => d.y));
}
Expand All @@ -198,6 +340,7 @@ export default class BubbleChart extends React.Component<Props, State> {
return (
<div className={this.styles.chartDiv}>
<svg className={this.styles.chartSvg} ref={(svg) => (this.svgRef = svg)} />
<BubbleToolTip data={this.state.tooltipData} x={this.state.tooltipX} y={this.state.tooltipY} visible={this.state.tooltipVisible} />
</div>
);
}
Expand Down
Loading

0 comments on commit a2fed6f

Please sign in to comment.