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:
Vendored
+1
@@ -2,6 +2,7 @@
|
|||||||
"cSpell.words": [
|
"cSpell.words": [
|
||||||
"approximative",
|
"approximative",
|
||||||
"colour",
|
"colour",
|
||||||
|
"colours",
|
||||||
"contactez",
|
"contactez",
|
||||||
"defaulticon",
|
"defaulticon",
|
||||||
"Échecs",
|
"Échecs",
|
||||||
|
|||||||
@@ -13,24 +13,20 @@ import {
|
|||||||
} from "react-leaflet";
|
} from "react-leaflet";
|
||||||
import { FaAngleDoubleDown } from "react-icons/fa";
|
import { FaAngleDoubleDown } from "react-icons/fa";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import { groupBy } from "lodash";
|
import { countBy, groupBy } from "lodash";
|
||||||
|
|
||||||
import "leaflet/dist/leaflet.css";
|
import "leaflet/dist/leaflet.css";
|
||||||
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
import "leaflet-defaulticon-compatibility/dist/leaflet-defaulticon-compatibility.css";
|
||||||
import "@/css/marker-cluster.css";
|
|
||||||
import "leaflet-defaulticon-compatibility";
|
import "leaflet-defaulticon-compatibility";
|
||||||
import "leaflet.smooth_marker_bouncing";
|
import "leaflet.smooth_marker_bouncing";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
mapBoundsAtom,
|
mapBoundsAtom,
|
||||||
debouncedHoveredListTournamentIdAtom,
|
debouncedHoveredListTournamentIdAtom,
|
||||||
tournamentsAtom,
|
filteredTournamentsByTimeControlAtom,
|
||||||
classicAtom,
|
|
||||||
rapidAtom,
|
|
||||||
blitzAtom,
|
|
||||||
otherAtom,
|
|
||||||
normsOnlyAtom,
|
normsOnlyAtom,
|
||||||
} from "@/app/atoms";
|
} from "@/app/atoms";
|
||||||
|
import { pie } from "@/lib/pie";
|
||||||
|
|
||||||
import Legend from "./Legend";
|
import Legend from "./Legend";
|
||||||
import { TournamentMarker } from "./TournamentMarker";
|
import { TournamentMarker } from "./TournamentMarker";
|
||||||
@@ -68,16 +64,11 @@ const MapEvents = () => {
|
|||||||
map.setView(franceBounds.getCenter(), map.getBoundsZoom(franceBounds));
|
map.setView(franceBounds.getCenter(), map.getBoundsZoom(franceBounds));
|
||||||
map.setMaxBounds(worldBounds);
|
map.setMaxBounds(worldBounds);
|
||||||
map.options.maxBoundsViscosity = 1.0; // Prevents going past bounds while dragging
|
map.options.maxBoundsViscosity = 1.0; // Prevents going past bounds while dragging
|
||||||
}, [map]);
|
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TimeControlGroupProps = {
|
|
||||||
timeControl: TimeControl;
|
|
||||||
colour: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopBouncingMarkers = () => {
|
const stopBouncingMarkers = () => {
|
||||||
const markers =
|
const markers =
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -99,27 +90,38 @@ const stopBouncingMarkers = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const TimeControlGroup = ({ timeControl, colour }: TimeControlGroupProps) => {
|
export default function TournamentMap() {
|
||||||
const tournaments = useAtomValue(tournamentsAtom);
|
const tournaments = useAtomValue(filteredTournamentsByTimeControlAtom);
|
||||||
const normsOnly = useAtomValue(normsOnlyAtom);
|
const normsOnly = useAtomValue(normsOnlyAtom);
|
||||||
|
|
||||||
|
const setMapBounds = useSetAtom(mapBoundsAtom);
|
||||||
|
const hoveredListTournamentId = useAtomValue(
|
||||||
|
debouncedHoveredListTournamentIdAtom,
|
||||||
|
);
|
||||||
|
|
||||||
const markerRefs = useRef<Record<string, L.Marker<any> | null>>({});
|
const markerRefs = useRef<Record<string, L.Marker<any> | null>>({});
|
||||||
const clusterRef = useRef<L.MarkerClusterGroup | null>(null);
|
const clusterRef = useRef<L.MarkerClusterGroup | null>(null);
|
||||||
const expandedClusterMarkerRef = useRef<L.MarkerCluster | null>(null);
|
const expandedClusterMarkerRef = useRef<L.MarkerCluster | null>(null);
|
||||||
|
|
||||||
const filteredTournaments = useMemo(
|
const filteredTournaments = useMemo(
|
||||||
() =>
|
() => tournaments.filter((t) => (normsOnly ? t.norm : true)),
|
||||||
tournaments
|
[normsOnly, tournaments],
|
||||||
.filter((t) => t.timeControl === timeControl)
|
|
||||||
.filter((t) => (normsOnly ? t.norm : true)),
|
|
||||||
[normsOnly, timeControl, tournaments],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupedTournaments = groupBy(filteredTournaments, (t) => t.groupId);
|
const groupedTournaments = groupBy(filteredTournaments, (t) => t.groupId);
|
||||||
|
|
||||||
const hoveredListTournamentId = useAtomValue(
|
useEffect(() => {
|
||||||
debouncedHoveredListTournamentIdAtom,
|
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(() => {
|
const expandAndBounceIfNeeded = useCallback(() => {
|
||||||
if (hoveredListTournamentId) {
|
if (hoveredListTournamentId) {
|
||||||
@@ -231,73 +233,73 @@ const TimeControlGroup = ({ timeControl, colour }: TimeControlGroupProps) => {
|
|||||||
expandedClusterMarkerRef.current.unspiderfy();
|
expandedClusterMarkerRef.current.unspiderfy();
|
||||||
}, [expandAndBounceIfNeeded, hoveredListTournamentId]);
|
}, [expandAndBounceIfNeeded, hoveredListTournamentId]);
|
||||||
|
|
||||||
const createClusterCustomIcon = useCallback(
|
const createClusterCustomIcon = useCallback((cluster: any) => {
|
||||||
(cluster: any) => {
|
|
||||||
let childCount = cluster.getChildCount();
|
let childCount = cluster.getChildCount();
|
||||||
|
|
||||||
|
const children = cluster.getAllChildMarkers();
|
||||||
|
|
||||||
|
// 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 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({
|
return new L.DivIcon({
|
||||||
html: "<div><span>" + childCount + "</span></div>",
|
html,
|
||||||
className: `marker-cluster marker-cluster-${timeControl}`,
|
className: "marker-cluster",
|
||||||
iconSize: new L.Point(40, 40),
|
iconSize: new L.Point(40, 40),
|
||||||
});
|
});
|
||||||
},
|
}, []);
|
||||||
[timeControl],
|
|
||||||
);
|
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",
|
||||||
|
};
|
||||||
|
|
||||||
const markers = Object.values(groupedTournaments).map((tournamentGroup) => {
|
|
||||||
return (
|
return (
|
||||||
<TournamentMarker
|
<TournamentMarker
|
||||||
ref={(ref) => (markerRefs.current[tournamentGroup[0].groupId] = ref)}
|
ref={(ref) => (markerRefs.current[groupId] = ref)}
|
||||||
key={tournamentGroup[0].groupId}
|
key={groupId}
|
||||||
tournamentGroup={tournamentGroup}
|
tournamentGroup={tournamentGroup}
|
||||||
colour={colour}
|
colour={colours[timeControl]}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
}),
|
||||||
|
[groupedTournaments],
|
||||||
const group = useMemo(
|
|
||||||
() => (
|
|
||||||
<LayerGroup>
|
|
||||||
<MarkerClusterGroup
|
|
||||||
ref={clusterRef}
|
|
||||||
chunkedLoading
|
|
||||||
iconCreateFunction={createClusterCustomIcon}
|
|
||||||
maxClusterRadius={40}
|
|
||||||
>
|
|
||||||
{markers}
|
|
||||||
</MarkerClusterGroup>
|
|
||||||
</LayerGroup>
|
|
||||||
),
|
|
||||||
[createClusterCustomIcon, markers],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
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 (
|
return (
|
||||||
<section id="tournament-map" className="flex h-content flex-col">
|
<section id="tournament-map" className="flex h-content flex-col">
|
||||||
<div className="p-3 lg:hidden">
|
<div className="p-3 lg:hidden">
|
||||||
@@ -323,19 +325,19 @@ export default function TournamentMap() {
|
|||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
<Legend />
|
<Legend />
|
||||||
|
<LayerGroup>
|
||||||
{classic && (
|
<MarkerClusterGroup
|
||||||
<TimeControlGroup timeControl={TimeControl.Classic} colour="green" />
|
ref={clusterRef}
|
||||||
)}
|
chunkedLoading
|
||||||
{rapid && (
|
iconCreateFunction={createClusterCustomIcon}
|
||||||
<TimeControlGroup timeControl={TimeControl.Rapid} colour="blue" />
|
maxClusterRadius={40}
|
||||||
)}
|
showCoverageOnHover={false}
|
||||||
{blitz && (
|
spiderfyOnMaxZoom
|
||||||
<TimeControlGroup timeControl={TimeControl.Blitz} colour="yellow" />
|
>
|
||||||
)}
|
{markers}
|
||||||
{other && (
|
</MarkerClusterGroup>
|
||||||
<TimeControlGroup timeControl={TimeControl.Other} colour="red" />
|
</LayerGroup>
|
||||||
)}
|
$
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
|
|
||||||
<div className="flex items-center justify-center lg:hidden">
|
<div className="flex items-center justify-center lg:hidden">
|
||||||
|
|||||||
@@ -22,21 +22,7 @@ export const TournamentMarker = forwardRef<
|
|||||||
>(({ tournamentGroup, colour, ...markerProps }, ref) => {
|
>(({ tournamentGroup, colour, ...markerProps }, ref) => {
|
||||||
const t = useTranslations("Tournaments");
|
const t = useTranslations("Tournaments");
|
||||||
|
|
||||||
const baseTournament = tournamentGroup[0];
|
const { date, latLng, groupId, timeControl } = 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 setHoveredMapTournamentGroupId = useSetAtom(
|
const setHoveredMapTournamentGroupId = useSetAtom(
|
||||||
debouncedHoveredMapTournamentGroupIdAtom,
|
debouncedHoveredMapTournamentGroupIdAtom,
|
||||||
@@ -51,23 +37,21 @@ export const TournamentMarker = forwardRef<
|
|||||||
iconAnchor: [12, 41],
|
iconAnchor: [12, 41],
|
||||||
popupAnchor: [1, -34],
|
popupAnchor: [1, -34],
|
||||||
shadowSize: [41, 41],
|
shadowSize: [41, 41],
|
||||||
|
timeControl,
|
||||||
}),
|
}),
|
||||||
[colour],
|
[colour, timeControl],
|
||||||
);
|
);
|
||||||
|
|
||||||
const startDate = baseTournament.date;
|
const startDate = date;
|
||||||
const endDate = last(tournamentGroup)!.date;
|
const endDate = last(tournamentGroup)!.date;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
ref={ref}
|
ref={ref}
|
||||||
position={position.current}
|
position={latLng}
|
||||||
icon={iconOptions}
|
icon={iconOptions}
|
||||||
eventHandlers={{
|
eventHandlers={{
|
||||||
mouseover: () =>
|
mouseover: () => setHoveredMapTournamentGroupId(groupId),
|
||||||
setHoveredMapTournamentGroupId(
|
|
||||||
`${baseTournament.groupId}_${baseTournament.timeControl}`,
|
|
||||||
),
|
|
||||||
mouseout: () => setHoveredMapTournamentGroupId(null),
|
mouseout: () => setHoveredMapTournamentGroupId(null),
|
||||||
}}
|
}}
|
||||||
{...markerProps}
|
{...markerProps}
|
||||||
@@ -75,7 +59,7 @@ export const TournamentMarker = forwardRef<
|
|||||||
<Popup maxWidth={10000}>
|
<Popup maxWidth={10000}>
|
||||||
<div className="flex max-w-[calc(100vw-80px)] flex-col gap-3 lg:max-w-[calc(100vw/2-80px)]">
|
<div className="flex max-w-[calc(100vw-80px)] flex-col gap-3 lg:max-w-[calc(100vw/2-80px)]">
|
||||||
<b>
|
<b>
|
||||||
{baseTournament.date}
|
{date}
|
||||||
{endDate !== startDate && ` - ${endDate}`}
|
{endDate !== startDate && ` - ${endDate}`}
|
||||||
</b>
|
</b>
|
||||||
|
|
||||||
|
|||||||
@@ -121,13 +121,12 @@ export default function TournamentTable() {
|
|||||||
<tr
|
<tr
|
||||||
key={tournament.id}
|
key={tournament.id}
|
||||||
id={tournament.id}
|
id={tournament.id}
|
||||||
data-group-id={`${tournament.groupId}_${tournament.timeControl}`}
|
data-group-id={tournament.groupId}
|
||||||
onMouseEnter={() => setHoveredListTournamentId(tournament.id)}
|
onMouseEnter={() => setHoveredListTournamentId(tournament.id)}
|
||||||
onMouseLeave={() => setHoveredListTournamentId(null)}
|
onMouseLeave={() => setHoveredListTournamentId(null)}
|
||||||
className={twMerge(
|
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",
|
"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 ===
|
hoveredMapTournamentGroupId === tournament.groupId &&
|
||||||
`${tournament.groupId}_${tournament.timeControl}` &&
|
|
||||||
"bg-gray-200 dark:bg-gray-900",
|
"bg-gray-200 dark:bg-gray-900",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -94,9 +94,10 @@ const getTournaments = async () => {
|
|||||||
ranges.some((d) => isSameDay(d, date)),
|
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.
|
// 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 {
|
return {
|
||||||
id: t._id.toString(),
|
id: t._id.toString(),
|
||||||
@@ -106,7 +107,7 @@ const getTournaments = async () => {
|
|||||||
department: t.department,
|
department: t.department,
|
||||||
date: t.date,
|
date: t.date,
|
||||||
url: t.url,
|
url: t.url,
|
||||||
timeControl: tcMap[t.time_control] ?? TimeControl.Other,
|
timeControl,
|
||||||
latLng: { lat: t.coordinates[0], lng: t.coordinates[1] },
|
latLng: { lat: t.coordinates[0], lng: t.coordinates[1] },
|
||||||
norm: t.norm_tournament,
|
norm: t.norm_tournament,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
.marker-cluster-Classic {
|
|
||||||
background-color: rgba(145, 231, 135, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.marker-cluster-Classic div {
|
|
||||||
background-color: rgba(36, 171, 33, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.marker-cluster-Rapid {
|
|
||||||
background-color: rgba(85, 150, 201, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.marker-cluster-Rapid div {
|
|
||||||
background-color: rgba(39, 128, 201, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.marker-cluster-Blitz {
|
|
||||||
background-color: rgba(217, 211, 84, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.marker-cluster-Blitz div {
|
|
||||||
background-color: rgba(202, 196, 39, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.marker-cluster-Other {
|
|
||||||
background-color: rgba(211, 67, 84, 0.6);
|
|
||||||
}
|
|
||||||
|
|
||||||
.marker-cluster-Other div {
|
|
||||||
background-color: rgba(203, 41, 60, 0.6);
|
|
||||||
}
|
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
function round(n: number) {
|
||||||
|
return Math.round(n * 10) / 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
function polarToCartesian(radius: number, angleInDegrees: number) {
|
||||||
|
var radians = (angleInDegrees - 90) * Math.PI / 180;
|
||||||
|
|
||||||
|
return {
|
||||||
|
x: round(radius + (radius * Math.cos(radians))),
|
||||||
|
y: round(radius + (radius * Math.sin(radians)))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDAttribute(radius: number, startAngle: number, endAngle: number) {
|
||||||
|
const isCircle = endAngle - startAngle === 360;
|
||||||
|
|
||||||
|
if (isCircle) {
|
||||||
|
endAngle--;
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = polarToCartesian(radius, startAngle);
|
||||||
|
const end = polarToCartesian(radius, endAngle);
|
||||||
|
|
||||||
|
const largeArcFlag = endAngle - startAngle <= 180 ? 0 : 1;
|
||||||
|
const d = [
|
||||||
|
"M", start.x, start.y,
|
||||||
|
"A", radius, radius, 0, largeArcFlag, 1, end.x, end.y];
|
||||||
|
|
||||||
|
if (isCircle) {
|
||||||
|
d.push("Z");
|
||||||
|
} else {
|
||||||
|
d.push("L", radius, radius, "L", start.x, start.y, "Z");
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function path(d: string, colour: string) {
|
||||||
|
return `<path d='${d}' style="fill: ${colour}" />`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function svg(className: string, width: number, content: string ) {
|
||||||
|
return `<svg class="${className}" viewBox="0 0 ${width} ${width}"><g class='pie'>${content}</g></svg>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PieSector = {
|
||||||
|
value: number;
|
||||||
|
colour: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pie(className: string, radius: number, values: PieSector[]) {
|
||||||
|
|
||||||
|
type Sector = {
|
||||||
|
colour: string;
|
||||||
|
degrees: number;
|
||||||
|
from?: number;
|
||||||
|
to?: number;
|
||||||
|
path?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = values.reduce((a, b) => a + b.value, 0);
|
||||||
|
const data: Sector[] = values.map(({ value, colour }) => ({ colour, degrees: value / total * 360 }));
|
||||||
|
|
||||||
|
data.forEach((value, i, arr) => {
|
||||||
|
if (i === 0) {
|
||||||
|
value.from = 0;
|
||||||
|
value.to = value.degrees;
|
||||||
|
} else {
|
||||||
|
value.from = arr[i - 1].to!;
|
||||||
|
value.to = value.from + value.degrees;
|
||||||
|
}
|
||||||
|
|
||||||
|
value.path = path(getDAttribute(radius, value.from, value.to ), value.colour);
|
||||||
|
});
|
||||||
|
|
||||||
|
return svg(className, radius * 2, data.map(o => o.path).join(''));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user