359 lines
11 KiB
TypeScript
359 lines
11 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||
import * as echarts from "echarts";
|
||
import indonesiaGeo from "../../public/assets/geo/indonesia.json";
|
||
import { getStatisticForMaps } from "@/services/article";
|
||
import { convertDateFormatNoTimeV2 } from "@/utils/global";
|
||
import {
|
||
parseDate,
|
||
getLocalTimeZone,
|
||
parseZonedDateTime,
|
||
parseAbsoluteToLocal,
|
||
} from "@internationalized/date";
|
||
import {
|
||
Button,
|
||
DateRangePicker,
|
||
Popover,
|
||
PopoverContent,
|
||
PopoverTrigger,
|
||
Select,
|
||
SelectItem,
|
||
} from "@heroui/react";
|
||
import { format } from "date-fns";
|
||
import { ChevronLeftIcon, ChevronRightIcon } from "../icons";
|
||
const months = [
|
||
"Jan",
|
||
"Feb",
|
||
"Mar",
|
||
"Apr",
|
||
"May",
|
||
"Jun",
|
||
"Jul",
|
||
"Aug",
|
||
"Sep",
|
||
"Oct",
|
||
"Nov",
|
||
"Dec",
|
||
];
|
||
|
||
const provinces = [
|
||
{ engName: "ACEH", inName: "ACEH" },
|
||
{ engName: "NORTH SUMATRA", inName: "SUMATERA UTARA" },
|
||
{ engName: "WEST SUMATRA", inName: "SUMATERA BARAT" },
|
||
{ engName: "RIAU", inName: "RIAU" },
|
||
{ engName: "JAMBI", inName: "JAMBI" },
|
||
{ engName: "SOUTH SUMATRA", inName: "SUMATERA SELATAN" },
|
||
{ engName: "BENGKULU", inName: "BENGKULU" },
|
||
{ engName: "LAMPUNG", inName: "LAMPUNG" },
|
||
{ engName: "BANGKA–BELITUNG ISLANDS", inName: "KEPULAUAN BANGKA BELITUNG" },
|
||
{ engName: "RIAU ISLANDS", inName: "KEPULAUAN RIAU" },
|
||
{ engName: "JAKARTA", inName: "DKI JAKARTA" },
|
||
{ engName: "WEST JAVA", inName: "JAWA BARAT" },
|
||
{ engName: "CENTRAL JAVA", inName: "JAWA TENGAH" },
|
||
{ engName: "YOGYAKARTA", inName: "DI YOGYAKARTA" },
|
||
{ engName: "EAST JAVA", inName: "JAWA TIMUR" },
|
||
{ engName: "BANTEN", inName: "BANTEN" },
|
||
{ engName: "BALI", inName: "BALI" },
|
||
{ engName: "WEST NUSA TENGGARA", inName: "NUSA TENGGARA BARAT" },
|
||
{ engName: "EAST NUSA TENGGARA", inName: "NUSA TENGGARA TIMUR" },
|
||
{ engName: "WEST KALIMANTAN", inName: "KALIMANTAN BARAT" },
|
||
{ engName: "CENTRAL KALIMANTAN", inName: "KALIMANTAN TENGAH" },
|
||
{ engName: "SOUTH KALIMANTAN", inName: "KALIMANTAN SELATAN" },
|
||
{ engName: "EAST KALIMANTAN", inName: "KALIMANTAN TIMUR" },
|
||
{ engName: "NORTH KALIMANTAN", inName: "KALIMANTAN UTARA" },
|
||
{ engName: "NORTH SULAWESI", inName: "SULAWESI UTARA" },
|
||
{ engName: "CENTRAL SULAWESI", inName: "SULAWESI TENGAH" },
|
||
{ engName: "SOUTH SULAWESI", inName: "SULAWESI SELATAN" },
|
||
{ engName: "SOUTHEAST SULAWESI", inName: "SULAWESI TENGGARA" },
|
||
{ engName: "GORONTALO", inName: "GORONTALO" },
|
||
{ engName: "WEST SULAWESI", inName: "SULAWESI BARAT" },
|
||
{ engName: "MALUKU", inName: "MALUKU" },
|
||
{ engName: "NORTH MALUKU", inName: "MALUKU UTARA" },
|
||
{ engName: "PAPUA", inName: "PAPUA" },
|
||
{ engName: "WEST PAPUA", inName: "PAPUA BARAT" },
|
||
{ engName: "SOUTH PAPUA", inName: "PAPUA SELATAN" },
|
||
{ engName: "CENTRAL PAPUA", inName: "PAPUA TENGAH" },
|
||
{ engName: "HIGHLAND PAPUA", inName: "PAPUA PEGUNUNGAN" },
|
||
{ engName: "SOUTHWEST PAPUA", inName: "PAPUA BARAT DAYA" },
|
||
];
|
||
|
||
const IndonesiaMap = () => {
|
||
const chartRef = useRef<HTMLDivElement>(null);
|
||
const myChart = useRef<echarts.EChartsType | null>(null);
|
||
const [selectedProvince, setSelectedProvince] = useState("");
|
||
|
||
const selectedProvinceRef = useRef<string | null>(null);
|
||
const chartOptionRef = useRef<any>(null);
|
||
const today = new Date();
|
||
|
||
const [typeDate, setTypeDate] = useState("daily");
|
||
const [year, setYear] = useState(today.getFullYear());
|
||
const [selectedMonth, setSelectedMonth] = useState<Date | null>(today);
|
||
const [viewsDailyDate, setViewsDailyDate] = useState<any>({
|
||
start: parseDate(
|
||
convertDateFormatNoTimeV2(
|
||
new Date(new Date().setDate(new Date().getDate() - 7))
|
||
)
|
||
),
|
||
end: parseDate(convertDateFormatNoTimeV2(today)),
|
||
});
|
||
|
||
const handleMonthClick = (monthIndex: number) => {
|
||
setSelectedMonth(new Date(year, monthIndex, 1));
|
||
};
|
||
const [minMax, setMinMax] = useState({ min: 0, max: 0 });
|
||
const [data, setData] = useState<{ name: string; value: number }[]>([]);
|
||
|
||
useEffect(() => {
|
||
fetchData();
|
||
}, [viewsDailyDate]);
|
||
|
||
const fetchData = async () => {
|
||
const getDate = (data: any) => {
|
||
return `${data.year}-${data.month < 10 ? `0${data.month}` : data.month}-${
|
||
data.day < 10 ? `0${data.day}` : data.day
|
||
}`;
|
||
};
|
||
|
||
const findRegion = (name: string): string => {
|
||
const find = provinces.find((a) => a.engName == name);
|
||
return find ? find.inName : name;
|
||
};
|
||
|
||
const res = await getStatisticForMaps(
|
||
getDate(viewsDailyDate.start),
|
||
getDate(viewsDailyDate.end)
|
||
);
|
||
if (res?.data?.data) {
|
||
const temp = [];
|
||
let min = Infinity;
|
||
let max = -Infinity;
|
||
for (const element of res?.data?.data) {
|
||
const value = element.totalVisitor;
|
||
|
||
const now = {
|
||
name: findRegion(element.regionName.toUpperCase()),
|
||
value,
|
||
};
|
||
|
||
if (value < min) min = value;
|
||
if (value > max) max = value;
|
||
temp.push(now);
|
||
}
|
||
setData(temp);
|
||
setMinMax({ min: min, max: max });
|
||
} else {
|
||
setData([]);
|
||
setMinMax({ min: 0, max: 0 });
|
||
}
|
||
};
|
||
|
||
const option = useMemo(
|
||
() => ({
|
||
backgroundColor: "#d3d3d3",
|
||
title: {
|
||
text: "Visitor Distribution",
|
||
left: "center",
|
||
textStyle: { color: "#000" },
|
||
},
|
||
visualMap: {
|
||
min: minMax.min,
|
||
max: minMax.max,
|
||
show: true,
|
||
seriesIndex: 0,
|
||
inRange: {
|
||
color: ["#ff0000", "#008000"],
|
||
},
|
||
},
|
||
series: [
|
||
{
|
||
type: "map",
|
||
map: "indonesia",
|
||
roam: true,
|
||
zoom: 1.2,
|
||
center: [118, -2],
|
||
label: {
|
||
show: false,
|
||
color: "#000",
|
||
},
|
||
itemStyle: {
|
||
borderColor: "#999",
|
||
},
|
||
emphasis: {
|
||
label: {
|
||
show: true,
|
||
color: "#000",
|
||
},
|
||
itemStyle: {
|
||
areaColor: "#E2E8F0",
|
||
},
|
||
},
|
||
data: data,
|
||
},
|
||
],
|
||
}),
|
||
[data, minMax]
|
||
);
|
||
|
||
useEffect(() => {
|
||
if (!chartRef.current) return;
|
||
(window as any).echarts = echarts;
|
||
|
||
const fixedGeo = {
|
||
...indonesiaGeo,
|
||
features: indonesiaGeo.features.map((f) => ({
|
||
...f,
|
||
properties: {
|
||
...f.properties,
|
||
name: f.properties.PROVINSI,
|
||
},
|
||
})),
|
||
};
|
||
|
||
echarts.registerMap("indonesia", fixedGeo as any);
|
||
|
||
const chart = echarts.init(chartRef.current);
|
||
myChart.current = chart;
|
||
|
||
chart.on("click", (params: any) => {
|
||
highlightProvince(params.name);
|
||
});
|
||
chartOptionRef.current = option;
|
||
chart.setOption(option);
|
||
|
||
return () => {
|
||
chart.dispose();
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!myChart.current) return;
|
||
|
||
chartOptionRef.current = option;
|
||
myChart.current.setOption(option);
|
||
}, [option]);
|
||
|
||
const highlightProvince = (name: string) => {
|
||
if (!myChart.current || !chartOptionRef.current) return;
|
||
|
||
myChart.current.clear();
|
||
myChart.current.setOption(chartOptionRef.current);
|
||
|
||
myChart.current.dispatchAction({
|
||
type: "highlight",
|
||
name,
|
||
});
|
||
|
||
myChart.current.dispatchAction({
|
||
type: "showTip",
|
||
name,
|
||
});
|
||
|
||
selectedProvinceRef.current = name;
|
||
setSelectedProvince(name);
|
||
};
|
||
|
||
return (
|
||
<div className="flex flex-col lg:flex-row gap-6 w-full pr-2 md:pr-0">
|
||
<div className="w-screen md:w-[70%] h-[360px] lg:h-[600px]">
|
||
<div ref={chartRef} style={{ height: "100%", width: "100%" }} />
|
||
</div>
|
||
<div className="w-screen md:w-[30%] h-[360px] lg:h-[600px] overflow-auto flex flex-col">
|
||
<div className="flex flex-col lg:flex-row gap-2">
|
||
{/* <Select
|
||
className="w-full md:w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px]"
|
||
label=""
|
||
labelPlacement="outside"
|
||
selectedKeys={[typeDate]}
|
||
onChange={(e) =>
|
||
e.target.value !== "" && setTypeDate(e.target.value)
|
||
}
|
||
>
|
||
<SelectItem key="monthly">Bulanan</SelectItem>
|
||
<SelectItem key="daily">Harian</SelectItem>
|
||
</Select> */}
|
||
|
||
{typeDate === "monthly" ? (
|
||
<Popover placement="bottom" showArrow={true} className="w-full">
|
||
<PopoverTrigger>
|
||
<Button className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg">
|
||
{" "}
|
||
{selectedMonth
|
||
? format(selectedMonth, "MMMM yyyy")
|
||
: "Pilih Bulan"}
|
||
</Button>
|
||
</PopoverTrigger>
|
||
<PopoverContent className="p-4 w-[200px]">
|
||
<div className="flex items-center justify-between mb-2 px-1 w-full">
|
||
<button
|
||
className="text-gray-500 hover:text-black"
|
||
onClick={() => setYear((prev) => prev - 1)}
|
||
>
|
||
<ChevronLeftIcon />
|
||
</button>
|
||
<span className="font-semibold text-center">{year}</span>
|
||
<button
|
||
className="text-gray-500 hover:text-black"
|
||
onClick={() => setYear((prev) => prev + 1)}
|
||
>
|
||
<ChevronRightIcon />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="grid grid-cols-3 gap-2 w-full">
|
||
{months.map((month, idx) => (
|
||
<button
|
||
key={idx}
|
||
onClick={() => handleMonthClick(idx)}
|
||
className={`py-1 rounded-md text-sm transition-colors ${
|
||
selectedMonth &&
|
||
selectedMonth.getMonth() === idx &&
|
||
selectedMonth.getFullYear() === year
|
||
? "bg-blue-500 text-white"
|
||
: "hover:bg-gray-200 text-gray-700"
|
||
}`}
|
||
>
|
||
{month}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</PopoverContent>
|
||
</Popover>
|
||
) : (
|
||
<div className="w-full mb-3">
|
||
<DateRangePicker
|
||
className="h-[40px] self-end"
|
||
value={viewsDailyDate}
|
||
onChange={(e) => e !== null && setViewsDailyDate(e)}
|
||
label=""
|
||
/>
|
||
</div>
|
||
)}
|
||
</div>
|
||
{data?.length > 0 &&
|
||
data?.map((list: any) => (
|
||
<a
|
||
key={list.name}
|
||
onClick={() => highlightProvince(list.name)}
|
||
className={`w-full flex flex-row cursor-pointer gap-2 ${
|
||
selectedProvince === list.name ? "bg-slate-300" : ""
|
||
}`}
|
||
>
|
||
<p className="w-4/5">{list.name}</p>{" "}
|
||
<p
|
||
className={`w-1/5 ${
|
||
selectedProvince === list.name
|
||
? "bg-slate-300"
|
||
: "bg-[#a9d7e4]"
|
||
} text-black border-white text-center`}
|
||
>
|
||
{list.value}
|
||
</p>
|
||
</a>
|
||
))}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default IndonesiaMap;
|