mirror of
https://github.com/TheRealOwenRees/echecsfrance.git
synced 2026-07-23 04:26:57 +00:00
Pie charts for clusters
This commit is contained in:
@@ -13,24 +13,20 @@ import {
|
||||
} from "react-leaflet";
|
||||
import { FaAngleDoubleDown } from "react-icons/fa";
|
||||
import { useAtomValue, useSetAtom } from "jotai";
|
||||
import { groupBy } from "lodash";
|
||||
import { countBy, groupBy } from "lodash";
|
||||
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
||||
import "@/css/marker-cluster.css";
|
||||
import "leaflet-defaulticon-compatibility";
|
||||
import "leaflet.smooth_marker_bouncing";
|
||||
|
||||
import {
|
||||
mapBoundsAtom,
|
||||
debouncedHoveredListTournamentIdAtom,
|
||||
tournamentsAtom,
|
||||
classicAtom,
|
||||
rapidAtom,
|
||||
blitzAtom,
|
||||
otherAtom,
|
||||
filteredTournamentsByTimeControlAtom,
|
||||
normsOnlyAtom,
|
||||
} from "@/app/atoms";
|
||||
import { pie } from "@/lib/pie";
|
||||
|
||||
import Legend from "./Legend";
|
||||
import { TournamentMarker } from "./TournamentMarker";
|
||||
@@ -68,16 +64,11 @@ const MapEvents = () => {
|
||||
map.setView(franceBounds.getCenter(), map.getBoundsZoom(franceBounds));
|
||||
map.setMaxBounds(worldBounds);
|
||||
map.options.maxBoundsViscosity = 1.0; // Prevents going past bounds while dragging
|
||||
}, [map]);
|
||||
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
type TimeControlGroupProps = {
|
||||
timeControl: TimeControl;
|
||||
colour: string;
|
||||
};
|
||||
|
||||
const stopBouncingMarkers = () => {
|
||||
const markers =
|
||||
// @ts-ignore
|
||||
@@ -99,27 +90,38 @@ const stopBouncingMarkers = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const TimeControlGroup = ({ timeControl, colour }: TimeControlGroupProps) => {
|
||||
const tournaments = useAtomValue(tournamentsAtom);
|
||||
export default function TournamentMap() {
|
||||
const tournaments = useAtomValue(filteredTournamentsByTimeControlAtom);
|
||||
const normsOnly = useAtomValue(normsOnlyAtom);
|
||||
|
||||
const setMapBounds = useSetAtom(mapBoundsAtom);
|
||||
const hoveredListTournamentId = useAtomValue(
|
||||
debouncedHoveredListTournamentIdAtom,
|
||||
);
|
||||
|
||||
const markerRefs = useRef<Record<string, L.Marker<any> | null>>({});
|
||||
const clusterRef = useRef<L.MarkerClusterGroup | null>(null);
|
||||
const expandedClusterMarkerRef = useRef<L.MarkerCluster | null>(null);
|
||||
|
||||
const filteredTournaments = useMemo(
|
||||
() =>
|
||||
tournaments
|
||||
.filter((t) => t.timeControl === timeControl)
|
||||
.filter((t) => (normsOnly ? t.norm : true)),
|
||||
[normsOnly, timeControl, tournaments],
|
||||
() => tournaments.filter((t) => (normsOnly ? t.norm : true)),
|
||||
[normsOnly, tournaments],
|
||||
);
|
||||
|
||||
const groupedTournaments = groupBy(filteredTournaments, (t) => t.groupId);
|
||||
|
||||
const hoveredListTournamentId = useAtomValue(
|
||||
debouncedHoveredListTournamentIdAtom,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (hoveredListTournamentId === null) {
|
||||
stopBouncingMarkers();
|
||||
}
|
||||
}, [hoveredListTournamentId]);
|
||||
|
||||
const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 };
|
||||
|
||||
const onScrollToTable = () => {
|
||||
const tournamentTable = document.getElementById("tournament-table");
|
||||
tournamentTable?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
const expandAndBounceIfNeeded = useCallback(() => {
|
||||
if (hoveredListTournamentId) {
|
||||
@@ -231,73 +233,73 @@ const TimeControlGroup = ({ timeControl, colour }: TimeControlGroupProps) => {
|
||||
expandedClusterMarkerRef.current.unspiderfy();
|
||||
}, [expandAndBounceIfNeeded, hoveredListTournamentId]);
|
||||
|
||||
const createClusterCustomIcon = useCallback(
|
||||
(cluster: any) => {
|
||||
let childCount = cluster.getChildCount();
|
||||
const createClusterCustomIcon = useCallback((cluster: any) => {
|
||||
let childCount = cluster.getChildCount();
|
||||
|
||||
return new L.DivIcon({
|
||||
html: "<div><span>" + childCount + "</span></div>",
|
||||
className: `marker-cluster marker-cluster-${timeControl}`,
|
||||
iconSize: new L.Point(40, 40),
|
||||
});
|
||||
},
|
||||
[timeControl],
|
||||
);
|
||||
const children = cluster.getAllChildMarkers();
|
||||
|
||||
const markers = Object.values(groupedTournaments).map((tournamentGroup) => {
|
||||
return (
|
||||
<TournamentMarker
|
||||
ref={(ref) => (markerRefs.current[tournamentGroup[0].groupId] = ref)}
|
||||
key={tournamentGroup[0].groupId}
|
||||
tournamentGroup={tournamentGroup}
|
||||
colour={colour}
|
||||
/>
|
||||
// We added the time control to the icon options when creating the marker
|
||||
const timeControlCounts = countBy(
|
||||
children,
|
||||
(child: any) => child.options.icon.options.timeControl,
|
||||
);
|
||||
});
|
||||
|
||||
const group = useMemo(
|
||||
() => (
|
||||
<LayerGroup>
|
||||
<MarkerClusterGroup
|
||||
ref={clusterRef}
|
||||
chunkedLoading
|
||||
iconCreateFunction={createClusterCustomIcon}
|
||||
maxClusterRadius={40}
|
||||
>
|
||||
{markers}
|
||||
</MarkerClusterGroup>
|
||||
</LayerGroup>
|
||||
),
|
||||
[createClusterCustomIcon, markers],
|
||||
const html = `
|
||||
<div>
|
||||
|
||||
${pie("absolute w-[30px] -z-10", 15, [
|
||||
{
|
||||
value: timeControlCounts[TimeControl.Classic] ?? 0,
|
||||
colour: "#00ac39",
|
||||
},
|
||||
{
|
||||
value: timeControlCounts[TimeControl.Rapid] ?? 0,
|
||||
colour: "#0086c7",
|
||||
},
|
||||
{
|
||||
value: timeControlCounts[TimeControl.Blitz] ?? 0,
|
||||
colour: "#cec348",
|
||||
},
|
||||
{
|
||||
value: timeControlCounts[TimeControl.Other] ?? 0,
|
||||
colour: "#d10c3e",
|
||||
},
|
||||
])}
|
||||
<span class="text-white font-semibold relative z-[300]">${childCount}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return new L.DivIcon({
|
||||
html,
|
||||
className: "marker-cluster",
|
||||
iconSize: new L.Point(40, 40),
|
||||
});
|
||||
}, []);
|
||||
|
||||
const markers = useMemo(
|
||||
() =>
|
||||
Object.values(groupedTournaments).map((tournamentGroup) => {
|
||||
const { groupId, timeControl } = tournamentGroup[0];
|
||||
|
||||
const colours = {
|
||||
[TimeControl.Classic]: "green",
|
||||
[TimeControl.Rapid]: "blue",
|
||||
[TimeControl.Blitz]: "yellow",
|
||||
[TimeControl.Other]: "red",
|
||||
};
|
||||
|
||||
return (
|
||||
<TournamentMarker
|
||||
ref={(ref) => (markerRefs.current[groupId] = ref)}
|
||||
key={groupId}
|
||||
tournamentGroup={tournamentGroup}
|
||||
colour={colours[timeControl]}
|
||||
/>
|
||||
);
|
||||
}),
|
||||
[groupedTournaments],
|
||||
);
|
||||
|
||||
return group;
|
||||
};
|
||||
|
||||
export default function TournamentMap() {
|
||||
const setMapBounds = useSetAtom(mapBoundsAtom);
|
||||
const hoveredListTournamentId = useAtomValue(
|
||||
debouncedHoveredListTournamentIdAtom,
|
||||
);
|
||||
|
||||
const classic = useAtomValue(classicAtom);
|
||||
const rapid = useAtomValue(rapidAtom);
|
||||
const blitz = useAtomValue(blitzAtom);
|
||||
const other = useAtomValue(otherAtom);
|
||||
|
||||
useEffect(() => {
|
||||
if (hoveredListTournamentId === null) {
|
||||
stopBouncingMarkers();
|
||||
}
|
||||
}, [hoveredListTournamentId]);
|
||||
|
||||
const center: LatLngLiteral = { lat: 47.0844, lng: 2.3964 };
|
||||
|
||||
const onScrollToTable = () => {
|
||||
const tournamentTable = document.getElementById("tournament-table");
|
||||
tournamentTable?.scrollIntoView({ behavior: "smooth" });
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="tournament-map" className="flex h-content flex-col">
|
||||
<div className="p-3 lg:hidden">
|
||||
@@ -323,19 +325,19 @@ export default function TournamentMap() {
|
||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||
/>
|
||||
<Legend />
|
||||
|
||||
{classic && (
|
||||
<TimeControlGroup timeControl={TimeControl.Classic} colour="green" />
|
||||
)}
|
||||
{rapid && (
|
||||
<TimeControlGroup timeControl={TimeControl.Rapid} colour="blue" />
|
||||
)}
|
||||
{blitz && (
|
||||
<TimeControlGroup timeControl={TimeControl.Blitz} colour="yellow" />
|
||||
)}
|
||||
{other && (
|
||||
<TimeControlGroup timeControl={TimeControl.Other} colour="red" />
|
||||
)}
|
||||
<LayerGroup>
|
||||
<MarkerClusterGroup
|
||||
ref={clusterRef}
|
||||
chunkedLoading
|
||||
iconCreateFunction={createClusterCustomIcon}
|
||||
maxClusterRadius={40}
|
||||
showCoverageOnHover={false}
|
||||
spiderfyOnMaxZoom
|
||||
>
|
||||
{markers}
|
||||
</MarkerClusterGroup>
|
||||
</LayerGroup>
|
||||
$
|
||||
</MapContainer>
|
||||
|
||||
<div className="flex items-center justify-center lg:hidden">
|
||||
|
||||
@@ -22,21 +22,7 @@ export const TournamentMarker = forwardRef<
|
||||
>(({ tournamentGroup, colour, ...markerProps }, ref) => {
|
||||
const t = useTranslations("Tournaments");
|
||||
|
||||
const baseTournament = tournamentGroup[0];
|
||||
|
||||
// We add shifts based on the time control, so that they don't hide each other
|
||||
const position = useRef({
|
||||
lat: baseTournament.latLng.lat,
|
||||
lng:
|
||||
baseTournament.latLng.lng +
|
||||
(baseTournament.timeControl === TimeControl.Rapid
|
||||
? -0.01
|
||||
: baseTournament.timeControl === TimeControl.Blitz
|
||||
? 0.01
|
||||
: baseTournament.timeControl === TimeControl.Other
|
||||
? 0.02
|
||||
: 0),
|
||||
});
|
||||
const { date, latLng, groupId, timeControl } = tournamentGroup[0];
|
||||
|
||||
const setHoveredMapTournamentGroupId = useSetAtom(
|
||||
debouncedHoveredMapTournamentGroupIdAtom,
|
||||
@@ -51,23 +37,21 @@ export const TournamentMarker = forwardRef<
|
||||
iconAnchor: [12, 41],
|
||||
popupAnchor: [1, -34],
|
||||
shadowSize: [41, 41],
|
||||
timeControl,
|
||||
}),
|
||||
[colour],
|
||||
[colour, timeControl],
|
||||
);
|
||||
|
||||
const startDate = baseTournament.date;
|
||||
const startDate = date;
|
||||
const endDate = last(tournamentGroup)!.date;
|
||||
|
||||
return (
|
||||
<Marker
|
||||
ref={ref}
|
||||
position={position.current}
|
||||
position={latLng}
|
||||
icon={iconOptions}
|
||||
eventHandlers={{
|
||||
mouseover: () =>
|
||||
setHoveredMapTournamentGroupId(
|
||||
`${baseTournament.groupId}_${baseTournament.timeControl}`,
|
||||
),
|
||||
mouseover: () => setHoveredMapTournamentGroupId(groupId),
|
||||
mouseout: () => setHoveredMapTournamentGroupId(null),
|
||||
}}
|
||||
{...markerProps}
|
||||
@@ -75,7 +59,7 @@ export const TournamentMarker = forwardRef<
|
||||
<Popup maxWidth={10000}>
|
||||
<div className="flex max-w-[calc(100vw-80px)] flex-col gap-3 lg:max-w-[calc(100vw/2-80px)]">
|
||||
<b>
|
||||
{baseTournament.date}
|
||||
{date}
|
||||
{endDate !== startDate && ` - ${endDate}`}
|
||||
</b>
|
||||
|
||||
|
||||
@@ -121,13 +121,12 @@ export default function TournamentTable() {
|
||||
<tr
|
||||
key={tournament.id}
|
||||
id={tournament.id}
|
||||
data-group-id={`${tournament.groupId}_${tournament.timeControl}`}
|
||||
data-group-id={tournament.groupId}
|
||||
onMouseEnter={() => setHoveredListTournamentId(tournament.id)}
|
||||
onMouseLeave={() => setHoveredListTournamentId(null)}
|
||||
className={twMerge(
|
||||
"scroll-m-20 bg-white text-gray-900 hover:bg-gray-200 dark:bg-gray-800 dark:text-white dark:hover:bg-gray-900",
|
||||
hoveredMapTournamentGroupId ===
|
||||
`${tournament.groupId}_${tournament.timeControl}` &&
|
||||
hoveredMapTournamentGroupId === tournament.groupId &&
|
||||
"bg-gray-200 dark:bg-gray-900",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -94,9 +94,10 @@ const getTournaments = async () => {
|
||||
ranges.some((d) => isSameDay(d, date)),
|
||||
);
|
||||
|
||||
// We place each tournament into a group based on location and date, so that
|
||||
// We place each tournament into a group based on location and date and time control, so that
|
||||
// we can display a single map marker.
|
||||
const groupId = `${location}_${rangeIndex}`;
|
||||
const timeControl = tcMap[t.time_control] ?? TimeControl.Other;
|
||||
const groupId = `${location}_${rangeIndex}_${timeControl}`;
|
||||
|
||||
return {
|
||||
id: t._id.toString(),
|
||||
@@ -106,7 +107,7 @@ const getTournaments = async () => {
|
||||
department: t.department,
|
||||
date: t.date,
|
||||
url: t.url,
|
||||
timeControl: tcMap[t.time_control] ?? TimeControl.Other,
|
||||
timeControl,
|
||||
latLng: { lat: t.coordinates[0], lng: t.coordinates[1] },
|
||||
norm: t.norm_tournament,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user