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

Exploration preview animation #307

Merged
merged 6 commits into from
Jun 16, 2021
Merged
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
10 changes: 5 additions & 5 deletions frontend/cypress/integration/exploration/exploration.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,20 @@ context('Exploration', () => {
cy.get('.Previews');

const layoutsDataValues = Object.values(layoutsData);
cy.get('.LayoutPreview').each(($el, index) => {
cy.wrap($el).should('have.text', layoutsDataValues[index].description);
});
for (const layoutsDataValue of layoutsDataValues) {
cy.get('.LayoutPreview').contains(layoutsDataValue.description);
}
});

it('routes to graph page', () => {
cy.get('.Previews');
cy.get('.LayoutPreview').eq(0).click();
cy.get('.LayoutPreview').contains('Structural Layout').click();
cy.url().should('eq', `http://localhost:3000${layoutsData.C.path}`);
});

it('routes to hierarchical page', () => {
cy.get('.Previews');
cy.get('.LayoutPreview').eq(2).click();
cy.get('.LayoutPreview').contains('Hierarchical Layout').click();
cy.url().should('eq', `http://localhost:3000${layoutsData.H.path}`);
});

Expand Down
140 changes: 140 additions & 0 deletions frontend/src/common/AnimatedList/AnimatedList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import React, {
Key,
RefObject,
useEffect,
useLayoutEffect,
useRef,
useState,
} from 'react';
import { List } from '@material-ui/core';

/**
* GUI element of the {@link AnimatedList}.
*/
export type AnimatedListElement = JSX.Element & {
ref?: RefObject<HTMLElement>;
};

type Props = {
/**
* List-items; must have a key and a ref.
*/
children: AnimatedListElement[];
className?: string;
};

/**
* Information about items in the AnimatedList-
*/
type ItemGuiInformation = {
/**
* Map with key = element key and value = bounding box.
*/
boxes: Map<Key, DOMRect>;
/**
* Scroll position at the creation of objects of this type.
*/
scrollY: number;
};

/**
* Returns an {@link ItemGuiInformation} object for the given list items
* @param children elements of the list
*/
function createItemGuiInformation(
children: AnimatedListElement[]
): ItemGuiInformation {
const boundingBoxes = new Map<Key, DOMRect>();

React.Children.forEach(children, (child) => {
const domNode = child.ref?.current;
if (domNode && child.key != null) {
boundingBoxes.set(child.key, domNode.getBoundingClientRect());
}
});

return { boxes: boundingBoxes, scrollY: window.scrollY };
}

/**
* Outputs the previous state from a variable of useState.
* See https://reactjs.org/docs/hooks-faq.html#how-to-get-the-previous-props-or-state
*/
function usePrevious<T>(value: T) {
const prevValue = useRef<T>();

useEffect(() => {
prevValue.current = value;
}, [value]);

return prevValue.current;
}

/**
* Returns a list that animated the reordering of its children.
* @param children list-items
* @param className react standard className attribute
*/
function AnimatedList({ children, className }: Props): JSX.Element {
const [currItemGuiInformation, setCurrItemGuiInformation] =
useState<ItemGuiInformation>({
boxes: new Map(),
scrollY: 0,
});
const prevItemGuiInformation = usePrevious(currItemGuiInformation);

// calculates the boxes while blocking the next render => consistent result
useLayoutEffect(() => {
joluj marked this conversation as resolved.
Show resolved Hide resolved
// When children change (e.g. reorder) => update item information
setCurrItemGuiInformation(createItemGuiInformation(children));
}, [children]);

useEffect(() => {
// if prevItemGuiInformation exists ... (else no animation on first draw)
if (prevItemGuiInformation && prevItemGuiInformation.boxes.size > 0) {
// Check each child for changed position
React.Children.forEach(children, (child) => {
const domNode = child.ref?.current;
// current/new bounding box for that child
const currBox = currItemGuiInformation.boxes.get(child.key ?? '');
// previous bounding box
const prevBox = prevItemGuiInformation.boxes.get(child.key ?? '');

// If box/box-position does not exists (e.g. on new item) => stop
if (domNode == null || currBox === undefined || prevBox === undefined)
return;

// y-positions with cleaned scrolling (y independent of scrolling)
// e.g. pos = 50, then 25px scrolling => normal y would be 75, cleaned is still 50
const currScrollCleanedY = prevBox.top - currItemGuiInformation.scrollY;
const prevScrollCleanedY = currBox.top - prevItemGuiInformation.scrollY;

// difference of prev and curr position
const deltaY = currScrollCleanedY - prevScrollCleanedY;

// if difference is there => animate
if (deltaY !== 0) {
requestAnimationFrame(() => {
// first, put item to its prev position
domNode.style.transform = `translateY(${deltaY}px)`;
domNode.style.transition = 'transform 0s';

requestAnimationFrame(() => {
// after the previous frame, remove the transition and play the animation
domNode.style.transform = '';
domNode.style.transition = 'transform 500ms';
});
});
}
});
}
}, [currItemGuiInformation, children]);

return <List className={className}>{children}</List>;
}

