Commit 2d4c6d27 authored by maerzhase's avatar maerzhase
Browse files

initial commit

parents
{
"extends": [
"next",
"prettier"
],
"plugins": ["prettier"],
"rules": {
"@next/next/no-img-element": "off",
"prettier/prettier": "error"
},
}
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Next.js
.next
*.log
/out
*.swo
engine-strict=true
The MIT License (MIT)
Copyright © 2022 Studio NAND GmbH
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
# De sphaera
Tool to explore the data of MPI's project "The Sphere: Knowledge System Evolution and the Shared Scientific Identity in Europe"
## Getting started
#### Requirements
- `node@>=15.0.0`
- `npm@>=7.0.0`
#### Install dependencies
```sh
npm install
```
#### Mapbox Api Access Token
In order to run the map with you need a mapbox access token.
Create a `.env.local` file in `/src` directory. With the following content:
```
NEXT_PUBLIC_MAPBOX_API_ACCESS_TOKEN=<YOUR_TOKEN_HERE>
```
- [More information about Mapbox Tokens and Alternarnatives](https://visgl.github.io/react-map-gl/docs/get-started/mapbox-tokens)
- [More information on NextJS Environment Variables](https://nextjs.org/docs/basic-features/environment-variables#loading-environment-variables)
## Running the project
```
npm run dev
```
{
"compilerOptions": {
"jsx": "react",
"baseUrl": "."
}
}
This diff is collapsed.
{
"name": "mpi-de-sphaera-fingerprintchain-explorer",
"license": "MIT",
"author": "Studio NAND GmbH <hello@nand.io> (https://nand.io/)",
"version": "1.0.0",
"engines": {
"npm": ">=7.0.0",
"node": ">=15.0.0"
},
"scripts": {
"dev": "next dev src",
"build": "next build src/",
"start": "next start src/",
"lint": "next lint --dir src",
"lint:fix": "next lint --dir src --fix"
},
"dependencies": {
"@emotion/cache": "latest",
"@emotion/react": "latest",
"@emotion/server": "latest",
"@emotion/styled": "latest",
"@mui/icons-material": "latest",
"@mui/material": "latest",
"@turf/turf": "^6.5.0",
"animated": "^0.2.2",
"d3": "^7.2.1",
"honeycomb-grid": "^3.1.8",
"lodash": "^4.17.21",
"maplibre-gl": "^1.15.2",
"mobx": "^6.3.12",
"mobx-react": "^7.2.1",
"next": "latest",
"prop-types": "latest",
"react": "latest",
"react-dom": "latest",
"react-map-gl": "^6.1.18",
"react-pixi-fiber": "^1.0.0-beta.15",
"react-virtualized-auto-sizer": "^1.0.6"
},
"devDependencies": {
"eslint": "7.32.0",
"eslint-config-next": "^11.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.3.2"
}
}
const API_BASE_URL = "http://0.0.0.0:3000/";
const API_BASE_PATH = `data/`;
const loadData = (file) => fetch(`${API_BASE_URL}${API_BASE_PATH}${file}`);
export const loadJson = async (file) => {
const res = await loadData(file);
const json = await res.json();
return json;
};
const getIndexedChainFilename = (index) => `FP Chain ${index}.json`;
const MAX = 56;
export const loadIndividualChains = async () => {
const chains = await Promise.all(
Array(MAX)
.fill(null)
.map((_, i) => loadJson(getIndexedChainFilename(i + 1)))
);
return chains;
};
import * as React from "react";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import Popper from "@mui/material/Popper";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import Box from "@mui/material/Box";
import Divider from "@mui/material/Divider";
import Fab from "@mui/material/Fab";
import { styled } from "@mui/material/styles";
import { observer } from "mobx-react";
import { useStores } from "../stores/";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import ArrowRightIcon from "@mui/icons-material/ArrowRight";
const MAX_WIDTH = 248;
const StyledFab = styled(Fab)((props, theme) => ({
boxShadow: "none",
background: props.isSelected
? props.theme.palette.common.greyMain
: props.chain.fillColor,
borderStyle: "solid",
borderWidth: 1,
borderColor: "transparent",
color: props.isSelected
? props.theme.palette.common.white
: props.chain.textColor,
"&:hover": {
background: "transparent",
borderColor: props.theme.palette.primary.main,
color: props.theme.palette.primary.main,
},
}));
function ChainFilter() {
const { dataStore, uiStore } = useStores();
const { chains } = dataStore;
const { excludedChains, focusedChain } = uiStore;
const [anchorEl, setAnchorEl] = React.useState(null);
const toggleAnchorElement = (e) => {
if (anchorEl) {
setAnchorEl(null);
} else {
setAnchorEl(e.currentTarget);
}
};
const handleClickButton = (event) => {
toggleAnchorElement(event);
};
const handleClose = () => {
setAnchorEl(null);
};
const createClickChainButtonHandler = (chain) => () => {
uiStore.toggleChainExclusion(chain);
};
const handleClickSelectAll = () => {
uiStore.includeAllChains();
};
const handleClickDeselectAll = () => {
uiStore.excludeAllChains();
};
const numChains = chains.asList.length;
const numExcludedChains = excludedChains.length;
const numSelectedChains = Math.abs(numChains - numExcludedChains);
const isChainFilterActive = numChains !== numSelectedChains;
const open = Boolean(anchorEl);
const id = open ? "simple-popover" : undefined;
return (
<>
<Button
sx={{
minWidth: MAX_WIDTH,
height: 40,
justifyContent: "space-between",
}}
endIcon={
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="caption" sx={{ mr: 0.5, lineHeight: 1 }}>
{isChainFilterActive && `${numSelectedChains}/`}
{numChains} selected
</Typography>
{open ? <ArrowDropDownIcon /> : <ArrowRightIcon />}
</Box>
}
aria-describedby={id}
variant="contained"
color="secondary"
onClick={handleClickButton}
>
Filter Chains
</Button>
<Popper id={id} open={open} anchorEl={anchorEl} onClose={handleClose}>
<ClickAwayListener onClickAway={handleClose}>
<Box
sx={{
display: "flex",
flexWrap: "wrap",
justifyContent: "center",
width: MAX_WIDTH,
my: 2,
borderRadius: 1,
boxShadow: 2,
backgroundColor: "background.paper",
py: 1,
}}
>
{chains.asList.map((chain) => {
const isSelected = excludedChains.includes(chain);
return (
<StyledFab
key={chain.index}
onClick={createClickChainButtonHandler(chain)}
size="small"
chain={chain}
sx={{
m: 0.5,
}}
isSelected={isSelected}
>
<Typography variant="small">{chain.index + 1}</Typography>
</StyledFab>
);
})}
<Box
sx={{
display: "flex",
justifyContent: "flex-start",
width: "100%",
px: 1,
mt: 0.5,
}}
>
<Button size="small" onClick={handleClickSelectAll}>
Select All
</Button>
<Divider
orientation="vertical"
variant="middle"
flexItem
sx={{ mx: 0.5 }}
/>
<Button size="small" onClick={handleClickDeselectAll}>
Deselect All
</Button>
</Box>
</Box>
</ClickAwayListener>
</Popper>
</>
);
}
export default observer(ChainFilter);
import React from "react";
import { Box, Typography } from "@mui/material";
import format from "../utils/format";
export const Value = (props) => {
const { value, label, noWrap, LabelProps, ValueProps, ...rest } = props;
return (
<Box mb={0.7} {...rest}>
<Typography
variant="overline"
color="text.secondary"
{...LabelProps}
sx={{ whiteSpace: noWrap ? "nowrap" : "normal", ...LabelProps.sx }}
>
{label}
</Typography>
<Typography
variant="body2"
component="div"
{...ValueProps}
sx={{ whiteSpace: noWrap ? "nowrap" : "normal", ...LabelProps.sx }}
>
{value}
</Typography>
</Box>
);
};
Value.defaultProps = {
noWrap: false,
LabelProps: {},
ValueProps: {},
};
const splitNames = (s) => s.split(";").join(" | ");
const EditionMeta = (props) => {
const { edition, fingerprint } = props;
if (!edition) return null;
return (
<>
<Value label="Title" value={edition.label} />
<Value label="Location" value={edition.place.label} />
<Value label="Year" value={edition.yearPublished} />
<Value label="Author" value={splitNames(edition.author)} />
<Value label="Publisher" value={splitNames(edition.publisher)} />
<Value label="Printer" value={splitNames(edition.printer)} />
<Value label="Format" value={edition.format} />
<Value label="Fingerprint" value={edition.fingerprint} />
{fingerprint && (
<>
<Value
label={<>Speed of Spread</>}
value={format(
fingerprint?.speedOfSpreadRelativeToPublishingInterval,
{ suffix: "relative to publishing interval" }
)}
/>
{fingerprint.isOutgoing && (
<Value
label={<>Speed of Spread</>}
value={format(fingerprint?.speedOfSpread, { suffix: "km/year" })}
/>
)}
</>
)}
</>
);
};
export default EditionMeta;
import * as React from "react";
import TimeSlider from "./TimeSlider";
import CloseIcon from "@mui/icons-material/Close";
import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
import InsertLinkIcon from "@mui/icons-material/InsertLink";
import Fade from "@mui/material/Fade";
import {
Typography,
Box,
Slide,
IconButton,
Button,
Divider,
Tooltip,
} from "@mui/material";
import { observer } from "mobx-react";
import { useStores } from "../stores";
import ChainFilter from "./ChainFilter";
import EditionMeta, { Value } from "./EditionMeta";
import format from "../utils/format";
const MAX_WIDTH = 240;
const Header = () => {
const { computedStore, dataStore, uiStore } = useStores();
const { chains } = dataStore;
const { focusedChain, focusedEdition } = uiStore;
const filteredCount = computedStore.filteredEditions.length;
const allCount = dataStore.editions.asList.length;
const isFilterActive = filteredCount !== allCount;
const [isOpen, setIsOpen] = React.useState(focusedEdition && focusedChain);
const handleClickNext = () => {
const { nextEdition } = computedStore;
if (nextEdition) {
uiStore.setFocusedEdition(nextEdition);
uiStore.setSelectedYear(nextEdition.yearPublished);
}
};
const handleClickPrev = () => {
const { prevEdition } = computedStore;
if (prevEdition) {
uiStore.setFocusedEdition(prevEdition);
uiStore.setSelectedYear(prevEdition.yearPublished);
}
};
const handleClickCloseMetaButton = () => {
setIsOpen(false);
setTimeout(() => {
uiStore.setFocusedEdition(null);
uiStore.setFocusedChain(null);
}, 300);
};
const editionIndex = React.useMemo(() => {
if (!focusedEdition || !focusedChain) return null;
return focusedChain.flattend.indexOf(focusedEdition);
}, [focusedEdition, focusedChain]);
React.useEffect(() => {
if (focusedChain && focusedEdition) {
setIsOpen(true);
}
}, [focusedChain, focusedEdition]);
return (
<>
<Box
sx={{
pointerEvents: "none",
position: "absolute",
width: "100%",
height: "100%",
top: 0,
zIndex: 1,
px: 4,
pt: 4,
pb: 5,
display: "flex",
flexDirection: "column",
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box
sx={{
width: MAX_WIDTH,
p: 1,
}}
>
<Box
component="img"
sx={{ width: 140, height: "auto", mb: 2 }}
src="/logo.svg"
/>
<Typography
sx={{ fontVariantNumeric: "tabular-nums" }}
variant="h3"
component="h1"
color="primary.main"
>
{isFilterActive && `${filteredCount}/`}
{allCount} editions
</Typography>
<Divider
sx={{ mt: 1, color: (theme) => theme.palette.primary.main }}
/>
</Box>
<Box
flexGrow={1}
p={2}
display="flex"
flexDirection="column"
sx={{ pointerEvents: "auto" }}
>
<Box mb={3}>
<TimeSlider />
</Box>
<Box sx={{ display: "flex", alignItems: "center" }}>
<ChainFilter />
<Fade in={isOpen}>
<div>
<Box
sx={{
ml: 4,
px: 1.5,
borderRadius: 1,
boxShadow: 2,
bgcolor: focusedChain?.fillColor,
color: focusedChain?.textColor,
opacity: 0.75,
height: 40, // this is the height of the button next to it...
display: "flex",
alignItems: "center",
overflow: "auto",
}}
>
<Typography
sx={{
mr: 4,
whiteSpace: "nowrap",
color: focusedChain?.textColor,
}}
color="inherit"
variant="h6"
>
Chain {focusedChain?.index + 1}
</Typography>
<Value
mb={0}
noWrap
sx={{ mr: 4 }}
LabelProps={{ sx: { color: "inherit" } }}
label={<>&#8709; Publishing interval </>}
value={format(focusedChain?.frequency, {
suffix: "years",
})}
/>
<Value
mb={0}
noWrap
sx={{ mr: 4 }}
LabelProps={{ sx: { color: "inherit" } }}
label={<>&#8709; Speed of Spread </>}
value={format(
focusedChain?.speedOfSpreadRelativeToPublishingInterval,
{ suffix: "relative to publishing interval" }
)}
/>
{focusedChain?.isOutgoing && (
<Value
mb={0}
noWrap
LabelProps={{ sx: { color: "inherit" } }}
label={<>&#8709; Speed of Spread</>}
value={format(focusedChain?.speedOfSpread, {
suffix: "km/year",
})}
/>
)}
</Box>
</div>
</Fade>
</Box>
</Box>
</Box>
<Slide direction="right" in={isOpen} mountOnEnter unmountOnExit>
<Box
sx={{
position: "relative",
width: MAX_WIDTH,
bgcolor: "background.paper",
boxShadow: 2,
borderRadius: 1,
pointerEvents: "auto",
display: "flex",
overflow: "auto",
flexDirection: "column",
height: "100%",
p: 2,
}}