Skip to content

Commit

Permalink
Merge pull request #25 from NewWays-TechForImpactKAIST/24-히스토그램과-metr…
Browse files Browse the repository at this point in the history
…ocouncil-추가
  • Loading branch information
withSang authored Nov 24, 2023
2 parents 4c327be + e645e1c commit 48fc40b
Show file tree
Hide file tree
Showing 11 changed files with 415 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export const AgeText = ({
후보로 나와 <Text strong>{ageHistogramParagraph.electedCount}</Text>명이{" "}
당선됐어요. 당선자의 20%가{" "}
<Text strong>{ageHistogramParagraph.firstQuintile}</Text>세 미만, 20%가{" "}
{ageHistogramParagraph.lastQuintile}세 이상이에요.
<Text strong>{ageHistogramParagraph.lastQuintile}</Text>세 이상이에요.
<br />
<br />
참고로 다양성 지표 전국 1위는 전체 인원의 20%가{" "}
Expand Down
2 changes: 1 addition & 1 deletion src/components/organisms/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const NewwaysHeader = () => (
span={22}
>
<Link
to="/"
to="/localCouncil"
aria-label="메인 페이지로 이동"
css={css`
display: flex;
Expand Down
89 changes: 77 additions & 12 deletions src/components/organisms/Histogram.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,99 @@
import { Histogram, type HistogramConfig } from "@ant-design/plots";
import { useState, useEffect } from "react";
import {
Histogram as AntHistogram,
type HistogramConfig,
} from "@ant-design/plots";
import { css } from "@emotion/react";

export interface HistogramData {
value: number;
export type ColorGroup = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;

export interface BinData {
binMin: number;
binMax: number;
count: number;
colorGroup: ColorGroup;
}

type HistogramData = {
value: number;
colorGroup?: ColorGroup;
}[];

interface Props {
data: HistogramData[];
data: BinData[];
}

export const AgeHistogram = ({ data }: Props) => {
const config: HistogramConfig = {
data,
interface CustomHistogramConfig extends HistogramConfig {
colorField: string;
}

export const Histogram = ({ data }: Props) => {
const ageMin = data[0].binMin - 1;
const ageMax = data[data.length - 1].binMax + 1;
const [countMax, setCountMax] = useState<number>(data[0].count);

const [binWidth, setBinWidth] = useState<number>(
data[0].binMax - data[0].binMin,
);
const [histogramData, setHistogramData] = useState<HistogramData>([]);

const getColorGroup = (minAge: number): ColorGroup =>
data.find(({ binMin }) => binMin === minAge)?.colorGroup ?? 0;

const getColor = (colorGroup: ColorGroup): string => {
const colors = [
"#FA9189",
"#FCAE7C",
"#FFE699",
"#F9FFB5",
"#B3F5BC",
"#D6F6FF",
"#E2CBF7",
"#D1BDFF",
];
return colors[colorGroup];
};

useEffect(() => {
const newHistogramData: HistogramData = [];
data.forEach(({ binMin, count, colorGroup }) => {
if (count > countMax) setCountMax(count);
Array(count)
.fill(0)
.forEach(() => {
newHistogramData.push({ value: binMin, colorGroup });
});
});
setHistogramData(newHistogramData);
setBinWidth(data[0].binMax - data[0].binMin);
}, [data]);

const config: CustomHistogramConfig = {
data: histogramData,
binField: "value",
binWidth: 10,
color: "#00BCE9",
binWidth,
colorField: "type",
color: param => {
const [binMin] = param.range as [number, number];
return getColor(getColorGroup(binMin));
},
meta: {
range: {
min: 0,
tickInterval: 5,
min: ageMin,
max: ageMax,
tickInterval: 1,
},
count: {
min: 0,
max: countMax + 1,
tickInterval: 1,
nice: true,
},
},
};

return (
<Histogram
<AntHistogram
css={css`
background: #f2f2f2;
padding: 20px;
Expand Down
57 changes: 54 additions & 3 deletions src/components/organisms/LocalCouncilReport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,27 @@ import {
type GenderTextData,
type PartyTextData,
} from "@/components/molecules/LocalCouncilReportText";
import { AgeHistogram } from "@/components/organisms/Histogram";
import {
Histogram,
type ColorGroup,
type BinData,
} from "@/components/organisms/Histogram";
import { PieChart, type PieChartData } from "@/components/organisms/PieChart";

import { axios, sampleAgeHistogramData, useGetNameFromId } from "@/utils";
import { axios, useGetNameFromId } from "@/utils";

const { Title } = Typography;

interface AgeHistogramDataAPIResponse {
LocalCouncil: number;
data: {
minAge: number;
maxAge: number;
count: number;
ageGroup: number;
}[];
}

type Gender = "남" | "여";

type GenderPieChartDataAPIResponse = {
Expand All @@ -43,12 +57,17 @@ interface Props {

const LocalCouncilReport = ({ metroName, localName, idMap }: Props) => {
const [metroId, localId] = idMap.get(metroName)?.get(localName) || [1, 1];

const [ageHistYear] = useState<number>(2022);
const [ageHistogramData, setAgeHistogramData] = useState<BinData[]>();
const [ageTextData, setAgeTextData] = useState<AgeTextData>();

const [genderTextData, setGenderTextData] = useState<GenderTextData>();
const [genderPieChartData, setGenderPieChartData] =
useState<PieChartData[]>();
const [genderPieChartColorMap, setGenderPieChartColorMap] =
useState<Map<string, string>>();

const [partyPieChartData, setPartyPieChartData] = useState<PieChartData[]>();
const [partyPieChartColorMap, setPartyPieChartColorMap] =
useState<Map<string, string>>();
Expand Down Expand Up @@ -119,6 +138,38 @@ const LocalCouncilReport = ({ metroName, localName, idMap }: Props) => {

// 백엔드로부터 그래프 데이터를 가져옵니다.
const fetchGraphData = () => {
axios
.get(
`localCouncil/age-hist/${metroId}/${localId}?ageHistType=elected&year=${ageHistYear}&method=kmeans`,
)
.then(response => {
const data = response.data as AgeHistogramDataAPIResponse;
const newAgeHistogramData: BinData[] = [];

// colorGroup이 백엔드에서 정렬되어 도착한다는 보장이 없습니다.
// 예를 들어, 35세의 colorGroup이 1이지만, 36세의 colorGroup이 0일 수 있습니다.
const colorGroupMap = new Map<ColorGroup, ColorGroup>();
let lastColorGroup: ColorGroup = 0;

data.data.forEach(({ minAge, maxAge, count, ageGroup }) => {
if (!colorGroupMap.has(ageGroup as ColorGroup)) {
colorGroupMap.set(ageGroup as ColorGroup, lastColorGroup);
lastColorGroup += 1;
}
newAgeHistogramData.push({
binMin: minAge,
binMax: maxAge,
count,
colorGroup: colorGroupMap.get(ageGroup as ColorGroup) || 0,
// colorGroup: ageGroup as ColorGroup,
});
});
setAgeHistogramData(newAgeHistogramData);
})
.catch(() => {
throw new Error("네트워크 문제가 발생했습니다. 다시 시도해주세요.");
});

axios
.get(`localCouncil/chart-data/${metroId}/${localId}?factor=gender`)
.then(response => {
Expand Down Expand Up @@ -173,7 +224,7 @@ const LocalCouncilReport = ({ metroName, localName, idMap }: Props) => {
level={1}
>{`${metroName} ${localName}의 지역의회 다양성 리포트`}</Title>
<Title level={2}>연령 다양성</Title>
<AgeHistogram data={sampleAgeHistogramData} />
{ageHistogramData ? <Histogram data={ageHistogramData} /> : null}
<AgeText data={ageTextData} getNameFromId={getNameFromId} />
<Title level={2}>성별 다양성</Title>
{genderPieChartData && genderPieChartColorMap ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import { RollbackOutlined } from "@ant-design/icons";

interface Props {
idMap: Map<MetroID, Map<string, [number, number]>>;
type?: "metro" | "local";
}

const LocalCouncilMapSelector = ({ idMap }: Props) => {
const MapSelector = ({ idMap, type = "local" }: Props) => {
const { metroName } = useParams();
const navigate = useNavigate();

Expand Down Expand Up @@ -77,17 +78,23 @@ const LocalCouncilMapSelector = ({ idMap }: Props) => {
<DropdownSelector
innerText="광역 의회를 선택하세요."
options={[...idMap.keys()]}
onClick={newMetroName => navigate(`/localCouncil/${newMetroName}`)}
onClick={newMetroName =>
type === "local"
? navigate(`/localCouncil/${newMetroName}`)
: navigate(`/metroCouncil/${newMetroName}`)
}
/>{" "}
<MetroSelector
onClick={newMetroName => {
navigate(`/localCouncil/${newMetroName}`);
}}
onClick={newMetroName =>
type === "local"
? navigate(`/localCouncil/${newMetroName}`)
: navigate(`/metroCouncil/${newMetroName}`)
}
/>
</>
)}
</Flex>
);
};

export default LocalCouncilMapSelector;
export default MapSelector;
Loading

0 comments on commit 48fc40b

Please sign in to comment.