add a new page - MapPage

This commit is contained in:
Dawid Wysokiński 2020-12-20 19:31:04 +01:00
parent ec933ce417
commit 8dd4b1e1bf
10 changed files with 806 additions and 45 deletions

View File

@ -32,6 +32,7 @@
"typescript": "^4.0.3",
"use-debounce": "^5.0.1",
"use-query-params": "^1.1.8",
"uuid": "^8.3.2",
"web-vitals": "^0.2.4"
},
"scripts": {
@ -61,6 +62,7 @@
"devDependencies": {
"@types/lodash": "^4.14.165",
"@types/react-router-dom": "^5.1.6",
"@types/uuid": "^8.3.0",
"babel-plugin-module-resolver": "^4.0.0",
"babel-plugin-transform-imports": "^2.0.0",
"customize-cra": "^1.0.0",

View File

@ -12,6 +12,9 @@ export const SERVER_STATUS = {
export const TWHELP =
process.env.REACT_APP_TWHELP ?? 'https://tribalwarshelp.com';
export const MAP_SERVICE =
process.env.REACT_APP_MAP_SERVICE ?? 'https://api.tribalwarshelp.com/map';
export const AUTHOR = 'Dawid Wysokiński';
export const DATE_FORMAT = {

View File

@ -8,6 +8,7 @@ import PlayerPage from './features/PlayerPage/PlayerPage';
import TribePage from './features/TribePage/TribePage';
import VillagePage from './features/VillagePage/VillagePage';
import RankingPage from './features/RankingPage/RankingPage';
import MapPage from './features/MapPage/MapPage';
import NotFoundPage from '../NotFoundPage/NotFoundPage';
const EnhancedRoute = ({ children, ...rest }: RouteProps) => {
@ -36,6 +37,9 @@ function ServerPage() {
<EnhancedRoute path={SERVER_PAGE.RANKING_PAGE.BASE}>
<RankingPage />
</EnhancedRoute>
<EnhancedRoute path={SERVER_PAGE.MAP_PAGE}>
<MapPage />
</EnhancedRoute>
<Route path="*">
<NotFoundPage />
</Route>

View File

@ -0,0 +1,404 @@
import React, { useState } from 'react';
import { useApolloClient } from '@apollo/client';
import { useDebouncedCallback } from 'use-debounce';
import useServer from '../../libs/ServerContext/useServer';
import useMarkers from './useMarkers';
import { MAP_SERVICE } from '@config/app';
import { PLAYERS, TRIBES } from './queries';
import { makeStyles } from '@material-ui/core/styles';
import {
Typography,
Container,
Button,
Grid,
TextField,
Checkbox,
FormControlLabel,
} from '@material-ui/core';
import ServerPageLayout from '@features/ServerPage/common/PageLayout/PageLayout';
import Map from './components/Map/Map';
import MarkerField from './components/MarkerField/MarkerField';
import {
PlayersQueryVariables,
TribesQueryVariables,
} from '@libs/graphql/types';
import {
Tribe,
Player,
PlayerList,
TribeList,
Settings,
PlayerMarker,
TribeMarker,
} from './types';
function MapPage() {
const [settings, setSettings] = useState<Settings>({
showBarbarian: false,
largerMarkers: false,
markersOnly: false,
centerX: 500,
centerY: 500,
scale: 1,
showGrid: true,
showContinentNumbers: true,
backgroundColor: '#000000',
gridLineColor: '#ffffff',
continentNumberColor: '#ffffff',
});
const {
markers: tribeMarkers,
createDeleteMarkerHandler: createDeleteTribeMarkerHandler,
createUpdateMarkerColorHandler: createUpdateTribeMarkerColorHandler,
createUpdateMarkerItemHandler: createUpdateTribeMarkerItemHandler,
handleAddMarker: handleAddTribeMarker,
} = useMarkers<Tribe>();
const {
markers: playerMarkers,
createDeleteMarkerHandler: createDeletePlayerMarkerHandler,
createUpdateMarkerColorHandler: createUpdatePlayerMarkerColorHandler,
createUpdateMarkerItemHandler: createUpdatePlayerMarkerItemHandler,
handleAddMarker: handleAddPlayerMarker,
} = useMarkers<Player>();
const [mapURL, setMapURL] = useState<string>('');
const { key } = useServer();
const classes = useStyles();
const client = useApolloClient();
const handleSettingsChange = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
setSettings({
...settings,
[e.target.name]:
e.target.type === 'checkbox' && 'checked' in e.target
? e.target.checked
: e.target.value,
});
};
const debouncedHandleSettingsChange = useDebouncedCallback(
handleSettingsChange,
500
);
const callDebouncedHandleSettingsChange = (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => {
e.persist();
debouncedHandleSettingsChange.callback(e);
};
const searchPlayers = async (searchValue: string): Promise<Player[]> => {
try {
const { data } = await client.query<PlayerList, PlayersQueryVariables>({
query: PLAYERS,
variables: {
limit: 10,
filter: {
exists: true,
nameIEQ: searchValue + '%',
},
server: key,
offset: 0,
sort: ['points DESC'],
},
fetchPolicy: 'network-only',
});
return data.players?.items ?? [];
} catch (error) {
return [];
}
};
const searchTribes = async (searchValue: string): Promise<Tribe[]> => {
try {
const { data } = await client.query<TribeList, TribesQueryVariables>({
query: TRIBES,
variables: {
limit: 10,
filter: {
exists: true,
tagIEQ: searchValue + '%',
},
server: key,
offset: 0,
sort: ['points DESC'],
},
fetchPolicy: 'network-only',
});
return data.tribes?.items ?? [];
} catch (error) {
return [];
}
};
const encodeMarker = (marker: PlayerMarker | TribeMarker): string => {
return encodeURIComponent(marker.item?.id + ',' + marker.color);
};
const handleSubmit = (e: React.FormEvent<{}>) => {
e.preventDefault();
let searchParams = '';
Object.entries(settings).forEach(
([key, value]: [string, string | number | boolean]) => {
searchParams += key + '=' + encodeURIComponent(value) + '&';
}
);
playerMarkers.forEach(marker => {
if (!marker.item) return;
searchParams += 'player=' + encodeMarker(marker) + '&';
});
tribeMarkers.forEach(marker => {
if (!marker.item) return;
searchParams += 'tribe=' + encodeMarker(marker) + '&';
});
setMapURL(MAP_SERVICE + '/' + key + '?' + searchParams);
};
const tribeGetOptionLabel = (tribe: Tribe) => (tribe ? tribe.tag : '');
const tribeGetOptionSelected = (option: Tribe, value: Tribe) =>
option && value ? option.tag === value.tag : false;
const playerGetOptionLabel = (player: Player) => (player ? player.name : '');
const playerGetOptionSelected = (option: Player, value: Player) =>
option && value ? option.name === value.name : false;
return (
<ServerPageLayout>
<Container>
<form onSubmit={handleSubmit}>
<Grid container spacing={2}>
<Grid item xs={12} md={4}>
<Typography
variant="h4"
component="h3"
align="center"
gutterBottom
>
Settings
</Typography>
<div className={classes.formGroup}>
<TextField
label="Zoom level"
type="number"
name="scale"
value={settings.scale}
onChange={handleSettingsChange}
fullWidth
variant="standard"
inputProps={{
min: 1,
max: 5,
step: '.01',
}}
/>
<TextField
label="Center X"
type="number"
name="centerX"
value={settings.centerX}
onChange={handleSettingsChange}
fullWidth
variant="standard"
inputProps={{
min: 0,
max: 1000,
step: '.01',
}}
/>
<TextField
label="Center Y"
type="number"
name="centerY"
value={settings.centerY}
onChange={handleSettingsChange}
fullWidth
variant="standard"
inputProps={{
min: 0,
max: 1000,
step: '.01',
}}
/>
<TextField
label="Background color"
name="backgroundColor"
onChange={callDebouncedHandleSettingsChange}
fullWidth
type="color"
variant="standard"
/>
<TextField
label="Grid line color"
name="gridLineColor"
onChange={callDebouncedHandleSettingsChange}
defaultValue={settings.gridLineColor}
fullWidth
variant="standard"
type="color"
/>
<TextField
label="Continent number color"
name="continentNumberColor"
onChange={callDebouncedHandleSettingsChange}
defaultValue={settings.continentNumberColor}
fullWidth
variant="standard"
type="color"
/>
<FormControlLabel
label="Markers only"
control={
<Checkbox
name="markersOnly"
checked={settings.markersOnly}
onChange={handleSettingsChange}
/>
}
/>
<FormControlLabel
label="Show barbarian villages"
control={
<Checkbox
name="showBarbarian"
checked={settings.showBarbarian}
onChange={handleSettingsChange}
/>
}
/>
<FormControlLabel
label="Larger markers"
control={
<Checkbox
name="largerMarkers"
checked={settings.largerMarkers}
onChange={handleSettingsChange}
/>
}
/>
<FormControlLabel
label="Continent grid"
control={
<Checkbox
name="showGrid"
checked={settings.showGrid}
onChange={handleSettingsChange}
/>
}
/>
<FormControlLabel
label="Continent numbers"
control={
<Checkbox
name="showContinentNumbers"
checked={settings.showContinentNumbers}
onChange={handleSettingsChange}
/>
}
/>
</div>
</Grid>
<Grid item xs={12} md={4}>
<Typography
variant="h4"
component="h3"
align="center"
gutterBottom
>
Tribe markers
</Typography>
<div className={classes.formGroup}>
{tribeMarkers.map(marker => {
return (
<MarkerField
key={marker.id}
onDelete={createDeleteTribeMarkerHandler(marker.id)}
onChange={createUpdateTribeMarkerItemHandler(marker.id)}
onChangeColor={createUpdateTribeMarkerColorHandler(
marker.id
)}
loadSuggestions={searchTribes}
getOptionLabel={tribeGetOptionLabel}
getOptionSelected={tribeGetOptionSelected}
/>
);
})}
<Button
variant="contained"
fullWidth
color="secondary"
onClick={handleAddTribeMarker}
disabled={tribeMarkers.length >= 100}
>
Add marker
</Button>
</div>
</Grid>
<Grid item xs={12} md={4}>
<Typography
variant="h4"
component="h3"
align="center"
gutterBottom
>
Player markers
</Typography>
<div className={classes.formGroup}>
{playerMarkers.map(marker => {
return (
<MarkerField
key={marker.id}
onDelete={createDeletePlayerMarkerHandler(marker.id)}
onChange={createUpdatePlayerMarkerItemHandler(marker.id)}
onChangeColor={createUpdatePlayerMarkerColorHandler(
marker.id
)}
loadSuggestions={searchPlayers}
getOptionLabel={playerGetOptionLabel}
getOptionSelected={playerGetOptionSelected}
/>
);
})}
<Button
variant="contained"
fullWidth
color="secondary"
onClick={handleAddPlayerMarker}
disabled={playerMarkers.length >= 100}
>
Add marker
</Button>
</div>
</Grid>
<Grid item xs={12}>
<Typography align="center" component="div">
<Button
type="submit"
size="large"
color="secondary"
variant="contained"
>
Generate new map
</Button>
</Typography>
</Grid>
</Grid>
</form>
{mapURL && <Map src={mapURL} alt={key} />}
</Container>
</ServerPageLayout>
);
}
const useStyles = makeStyles(theme => ({
formGroup: {
'& > *': {
marginBottom: theme.spacing(1),
},
},
}));
export default MapPage;

View File

@ -0,0 +1,82 @@
import React, { memo, useState, useEffect } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import { Link } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import Spinner from '@common/Spinner/Spinner';
export interface Props {
src: string;
alt: string;
maxWidth?: number;
}
function Map({ src = '', alt = 'Map', maxWidth = 1000 }: Props) {
const [loading, setLoading] = useState(true);
const classes = useStyles();
useEffect(() => {
setLoading(true);
}, [src]);
return (
<div className={classes.container}>
{loading ? (
<Alert severity="warning">It may take a while to generate a map!</Alert>
) : (
<Alert severity="info">
URL:{' '}
<Link className={classes.link} href={src}>
{src}
</Link>
</Alert>
)}
<div
className={classes.imageWrapper}
style={{ maxWidth: `${maxWidth}px` }}
>
{loading && (
<Spinner
size={150}
containerProps={{
alignItems: 'center',
justifyContent: 'center',
display: 'flex',
}}
/>
)}
<img
src={src}
alt={alt}
style={{
display: loading ? 'none' : 'block',
}}
onLoad={() => setLoading(false)}
className={classes.img}
/>
</div>
</div>
);
}
const useStyles = makeStyles(theme => {
return {
img: {
display: 'block',
width: '100%',
height: 'auto',
},
link: {
wordBreak: 'break-all',
},
container: {
marginTop: theme.spacing(2),
},
imageWrapper: {
marginTop: theme.spacing(1),
margin: 'auto',
},
};
});
export default memo(Map);

View File

@ -0,0 +1,96 @@
import React, { useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import useUpdateEffect from '@libs/useUpdateEffect';
import { TextField, Box, IconButton } from '@material-ui/core';
import { Autocomplete } from '@material-ui/lab';
import { Delete as DeleteIcon } from '@material-ui/icons';
export interface Props<T> {
onDelete: () => void;
onChange: (e: React.ChangeEvent<{}>, value: T | null) => void;
onChangeColor: (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
) => void;
getOptionLabel: (opt: T) => string;
getOptionSelected: (opt: T, value: T) => boolean;
loadSuggestions: (value: string) => Promise<T[]>;
}
function MarkerField<T extends object>({
onDelete,
onChange,
loadSuggestions,
onChangeColor,
getOptionSelected,
getOptionLabel,
}: Props<T>) {
const [searchValue, setSearchValue] = useState<string>('');
const [suggestions, setSuggestions] = useState<T[]>([]);
const [loading, setLoading] = useState(true);
const debouncedOnChangeColor = useDebouncedCallback(
(e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) =>
onChangeColor(e),
500
);
const debouncedLoadSuggestions = useDebouncedCallback(
(searchValue: string) => {
setLoading(true);
loadSuggestions(searchValue)
.then(data => {
setSuggestions(data);
})
.finally(() => setLoading(false));
},
1000
);
useUpdateEffect(() => {
debouncedLoadSuggestions.callback(searchValue);
}, [searchValue, debouncedLoadSuggestions]);
return (
<Box display="flex" justifyContent="space-between" alignItems="flex-end">
<Autocomplete
options={suggestions}
getOptionLabel={getOptionLabel}
fullWidth
autoSelect
autoHighlight
loading={loading}
getOptionSelected={getOptionSelected}
onChange={onChange}
renderInput={params => {
return (
<TextField
{...params}
variant="standard"
InputProps={{
...params.InputProps,
startAdornment: (
<IconButton onClick={onDelete} size="small">
<DeleteIcon />
</IconButton>
),
}}
type="text"
name="value"
onChange={e => setSearchValue(e.target.value)}
/>
);
}}
/>
<TextField
style={{ width: '40%' }}
type="color"
name="color"
variant="standard"
onChange={e => {
e.persist();
debouncedOnChangeColor.callback(e);
}}
/>
</Box>
);
}
export default MarkerField;

View File

@ -0,0 +1,47 @@
import { gql } from '@apollo/client';
export const PLAYERS = gql`
query players(
$server: String!
$filter: PlayerFilter!
$limit: Int!
$offset: Int!
$sort: [String!]
) {
players(
server: $server
filter: $filter
limit: $limit
offset: $offset
sort: $sort
) {
items {
id
name
}
}
}
`;
export const TRIBES = gql`
query tribes(
$server: String!
$filter: TribeFilter!
$limit: Int!
$offset: Int!
$sort: [String!]
) {
tribes(
server: $server
filter: $filter
limit: $limit
offset: $offset
sort: $sort
) {
items {
id
tag
}
}
}
`;

View File

@ -0,0 +1,38 @@
import { List } from '@libs/graphql/types';
export type Tribe = {
id: number;
tag: string;
};
export type TribeList = {
tribes?: List<Tribe[]>;
};
export type Player = {
id: number;
name: string;
};
export type PlayerList = {
players?: List<Player[]>;
};
export type Marker<T = undefined> = {
id: string;
item?: T | null;
color: string;
};
export type PlayerMarker = Marker<Player>;
export type TribeMarker = Marker<Tribe>;
export type Settings = Object & {
showBarbarian: boolean;
largerMarkers: boolean;
markersOnly: boolean;
centerX: number;
centerY: number;
scale: number;
showGrid: boolean;
showContinentNumbers: boolean;
backgroundColor: string;
gridLineColor: string;
continentNumberColor: string;
};

View File

@ -0,0 +1,75 @@
import { useState } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { Marker } from './types';
export type MarkerBag<T> = {
markers: Marker<T>[];
handleAddMarker: () => void;
createUpdateMarkerItemHandler: (
id: string
) => (e: React.ChangeEvent<{}>, value: T | null) => void;
createUpdateMarkerColorHandler: (
id: string
) => (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
createDeleteMarkerHandler: (id: string) => () => void;
};
const useMarkers = <T>(): MarkerBag<T> => {
const [markers, setMarkers] = useState<Marker<T>[]>([]);
const getNewMarker = (): Marker<T> => ({
id: uuidv4(),
item: undefined,
color: '#000000',
});
const handleAddMarker = () => {
setMarkers([...markers, getNewMarker()]);
};
const createDeleteMarkerHandler = (id: string) => () => {
setMarkers(markers.filter(marker => marker.id !== id));
};
const createUpdateMarkerItemHandler = (id: string) => (
e: React.ChangeEvent<{}>,
item: T | null
): void => {
setMarkers(
markers.map(marker => {
if (marker.id !== id) return marker;
if (item || item === null) {
return {
...marker,
item,
};
}
return marker;
})
);
};
const createUpdateMarkerColorHandler = (id: string) => (
e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>
): void => {
setMarkers(
markers.map(marker => {
if (marker.id !== id) return marker;
return {
...marker,
color: e.target.value,
};
})
);
};
return {
markers,
handleAddMarker,
createUpdateMarkerItemHandler,
createUpdateMarkerColorHandler,
createDeleteMarkerHandler,
};
};
export default useMarkers;

100
yarn.lock
View File

@ -1384,30 +1384,30 @@
prop-types "^15.7.2"
react-is "^16.8.0 || ^17.0.0"
"@nivo/annotations@0.65.1":
version "0.65.1"
resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.65.1.tgz#d9bdf6b66b636183191109446759c06cc2e778b2"
integrity sha512-JNViguRGDm8vt6N5JhV/ugPsnesFuL826CWUnPfqkHj06eLn4fnsnGKBnOqxXwNSrV4GbJJBBfrzWOpz3drW/g==
"@nivo/annotations@0.67.0":
version "0.67.0"
resolved "https://registry.yarnpkg.com/@nivo/annotations/-/annotations-0.67.0.tgz#81f309d4427295a8afe847989480c4f3a71137d8"
integrity sha512-Qfracwd9we2nWBaNOAp0Lnt9uc86yRFDcQI3jsOdjFdzDA4lRwhkD85Lq477w1XrU7mtpjeRGsaKTCJtaorWEg==
dependencies:
"@nivo/colors" "0.65.1"
"@nivo/colors" "0.67.0"
lodash "^4.17.11"
react-spring "9.0.0-rc.3"
"@nivo/axes@0.65.1":
version "0.65.1"
resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.65.1.tgz#ea9576fe9d7b9ac84430cc91e0b30af3ee9bba76"
integrity sha512-H2/8wM9xwn/F0GH47IgLmPaw6TuLqRIIojfPvQNM1gWgJYaBsghPxGfrXF2f8URjwRpihptbrlDVmg3OwRykVQ==
"@nivo/axes@0.67.0":
version "0.67.0"
resolved "https://registry.yarnpkg.com/@nivo/axes/-/axes-0.67.0.tgz#5892a8c90f782e478015865f779b62f2e74b7665"
integrity sha512-csqEgFoJQGoB5/TDSy7SXCy/2QSdrfSa51AMU/RhzpnZ35wNO/zHkEfP+ApvB8nuuwVuBNOShio80PNZOOD+Og==
dependencies:
"@nivo/scales" "0.65.0"
"@nivo/scales" "0.67.0"
d3-format "^1.4.4"
d3-time "^1.0.11"
d3-time-format "^2.1.3"
react-spring "9.0.0-rc.3"
"@nivo/colors@0.65.1":
version "0.65.1"
resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.65.1.tgz#d55ec085c991358d88e3bce7c94be468e58b3817"
integrity sha512-1dU6fuHYUDbU2oqOA8jzlYI9qXOajgbwH2cs8s9XktHvC3rfxp6ZedqAI+FxqKuk9QFaAxcyFftYUVlpdsFFkQ==
"@nivo/colors@0.67.0":
version "0.67.0"
resolved "https://registry.yarnpkg.com/@nivo/colors/-/colors-0.67.0.tgz#5a2adc4b55406466efb8bbafecc08249e4e94e69"
integrity sha512-y8x76SzQ4HYm6kkWYfBGQSE5fy/jtqGhCBjivlKsouU9Hv2G8g4a2JVMsfadJQHQIUjIsM/7Q0Y7zasFM9wkgA==
dependencies:
d3-color "^2.0.0"
d3-scale "^3.0.0"
@ -1415,10 +1415,10 @@
lodash "^4.17.11"
react-motion "^0.5.2"
"@nivo/core@^0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.65.0.tgz#a356dcd740d7f70027edece36828185741fc3ff9"
integrity sha512-9ag4Stx0aWkiIFArg6X+eTDzZEBJyx08UhIfxvUcMnkda8OcYlAPrnp2K7sV5d7egIrnsxBpDt2lx/cC7XL/pg==
"@nivo/core@^0.67.0":
version "0.67.0"
resolved "https://registry.yarnpkg.com/@nivo/core/-/core-0.67.0.tgz#29f07b8bed2bcefb9402d14a9abe2895c5b2bfa2"
integrity sha512-F8amsf/MnIuZLoN6ikla8bKlUkUI3HVhy6R2qMF2jDS5xnYch3Sno9OPTTuR8RnYGCr57yClnvFuJzw251GE6g==
dependencies:
d3-color "^2.0.0"
d3-format "^1.4.4"
@ -1433,49 +1433,49 @@
recompose "^0.30.0"
resize-observer-polyfill "^1.5.1"
"@nivo/legends@0.65.1":
version "0.65.1"
resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.65.1.tgz#6a49329a8a7939d889345da95974d8520c2d4ede"
integrity sha512-ulJmhu9ZO5b5qp5YhMbEZ6+2PZfahd9jkXECSizwKyZ49FsTptiyLx8H4NTkmk2fmonhRt/W6IxEaUNG0/12Lg==
"@nivo/legends@0.67.0":
version "0.67.0"
resolved "https://registry.yarnpkg.com/@nivo/legends/-/legends-0.67.0.tgz#9cbe99d8929c6addafd8253f65b7e040d9d4bb6c"
integrity sha512-W6JWMSiiAPsDhuofwq7x3ERA2fFJ2J/aj+Y3lvrZh5OAsG6/PCljwhlhMklNgYoN6bT/+Jgtut3Agdl0Dlzspg==
dependencies:
lodash "^4.17.11"
recompose "^0.30.0"
"@nivo/line@^0.65.1":
version "0.65.1"
resolved "https://registry.yarnpkg.com/@nivo/line/-/line-0.65.1.tgz#9efced4734d6d16a89a1423069f81eb58a2b6103"
integrity sha512-dcw3fxQeFE6NHLexYLKipvlOSG7/gUoHTt8mpcrjxQfThR3ugAjPW6O/1frVcrKYzouLxDTWpl9C7uWupgtzaQ==
"@nivo/line@^0.67.0":
version "0.67.0"
resolved "https://registry.yarnpkg.com/@nivo/line/-/line-0.67.0.tgz#c9abde9e1ac4f758377775e1d266a40aedb5c1ea"
integrity sha512-+iZpvVekl6GXZl+GnqL5kvtIhNl05tkcCJ6ajheVmOuzaFV7J9xBk8E3bFqNtFDM+vt4b0ggujW8zrlrZvrwUg==
dependencies:
"@nivo/annotations" "0.65.1"
"@nivo/axes" "0.65.1"
"@nivo/colors" "0.65.1"
"@nivo/legends" "0.65.1"
"@nivo/scales" "0.65.0"
"@nivo/tooltip" "0.65.1"
"@nivo/voronoi" "0.65.1"
"@nivo/annotations" "0.67.0"
"@nivo/axes" "0.67.0"
"@nivo/colors" "0.67.0"
"@nivo/legends" "0.67.0"
"@nivo/scales" "0.67.0"
"@nivo/tooltip" "0.67.0"
"@nivo/voronoi" "0.67.0"
d3-shape "^1.3.5"
react-spring "9.0.0-rc.3"
"@nivo/scales@0.65.0":
version "0.65.0"
resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.65.0.tgz#60eb790680b49d06396c403c08cfbef68fb7e82f"
integrity sha512-0mRyvPJg3LGKkXraH8sQPvDVnLk2kmRNVjJKPRXWZ9ezMJUTfQb802p74/t/lo/AtAyscN7/sIfMsJ5ZJW4jkQ==
"@nivo/scales@0.67.0":
version "0.67.0"
resolved "https://registry.yarnpkg.com/@nivo/scales/-/scales-0.67.0.tgz#8e79e990ebd555bdce7c3dcdd73b8ad46cb0a74a"
integrity sha512-hXDeh3GzjZA3CcvJp14vCp0/QeoYMxVC2dZl55rIJdW200M+ET/ZtJQA2W1mDOGQUxR9z1flFvGJA+EqXhThwQ==
dependencies:
d3-scale "^3.0.0"
d3-time-format "^2.1.3"
lodash "^4.17.11"
"@nivo/tooltip@0.65.1":
version "0.65.1"
resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.65.1.tgz#7b3215e54dbfbaa2b4066b764f74c85137a8a576"
integrity sha512-m08E1ZSoSUS7O8Bi/6ewXM87w+t09ESNobFL6YTRro1vhnBZ2g8MlglIkXBkEP/mYqfyUMzw6DzT/tOaA3Cnlg==
"@nivo/tooltip@0.67.0":
version "0.67.0"
resolved "https://registry.yarnpkg.com/@nivo/tooltip/-/tooltip-0.67.0.tgz#c849ab7a3ee82d88ff6376894a2de5ef266c8e8d"
integrity sha512-Z4h4Ks/fFd7Cl2uSG/trfSYLXaiQNyFX2wGJrE/UPzDSpf4XqaW8uEBaK3p46200WG2AgltG0fHrouGNyMJ+1g==
dependencies:
react-spring "9.0.0-rc.3"
"@nivo/voronoi@0.65.1":
version "0.65.1"
resolved "https://registry.yarnpkg.com/@nivo/voronoi/-/voronoi-0.65.1.tgz#6a3ff0bb4d60ce78e96cfe3052fde622743e3ec5"
integrity sha512-9WUbLS4f9TQ3v3k8fhzKT9U0LOqKtGH4QcYhMNJxzJgsvb1wZavjPFd/CT+RsV7OD3zOWn/Nd7QEqeFLHwsJnA==
"@nivo/voronoi@0.67.0":
version "0.67.0"
resolved "https://registry.yarnpkg.com/@nivo/voronoi/-/voronoi-0.67.0.tgz#07ec873846ec12aa62d1c1884e274ea114b4341b"
integrity sha512-Z+e+fa92xWIhbpSRdQGJtei3bBHTbLnsmXtfje+Xz8LYpkyxp8R/iTs9cqzQ4KILp1Tv6EnQYWrFWNizN3fNGA==
dependencies:
d3-delaunay "^5.1.1"
d3-scale "^3.0.0"
@ -2048,6 +2048,11 @@
dependencies:
source-map "^0.6.1"
"@types/uuid@^8.3.0":
version "8.3.0"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
"@types/webpack-sources@*":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@types/webpack-sources/-/webpack-sources-2.0.0.tgz#08216ab9be2be2e1499beaebc4d469cec81e82a7"
@ -11849,6 +11854,11 @@ uuid@^8.3.0:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache@^2.0.3:
version "2.1.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745"