2025-06-28 16:39:22 +00:00
|
|
|
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
2025-06-23 15:51:20 +00:00
|
|
|
|
import * as echarts from "echarts";
|
|
|
|
|
|
import indonesiaGeo from "../../public/assets/geo/indonesia.json";
|
2025-06-28 05:14:54 +00:00
|
|
|
|
import { getStatisticForMaps } from "@/services/article";
|
|
|
|
|
|
import { convertDateFormatNoTimeV2 } from "@/utils/global";
|
|
|
|
|
|
import {
|
|
|
|
|
|
parseDate,
|
|
|
|
|
|
getLocalTimeZone,
|
|
|
|
|
|
parseZonedDateTime,
|
|
|
|
|
|
parseAbsoluteToLocal,
|
|
|
|
|
|
} from "@internationalized/date";
|
2025-06-28 16:39:22 +00:00
|
|
|
|
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",
|
|
|
|
|
|
];
|
2025-06-23 15:51:20 +00:00
|
|
|
|
|
2025-07-02 02:02:34 +00:00
|
|
|
|
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" },
|
2025-07-02 02:17:22 +00:00
|
|
|
|
{ engName: "BANGKA–BELITUNG ISLANDS", inName: "KEPULAUAN BANGKA BELITUNG" },
|
2025-07-02 02:02:34 +00:00
|
|
|
|
{ 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" },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2025-06-28 16:39:22 +00:00
|
|
|
|
const IndonesiaMap = () => {
|
2025-06-23 15:51:20 +00:00
|
|
|
|
const chartRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
const myChart = useRef<echarts.EChartsType | null>(null);
|
2025-06-26 06:01:49 +00:00
|
|
|
|
const [selectedProvince, setSelectedProvince] = useState("");
|
|
|
|
|
|
|
|
|
|
|
|
const selectedProvinceRef = useRef<string | null>(null);
|
|
|
|
|
|
const chartOptionRef = useRef<any>(null);
|
2025-06-28 05:14:54 +00:00
|
|
|
|
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({
|
|
|
|
|
|
start: parseDate(
|
|
|
|
|
|
convertDateFormatNoTimeV2(
|
|
|
|
|
|
new Date(new Date().setDate(new Date().getDate() - 7))
|
|
|
|
|
|
)
|
|
|
|
|
|
),
|
|
|
|
|
|
end: parseDate(convertDateFormatNoTimeV2(today)),
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-06-28 16:39:22 +00:00
|
|
|
|
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 }[]>([]);
|
2025-06-28 05:14:54 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
fetchData();
|
2025-06-28 16:39:22 +00:00
|
|
|
|
}, [viewsDailyDate]);
|
2025-06-28 05:14:54 +00:00
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
}`;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-07-02 02:02:34 +00:00
|
|
|
|
const findRegion = (name: string): string => {
|
2025-07-02 02:17:22 +00:00
|
|
|
|
const find = provinces.find((a) => a.engName == name);
|
|
|
|
|
|
return find ? find.inName : name;
|
2025-07-02 02:02:34 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-06-28 05:14:54 +00:00
|
|
|
|
const res = await getStatisticForMaps(
|
|
|
|
|
|
getDate(viewsDailyDate.start),
|
|
|
|
|
|
getDate(viewsDailyDate.end)
|
|
|
|
|
|
);
|
2025-06-28 16:39:22 +00:00
|
|
|
|
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 = {
|
2025-07-02 02:02:34 +00:00
|
|
|
|
name: findRegion(element.regionName.toUpperCase()),
|
2025-06-28 16:39:22 +00:00
|
|
|
|
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 });
|
|
|
|
|
|
}
|
2025-06-28 05:14:54 +00:00
|
|
|
|
};
|
2025-06-26 06:01:49 +00:00
|
|
|
|
|
2025-06-28 16:39:22 +00:00
|
|
|
|
const option = useMemo(
|
|
|
|
|
|
() => ({
|
|
|
|
|
|
backgroundColor: "#d3d3d3",
|
|
|
|
|
|
title: {
|
2025-06-30 01:24:18 +00:00
|
|
|
|
text: "Visitor Distribution",
|
2025-06-28 16:39:22 +00:00
|
|
|
|
left: "center",
|
|
|
|
|
|
textStyle: { color: "#000" },
|
2025-06-26 06:01:49 +00:00
|
|
|
|
},
|
2025-06-28 16:39:22 +00:00
|
|
|
|
visualMap: {
|
|
|
|
|
|
min: minMax.min,
|
|
|
|
|
|
max: minMax.max,
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
seriesIndex: 0,
|
|
|
|
|
|
inRange: {
|
2025-06-29 13:11:21 +00:00
|
|
|
|
color: ["#ff0000", "#008000"],
|
2025-06-26 06:01:49 +00:00
|
|
|
|
},
|
2025-06-28 16:39:22 +00:00
|
|
|
|
},
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{
|
|
|
|
|
|
type: "map",
|
|
|
|
|
|
map: "indonesia",
|
|
|
|
|
|
roam: true,
|
|
|
|
|
|
zoom: 1.2,
|
|
|
|
|
|
center: [118, -2],
|
2025-06-26 06:01:49 +00:00
|
|
|
|
label: {
|
2025-06-28 16:39:22 +00:00
|
|
|
|
show: false,
|
2025-06-26 06:01:49 +00:00
|
|
|
|
color: "#000",
|
|
|
|
|
|
},
|
|
|
|
|
|
itemStyle: {
|
2025-06-28 16:39:22 +00:00
|
|
|
|
borderColor: "#999",
|
2025-06-26 06:01:49 +00:00
|
|
|
|
},
|
2025-06-28 16:39:22 +00:00
|
|
|
|
emphasis: {
|
|
|
|
|
|
label: {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
color: "#000",
|
|
|
|
|
|
},
|
|
|
|
|
|
itemStyle: {
|
2025-06-29 13:11:21 +00:00
|
|
|
|
areaColor: "#E2E8F0",
|
2025-06-28 16:39:22 +00:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
data: data,
|
2025-06-26 06:01:49 +00:00
|
|
|
|
},
|
2025-06-28 16:39:22 +00:00
|
|
|
|
],
|
|
|
|
|
|
}),
|
|
|
|
|
|
[data, minMax]
|
|
|
|
|
|
);
|
2025-06-23 15:51:20 +00:00
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!chartRef.current) return;
|
|
|
|
|
|
(window as any).echarts = echarts;
|
|
|
|
|
|
|
|
|
|
|
|
const fixedGeo = {
|
|
|
|
|
|
...indonesiaGeo,
|
|
|
|
|
|
features: indonesiaGeo.features.map((f) => ({
|
|
|
|
|
|
...f,
|
|
|
|
|
|
properties: {
|
|
|
|
|
|
...f.properties,
|
2025-06-26 06:01:49 +00:00
|
|
|
|
name: f.properties.PROVINSI,
|
2025-06-23 15:51:20 +00:00
|
|
|
|
},
|
|
|
|
|
|
})),
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
echarts.registerMap("indonesia", fixedGeo as any);
|
|
|
|
|
|
|
|
|
|
|
|
const chart = echarts.init(chartRef.current);
|
|
|
|
|
|
myChart.current = chart;
|
|
|
|
|
|
|
2025-06-26 06:01:49 +00:00
|
|
|
|
chart.on("click", (params: any) => {
|
|
|
|
|
|
highlightProvince(params.name);
|
2025-06-23 15:51:20 +00:00
|
|
|
|
});
|
2025-06-26 06:01:49 +00:00
|
|
|
|
chartOptionRef.current = option;
|
|
|
|
|
|
chart.setOption(option);
|
2025-06-23 15:51:20 +00:00
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
|
chart.dispose();
|
|
|
|
|
|
};
|
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
2025-06-28 16:39:22 +00:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
if (!myChart.current) return;
|
|
|
|
|
|
|
|
|
|
|
|
chartOptionRef.current = option;
|
|
|
|
|
|
myChart.current.setOption(option);
|
|
|
|
|
|
}, [option]);
|
|
|
|
|
|
|
2025-06-26 06:01:49 +00:00
|
|
|
|
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;
|
2025-06-28 16:39:22 +00:00
|
|
|
|
setSelectedProvince(name);
|
2025-06-26 06:01:49 +00:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
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">
|
2025-06-28 16:39:22 +00:00
|
|
|
|
<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)
|
|
|
|
|
|
}
|
2025-06-26 06:01:49 +00:00
|
|
|
|
>
|
2025-06-28 16:39:22 +00:00
|
|
|
|
<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" : ""
|
|
|
|
|
|
}`}
|
2025-06-26 06:01:49 +00:00
|
|
|
|
>
|
2025-06-28 16:39:22 +00:00
|
|
|
|
<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>
|
|
|
|
|
|
))}
|
2025-06-26 06:01:49 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
);
|
2025-06-23 15:51:20 +00:00
|
|
|
|
};
|
2025-06-26 06:01:49 +00:00
|
|
|
|
|
|
|
|
|
|
export default IndonesiaMap;
|