AnimatedList.defaultProps = {
className: '',
};

export default AnimatedList;
61 changes: 41 additions & 20 deletions frontend/src/exploration/Exploration.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,59 @@
import React from 'react';
import React, { useRef, useState } from 'react';
import { Box, makeStyles } from '@material-ui/core';
import { createStyles } from '@material-ui/core/styles';
import { createStyles, Theme } from '@material-ui/core/styles';
import useResizeObserver from '@react-hook/resize-observer';
import Previews from './previews/Previews';
import Questions from './questions/Questions';

const useStyle = makeStyles((theme) =>
type StyleProps = {
previewHeaderHeight: number;
};

const useStyle = makeStyles<Theme, StyleProps>((theme) =>
createStyles({
container: {
margin: theme.spacing(4),
margin: theme.spacing(10),
// top margin should be a bit smaller
marginTop: theme.spacing(5),
gap: theme.spacing(4),
},
preview: {
height: '66vh',
},
questions: (props) => ({
marginTop: props.previewHeaderHeight,
}),
})
);

function Exploration(): JSX.Element {
const classes = useStyle();
// ref to the main content container
const containerRef = useRef<HTMLDivElement>(null);

// height of the header of the preview
const [previewHeaderHeight, setPreviewHeaderHeight] = useState<number>(0);

// Whenever the document resizes, calculate the height of the the header of the previews
// so that the questions are on the same height as the preview card.
// Used in favor to fixes pixels in order to react on a possible multi lined header.
useResizeObserver(containerRef, () => {
joluj marked this conversation as resolved.
Show resolved Hide resolved
setPreviewHeaderHeight(
document.querySelector('.Previews h1')?.getBoundingClientRect().height ??
0
);
});

const classes = useStyle({ previewHeaderHeight });
return (
<>
<h1>Exploration</h1>
<Box
className={classes.container}
display="flex"
justifyContent="space-around"
>
<Box flex={3}>
<Questions />
</Box>
<Box flex={2} className={classes.preview}>
<Previews />
<h1 id="ExplorationHeader">Exploration</h1>
<div ref={containerRef}>
<Box className={classes.container} display="flex">
<Box flex={3} className={classes.questions}>
<Questions />
</Box>
<Box flex={2}>
<Previews />
</Box>
</Box>
</Box>
</div>
</>
);
}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/exploration/previews/LayoutCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function LayoutCard(layout: LayoutDefinition): JSX.Element {
const imagePath = `/exploration-preview/${filename}`;

return (
<ListItem key={description}>
<ListItem>
<Link to={path} style={{ textDecoration: 'none' }}>
<Card className={`${classes.card} LayoutPreview`}>
<CardMedia
Expand Down
27 changes: 22 additions & 5 deletions frontend/src/exploration/previews/Previews.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from 'react';
import { Box, List, makeStyles, Paper } from '@material-ui/core';
import React, { createRef } from 'react';
import { Box, makeStyles, Paper } from '@material-ui/core';
import { createStyles } from '@material-ui/core/styles';
import LayoutCard from './LayoutCard';
import useService from '../../dependency-injection/useService';
import ExplorationStore from '../../stores/exploration/ExplorationStore';
import useObservable from '../../utils/useObservable';
import layoutsData from './layoutsData';
import { ExplorationWeight } from '../../stores/exploration';
import AnimatedList from '../../common/AnimatedList/AnimatedList';

const useStyle = makeStyles(() =>
createStyles({
Expand All @@ -17,6 +18,12 @@ const useStyle = makeStyles(() =>
paper: {
maxHeight: '100%',
overflow: 'auto',
maxWidth: '22em',
margin: 'auto',
},
header: {
margin: 0,
textAlign: 'center',
},
})
);
Expand All @@ -37,14 +44,24 @@ function Previews(): JSX.Element {
const layoutsPreviewData = (
Object.keys(weights) as (keyof ExplorationWeight)[]
)
.sort((a, b) => weights[a] - weights[b])
.sort((a, b) => weights[a] - weights[b] || a.localeCompare(b))
.map((layout) => layoutsData[layout]);

return (
<Box className={`${classes.container} Previews`}>
<h1>Recommended Visualisations</h1>
<h1 className={classes.header}>Recommended Visualisations</h1>
<Paper className={classes.paper}>
<List>{layoutsPreviewData.map((elem) => LayoutCard(elem))}</List>
<AnimatedList>
{layoutsPreviewData.map((preview) => (
<div key={preview.description} ref={createRef()}>
<LayoutCard
filename={preview.filename}
description={preview.description}
path={preview.path}
/>
</div>
))}
</AnimatedList>
</Paper>
</Box>
);
Expand Down