Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Zoom-in feature: d3 approach #7

Open
wants to merge 27 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
239 changes: 162 additions & 77 deletions src/components/Bubble.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,89 +2,174 @@ import React from 'react';
import {observer} from 'mobx-react';
import HighlightableText from './HighlightableText';
import {onBubbleMouseEnter, onBubbleMouseLeave, onBubbleClick, onBubbleDoubleClick} from '../eventhandlers/BubbleEvents';
import * as d3 from "d3";

/**
* Bubble component
* Sets Bubble coordinates/dimensions and other styling according to state
* Binds eventlisteners to bubbles
* @type {<T extends IReactComponent>(clazz: T) => void | IReactComponent | (function({node?: *, store?: *}))}
*/
const Bubble =
observer(
({node, store}) => {
// TODO remove all magical numbers and make styling clearer through classes/external definitions
/** TODO
* Inline styles in the render function should be either
* a) extracted and incorporated in the SASS stylesheets (src/stylesheets)
* b) extracted to a Javascript Object that belongs specifically to this
* Component. e.g. let infoModalStyles = { div: { margin: "0 0 30px" } }
* which we could use like <div style={infoModalStyles.div}> ... </div>
*/
let circleClassName = null;
let circleStyle = {fillOpacity: "0.8"};
if (store.bubblesStore.hasSelectedEntities) {
circleClassName = (node.selected) ? "zoom_selected" : "zoom_unselected";
circleStyle.fillOpacity = (node.selected) ? "1." : "0.1";
} else {
circleClassName = node.active ? "zoom_selected" : "area";
circleStyle.fillOpacity = (node.active || node.selected) ? "1." : "0.8";
}
const {orig_x, orig_y, orig_r} = node;
const {zoomFactor, translationVecX, translationVecY} = store;

const x_ = zoomFactor * orig_x + translationVecX;
const y_ = zoomFactor * orig_y + translationVecY;
const r_ = zoomFactor * orig_r;

const sqrtOfTwo = Math.sqrt(2);
let areaTitleStyle = {wordWrap : "break-word", fontSize : "12px", width: 2*r_/sqrtOfTwo, height: 2*r_/sqrtOfTwo};
if ((node.active || node.selected) || (store.bubblesStore.hasSelectedEntities && !node.selected)) {
areaTitleStyle.display = "none";
}
const translateString = "translate(" + x_ + " " + y_ + ")";
let titleFontSize = "16px";
if (store.svgWidth <= 650) {
titleFontSize = "12px";
} else if (store.svgWidth <= 1050) {
titleFontSize = "14px";
}

const highlightStrings = store.searchString.split(' ');

const areaName = (node.area.length > 55) ? node.area.slice(0,55) + "..." : node.area;
return (
<g onMouseEnter={onBubbleMouseEnter.bind(this, store, node)}
onMouseLeave={onBubbleMouseLeave.bind(this, store)}
onClick={onBubbleClick.bind(this, store, node)}
onDoubleClick={onBubbleDoubleClick.bind(this, store, node)}
className="bubble_frame"
transform={translateString}
class Bubble extends React.Component {
constructor(props) {
super(props);
this.isAnimatedOnMount = this.isAnimated();
const { x_, y_, r_ } = this.getCoordinates(this.isAnimatedOnMount);
this.state = {
x: x_,
y: y_,
r: r_
};
this.circleRef = React.createRef();
}

isAnimated() {
return this.props.store.animationLock;
}

getCoordinates(prev = false) {
const { orig_x, orig_y, orig_r } = this.props.node;
let { zoomFactor, translationVecX, translationVecY } = this.props.store;
if (prev) {
const { prevZoomFactor, prevTranslationVecX, prevTranslationVecY } = this.props.store;
zoomFactor = prevZoomFactor;
translationVecX = prevTranslationVecX;
translationVecY = prevTranslationVecY;
}

const x_ = zoomFactor * orig_x + translationVecX;
const y_ = zoomFactor * orig_y + translationVecY;
const r_ = zoomFactor * orig_r;

return { x_, y_, r_ };
};

componentDidMount() {
if (this.isAnimatedOnMount) {
this.animate();
}
}

componentDidUpdate() {
const { x_, y_, r_ } = this.getCoordinates();
const { x, y, r } = this.state;
if (x === x_ && y === y_ && r === r_ ) {
return;
}
if (this.props.store.animationLock) {
this.animate();
} else {
this.setState({
...this.state,
x: x_,
y: y_,
r: r_
});
}
}

animate() {
let el = d3.select(this.circleRef.current);
const { x_, y_, r_ } = this.getCoordinates();

el.transition(this.props.store.transition)
.attr("cx", x_)
.attr("cy", y_)
.attr("r", r_)
.on("end", () => {
this.setState({
...this.state,
x: x_,
y: y_,
r: r_
});
});
}

componentWillUnmount() {
let el = d3.select(this.circleRef.current);
el.interrupt();
}

render() {
let node = this.props.node;
let store = this.props.store;

let circleClassName = null;
let circleStyle = {fillOpacity: "0.8"};
if (store.bubblesStore.hasSelectedEntities) {
circleClassName = (node.selected) ? "zoom_selected" : "zoom_unselected";
circleStyle.fillOpacity = (node.selected) ? "1." : "0.1";
} else {
circleClassName = node.active ? "zoom_selected" : "area";
circleStyle.fillOpacity = (node.active || node.selected) ? "1." : "0.8";
}

let areaStyle = {};
if ((node.active || store.bubblesStore.hasSelectedEntities) && !node.selected) {
areaStyle.cursor = "zoom-in";
} else {
areaStyle.cursor = "auto";
}

const { x_, y_, r_ } = this.getCoordinates();
const { x, y, r } = this.state;

const sqrtOfTwo = Math.sqrt(2);
let areaTitleStyle = {wordWrap : "break-word", fontSize : "12px", width: 2*r_/sqrtOfTwo, height: 2*r_/sqrtOfTwo};
if ((node.active || node.selected) || (store.bubblesStore.hasSelectedEntities && !node.selected)) {
areaTitleStyle.display = "none";
}

if (x !== x_ || y !== y_ || r !== r_) {
areaTitleStyle.display = "none";
}

let titleFontSize = "16px";
if (store.svgWidth <= 650) {
titleFontSize = "12px";
} else if (store.svgWidth <= 1050) {
titleFontSize = "14px";
}

const highlightStrings = store.searchString.split(' ');

const areaName = (node.area.length > 55) ? node.area.slice(0,55) + "..." : node.area;
return (
<g onMouseEnter={onBubbleMouseEnter.bind(this, store, node)}
onMouseLeave={onBubbleMouseLeave.bind(this, store)}
onClick={onBubbleClick.bind(this, store, node)}
onDoubleClick={onBubbleDoubleClick.bind(this, store, node)}
className="bubble_frame"
style={areaStyle}
>
<circle
ref={this.circleRef}
className={circleClassName}
r={r}
cx={x}
cy={y}
style={{...circleStyle, ...areaStyle}}
/>
<foreignObject
x={x_ - r_/sqrtOfTwo}
y={y_ - r_/sqrtOfTwo}
width={2 * r_ / sqrtOfTwo}
height={2 * r_ / sqrtOfTwo}
id="area_title_object"
className="headstart"
>
<circle
className={circleClassName}
r={r_}
cx={0}
cy={0}
style={circleStyle}
/>
<foreignObject
x={0 - r_/sqrtOfTwo}
y={0 - r_/sqrtOfTwo}
width={2*r_/sqrtOfTwo}
height={2*r_/sqrtOfTwo}
id="area_title_object"
className="headstart"
>
<div className="outerDiv">
<div id="area_title" style={areaTitleStyle} className="innerDiv">
<h2 className="highlightable" style={{fontSize: titleFontSize}}>
<HighlightableText highlightStrings={highlightStrings} value={areaName} />
</h2>
</div>
<div className="outerDiv">
<div id="area_title" style={areaTitleStyle} className="innerDiv">
<h2 className="highlightable" style={{fontSize: titleFontSize}}>
<HighlightableText highlightStrings={highlightStrings} value={areaName} />
</h2>
</div>
</foreignObject>
</g>
);
}
);
export default Bubble;
</div>
</foreignObject>
</g>
);
}
}

export default observer(Bubble);
8 changes: 7 additions & 1 deletion src/components/Chart.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ const Chart = observer(
const hoverPapers = !hasHoverEntities ? '' :
<Papers store={store} papers={hoveredEntity.filter((paper) => hasSubstring(paper, searchString))}/>;

let areaStyle = {};
if (this.props.store.bubblesStore.hasSelectedEntities && ! this.props.store.animationLock) {
areaStyle.cursor = "zoom-out";
}

return (
<div className="vis-col">

Expand All @@ -78,10 +83,11 @@ const Chart = observer(
id="chart-svg"
onClick={onSVGClick.bind(this, store)}
onMouseOver={onSVGMouseOver.bind(this, store)}
style={areaStyle}
>
<g id="chart_canvas">
<rect width={svgWidth} height={svgHeight}/>
{flaglessPapers}
{this.props.store.forceSimIsDone && flaglessPapers}
<Nodes store={store} nodes={bubblesStore}/>
{activePapers}
{selectedPapers}
Expand Down
Loading