저번 글에서 부산시 지도까지 그려보았는데
이젠 그 지도 위에 원을 그리고 구별 데이터 개수를 보여주려고 한다
사전준비
구별 좌표 데이터 찾기
일단 구를 그리기 위해서는 좌표 값이 필요한데
구글 형님한테 부산시 구별 좌표값 있니? 하고 물어봤지만 마땅한 게 없어서
결국 내가 가내 수공업으로 하나씩 좌표를 찍어서 데이터를 모았다
좌표를 찾는 방법을 간단했다
구글 지도에서 동래구를 찾고
빨간색 표시 위해서 마우스 오른쪽 버튼을 클릭 하면 오른쪽에 메뉴가 뜨는데
저기서 이 위치 공유를 클릭하면
아래처럼 팝업창이 뜨는데
빨간색 원 안의 좌표를 이용하면 된다
나머지 구들을 열심히 찍어서 이렇게 데이터를 만들었다
//좌표 데이터
const districts = [
{ name: "강서구", coords: [128.922211, 35.140278] },
{ name: "금정구", coords: [129.091089, 35.260577] },
{ name: "남구", coords: [129.094324, 35.127789] },
{ name: "동구", coords: [129.045618, 35.129546] },
{ name: "동래구", coords: [129.084290, 35.203190] },
{ name: "부산진구", coords: [129.042895, 35.167692] },
{ name: "북구", coords: [128.997958, 35.205165] },
{ name: "사상구", coords: [128.986167, 35.161702] },
{ name: "사하구", coords: [128.973968, 35.091949] },
{ name: "서구", coords: [129.015118, 35.105154] },
{ name: "수영구", coords: [129.119828, 35.162092] },
{ name: "연제구", coords: [129.082075, 35.17318611] },
{ name: "영도구", coords: [129.064342, 35.080142] },
{ name: "중구", coords: [129.031265, 35.106037] },
{ name: "해운대구", coords: [129.173831, 35.174199] },
{ name: "기장군", coords: [129.201936, 35.302117] },
];
그리고 구별 데이터 갯수 데이터도 추가로 만들었다
// 구별 데이터 갯수
const districtData = [
{ name: "강서구", count: 117 },
{ name: "금정구", count: 125 },
{ name: "남구", count: 163 },
{ name: "동구", count: 82 },
{ name: "동래구", count: 148},
{ name: "부산진구", count: 92 },
{ name: "북구", count: 151 },
{ name: "사상구", count: 151 },
{ name: "사하구", count: 83 },
{ name: "서구", count: 104 },
{ name: "수영구", count: 134 },
{ name: "연제구", count: 195 },
{ name: "영도구", count: 117 },
{ name: "중구", count: 107 },
{ name: "해운대구", count: 120 },
{ name: "기장군", count: 137 },
];
여기까지 사전 준비는 끝!
코드 작성
1. 줌 컨테이너 추가
지도를 마우스 휠로 줌인 줌 아웃을 하기 위해서 줌 컨테이너를 추가해 주었다
// 줌 컨테이너 추가
const zoomContainer = svg.append("g").attr("class", "zoom-container");
//SVG 생성
const svg = d3.select("#main")
.append('svg')
.attr('id', 'maps')
.style("width", "100%")
.style("height", "100%")
.style("position", "absolute")
.style("background-color", "rgb(164, 204, 239)");
// 줌 컨테이너 추가
const zoomContainer = svg.append("g").attr("class", "zoom-container");
const width = document.querySelector("#main").clientWidth;
const height = document.querySelector("#main").clientHeight;
/* 랜덤 색상 얻기 */
const getColor = () => '#' + Math.round(Math.random() * 0xffffff).toString(16);
// 투영법 (projection) 설정
const projection = d3.geoMercator()
.center([129.05562775, 35.1379222])
.scale(100000)
.translate([width / 2, height / 2]);
const geoPath = d3.geoPath().projection(projection);
2. 지도 데이터 로드
d3.json 메서드를 사용해 GeoJSON 형식의 부산시 지도 데이터를 로드하고
로드된 데이터를 path 태그로 변환하여 화면에 렌더링 해주는 코드 작성
d3.json('<c:url value="/data/busan.geojson" />').then(data => {
const paths = zoomContainer.selectAll('path')
.data(data.features)
.enter()
.append('path')
.attr('fill', '#FAFAFA')
.attr('d', geoPath)
.attr('class', 'OUTLINE')
.style("stroke", "#EAEAEA");
});
d3.json 지정된 URL에서 GeoJSON 데이터를 비동기적으로 가져와서
geoPath D3의 지오메트리 랜더링 메서드로 지도 데이터를 SVG 경로로 변환하고
zoomContainer D3에서 줌 및 팬을 위한 그룹 요소로 맵의 확대 및 축소, 이동을 위해 추가했다
3. 데이터 개수에 따라 원 크기 계산
각 구별 데이터 갯수에 따라서 원 크기를 다르게 하여 데이터 개수를 시각화해보았다
// 원 크기를 비례적으로 계산하는 함수
const calculateRadius = (count, minCount, maxCount, minRadius, maxRadius) => {
// minCount와 maxCount가 같을 경우 고정 반지름 반환
if (minCount === maxCount) {
return (minRadius + maxRadius) / 2; // 최소와 최대 반지름의 평균 사용
}
// 비례적으로 반지름 계산
const scale = (count - minCount) / (maxCount - minCount);
return minRadius + scale * (maxRadius - minRadius);
};
calculateRadius 함수는 최솟값 최댓값을 기준으로 count 값에 따라 비례하는 반지름을 계산한다
최솟값과 최댓값이 동일한 경우는 기본 반지름은 반환하고
비례 스케일을 이용해 최소 반지름과 최대 반지름 사이의 값을 반환하도록 구현했다
4. 데이터의 최소값 최대값 계산
각 구별 데이터를 비교해서 count의 최댓값과 최솟값을 찾아보았다
// 데이터에서 count의 최소, 최대 값 찾기
const counts = districtData.map(d => d.count);
const minCount = Math.min(...counts);
const maxCount = Math.max(...counts);
5. 구별 데이터 시각화
각 구의 데이터를 지도에 시각화하는 코드
원에 구 이름과 데이터 개수를 텍스트로 표시하도록 했다
districts.forEach(district => {
const [x, y] = projection(district.coords);
const districtInfo = districtData.find(d => d.name === district.name);
const count = districtInfo ? districtInfo.count : 0;
const radius = calculateRadius(count, minCount, maxCount, 40, 70);
const group = zoomContainer.append("g")
.attr("class", "district-group");
group.append("circle")
.attr("cx", x)
.attr("cy", y)
.attr("r", radius)
.attr("fill", "red")
.attr("fill-opacity", 0.5);
group.append("text")
.attr("x", x)
.attr("y", y - 5)
.text(district.name)
.style("font-size", "10px")
.style("font-weight", "bold")
.attr("fill", "white")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle");
group.append("text")
.attr("x", x)
.attr("y", y + 10)
.text(count)
.style("font-size", "9px")
.attr("fill", "white")
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle");
});
6. 줌 기능 설정
지도 위에서 마우스 휠로 줌인 줌 아웃할 수 있도록 설정과 SVG에 줌 기능 연결 코드 추가했다
//줌 기능 설정
const zoomBehavior = d3.zoom()
.scaleExtent([1, 10]) // 줌 배율 제한
.on("zoom", () => {
const transform = d3.event.transform; // d3.v5에서는 d3.event를 사용
zoomContainer.attr("transform", transform); // 트랜스폼 적용
});
// SVG에 줌 기능 연결
svg.call(zoomBehavior);
이렇게 하면
이렇게 지도위세 구를 표시 할 수 있다
휴...
진짜 쉽지 않은 작업이었다
오늘은 여기까지
'개발이야기' 카테고리의 다른 글
D3.js 를 이용해 부산시 데이터 지도 그리기 구현 [1] (1) | 2024.12.12 |
---|---|
GROUP BY 와 MAX 를 이용한 LEFT OUTER JOIN 데이터 중복 제거 (2) | 2024.11.27 |
JavaScript Code-Refactoring(코드 리펙토링) 을 해보자 (1) | 2024.11.20 |
자바 정렬 Collections.sort 이용한 날짜 정렬 (1) | 2024.11.18 |
Dart 다트 입문기 다트 언어 기본 알아보기 [4] -제어문 (2) | 2024.11.02 |