Upload 55 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- src/App.tsx +79 -0
- src/algorithm/DemographicsPieByGender.ts +39 -0
- src/algorithm/DemographicsPieBySocialGroup.ts +24 -0
- src/algorithm/WordCloud.ts +98 -0
- src/assets/fonts/AirbnbCereal_W_Bd.otf +0 -0
- src/assets/fonts/AirbnbCereal_W_Bk.otf +0 -0
- src/assets/fonts/AirbnbCereal_W_Blk.otf +0 -0
- src/assets/fonts/AirbnbCereal_W_Lt.otf +0 -0
- src/assets/fonts/AirbnbCereal_W_Md.otf +0 -0
- src/assets/fonts/AirbnbCereal_W_XBd.otf +0 -0
- src/assets/fonts/fonts.css +29 -0
- src/assets/images/pin.png +0 -0
- src/assets/images/seiki.png +0 -0
- src/components/charts/brushChart/Area.tsx +94 -0
- src/components/charts/brushChart/AreaChart.tsx +245 -0
- src/components/charts/pieChart/Pie.tsx +278 -0
- src/components/charts/singleChart/SimpleChart.tsx +141 -0
- src/components/charts/singleChart/SimpleChartTripPurpose.tsx +141 -0
- src/components/charts/tripeChart/DayTypeGraph.tsx +204 -0
- src/components/charts/wordCloudChart/CloudWord.tsx +114 -0
- src/components/generalInformation/dropdown/DropDown.tsx +78 -0
- src/components/generalInformation/legend/Legend.tsx +343 -0
- src/components/main/GraphSections.tsx +55 -0
- src/constants/main.ts +1 -0
- src/data/data.ts +0 -0
- src/declarations.d.ts +4 -0
- src/index.tsx +19 -0
- src/redux/actions/poiActions.ts +6 -0
- src/redux/reducers/poiReducer.ts +37 -0
- src/redux/sagas/poiSaga.ts +31 -0
- src/redux/store/store.ts +19 -0
- src/redux/types/Poi.ts +62 -0
- src/sections/demographics/index.tsx +95 -0
- src/sections/description/index.tsx +39 -0
- src/sections/drawer/index.tsx +303 -0
- src/sections/footfall/index.tsx +60 -0
- src/sections/information/index.tsx +107 -0
- src/sections/map/control-panel.tsx +129 -0
- src/sections/map/index.tsx +64 -0
- src/sections/map/map-style-basic-v8.json +866 -0
- src/sections/mode/index.tsx +37 -0
- src/sections/trafic/index.tsx +79 -0
- src/styles/global/App.css +381 -0
- src/styles/global/customScrollBar.css +24 -0
- src/styles/global/index.css +20 -0
- src/styles/sections/demographics/index.css +0 -0
- src/styles/sections/description/index.css +0 -0
- src/styles/sections/drawer/index.css +0 -0
- src/styles/sections/footfall/index.css +0 -0
- src/styles/sections/information/index.css +0 -0
src/App.tsx
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useEffect } from "react";
|
2 |
+
import { useDispatch } from "react-redux";
|
3 |
+
import { fetchPoiRequest } from "./redux/actions/poiActions";
|
4 |
+
import { poiList } from "./data/data";
|
5 |
+
import { Poi } from "./redux/types/Poi";
|
6 |
+
|
7 |
+
import DropDown from "./components/generalInformation/dropdown/DropDown";
|
8 |
+
import DemographicsSection from "./sections/demographics";
|
9 |
+
import FootfallSection from "./sections/footfall";
|
10 |
+
import ModeSection from "./sections/mode";
|
11 |
+
import DescriptionSection from "./sections/description";
|
12 |
+
import InformationSection from "./sections/information";
|
13 |
+
import DrawerSection from "./sections/drawer";
|
14 |
+
import TraficSection from "./sections/trafic";
|
15 |
+
import MapSection from "./sections/map";
|
16 |
+
import GraphSections from "./components/main/GraphSections";
|
17 |
+
|
18 |
+
import "./styles/global/App.css";
|
19 |
+
import "./styles/global/customScrollBar.css";
|
20 |
+
|
21 |
+
const App = () => {
|
22 |
+
const [selectedPoi, setSelectedPoi] = useState<Poi>(poiList.items[0]);
|
23 |
+
const [isFootfallSection, setFootfallSection] = useState(true);
|
24 |
+
const [isModeSection, setModeSection] = useState(true);
|
25 |
+
const [isDemographicsSection, setDemographicsSection] = useState(true);
|
26 |
+
const [isInformationSection, setInformationSection] = useState(true);
|
27 |
+
const [isTraficSection, setTraficSection] = useState(true);
|
28 |
+
const [isMapView, setIsMapView] = useState(false);
|
29 |
+
|
30 |
+
const dispatch = useDispatch();
|
31 |
+
|
32 |
+
useEffect(() => {
|
33 |
+
dispatch(fetchPoiRequest());
|
34 |
+
}, [dispatch]);
|
35 |
+
|
36 |
+
return (
|
37 |
+
<div className="rootContent">
|
38 |
+
<DrawerSection
|
39 |
+
poiList={poiList.items}
|
40 |
+
setSelectedPoi={setSelectedPoi}
|
41 |
+
isFootfallSection={isFootfallSection}
|
42 |
+
setFootfallSection={setFootfallSection}
|
43 |
+
isModeSection={isModeSection}
|
44 |
+
setModeSection={setModeSection}
|
45 |
+
isDemographicsSection={isDemographicsSection}
|
46 |
+
setDemographicsSection={setDemographicsSection}
|
47 |
+
isInformationSection={isInformationSection}
|
48 |
+
setInformationSection={setInformationSection}
|
49 |
+
isTraficSection={isTraficSection}
|
50 |
+
setTraficSection={setTraficSection}
|
51 |
+
isMapView={isMapView}
|
52 |
+
setIsMapView={setIsMapView}
|
53 |
+
/>
|
54 |
+
|
55 |
+
{isMapView ? (
|
56 |
+
<MapSection selectedPoi={selectedPoi} />
|
57 |
+
) : (
|
58 |
+
<div className="mainContent">
|
59 |
+
<br></br>
|
60 |
+
<DropDown
|
61 |
+
poiList={poiList.items}
|
62 |
+
selectedPoi={selectedPoi}
|
63 |
+
setSelectedPoi={setSelectedPoi}
|
64 |
+
/>
|
65 |
+
<GraphSections
|
66 |
+
selectedPoi={selectedPoi}
|
67 |
+
isDemographicsSection={isDemographicsSection}
|
68 |
+
isFootfallSection={isFootfallSection}
|
69 |
+
isModeSection={isModeSection}
|
70 |
+
isInformationSection={isInformationSection}
|
71 |
+
isTraficSection={isTraficSection}
|
72 |
+
/>
|
73 |
+
</div>
|
74 |
+
)}
|
75 |
+
</div>
|
76 |
+
);
|
77 |
+
};
|
78 |
+
|
79 |
+
export default App;
|
src/algorithm/DemographicsPieByGender.ts
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
interface SocioDemographyItem {
|
2 |
+
age: string;
|
3 |
+
gender: string;
|
4 |
+
percentage: number;
|
5 |
+
social_group: string;
|
6 |
+
}
|
7 |
+
|
8 |
+
export interface SummaryItem {
|
9 |
+
label: string;
|
10 |
+
usage: number;
|
11 |
+
}
|
12 |
+
|
13 |
+
export function generateSummary(data: SocioDemographyItem[]): SummaryItem[] {
|
14 |
+
const summaryData: SummaryItem[] = [];
|
15 |
+
|
16 |
+
function calculateUsage(age: string, gender: string): number {
|
17 |
+
const filteredData = data.filter(
|
18 |
+
(item) => item.age === age && item.gender.toUpperCase() === gender
|
19 |
+
);
|
20 |
+
const totalPercentage = filteredData.reduce(
|
21 |
+
(total, item) => total + item.percentage,
|
22 |
+
0
|
23 |
+
);
|
24 |
+
return totalPercentage;
|
25 |
+
}
|
26 |
+
|
27 |
+
const ageGroups = ["15-24", "25-34", "35-49", "50-64", "65-PLUS"];
|
28 |
+
const genders = ["MALE", "FEMALE"];
|
29 |
+
|
30 |
+
for (const gender of genders) {
|
31 |
+
for (const age of ageGroups) {
|
32 |
+
const label = `${gender} ${age}`;
|
33 |
+
const usage = calculateUsage(age, gender);
|
34 |
+
summaryData.push({ label, usage });
|
35 |
+
}
|
36 |
+
}
|
37 |
+
|
38 |
+
return summaryData;
|
39 |
+
}
|
src/algorithm/DemographicsPieBySocialGroup.ts
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface SummaryItemSocial {
|
2 |
+
label: string;
|
3 |
+
usage: number;
|
4 |
+
}
|
5 |
+
|
6 |
+
export function summarizeSocialGroups(data: any[]): SummaryItemSocial[] {
|
7 |
+
const summary: { [key: string]: number } = {};
|
8 |
+
|
9 |
+
data.forEach((item) => {
|
10 |
+
const socialGroup = item.social_group;
|
11 |
+
const percentage = item.percentage;
|
12 |
+
summary[socialGroup] = (summary[socialGroup] || 0) + percentage;
|
13 |
+
});
|
14 |
+
|
15 |
+
const summaryArray: SummaryItemSocial[] = [];
|
16 |
+
for (const socialGroup in summary) {
|
17 |
+
summaryArray.push({
|
18 |
+
label: `${socialGroup}`,
|
19 |
+
usage: summary[socialGroup],
|
20 |
+
});
|
21 |
+
}
|
22 |
+
|
23 |
+
return summaryArray;
|
24 |
+
}
|
src/algorithm/WordCloud.ts
ADDED
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
interface SocioDemographicData {
|
2 |
+
age: string;
|
3 |
+
gender: string;
|
4 |
+
percentage: number;
|
5 |
+
social_group: string;
|
6 |
+
}
|
7 |
+
|
8 |
+
type SynonymsMap = Record<string, string[]>;
|
9 |
+
|
10 |
+
export function characterizeSocioDemographicData(
|
11 |
+
data: SocioDemographicData[]
|
12 |
+
): string {
|
13 |
+
const result: string[] = [];
|
14 |
+
|
15 |
+
const ageSynonyms: SynonymsMap = {
|
16 |
+
"15-24": ["young", "youthful", "adolescent", "teenage", "juvenile"],
|
17 |
+
"25-34": ["young adult", "early adult", "youthful", "prime of life"],
|
18 |
+
"35-49": ["middle-aged", "adult", "mature", "grown-up"],
|
19 |
+
"50-64": ["senior", "elderly", "aged", "mature", "golden-aged"],
|
20 |
+
"65-PLUS": ["elderly", "senior", "aged", "retired", "elder"],
|
21 |
+
};
|
22 |
+
|
23 |
+
const socialGroupSynonyms: SynonymsMap = {
|
24 |
+
"1": ["poorest", "impoverished", "deprived", "underprivileged", "needy"],
|
25 |
+
"2": [
|
26 |
+
"poor",
|
27 |
+
"low-income",
|
28 |
+
"economically challenged",
|
29 |
+
"struggling",
|
30 |
+
"disadvantaged",
|
31 |
+
],
|
32 |
+
"3": ["poor", "struggling", "disadvantaged", "less fortunate", "in need"],
|
33 |
+
"4": ["middle-income", "average", "moderate", "ordinary", "typical"],
|
34 |
+
"5": ["middle-income", "average", "moderate", "ordinary", "typical"],
|
35 |
+
"6": ["middle-income", "average", "moderate", "ordinary", "typical"],
|
36 |
+
"7": ["rich", "affluent", "well-off", "prosperous", "wealthy"],
|
37 |
+
"8": ["rich", "wealthy", "opulent", "affluent", "privileged"],
|
38 |
+
"9": ["richest", "privileged", "wealthiest", "affluent", "opulent"],
|
39 |
+
"10": ["richest", "wealthiest", "opulent", "affluent", "privileged"],
|
40 |
+
};
|
41 |
+
|
42 |
+
const crowdedSynonyms = [
|
43 |
+
"crowded",
|
44 |
+
"dense",
|
45 |
+
"populated",
|
46 |
+
"teeming",
|
47 |
+
"packed",
|
48 |
+
];
|
49 |
+
const notCrowdedSynonyms = [
|
50 |
+
"not that crowded",
|
51 |
+
"sparsely populated",
|
52 |
+
"unpopulated",
|
53 |
+
"deserted",
|
54 |
+
"vacant",
|
55 |
+
];
|
56 |
+
|
57 |
+
const groupedData = data.reduce((groups, item) => {
|
58 |
+
const key = `${item.age}-${item.gender}-${item.social_group}`;
|
59 |
+
if (!groups[key]) {
|
60 |
+
groups[key] = [];
|
61 |
+
}
|
62 |
+
groups[key].push(item.percentage);
|
63 |
+
return groups;
|
64 |
+
}, {} as Record<string, number[]>);
|
65 |
+
|
66 |
+
for (const key in groupedData) {
|
67 |
+
if (groupedData.hasOwnProperty(key)) {
|
68 |
+
const percentages = groupedData[key];
|
69 |
+
const averagePercentage =
|
70 |
+
percentages.reduce((sum, percentage) => sum + percentage, 0) /
|
71 |
+
percentages.length;
|
72 |
+
|
73 |
+
if (averagePercentage > 0.1) {
|
74 |
+
result.push(...crowdedSynonyms);
|
75 |
+
} else {
|
76 |
+
result.push(...notCrowdedSynonyms);
|
77 |
+
}
|
78 |
+
|
79 |
+
const age = key.split("-")[0];
|
80 |
+
const randomAgeSynonym = ageSynonyms[age]
|
81 |
+
? ageSynonyms[age][Math.floor(Math.random() * ageSynonyms[age].length)]
|
82 |
+
: age;
|
83 |
+
result.push(randomAgeSynonym);
|
84 |
+
|
85 |
+
const socialGroup = key.split("-")[2];
|
86 |
+
const randomSocialGroupSynonym = socialGroupSynonyms[socialGroup]
|
87 |
+
? socialGroupSynonyms[socialGroup][
|
88 |
+
Math.floor(Math.random() * socialGroupSynonyms[socialGroup].length)
|
89 |
+
]
|
90 |
+
: "social group " + socialGroup;
|
91 |
+
result.push(randomSocialGroupSynonym);
|
92 |
+
}
|
93 |
+
}
|
94 |
+
|
95 |
+
const finalString = result.filter((word) => isNaN(Number(word))).join(" ");
|
96 |
+
|
97 |
+
return finalString;
|
98 |
+
}
|
src/assets/fonts/AirbnbCereal_W_Bd.otf
ADDED
Binary file (56.4 kB). View file
|
|
src/assets/fonts/AirbnbCereal_W_Bk.otf
ADDED
Binary file (55.6 kB). View file
|
|
src/assets/fonts/AirbnbCereal_W_Blk.otf
ADDED
Binary file (57.1 kB). View file
|
|
src/assets/fonts/AirbnbCereal_W_Lt.otf
ADDED
Binary file (53.9 kB). View file
|
|
src/assets/fonts/AirbnbCereal_W_Md.otf
ADDED
Binary file (56.2 kB). View file
|
|
src/assets/fonts/AirbnbCereal_W_XBd.otf
ADDED
Binary file (57.3 kB). View file
|
|
src/assets/fonts/fonts.css
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@font-face {
|
2 |
+
font-family: "airbnb_extra_bold";
|
3 |
+
src: url("./AirbnbCereal_W_Blk.otf") format("opentype");
|
4 |
+
}
|
5 |
+
|
6 |
+
@font-face {
|
7 |
+
font-family: "airbnb_bold";
|
8 |
+
src: url("./AirbnbCereal_W_XBd.otf") format("opentype");
|
9 |
+
}
|
10 |
+
|
11 |
+
@font-face {
|
12 |
+
font-family: "airbnb_semi_bold";
|
13 |
+
src: url("./AirbnbCereal_W_Bd.otf") format("opentype");
|
14 |
+
}
|
15 |
+
|
16 |
+
@font-face {
|
17 |
+
font-family: "airbnb_regular";
|
18 |
+
src: url("./AirbnbCereal_W_Md.otf") format("opentype");
|
19 |
+
}
|
20 |
+
|
21 |
+
@font-face {
|
22 |
+
font-family: "airbnb_light";
|
23 |
+
src: url("./AirbnbCereal_W_Bk.otf") format("opentype");
|
24 |
+
}
|
25 |
+
|
26 |
+
@font-face {
|
27 |
+
font-family: "airbnb_extra_light";
|
28 |
+
src: url("./AirbnbCereal_W_Lt.otf") format("opentype");
|
29 |
+
}
|
src/assets/images/pin.png
ADDED
src/assets/images/seiki.png
ADDED
src/components/charts/brushChart/Area.tsx
ADDED
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from "react";
|
2 |
+
import { Group } from "@visx/group";
|
3 |
+
import { AreaClosed } from "@visx/shape";
|
4 |
+
import { AxisLeft, AxisBottom, AxisScale } from "@visx/axis";
|
5 |
+
import { LinearGradient } from "@visx/gradient";
|
6 |
+
import { curveMonotoneX } from "@visx/curve";
|
7 |
+
import { week_distribution } from "./AreaChart";
|
8 |
+
|
9 |
+
const axisColor = "#fff";
|
10 |
+
const axisBottomTickLabelProps = {
|
11 |
+
textAnchor: "middle" as const,
|
12 |
+
fontFamily: "Arial",
|
13 |
+
fontSize: 10,
|
14 |
+
fill: axisColor,
|
15 |
+
};
|
16 |
+
const axisLeftTickLabelProps = {
|
17 |
+
dx: "-0.25em",
|
18 |
+
dy: "0.25em",
|
19 |
+
fontFamily: "Arial",
|
20 |
+
fontSize: 10,
|
21 |
+
textAnchor: "end" as const,
|
22 |
+
fill: axisColor,
|
23 |
+
};
|
24 |
+
|
25 |
+
export default function AreaChart({
|
26 |
+
data,
|
27 |
+
gradientColor,
|
28 |
+
width,
|
29 |
+
yMax,
|
30 |
+
margin,
|
31 |
+
xScale,
|
32 |
+
yScale,
|
33 |
+
hideBottomAxis = false,
|
34 |
+
hideLeftAxis = false,
|
35 |
+
top,
|
36 |
+
left,
|
37 |
+
children,
|
38 |
+
}: {
|
39 |
+
data: week_distribution[];
|
40 |
+
gradientColor: string;
|
41 |
+
xScale: AxisScale<number>;
|
42 |
+
yScale: AxisScale<number>;
|
43 |
+
width: number;
|
44 |
+
yMax: number;
|
45 |
+
margin: { top: number; right: number; bottom: number; left: number };
|
46 |
+
hideBottomAxis?: boolean;
|
47 |
+
hideLeftAxis?: boolean;
|
48 |
+
top?: number;
|
49 |
+
left?: number;
|
50 |
+
children?: React.ReactNode;
|
51 |
+
}) {
|
52 |
+
if (width < 10) return null;
|
53 |
+
return (
|
54 |
+
<Group left={left || margin.left} top={top || margin.top}>
|
55 |
+
<LinearGradient
|
56 |
+
id="gradient"
|
57 |
+
from={gradientColor}
|
58 |
+
fromOpacity={1}
|
59 |
+
to={gradientColor}
|
60 |
+
toOpacity={0.2}
|
61 |
+
/>
|
62 |
+
<AreaClosed<week_distribution>
|
63 |
+
data={data}
|
64 |
+
x={(d) => xScale(d.week) || 0}
|
65 |
+
y={(d) => yScale(d.indice_base_100) || 0}
|
66 |
+
yScale={yScale}
|
67 |
+
strokeWidth={1}
|
68 |
+
stroke="url(#gradient)"
|
69 |
+
fill="url(#gradient)"
|
70 |
+
curve={curveMonotoneX}
|
71 |
+
/>
|
72 |
+
{!hideBottomAxis && (
|
73 |
+
<AxisBottom
|
74 |
+
top={yMax}
|
75 |
+
scale={xScale}
|
76 |
+
numTicks={width > 520 ? 10 : 5}
|
77 |
+
stroke={axisColor}
|
78 |
+
tickStroke={axisColor}
|
79 |
+
tickLabelProps={axisBottomTickLabelProps}
|
80 |
+
/>
|
81 |
+
)}
|
82 |
+
{!hideLeftAxis && (
|
83 |
+
<AxisLeft
|
84 |
+
scale={yScale}
|
85 |
+
numTicks={5}
|
86 |
+
stroke={axisColor}
|
87 |
+
tickStroke={axisColor}
|
88 |
+
tickLabelProps={axisLeftTickLabelProps}
|
89 |
+
/>
|
90 |
+
)}
|
91 |
+
{children}
|
92 |
+
</Group>
|
93 |
+
);
|
94 |
+
}
|
src/components/charts/brushChart/AreaChart.tsx
ADDED
@@ -0,0 +1,245 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
2 |
+
import React, { useRef, useState, useMemo } from "react";
|
3 |
+
import { scaleLinear } from "@visx/scale";
|
4 |
+
import { Brush } from "@visx/brush";
|
5 |
+
import { Bounds } from "@visx/brush/lib/types";
|
6 |
+
import BaseBrush, {
|
7 |
+
BaseBrushState,
|
8 |
+
UpdateBrush,
|
9 |
+
} from "@visx/brush/lib/BaseBrush";
|
10 |
+
import { PatternLines } from "@visx/pattern";
|
11 |
+
import { Group } from "@visx/group";
|
12 |
+
import { LinearGradient } from "@visx/gradient";
|
13 |
+
import { extent } from "@visx/vendor/d3-array";
|
14 |
+
import { BrushHandleRenderProps } from "@visx/brush/lib/BrushHandle";
|
15 |
+
import AreaChart from "./Area";
|
16 |
+
|
17 |
+
const brushMargin = { top: 0, bottom: 15, left: 50, right: 20 };
|
18 |
+
const chartSeparation = 30;
|
19 |
+
const PATTERN_ID = "brush_pattern";
|
20 |
+
const GRADIENT_ID = "brush_gradient";
|
21 |
+
export const accentColor = "#f6acc8";
|
22 |
+
export const background = "#584153";
|
23 |
+
export const background2 = "#af8baf";
|
24 |
+
|
25 |
+
const getWeek = (d: any) => d.week;
|
26 |
+
const getIndice = (d: any) => d.indice_base_100;
|
27 |
+
|
28 |
+
export interface week_distribution {
|
29 |
+
indice_base_100: number;
|
30 |
+
week: number;
|
31 |
+
}
|
32 |
+
|
33 |
+
export type BrushProps = {
|
34 |
+
data: week_distribution[];
|
35 |
+
width: number;
|
36 |
+
height: number;
|
37 |
+
margin?: { top: number; right: number; bottom: number; left: number };
|
38 |
+
compact?: boolean;
|
39 |
+
};
|
40 |
+
|
41 |
+
function BrushChart({
|
42 |
+
data,
|
43 |
+
compact = false,
|
44 |
+
width,
|
45 |
+
height,
|
46 |
+
margin = {
|
47 |
+
top: 20,
|
48 |
+
left: 50,
|
49 |
+
bottom: 20,
|
50 |
+
right: 20,
|
51 |
+
},
|
52 |
+
}: BrushProps) {
|
53 |
+
const brushRef = useRef<BaseBrush | null>(null);
|
54 |
+
|
55 |
+
const [filteredData, setFilteredData] = useState(data);
|
56 |
+
|
57 |
+
const onBrushChange = (domain: Bounds | null) => {
|
58 |
+
if (!domain) return;
|
59 |
+
const { x0, x1, y0, y1 } = domain;
|
60 |
+
const dataCopy = data.filter((d) => {
|
61 |
+
const x = getWeek(d);
|
62 |
+
const y = getIndice(d);
|
63 |
+
return x > x0 && x < x1 && y > y0 && y < y1;
|
64 |
+
});
|
65 |
+
setFilteredData(dataCopy);
|
66 |
+
};
|
67 |
+
|
68 |
+
const innerHeight = height - margin.top - margin.bottom;
|
69 |
+
const topChartBottomMargin = compact
|
70 |
+
? chartSeparation / 2
|
71 |
+
: chartSeparation + 10;
|
72 |
+
const topChartHeight = 0.8 * innerHeight - topChartBottomMargin;
|
73 |
+
const bottomChartHeight = innerHeight - topChartHeight - chartSeparation;
|
74 |
+
|
75 |
+
const xMax = Math.max(width - margin.left - margin.right, 0);
|
76 |
+
const yMax = Math.max(topChartHeight, 0);
|
77 |
+
const xBrushMax = Math.max(width - brushMargin.left - brushMargin.right, 0);
|
78 |
+
const yBrushMax = Math.max(
|
79 |
+
bottomChartHeight - brushMargin.top - brushMargin.bottom,
|
80 |
+
0
|
81 |
+
);
|
82 |
+
|
83 |
+
const weekScale = useMemo(
|
84 |
+
() =>
|
85 |
+
scaleLinear<number>({
|
86 |
+
range: [0, xMax],
|
87 |
+
domain: extent(data, getWeek) as [number, number],
|
88 |
+
}),
|
89 |
+
[xMax, data]
|
90 |
+
);
|
91 |
+
|
92 |
+
const brushWeekScale = useMemo(
|
93 |
+
() =>
|
94 |
+
scaleLinear<number>({
|
95 |
+
range: [0, xBrushMax],
|
96 |
+
domain: extent(data, getWeek) as [number, number],
|
97 |
+
}),
|
98 |
+
[xBrushMax, data]
|
99 |
+
);
|
100 |
+
|
101 |
+
const indiceScale = useMemo(
|
102 |
+
() =>
|
103 |
+
scaleLinear<number>({
|
104 |
+
range: [topChartHeight, 0],
|
105 |
+
domain: [70, 105],
|
106 |
+
}),
|
107 |
+
[topChartHeight, data]
|
108 |
+
);
|
109 |
+
|
110 |
+
const initialBrushPosition = useMemo(
|
111 |
+
() => ({
|
112 |
+
start: { x: brushWeekScale(data[0].week) },
|
113 |
+
end: { x: brushWeekScale(data[10].week) },
|
114 |
+
}),
|
115 |
+
[brushWeekScale]
|
116 |
+
);
|
117 |
+
|
118 |
+
const handleClearClick = () => {
|
119 |
+
if (brushRef?.current) {
|
120 |
+
setFilteredData(data);
|
121 |
+
brushRef.current.reset();
|
122 |
+
}
|
123 |
+
};
|
124 |
+
|
125 |
+
const handleResetClick = () => {
|
126 |
+
if (brushRef?.current) {
|
127 |
+
const updater: UpdateBrush = (prevBrush) => {
|
128 |
+
const newExtent = brushRef.current!.getExtent(
|
129 |
+
initialBrushPosition.start,
|
130 |
+
initialBrushPosition.end
|
131 |
+
);
|
132 |
+
|
133 |
+
const newState: BaseBrushState = {
|
134 |
+
...prevBrush,
|
135 |
+
start: { y: newExtent.y0, x: newExtent.x0 },
|
136 |
+
end: { y: newExtent.y1, x: newExtent.x1 },
|
137 |
+
extent: newExtent,
|
138 |
+
};
|
139 |
+
|
140 |
+
return newState;
|
141 |
+
};
|
142 |
+
brushRef.current.updateBrush(updater);
|
143 |
+
}
|
144 |
+
};
|
145 |
+
|
146 |
+
return (
|
147 |
+
<div>
|
148 |
+
<svg
|
149 |
+
width={width}
|
150 |
+
height={height}
|
151 |
+
style={{
|
152 |
+
borderBottomLeftRadius: "14px",
|
153 |
+
borderBottomRightRadius: "14px",
|
154 |
+
boxShadow: "0 0 8px rgba(0, 0, 0, 0.2)",
|
155 |
+
}}
|
156 |
+
>
|
157 |
+
<LinearGradient
|
158 |
+
id={GRADIENT_ID}
|
159 |
+
from={background}
|
160 |
+
to={background2}
|
161 |
+
rotate={45}
|
162 |
+
/>
|
163 |
+
<rect
|
164 |
+
x={0}
|
165 |
+
y={0}
|
166 |
+
width={width}
|
167 |
+
height={height}
|
168 |
+
fill={`url(#${GRADIENT_ID})`}
|
169 |
+
/>
|
170 |
+
<AreaChart
|
171 |
+
hideBottomAxis={compact}
|
172 |
+
data={filteredData}
|
173 |
+
width={width}
|
174 |
+
margin={{ ...margin, bottom: topChartBottomMargin }}
|
175 |
+
yMax={yMax}
|
176 |
+
xScale={weekScale}
|
177 |
+
yScale={indiceScale}
|
178 |
+
gradientColor={background2}
|
179 |
+
/>
|
180 |
+
<AreaChart
|
181 |
+
hideBottomAxis
|
182 |
+
hideLeftAxis
|
183 |
+
data={data}
|
184 |
+
width={width}
|
185 |
+
yMax={yBrushMax}
|
186 |
+
xScale={brushWeekScale}
|
187 |
+
yScale={indiceScale}
|
188 |
+
margin={brushMargin}
|
189 |
+
top={topChartHeight + topChartBottomMargin + margin.top}
|
190 |
+
gradientColor={background2}
|
191 |
+
>
|
192 |
+
<PatternLines
|
193 |
+
id={PATTERN_ID}
|
194 |
+
height={8}
|
195 |
+
width={8}
|
196 |
+
stroke={accentColor}
|
197 |
+
strokeWidth={1}
|
198 |
+
orientation={["diagonal"]}
|
199 |
+
/>
|
200 |
+
<Brush
|
201 |
+
xScale={brushWeekScale}
|
202 |
+
yScale={indiceScale}
|
203 |
+
width={xBrushMax}
|
204 |
+
height={yBrushMax}
|
205 |
+
margin={brushMargin}
|
206 |
+
handleSize={8}
|
207 |
+
innerRef={brushRef}
|
208 |
+
resizeTriggerAreas={["left", "right"]}
|
209 |
+
brushDirection="horizontal"
|
210 |
+
initialBrushPosition={initialBrushPosition}
|
211 |
+
onChange={onBrushChange}
|
212 |
+
onClick={() => setFilteredData(data)}
|
213 |
+
selectedBoxStyle={{
|
214 |
+
fill: `url(#${PATTERN_ID})`,
|
215 |
+
stroke: "white",
|
216 |
+
}}
|
217 |
+
useWindowMoveEvents
|
218 |
+
renderBrushHandle={(props) => <BrushHandle {...props} />}
|
219 |
+
/>
|
220 |
+
</AreaChart>
|
221 |
+
</svg>
|
222 |
+
</div>
|
223 |
+
);
|
224 |
+
}
|
225 |
+
|
226 |
+
function BrushHandle({ x, height, isBrushActive }: BrushHandleRenderProps) {
|
227 |
+
const pathWidth = 8;
|
228 |
+
const pathHeight = 15;
|
229 |
+
if (!isBrushActive) {
|
230 |
+
return null;
|
231 |
+
}
|
232 |
+
return (
|
233 |
+
<Group left={x + pathWidth / 2} top={(height - pathHeight) / 2}>
|
234 |
+
<path
|
235 |
+
fill="#f2f2f2"
|
236 |
+
d="M -4.5 0.5 L 3.5 0.5 L 3.5 15.5 L -4.5 15.5 L -4.5 0.5 M -1.5 4 L -1.5 12 M 0.5 4 L 0.5 12"
|
237 |
+
stroke="#999999"
|
238 |
+
strokeWidth="1"
|
239 |
+
style={{ cursor: "ew-resize" }}
|
240 |
+
/>
|
241 |
+
</Group>
|
242 |
+
);
|
243 |
+
}
|
244 |
+
|
245 |
+
export default BrushChart;
|
src/components/charts/pieChart/Pie.tsx
ADDED
@@ -0,0 +1,278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* eslint-disable @typescript-eslint/no-use-before-define */
|
2 |
+
import React, { useState } from "react";
|
3 |
+
import Pie, { ProvidedProps, PieArcDatum } from "@visx/shape/lib/shapes/Pie";
|
4 |
+
import { scaleOrdinal } from "@visx/scale";
|
5 |
+
import { Group } from "@visx/group";
|
6 |
+
import {
|
7 |
+
GradientPinkBlue,
|
8 |
+
GradientOrangeRed,
|
9 |
+
LinearGradient,
|
10 |
+
} from "@visx/gradient";
|
11 |
+
import { animated, useTransition, interpolate } from "@react-spring/web";
|
12 |
+
import {
|
13 |
+
generateSummary,
|
14 |
+
SummaryItem,
|
15 |
+
} from "../../../algorithm/DemographicsPieByGender";
|
16 |
+
import {
|
17 |
+
summarizeSocialGroups,
|
18 |
+
SummaryItemSocial,
|
19 |
+
} from "../../../algorithm/DemographicsPieBySocialGroup";
|
20 |
+
|
21 |
+
interface demographics {
|
22 |
+
age: string;
|
23 |
+
gender: string;
|
24 |
+
percentage: number;
|
25 |
+
social_group: string;
|
26 |
+
}
|
27 |
+
|
28 |
+
const defaultMargin = { top: 20, right: 20, bottom: 20, left: 20 };
|
29 |
+
|
30 |
+
export type PieProps = {
|
31 |
+
data: demographics[];
|
32 |
+
width: number;
|
33 |
+
height: number;
|
34 |
+
margin?: typeof defaultMargin;
|
35 |
+
animate?: boolean;
|
36 |
+
flag?: number;
|
37 |
+
};
|
38 |
+
|
39 |
+
export default function PieChart({
|
40 |
+
data,
|
41 |
+
width,
|
42 |
+
height,
|
43 |
+
margin = defaultMargin,
|
44 |
+
animate = true,
|
45 |
+
flag = 0,
|
46 |
+
}: PieProps) {
|
47 |
+
const [selectedDemographic, setSelectedDemographic] = useState<string | null>(
|
48 |
+
null
|
49 |
+
);
|
50 |
+
const [selectedSocialGroup, setSelectedSocialGroup] = useState<string | null>(
|
51 |
+
null
|
52 |
+
);
|
53 |
+
|
54 |
+
const proccessedData = generateSummary(data);
|
55 |
+
const proccessedSocialGroupData = summarizeSocialGroups(data);
|
56 |
+
if (width < 10) return null;
|
57 |
+
|
58 |
+
const innerWidth = width - margin.left - margin.right;
|
59 |
+
const innerHeight = height - margin.top - margin.bottom;
|
60 |
+
const radius = Math.min(innerWidth, innerHeight) / 2;
|
61 |
+
const centerY = innerHeight / 2;
|
62 |
+
const centerX = innerWidth / 2;
|
63 |
+
const donutThickness = 50;
|
64 |
+
|
65 |
+
const backgroundDefined = () => {
|
66 |
+
switch (flag) {
|
67 |
+
case 0:
|
68 |
+
return (
|
69 |
+
<>
|
70 |
+
<rect
|
71 |
+
width={width}
|
72 |
+
height={height}
|
73 |
+
fill="url('#visx-pie-gradient')"
|
74 |
+
/>
|
75 |
+
<LinearGradient
|
76 |
+
id="visx-pie-gradient"
|
77 |
+
from="#000000"
|
78 |
+
to="#3C3C3C"
|
79 |
+
rotate="-45"
|
80 |
+
/>
|
81 |
+
</>
|
82 |
+
);
|
83 |
+
case 1:
|
84 |
+
return (
|
85 |
+
<>
|
86 |
+
<rect width={width} height={height} fill="url('#men')" />
|
87 |
+
<GradientOrangeRed id="men" />
|
88 |
+
</>
|
89 |
+
);
|
90 |
+
case 2:
|
91 |
+
return (
|
92 |
+
<>
|
93 |
+
<rect width={width} height={height} fill="url('#women')" />
|
94 |
+
<GradientPinkBlue id="women" />
|
95 |
+
</>
|
96 |
+
);
|
97 |
+
}
|
98 |
+
};
|
99 |
+
// accessor functions
|
100 |
+
const usage = (d: SummaryItem) => d.usage;
|
101 |
+
const frequency = (d: SummaryItemSocial) => d.usage;
|
102 |
+
|
103 |
+
// color scales
|
104 |
+
const getBrowserColor = scaleOrdinal({
|
105 |
+
domain: proccessedData.map((d) => d.label),
|
106 |
+
range: [
|
107 |
+
"rgba(255,255,255,0.7)",
|
108 |
+
"rgba(255,255,255,0.6)",
|
109 |
+
"rgba(255,255,255,0.5)",
|
110 |
+
"rgba(255,255,255,0.4)",
|
111 |
+
"rgba(255,255,255,0.3)",
|
112 |
+
"rgba(255,255,255,0.2)",
|
113 |
+
"rgba(255,255,255,0.1)",
|
114 |
+
],
|
115 |
+
});
|
116 |
+
|
117 |
+
const getLetterFrequencyColor = scaleOrdinal({
|
118 |
+
domain: proccessedSocialGroupData.map((l) => l.label),
|
119 |
+
range: [
|
120 |
+
"rgba(93,30,91,1)",
|
121 |
+
"rgba(93,30,91,0.8)",
|
122 |
+
"rgba(93,30,91,0.6)",
|
123 |
+
"rgba(93,30,91,0.4)",
|
124 |
+
],
|
125 |
+
});
|
126 |
+
|
127 |
+
return (
|
128 |
+
<svg
|
129 |
+
width={width}
|
130 |
+
height={height}
|
131 |
+
style={{
|
132 |
+
borderBottomLeftRadius: "14px",
|
133 |
+
borderBottomRightRadius: "14px",
|
134 |
+
boxShadow: "0 0 8px rgba(0, 0, 0, 0.2)",
|
135 |
+
}}
|
136 |
+
>
|
137 |
+
{backgroundDefined()}
|
138 |
+
<Group top={centerY + margin.top} left={centerX + margin.left}>
|
139 |
+
<Pie
|
140 |
+
data={
|
141 |
+
selectedDemographic
|
142 |
+
? proccessedData.filter(
|
143 |
+
({ label }) => label === selectedDemographic
|
144 |
+
)
|
145 |
+
: proccessedData
|
146 |
+
}
|
147 |
+
pieValue={usage}
|
148 |
+
outerRadius={radius}
|
149 |
+
innerRadius={radius - donutThickness}
|
150 |
+
cornerRadius={3}
|
151 |
+
padAngle={0.005}
|
152 |
+
>
|
153 |
+
{(pie) => (
|
154 |
+
<AnimatedPie<SummaryItem>
|
155 |
+
{...pie}
|
156 |
+
animate={animate}
|
157 |
+
getKey={(arc) => arc.data.label}
|
158 |
+
onClickDatum={({ data: { label } }) =>
|
159 |
+
animate &&
|
160 |
+
setSelectedDemographic(
|
161 |
+
selectedDemographic && selectedDemographic === label
|
162 |
+
? null
|
163 |
+
: label
|
164 |
+
)
|
165 |
+
}
|
166 |
+
getColor={(arc) => getBrowserColor(arc.data.label)}
|
167 |
+
/>
|
168 |
+
)}
|
169 |
+
</Pie>
|
170 |
+
<Pie
|
171 |
+
data={
|
172 |
+
selectedSocialGroup
|
173 |
+
? proccessedSocialGroupData.filter(
|
174 |
+
({ label }) => label === selectedSocialGroup
|
175 |
+
)
|
176 |
+
: proccessedSocialGroupData
|
177 |
+
}
|
178 |
+
pieValue={frequency}
|
179 |
+
pieSortValues={() => -1}
|
180 |
+
outerRadius={radius - donutThickness * 1.3}
|
181 |
+
>
|
182 |
+
{(pie) => (
|
183 |
+
<AnimatedPie<SummaryItemSocial>
|
184 |
+
{...pie}
|
185 |
+
animate={animate}
|
186 |
+
getKey={({ data: { label } }) => label}
|
187 |
+
onClickDatum={({ data: { label } }) =>
|
188 |
+
animate &&
|
189 |
+
setSelectedSocialGroup(
|
190 |
+
selectedSocialGroup && selectedSocialGroup === label
|
191 |
+
? null
|
192 |
+
: label
|
193 |
+
)
|
194 |
+
}
|
195 |
+
getColor={({ data: { label } }) => getLetterFrequencyColor(label)}
|
196 |
+
/>
|
197 |
+
)}
|
198 |
+
</Pie>
|
199 |
+
</Group>
|
200 |
+
</svg>
|
201 |
+
);
|
202 |
+
}
|
203 |
+
|
204 |
+
type AnimatedStyles = { startAngle: number; endAngle: number; opacity: number };
|
205 |
+
|
206 |
+
const fromLeaveTransition = ({ endAngle }: PieArcDatum<any>) => ({
|
207 |
+
startAngle: endAngle > Math.PI ? 2 * Math.PI : 0,
|
208 |
+
endAngle: endAngle > Math.PI ? 2 * Math.PI : 0,
|
209 |
+
opacity: 0,
|
210 |
+
});
|
211 |
+
const enterUpdateTransition = ({ startAngle, endAngle }: PieArcDatum<any>) => ({
|
212 |
+
startAngle,
|
213 |
+
endAngle,
|
214 |
+
opacity: 1,
|
215 |
+
});
|
216 |
+
|
217 |
+
type AnimatedPieProps<Datum> = ProvidedProps<Datum> & {
|
218 |
+
animate?: boolean;
|
219 |
+
getKey: (d: PieArcDatum<Datum>) => string;
|
220 |
+
getColor: (d: PieArcDatum<Datum>) => string;
|
221 |
+
onClickDatum: (d: PieArcDatum<Datum>) => void;
|
222 |
+
delay?: number;
|
223 |
+
};
|
224 |
+
|
225 |
+
function AnimatedPie<Datum>({
|
226 |
+
animate,
|
227 |
+
arcs,
|
228 |
+
path,
|
229 |
+
getKey,
|
230 |
+
getColor,
|
231 |
+
onClickDatum,
|
232 |
+
}: AnimatedPieProps<Datum>) {
|
233 |
+
const transitions = useTransition<PieArcDatum<Datum>, AnimatedStyles>(arcs, {
|
234 |
+
from: animate ? fromLeaveTransition : enterUpdateTransition,
|
235 |
+
enter: enterUpdateTransition,
|
236 |
+
update: enterUpdateTransition,
|
237 |
+
leave: animate ? fromLeaveTransition : enterUpdateTransition,
|
238 |
+
keys: getKey,
|
239 |
+
});
|
240 |
+
return transitions((props: any, arc: PieArcDatum<Datum>, { key }: any) => {
|
241 |
+
const [centroidX, centroidY] = path.centroid(arc);
|
242 |
+
const hasSpaceForLabel = arc.endAngle - arc.startAngle >= 0.1;
|
243 |
+
|
244 |
+
return (
|
245 |
+
<g key={key}>
|
246 |
+
<animated.path
|
247 |
+
d={interpolate(
|
248 |
+
[props.startAngle, props.endAngle],
|
249 |
+
(startAngle: any, endAngle: any) =>
|
250 |
+
path({
|
251 |
+
...arc,
|
252 |
+
startAngle,
|
253 |
+
endAngle,
|
254 |
+
})
|
255 |
+
)}
|
256 |
+
fill={getColor(arc)}
|
257 |
+
onClick={() => onClickDatum(arc)}
|
258 |
+
onTouchStart={() => onClickDatum(arc)}
|
259 |
+
/>
|
260 |
+
{hasSpaceForLabel && (
|
261 |
+
<animated.g style={{ opacity: props.opacity }}>
|
262 |
+
<text
|
263 |
+
fill="white"
|
264 |
+
x={centroidX}
|
265 |
+
y={centroidY}
|
266 |
+
dy=".33em"
|
267 |
+
fontSize={9}
|
268 |
+
textAnchor="middle"
|
269 |
+
pointerEvents="none"
|
270 |
+
>
|
271 |
+
{getKey(arc)}
|
272 |
+
</text>
|
273 |
+
</animated.g>
|
274 |
+
)}
|
275 |
+
</g>
|
276 |
+
);
|
277 |
+
});
|
278 |
+
}
|
src/components/charts/singleChart/SimpleChart.tsx
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useMemo, useState } from "react";
|
2 |
+
import { Bar } from "@visx/shape";
|
3 |
+
import { Group } from "@visx/group";
|
4 |
+
import { LinearGradient } from "@visx/gradient";
|
5 |
+
import { AxisBottom, AxisLeft } from "@visx/axis";
|
6 |
+
import { scaleBand, scaleLinear } from "@visx/scale";
|
7 |
+
|
8 |
+
const verticalMargin = 20;
|
9 |
+
const leftMargin = 50;
|
10 |
+
const barPadding = 4;
|
11 |
+
|
12 |
+
interface originTopTen {
|
13 |
+
commune: string;
|
14 |
+
percentage: number;
|
15 |
+
}
|
16 |
+
export type BarsProps = {
|
17 |
+
data: originTopTen[];
|
18 |
+
width: number;
|
19 |
+
height: number;
|
20 |
+
events?: boolean;
|
21 |
+
};
|
22 |
+
|
23 |
+
const getCommuneValue = (d: originTopTen) => Number(d.percentage);
|
24 |
+
|
25 |
+
export default function SimpleChart({
|
26 |
+
data,
|
27 |
+
width,
|
28 |
+
height,
|
29 |
+
events = false,
|
30 |
+
}: BarsProps) {
|
31 |
+
const xMax = width - leftMargin;
|
32 |
+
const yMax = height - verticalMargin - 20;
|
33 |
+
|
34 |
+
const xScale = useMemo(
|
35 |
+
() =>
|
36 |
+
scaleBand<number>({
|
37 |
+
range: [0, xMax],
|
38 |
+
round: true,
|
39 |
+
domain: data.map((d: any) => d.commune),
|
40 |
+
paddingInner: 0.2,
|
41 |
+
paddingOuter: 0.1,
|
42 |
+
}),
|
43 |
+
[xMax]
|
44 |
+
);
|
45 |
+
const yScale = useMemo(
|
46 |
+
() =>
|
47 |
+
scaleLinear<number>({
|
48 |
+
range: [yMax, 0],
|
49 |
+
round: true,
|
50 |
+
domain: [0, Math.max(...data.map(getCommuneValue))],
|
51 |
+
}),
|
52 |
+
[yMax]
|
53 |
+
);
|
54 |
+
|
55 |
+
const [isHover, setIsHover] = useState(Array(data.length).fill(false));
|
56 |
+
|
57 |
+
return width < 10 ? null : (
|
58 |
+
<svg
|
59 |
+
width={width}
|
60 |
+
height={height}
|
61 |
+
style={{
|
62 |
+
borderBottomLeftRadius: "14px",
|
63 |
+
borderBottomRightRadius: "14px",
|
64 |
+
boxShadow: "0 0 8px rgba(0, 0, 0, 0.2)",
|
65 |
+
}}
|
66 |
+
>
|
67 |
+
<LinearGradient from="#351CAB" to="#621A61" id="teal" />
|
68 |
+
<rect width={width} height={height} fill="url(#teal)" />
|
69 |
+
<Group top={verticalMargin}>
|
70 |
+
{data.map((d: any, index: any) => {
|
71 |
+
const barWidth = xScale.bandwidth();
|
72 |
+
const barHeight = yMax - (yScale(d.percentage) ?? 0);
|
73 |
+
const barX = xScale(d.commune) ?? 0;
|
74 |
+
const barY = yMax - barHeight;
|
75 |
+
return (
|
76 |
+
<Bar
|
77 |
+
key={`bar-${d.commune}`}
|
78 |
+
className={isHover[index] ? "hovered" : ""}
|
79 |
+
style={{
|
80 |
+
cursor: "pointer",
|
81 |
+
transition: "all 0.5s",
|
82 |
+
opacity: "1",
|
83 |
+
}}
|
84 |
+
x={barX + 35}
|
85 |
+
y={barY - 10}
|
86 |
+
rx={5}
|
87 |
+
ry={5}
|
88 |
+
width={barWidth - barPadding}
|
89 |
+
height={barHeight}
|
90 |
+
fill={isHover[index] ? "#FFFFFF" : "#AAB1FF"}
|
91 |
+
onClick={() => {
|
92 |
+
if (events) alert(`clicked: ${JSON.stringify(d)}`);
|
93 |
+
}}
|
94 |
+
onMouseMove={() => {
|
95 |
+
const updatedHover = [...isHover];
|
96 |
+
updatedHover[index] = true;
|
97 |
+
setIsHover(updatedHover);
|
98 |
+
}}
|
99 |
+
onMouseLeave={() => {
|
100 |
+
const updatedHover = [...isHover];
|
101 |
+
updatedHover[index] = false;
|
102 |
+
setIsHover(updatedHover);
|
103 |
+
}}
|
104 |
+
/>
|
105 |
+
);
|
106 |
+
})}
|
107 |
+
<AxisBottom
|
108 |
+
top={height - verticalMargin - 30}
|
109 |
+
left={leftMargin - 15}
|
110 |
+
scale={xScale}
|
111 |
+
tickStroke="white"
|
112 |
+
stroke="white"
|
113 |
+
strokeWidth={2}
|
114 |
+
tickLabelProps={() => ({
|
115 |
+
fill: "white",
|
116 |
+
fontSize: 12,
|
117 |
+
textAnchor: "middle",
|
118 |
+
fontWeight: "bold",
|
119 |
+
})}
|
120 |
+
/>
|
121 |
+
<AxisLeft
|
122 |
+
left={leftMargin - 15}
|
123 |
+
top={verticalMargin - 30}
|
124 |
+
scale={yScale}
|
125 |
+
tickFormat={(value) => value.toString()}
|
126 |
+
tickStroke="white"
|
127 |
+
strokeWidth={2}
|
128 |
+
stroke="white"
|
129 |
+
tickLabelProps={() => ({
|
130 |
+
fill: "white",
|
131 |
+
fontSize: 11,
|
132 |
+
textAnchor: "end",
|
133 |
+
dy: "0.3em",
|
134 |
+
dx: "-0.25em",
|
135 |
+
fontWeight: "bold",
|
136 |
+
})}
|
137 |
+
/>
|
138 |
+
</Group>
|
139 |
+
</svg>
|
140 |
+
);
|
141 |
+
}
|
src/components/charts/singleChart/SimpleChartTripPurpose.tsx
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useMemo, useState } from "react";
|
2 |
+
import { Bar } from "@visx/shape";
|
3 |
+
import { Group } from "@visx/group";
|
4 |
+
import { LinearGradient } from "@visx/gradient";
|
5 |
+
import { AxisBottom, AxisLeft } from "@visx/axis";
|
6 |
+
import { scaleBand, scaleLinear } from "@visx/scale";
|
7 |
+
|
8 |
+
const verticalMargin = 20;
|
9 |
+
const leftMargin = 50;
|
10 |
+
const barPadding = 4;
|
11 |
+
|
12 |
+
interface tripPurpose {
|
13 |
+
trip_purpose_group: string;
|
14 |
+
percentage: number;
|
15 |
+
}
|
16 |
+
export type BarsProps = {
|
17 |
+
data: tripPurpose[];
|
18 |
+
width: number;
|
19 |
+
height: number;
|
20 |
+
events?: boolean;
|
21 |
+
};
|
22 |
+
|
23 |
+
const getTripPurposeValue = (d: tripPurpose) => Number(d.percentage);
|
24 |
+
|
25 |
+
export default function SimpleChartTripPurpose({
|
26 |
+
data,
|
27 |
+
width,
|
28 |
+
height,
|
29 |
+
events = false,
|
30 |
+
}: BarsProps) {
|
31 |
+
const xMax = width - leftMargin;
|
32 |
+
const yMax = height - verticalMargin - 20;
|
33 |
+
|
34 |
+
const xScale = useMemo(
|
35 |
+
() =>
|
36 |
+
scaleBand<number>({
|
37 |
+
range: [0, xMax],
|
38 |
+
round: true,
|
39 |
+
domain: data.map((d: any) => d.trip_purpose_group),
|
40 |
+
paddingInner: 0.2,
|
41 |
+
paddingOuter: 0.1,
|
42 |
+
}),
|
43 |
+
[xMax]
|
44 |
+
);
|
45 |
+
const yScale = useMemo(
|
46 |
+
() =>
|
47 |
+
scaleLinear<number>({
|
48 |
+
range: [yMax, 0],
|
49 |
+
round: true,
|
50 |
+
domain: [0, Math.max(...data.map(getTripPurposeValue))],
|
51 |
+
}),
|
52 |
+
[yMax]
|
53 |
+
);
|
54 |
+
|
55 |
+
const [isHover, setIsHover] = useState(Array(data.length).fill(false));
|
56 |
+
|
57 |
+
return width < 10 ? null : (
|
58 |
+
<svg
|
59 |
+
width={width}
|
60 |
+
height={height}
|
61 |
+
style={{
|
62 |
+
borderBottomLeftRadius: "14px",
|
63 |
+
borderBottomRightRadius: "14px",
|
64 |
+
boxShadow: "0 0 8px rgba(0, 0, 0, 0.2)",
|
65 |
+
}}
|
66 |
+
>
|
67 |
+
<LinearGradient from="#351CAB" to="#621A61" id="teal" />
|
68 |
+
<rect width={width} height={height} fill="url(#teal)" />
|
69 |
+
<Group top={verticalMargin}>
|
70 |
+
{data.map((d: any, index: any) => {
|
71 |
+
const barWidth = xScale.bandwidth();
|
72 |
+
const barHeight = yMax - (yScale(d.percentage) ?? 0);
|
73 |
+
const barX = xScale(d.trip_purpose_group) ?? 0;
|
74 |
+
const barY = yMax - barHeight;
|
75 |
+
return (
|
76 |
+
<Bar
|
77 |
+
key={`bar-${d.trip_purpose_group}`}
|
78 |
+
className={isHover[index] ? "hovered" : ""}
|
79 |
+
style={{
|
80 |
+
cursor: "pointer",
|
81 |
+
transition: "all 0.5s",
|
82 |
+
opacity: "1",
|
83 |
+
}}
|
84 |
+
x={barX + 35}
|
85 |
+
y={barY - 10}
|
86 |
+
rx={5}
|
87 |
+
ry={5}
|
88 |
+
width={barWidth - barPadding}
|
89 |
+
height={barHeight}
|
90 |
+
fill={isHover[index] ? "#FFFFFF" : "#AAB1FF"}
|
91 |
+
onClick={() => {
|
92 |
+
if (events) alert(`clicked: ${JSON.stringify(d)}`);
|
93 |
+
}}
|
94 |
+
onMouseMove={() => {
|
95 |
+
const updatedHover = [...isHover];
|
96 |
+
updatedHover[index] = true;
|
97 |
+
setIsHover(updatedHover);
|
98 |
+
}}
|
99 |
+
onMouseLeave={() => {
|
100 |
+
const updatedHover = [...isHover];
|
101 |
+
updatedHover[index] = false;
|
102 |
+
setIsHover(updatedHover);
|
103 |
+
}}
|
104 |
+
/>
|
105 |
+
);
|
106 |
+
})}
|
107 |
+
<AxisBottom
|
108 |
+
top={height - verticalMargin - 30}
|
109 |
+
left={leftMargin - 15}
|
110 |
+
scale={xScale}
|
111 |
+
tickStroke="white"
|
112 |
+
stroke="white"
|
113 |
+
strokeWidth={2}
|
114 |
+
tickLabelProps={() => ({
|
115 |
+
fill: "white",
|
116 |
+
fontSize: 10,
|
117 |
+
textAnchor: "middle",
|
118 |
+
fontWeight: "bold",
|
119 |
+
})}
|
120 |
+
/>
|
121 |
+
<AxisLeft
|
122 |
+
left={leftMargin - 15}
|
123 |
+
top={verticalMargin - 30}
|
124 |
+
scale={yScale}
|
125 |
+
tickFormat={(value) => value.toString()}
|
126 |
+
tickStroke="white"
|
127 |
+
strokeWidth={2}
|
128 |
+
stroke="white"
|
129 |
+
tickLabelProps={() => ({
|
130 |
+
fill: "white",
|
131 |
+
fontSize: 11,
|
132 |
+
textAnchor: "end",
|
133 |
+
dy: "0.3em",
|
134 |
+
dx: "-0.25em",
|
135 |
+
fontWeight: "bold",
|
136 |
+
})}
|
137 |
+
/>
|
138 |
+
</Group>
|
139 |
+
</svg>
|
140 |
+
);
|
141 |
+
}
|
src/components/charts/tripeChart/DayTypeGraph.tsx
ADDED
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from "react";
|
2 |
+
import { Group } from "@visx/group";
|
3 |
+
import { BarGroup } from "@visx/shape";
|
4 |
+
import { AxisBottom, AxisLeft } from "@visx/axis";
|
5 |
+
import { scaleBand, scaleLinear, scaleOrdinal } from "@visx/scale";
|
6 |
+
import { LinearGradient } from "@visx/gradient";
|
7 |
+
|
8 |
+
const GRADIENT_ID = "brush_gradient";
|
9 |
+
|
10 |
+
export const green = "#ffffff";
|
11 |
+
export const background = "#ff452f";
|
12 |
+
export const background2 = "#ff8677";
|
13 |
+
|
14 |
+
export type BarGroupProps = {
|
15 |
+
data:
|
16 |
+
| {
|
17 |
+
day_type: string;
|
18 |
+
hour: number;
|
19 |
+
percentage: number;
|
20 |
+
}[]
|
21 |
+
| undefined;
|
22 |
+
width: number;
|
23 |
+
height: number;
|
24 |
+
margin?: { top: number; right: number; bottom: number; left: number };
|
25 |
+
events?: boolean;
|
26 |
+
};
|
27 |
+
|
28 |
+
const defaultMargin = { top: 40, right: 0, bottom: 40, left: 60 };
|
29 |
+
|
30 |
+
export default function DayTypeGraph({
|
31 |
+
data,
|
32 |
+
width,
|
33 |
+
height,
|
34 |
+
events = false,
|
35 |
+
margin = defaultMargin,
|
36 |
+
}: BarGroupProps) {
|
37 |
+
interface NewHourlyPercentage {
|
38 |
+
hour: number;
|
39 |
+
SATURDAY: number;
|
40 |
+
SUNDAY_PUBLIC_HOLIDAY: number;
|
41 |
+
WORKING_DAY: number;
|
42 |
+
}
|
43 |
+
|
44 |
+
function processRawDataToGraphData(
|
45 |
+
data:
|
46 |
+
| {
|
47 |
+
day_type: string;
|
48 |
+
hour: number;
|
49 |
+
percentage: number;
|
50 |
+
}[]
|
51 |
+
| undefined
|
52 |
+
): NewHourlyPercentage[] {
|
53 |
+
const transformedData: NewHourlyPercentage[] = [];
|
54 |
+
|
55 |
+
if (!data || data === undefined) {
|
56 |
+
return transformedData;
|
57 |
+
}
|
58 |
+
for (let i = 0; i < 24; i++) {
|
59 |
+
const newObj: NewHourlyPercentage = {
|
60 |
+
hour: i,
|
61 |
+
SATURDAY: 0,
|
62 |
+
SUNDAY_PUBLIC_HOLIDAY: 0,
|
63 |
+
WORKING_DAY: 0,
|
64 |
+
};
|
65 |
+
|
66 |
+
transformedData.push(newObj);
|
67 |
+
}
|
68 |
+
|
69 |
+
for (let index = 0; data[index]; index++) {
|
70 |
+
if (data[index].day_type === "SATURDAY") {
|
71 |
+
transformedData[data[index].hour].hour = data[index].hour;
|
72 |
+
transformedData[data[index].hour].SATURDAY = data[index].percentage;
|
73 |
+
} else if (data[index].day_type === "SUNDAY_&_PUBLIC_HOLIDAY") {
|
74 |
+
transformedData[data[index].hour].hour = data[index].hour;
|
75 |
+
transformedData[data[index].hour].SUNDAY_PUBLIC_HOLIDAY =
|
76 |
+
data[index].percentage;
|
77 |
+
} else {
|
78 |
+
transformedData[data[index].hour].hour = data[index].hour;
|
79 |
+
transformedData[data[index].hour].WORKING_DAY = data[index].percentage;
|
80 |
+
}
|
81 |
+
}
|
82 |
+
|
83 |
+
return transformedData;
|
84 |
+
}
|
85 |
+
|
86 |
+
const [currentData, setCurrentData] = useState<NewHourlyPercentage[]>(
|
87 |
+
processRawDataToGraphData(data)
|
88 |
+
);
|
89 |
+
|
90 |
+
const keys = ["SATURDAY", "SUNDAY_PUBLIC_HOLIDAY", "WORKING_DAY"];
|
91 |
+
|
92 |
+
const getDate = (d: any) => d.hour;
|
93 |
+
|
94 |
+
const hourScale = scaleBand<number>({
|
95 |
+
domain: currentData.map((d) => d.hour),
|
96 |
+
padding: 0.2,
|
97 |
+
});
|
98 |
+
const dayScale = scaleBand<string>({
|
99 |
+
domain: keys,
|
100 |
+
padding: 0.2,
|
101 |
+
});
|
102 |
+
const percentageScale = scaleLinear<number>({
|
103 |
+
domain: [0, 0.12],
|
104 |
+
});
|
105 |
+
|
106 |
+
const colorScale = scaleOrdinal<string, string>({
|
107 |
+
domain: keys,
|
108 |
+
range: ["#AAB1FF", "#6A4CEE", "#FABA00"],
|
109 |
+
});
|
110 |
+
|
111 |
+
const xMax = width - margin.left - margin.right;
|
112 |
+
const yMax = height - margin.top - margin.bottom;
|
113 |
+
|
114 |
+
hourScale.rangeRound([0, xMax]);
|
115 |
+
dayScale.rangeRound([0, hourScale.bandwidth()]);
|
116 |
+
percentageScale.range([yMax, 0]);
|
117 |
+
|
118 |
+
return width < 10 ? null : (
|
119 |
+
<svg
|
120 |
+
width={width}
|
121 |
+
height={height}
|
122 |
+
style={{
|
123 |
+
borderBottomLeftRadius: "14px",
|
124 |
+
borderBottomRightRadius: "14px",
|
125 |
+
boxShadow: "0 0 8px rgba(0, 0, 0, 0.2)",
|
126 |
+
}}
|
127 |
+
>
|
128 |
+
<rect x={0} y={0} width={width} height={height} />
|
129 |
+
<LinearGradient
|
130 |
+
id={GRADIENT_ID}
|
131 |
+
from={background}
|
132 |
+
to={background2}
|
133 |
+
rotate={45}
|
134 |
+
/>
|
135 |
+
<Group top={margin.top} left={margin.left}>
|
136 |
+
<BarGroup
|
137 |
+
data={currentData}
|
138 |
+
keys={keys}
|
139 |
+
height={yMax}
|
140 |
+
x0={getDate}
|
141 |
+
x0Scale={hourScale}
|
142 |
+
x1Scale={dayScale}
|
143 |
+
yScale={percentageScale}
|
144 |
+
color={colorScale}
|
145 |
+
>
|
146 |
+
{(barGroups) =>
|
147 |
+
barGroups.map((barGroup) => (
|
148 |
+
<Group
|
149 |
+
key={`bar-group-${barGroup.index}-${barGroup.x0}`}
|
150 |
+
left={barGroup.x0}
|
151 |
+
>
|
152 |
+
{barGroup.bars.map((bar) => (
|
153 |
+
<rect
|
154 |
+
key={`bar-group-bar-${barGroup.index}-${bar.index}-${bar.value}-${bar.key}`}
|
155 |
+
x={bar.x}
|
156 |
+
y={bar.y}
|
157 |
+
width={bar.width}
|
158 |
+
height={bar.height}
|
159 |
+
fill={bar.color}
|
160 |
+
rx={4}
|
161 |
+
onClick={() => {
|
162 |
+
if (!events) return;
|
163 |
+
const { key, value } = bar;
|
164 |
+
alert(JSON.stringify({ key, value }));
|
165 |
+
}}
|
166 |
+
/>
|
167 |
+
))}
|
168 |
+
</Group>
|
169 |
+
))
|
170 |
+
}
|
171 |
+
</BarGroup>
|
172 |
+
</Group>
|
173 |
+
<AxisLeft
|
174 |
+
left={margin.left - 10}
|
175 |
+
top={margin.top}
|
176 |
+
scale={percentageScale}
|
177 |
+
numTicks={5}
|
178 |
+
stroke="#ffffff"
|
179 |
+
tickLabelProps={() => ({
|
180 |
+
fill: "#ffffff",
|
181 |
+
fontSize: 11,
|
182 |
+
textAnchor: "end",
|
183 |
+
dy: "0.3em",
|
184 |
+
dx: "-0.25em",
|
185 |
+
fontWeight: "bold",
|
186 |
+
})}
|
187 |
+
/>
|
188 |
+
<AxisBottom
|
189 |
+
top={yMax + margin.top}
|
190 |
+
left={margin.left}
|
191 |
+
scale={hourScale}
|
192 |
+
stroke={green}
|
193 |
+
tickStroke={green}
|
194 |
+
hideAxisLine
|
195 |
+
tickLabelProps={{
|
196 |
+
fill: "#ffffff",
|
197 |
+
fontSize: 11,
|
198 |
+
textAnchor: "middle",
|
199 |
+
fontWeight: "bold",
|
200 |
+
}}
|
201 |
+
/>
|
202 |
+
</svg>
|
203 |
+
);
|
204 |
+
}
|
src/components/charts/wordCloudChart/CloudWord.tsx
ADDED
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState } from "react";
|
2 |
+
import { Text } from "@visx/text";
|
3 |
+
import { scaleLog } from "@visx/scale";
|
4 |
+
import Wordcloud from "@visx/wordcloud/lib/Wordcloud";
|
5 |
+
|
6 |
+
interface ExampleProps {
|
7 |
+
demographicWords: string;
|
8 |
+
width: number;
|
9 |
+
height: number;
|
10 |
+
showControls?: boolean;
|
11 |
+
}
|
12 |
+
|
13 |
+
export interface WordData {
|
14 |
+
text: string;
|
15 |
+
value: number;
|
16 |
+
}
|
17 |
+
|
18 |
+
export default function CloudWord({
|
19 |
+
demographicWords,
|
20 |
+
width,
|
21 |
+
height,
|
22 |
+
showControls = true,
|
23 |
+
}: ExampleProps) {
|
24 |
+
function wordFreq(text: string): WordData[] {
|
25 |
+
const words: string[] = text.replace(/\./g, "").split(/\s/);
|
26 |
+
const freqMap: Record<string, number> = {};
|
27 |
+
|
28 |
+
for (const w of words) {
|
29 |
+
if (!freqMap[w]) freqMap[w] = 0;
|
30 |
+
freqMap[w] += 1;
|
31 |
+
}
|
32 |
+
return Object.keys(freqMap).map((word) => ({
|
33 |
+
text: word,
|
34 |
+
value: freqMap[word],
|
35 |
+
}));
|
36 |
+
}
|
37 |
+
|
38 |
+
function getRotationDegree() {
|
39 |
+
const rand = Math.random();
|
40 |
+
const degree = rand > 0.5 ? 60 : -60;
|
41 |
+
return rand * degree;
|
42 |
+
}
|
43 |
+
|
44 |
+
const words = wordFreq(demographicWords);
|
45 |
+
|
46 |
+
const fontScale = scaleLog({
|
47 |
+
domain: [
|
48 |
+
Math.min(...words.map((w) => w.value)),
|
49 |
+
Math.max(...words.map((w) => w.value)),
|
50 |
+
],
|
51 |
+
range: [10, 100],
|
52 |
+
});
|
53 |
+
const fontSizeSetter = (datum: WordData) => fontScale(datum.value);
|
54 |
+
|
55 |
+
const fixedValueGenerator = () => 0.5;
|
56 |
+
|
57 |
+
type SpiralType = "archimedean" | "rectangular";
|
58 |
+
|
59 |
+
// eslint-disable-next-line
|
60 |
+
const [spiralType, setSpiralType] = useState<SpiralType>("archimedean");
|
61 |
+
// eslint-disable-next-line
|
62 |
+
const [withRotation, setWithRotation] = useState(false);
|
63 |
+
|
64 |
+
return (
|
65 |
+
<div className="wordcloud">
|
66 |
+
<Wordcloud
|
67 |
+
words={words}
|
68 |
+
width={width}
|
69 |
+
height={height}
|
70 |
+
fontSize={fontSizeSetter}
|
71 |
+
font={"Impact"}
|
72 |
+
padding={2}
|
73 |
+
spiral={spiralType}
|
74 |
+
rotate={withRotation ? getRotationDegree : 0}
|
75 |
+
random={fixedValueGenerator}
|
76 |
+
>
|
77 |
+
{(cloudWords) =>
|
78 |
+
cloudWords.map((w, i) => (
|
79 |
+
<Text
|
80 |
+
key={w.text}
|
81 |
+
fill={"#00000"}
|
82 |
+
textAnchor={"middle"}
|
83 |
+
transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
|
84 |
+
fontSize={w.size}
|
85 |
+
fontFamily={w.font}
|
86 |
+
>
|
87 |
+
{w.text}
|
88 |
+
</Text>
|
89 |
+
))
|
90 |
+
}
|
91 |
+
</Wordcloud>
|
92 |
+
<style>{`
|
93 |
+
.wordcloud {
|
94 |
+
display: flex;
|
95 |
+
flex-direction: column;
|
96 |
+
user-select: none;
|
97 |
+
}
|
98 |
+
.wordcloud svg {
|
99 |
+
margin: 1rem 0;
|
100 |
+
cursor: pointer;
|
101 |
+
}
|
102 |
+
.wordcloud label {
|
103 |
+
display: inline-flex;
|
104 |
+
align-items: center;
|
105 |
+
font-size: 14px;
|
106 |
+
margin-right: 8px;
|
107 |
+
}
|
108 |
+
.wordcloud textarea {
|
109 |
+
min-height: 100px;
|
110 |
+
}
|
111 |
+
`}</style>
|
112 |
+
</div>
|
113 |
+
);
|
114 |
+
}
|
src/components/generalInformation/dropdown/DropDown.tsx
ADDED
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { Fragment } from "react";
|
2 |
+
import { Listbox, Transition } from "@headlessui/react";
|
3 |
+
import { CheckIcon, ChevronUpDownIcon } from "@heroicons/react/20/solid";
|
4 |
+
import { Poi } from "../../../redux/types/Poi";
|
5 |
+
|
6 |
+
type DropDownProps = {
|
7 |
+
poiList: Poi[];
|
8 |
+
selectedPoi: Poi;
|
9 |
+
setSelectedPoi: React.Dispatch<React.SetStateAction<Poi>>;
|
10 |
+
};
|
11 |
+
|
12 |
+
const DropDown: React.FC<DropDownProps> = ({
|
13 |
+
poiList,
|
14 |
+
selectedPoi,
|
15 |
+
setSelectedPoi,
|
16 |
+
}) => {
|
17 |
+
return (
|
18 |
+
<div className="w-72" style={{ fontFamily: "airbnb_light" }}>
|
19 |
+
<Listbox value={selectedPoi} onChange={setSelectedPoi}>
|
20 |
+
<div className="relative mt-1">
|
21 |
+
<Listbox.Button
|
22 |
+
className="relative w-full cursor-pointer rounded-lg bg-white py-2 pl-3 pr-10 text-left shadow-md focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300 sm:text-sm border border-gray-300" // Add border classes here
|
23 |
+
>
|
24 |
+
<span className="block truncate">{selectedPoi.address}</span>
|
25 |
+
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
26 |
+
<ChevronUpDownIcon
|
27 |
+
className="h-5 w-5 text-gray-400"
|
28 |
+
aria-hidden="true"
|
29 |
+
/>
|
30 |
+
</span>
|
31 |
+
</Listbox.Button>
|
32 |
+
<Transition
|
33 |
+
as={Fragment}
|
34 |
+
leave="transition ease-in duration-100"
|
35 |
+
leaveFrom="opacity-100"
|
36 |
+
leaveTo="opacity-0"
|
37 |
+
>
|
38 |
+
<Listbox.Options
|
39 |
+
className="absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm border border-gray-300" // Add border classes here
|
40 |
+
style={{ zIndex: 9999 }}
|
41 |
+
>
|
42 |
+
{poiList.map((poi, index) => (
|
43 |
+
<Listbox.Option
|
44 |
+
key={index}
|
45 |
+
className={({ active }) =>
|
46 |
+
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
|
47 |
+
active ? "bg-[#ECEEFF] text-[#00000]" : "text-gray-900"
|
48 |
+
}`
|
49 |
+
}
|
50 |
+
value={poi}
|
51 |
+
>
|
52 |
+
{({ selected }) => (
|
53 |
+
<>
|
54 |
+
<span
|
55 |
+
className={`block truncate ${
|
56 |
+
selected ? "font-medium" : "font-normal"
|
57 |
+
}`}
|
58 |
+
>
|
59 |
+
{poi.address}
|
60 |
+
</span>
|
61 |
+
{selected ? (
|
62 |
+
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-[#9BA4FF]">
|
63 |
+
<CheckIcon className="h-5 w-5" aria-hidden="true" />
|
64 |
+
</span>
|
65 |
+
) : null}
|
66 |
+
</>
|
67 |
+
)}
|
68 |
+
</Listbox.Option>
|
69 |
+
))}
|
70 |
+
</Listbox.Options>
|
71 |
+
</Transition>
|
72 |
+
</div>
|
73 |
+
</Listbox>
|
74 |
+
</div>
|
75 |
+
);
|
76 |
+
};
|
77 |
+
|
78 |
+
export default DropDown;
|
src/components/generalInformation/legend/Legend.tsx
ADDED
@@ -0,0 +1,343 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from "react";
|
2 |
+
import { format } from "@visx/vendor/d3-format";
|
3 |
+
import {
|
4 |
+
scaleLinear,
|
5 |
+
scaleOrdinal,
|
6 |
+
scaleThreshold,
|
7 |
+
scaleQuantile,
|
8 |
+
} from "@visx/scale";
|
9 |
+
import { GlyphStar, GlyphWye, GlyphTriangle, GlyphDiamond } from "@visx/glyph";
|
10 |
+
import {
|
11 |
+
Legend,
|
12 |
+
LegendLinear,
|
13 |
+
LegendQuantile,
|
14 |
+
LegendOrdinal,
|
15 |
+
LegendSize,
|
16 |
+
LegendThreshold,
|
17 |
+
LegendItem,
|
18 |
+
LegendLabel,
|
19 |
+
} from "@visx/legend";
|
20 |
+
|
21 |
+
const oneDecimalFormat = format(".1f");
|
22 |
+
|
23 |
+
const sizeScale = scaleLinear<number>({
|
24 |
+
domain: [0, 10],
|
25 |
+
range: [5, 13],
|
26 |
+
});
|
27 |
+
|
28 |
+
const sizeColorScale = scaleLinear({
|
29 |
+
domain: [0, 10],
|
30 |
+
range: ["#75fcfc", "#3236b8"],
|
31 |
+
});
|
32 |
+
|
33 |
+
const quantileScale = scaleQuantile({
|
34 |
+
domain: [0, 0.15],
|
35 |
+
range: ["#eb4d70", "#f19938", "#6ce18b", "#78f6ef", "#9096f8"],
|
36 |
+
});
|
37 |
+
|
38 |
+
const linearScale = scaleLinear({
|
39 |
+
domain: [0, 10],
|
40 |
+
range: ["#ed4fbb", "#e9a039"],
|
41 |
+
});
|
42 |
+
|
43 |
+
const thresholdScale = scaleThreshold({
|
44 |
+
domain: [0.01, 0.02, 0.04, 0.06, 0.08],
|
45 |
+
range: ["#f2f0f7", "#dadaeb", "#bcbddc", "#9e9ac8", "#756bb1", "#54278f"],
|
46 |
+
});
|
47 |
+
|
48 |
+
const ordinalColorScale = scaleOrdinal({
|
49 |
+
domain: ["a", "b", "c", "d"],
|
50 |
+
range: ["#66d981", "#71f5ef", "#4899f1", "#7d81f6"],
|
51 |
+
});
|
52 |
+
|
53 |
+
const ordinalColor2Scale = scaleOrdinal({
|
54 |
+
domain: ["a", "b", "c", "d"],
|
55 |
+
range: ["#fae856", "#f29b38", "#e64357", "#8386f7"],
|
56 |
+
});
|
57 |
+
|
58 |
+
const shapeScale = scaleOrdinal<string, React.FC | React.ReactNode>({
|
59 |
+
domain: ["a", "b", "c", "d", "e"],
|
60 |
+
range: [
|
61 |
+
<GlyphStar key="a" size={50} top={50 / 6} left={50 / 6} fill="#dd59b8" />,
|
62 |
+
<GlyphWye key="b" size={50} top={50 / 6} left={50 / 6} fill="#de6a9a" />,
|
63 |
+
<GlyphTriangle
|
64 |
+
key="c"
|
65 |
+
size={50}
|
66 |
+
top={50 / 6}
|
67 |
+
left={50 / 6}
|
68 |
+
fill="#de7d7b"
|
69 |
+
/>,
|
70 |
+
<GlyphDiamond
|
71 |
+
key="d"
|
72 |
+
size={50}
|
73 |
+
top={50 / 6}
|
74 |
+
left={50 / 6}
|
75 |
+
fill="#df905f"
|
76 |
+
/>,
|
77 |
+
() => (
|
78 |
+
<text key="e" fontSize="12" dy="1em" dx=".33em" fill="#e0a346">
|
79 |
+
$
|
80 |
+
</text>
|
81 |
+
),
|
82 |
+
],
|
83 |
+
});
|
84 |
+
|
85 |
+
function LegendDemo({
|
86 |
+
title,
|
87 |
+
children,
|
88 |
+
}: {
|
89 |
+
title: string;
|
90 |
+
children: React.ReactNode;
|
91 |
+
}) {
|
92 |
+
return (
|
93 |
+
<div className="legend">
|
94 |
+
<div className="title">{title}</div>
|
95 |
+
{children}
|
96 |
+
<style>{`
|
97 |
+
.legend {
|
98 |
+
line-height: 0.9em;
|
99 |
+
color: #efefef;
|
100 |
+
font-size: 10px;
|
101 |
+
font-family: arial;
|
102 |
+
padding: 10px 10px;
|
103 |
+
float: left;
|
104 |
+
border: 1px solid rgba(255, 255, 255, 0.3);
|
105 |
+
border-radius: 8px;
|
106 |
+
margin: 5px 5px;
|
107 |
+
}
|
108 |
+
.title {
|
109 |
+
font-size: 12px;
|
110 |
+
margin-bottom: 10px;
|
111 |
+
font-weight: 100;
|
112 |
+
}
|
113 |
+
`}</style>
|
114 |
+
</div>
|
115 |
+
);
|
116 |
+
}
|
117 |
+
|
118 |
+
const legendGlyphSize = 15;
|
119 |
+
|
120 |
+
export default function LegendChart({
|
121 |
+
width,
|
122 |
+
height,
|
123 |
+
events = false,
|
124 |
+
}: {
|
125 |
+
width?: number;
|
126 |
+
height?: number;
|
127 |
+
events?: boolean;
|
128 |
+
}) {
|
129 |
+
return (
|
130 |
+
<div className="legends" style={{ width: width, height: height }}>
|
131 |
+
<LegendDemo title="Size">
|
132 |
+
<LegendSize scale={sizeScale}>
|
133 |
+
{(labels) =>
|
134 |
+
labels.map((label) => {
|
135 |
+
const size = sizeScale(label.datum) ?? 0;
|
136 |
+
const color = sizeColorScale(label.datum);
|
137 |
+
return (
|
138 |
+
<LegendItem
|
139 |
+
key={`legend-${label.text}-${label.index}`}
|
140 |
+
onClick={() => {
|
141 |
+
if (events) alert(`clicked: ${JSON.stringify(label)}`);
|
142 |
+
}}
|
143 |
+
>
|
144 |
+
<svg width={size} height={size} style={{ margin: "5px 0" }}>
|
145 |
+
<circle
|
146 |
+
fill={color}
|
147 |
+
r={size / 2}
|
148 |
+
cx={size / 2}
|
149 |
+
cy={size / 2}
|
150 |
+
/>
|
151 |
+
</svg>
|
152 |
+
<LegendLabel align="left" margin="0 4px">
|
153 |
+
{label.text}
|
154 |
+
</LegendLabel>
|
155 |
+
</LegendItem>
|
156 |
+
);
|
157 |
+
})
|
158 |
+
}
|
159 |
+
</LegendSize>
|
160 |
+
</LegendDemo>
|
161 |
+
<LegendDemo title="Quantile">
|
162 |
+
<LegendQuantile scale={quantileScale}>
|
163 |
+
{(labels) =>
|
164 |
+
labels.map((label, i) => (
|
165 |
+
<LegendItem
|
166 |
+
key={`legend-${i}`}
|
167 |
+
onClick={() => {
|
168 |
+
if (events) alert(`clicked: ${JSON.stringify(label)}`);
|
169 |
+
}}
|
170 |
+
>
|
171 |
+
<svg
|
172 |
+
width={legendGlyphSize}
|
173 |
+
height={legendGlyphSize}
|
174 |
+
style={{ margin: "2px 0" }}
|
175 |
+
>
|
176 |
+
<circle
|
177 |
+
fill={label.value}
|
178 |
+
r={legendGlyphSize / 2}
|
179 |
+
cx={legendGlyphSize / 2}
|
180 |
+
cy={legendGlyphSize / 2}
|
181 |
+
/>
|
182 |
+
</svg>
|
183 |
+
<LegendLabel align="left" margin="0 4px">
|
184 |
+
{label.text}
|
185 |
+
</LegendLabel>
|
186 |
+
</LegendItem>
|
187 |
+
))
|
188 |
+
}
|
189 |
+
</LegendQuantile>
|
190 |
+
</LegendDemo>
|
191 |
+
<LegendDemo title="Linear">
|
192 |
+
<LegendLinear
|
193 |
+
scale={linearScale}
|
194 |
+
labelFormat={(d, i) => (i % 2 === 0 ? oneDecimalFormat(d) : "")}
|
195 |
+
>
|
196 |
+
{(labels) =>
|
197 |
+
labels.map((label, i) => (
|
198 |
+
<LegendItem
|
199 |
+
key={`legend-quantile-${i}`}
|
200 |
+
onClick={() => {
|
201 |
+
if (events) alert(`clicked: ${JSON.stringify(label)}`);
|
202 |
+
}}
|
203 |
+
>
|
204 |
+
<svg
|
205 |
+
width={legendGlyphSize}
|
206 |
+
height={legendGlyphSize}
|
207 |
+
style={{ margin: "2px 0" }}
|
208 |
+
>
|
209 |
+
<circle
|
210 |
+
fill={label.value}
|
211 |
+
r={legendGlyphSize / 2}
|
212 |
+
cx={legendGlyphSize / 2}
|
213 |
+
cy={legendGlyphSize / 2}
|
214 |
+
/>
|
215 |
+
</svg>
|
216 |
+
<LegendLabel align="left" margin="0 4px">
|
217 |
+
{label.text}
|
218 |
+
</LegendLabel>
|
219 |
+
</LegendItem>
|
220 |
+
))
|
221 |
+
}
|
222 |
+
</LegendLinear>
|
223 |
+
</LegendDemo>
|
224 |
+
<LegendDemo title="Threshold">
|
225 |
+
<LegendThreshold scale={thresholdScale}>
|
226 |
+
{(labels) =>
|
227 |
+
labels.reverse().map((label, i) => (
|
228 |
+
<LegendItem
|
229 |
+
key={`legend-quantile-${i}`}
|
230 |
+
margin="1px 0"
|
231 |
+
onClick={() => {
|
232 |
+
if (events) alert(`clicked: ${JSON.stringify(label)}`);
|
233 |
+
}}
|
234 |
+
>
|
235 |
+
<svg width={legendGlyphSize} height={legendGlyphSize}>
|
236 |
+
<rect
|
237 |
+
fill={label.value}
|
238 |
+
width={legendGlyphSize}
|
239 |
+
height={legendGlyphSize}
|
240 |
+
/>
|
241 |
+
</svg>
|
242 |
+
<LegendLabel align="left" margin="2px 0 0 10px">
|
243 |
+
{label.text}
|
244 |
+
</LegendLabel>
|
245 |
+
</LegendItem>
|
246 |
+
))
|
247 |
+
}
|
248 |
+
</LegendThreshold>
|
249 |
+
</LegendDemo>
|
250 |
+
<LegendDemo title="Ordinal">
|
251 |
+
<LegendOrdinal
|
252 |
+
scale={ordinalColorScale}
|
253 |
+
labelFormat={(label) => `${label.toUpperCase()}`}
|
254 |
+
>
|
255 |
+
{(labels) => (
|
256 |
+
<div style={{ display: "flex", flexDirection: "row" }}>
|
257 |
+
{labels.map((label, i) => (
|
258 |
+
<LegendItem
|
259 |
+
key={`legend-quantile-${i}`}
|
260 |
+
margin="0 5px"
|
261 |
+
onClick={() => {
|
262 |
+
if (events) alert(`clicked: ${JSON.stringify(label)}`);
|
263 |
+
}}
|
264 |
+
>
|
265 |
+
<svg width={legendGlyphSize} height={legendGlyphSize}>
|
266 |
+
<rect
|
267 |
+
fill={label.value}
|
268 |
+
width={legendGlyphSize}
|
269 |
+
height={legendGlyphSize}
|
270 |
+
/>
|
271 |
+
</svg>
|
272 |
+
<LegendLabel align="left" margin="0 0 0 4px">
|
273 |
+
{label.text}
|
274 |
+
</LegendLabel>
|
275 |
+
</LegendItem>
|
276 |
+
))}
|
277 |
+
</div>
|
278 |
+
)}
|
279 |
+
</LegendOrdinal>
|
280 |
+
</LegendDemo>
|
281 |
+
<LegendDemo title="Custom Legend">
|
282 |
+
<Legend scale={shapeScale}>
|
283 |
+
{(labels) => (
|
284 |
+
<div style={{ display: "flex", flexDirection: "row" }}>
|
285 |
+
{labels.map((label, i) => {
|
286 |
+
const color = ordinalColor2Scale(label.datum);
|
287 |
+
const shape = shapeScale(label.datum);
|
288 |
+
const isValidElement = React.isValidElement(shape);
|
289 |
+
return (
|
290 |
+
<LegendItem
|
291 |
+
key={`legend-quantile-${i}`}
|
292 |
+
margin="0 4px 0 0"
|
293 |
+
flexDirection="column"
|
294 |
+
onClick={() => {
|
295 |
+
const { datum, index } = label;
|
296 |
+
if (events)
|
297 |
+
alert(
|
298 |
+
`clicked: ${JSON.stringify({ datum, color, index })}`
|
299 |
+
);
|
300 |
+
}}
|
301 |
+
>
|
302 |
+
<svg
|
303 |
+
width={legendGlyphSize}
|
304 |
+
height={legendGlyphSize}
|
305 |
+
style={{ margin: "0 0 8px 0" }}
|
306 |
+
>
|
307 |
+
{isValidElement
|
308 |
+
? React.cloneElement(shape as React.ReactElement)
|
309 |
+
: React.createElement(
|
310 |
+
shape as React.ComponentType<{ fill: string }>,
|
311 |
+
{
|
312 |
+
fill: color,
|
313 |
+
}
|
314 |
+
)}
|
315 |
+
</svg>
|
316 |
+
<LegendLabel align="left" margin={0}>
|
317 |
+
{label.text}
|
318 |
+
</LegendLabel>
|
319 |
+
</LegendItem>
|
320 |
+
);
|
321 |
+
})}
|
322 |
+
</div>
|
323 |
+
)}
|
324 |
+
</Legend>
|
325 |
+
</LegendDemo>
|
326 |
+
|
327 |
+
<style>{`
|
328 |
+
.legends {
|
329 |
+
font-family: arial;
|
330 |
+
font-weight: 900;
|
331 |
+
background-color: black;
|
332 |
+
border-radius: 14px;
|
333 |
+
padding: 24px 24px 24px 32px;
|
334 |
+
overflow-y: auto;
|
335 |
+
flex-grow: 1;
|
336 |
+
}
|
337 |
+
.chart h2 {
|
338 |
+
margin-left: 10px;
|
339 |
+
}
|
340 |
+
`}</style>
|
341 |
+
</div>
|
342 |
+
);
|
343 |
+
}
|
src/components/main/GraphSections.tsx
ADDED
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from "react";
|
2 |
+
import { Poi } from "../../redux/types/Poi";
|
3 |
+
import DemographicsSection from "../../sections/demographics";
|
4 |
+
import DescriptionSection from "../../sections/description";
|
5 |
+
import FootfallSection from "../../sections/footfall";
|
6 |
+
import ModeSection from "../../sections/mode";
|
7 |
+
import InformationSection from "../../sections/information";
|
8 |
+
import TraficSection from "../../sections/trafic";
|
9 |
+
|
10 |
+
type GraphSectionsProps = {
|
11 |
+
selectedPoi: Poi;
|
12 |
+
isDemographicsSection: boolean;
|
13 |
+
isFootfallSection: boolean;
|
14 |
+
isModeSection: boolean;
|
15 |
+
isInformationSection: boolean;
|
16 |
+
isTraficSection: boolean;
|
17 |
+
};
|
18 |
+
|
19 |
+
const GraphSections = ({
|
20 |
+
selectedPoi,
|
21 |
+
isDemographicsSection,
|
22 |
+
isFootfallSection,
|
23 |
+
isModeSection,
|
24 |
+
isInformationSection,
|
25 |
+
isTraficSection,
|
26 |
+
}: GraphSectionsProps) => {
|
27 |
+
const renderSection = (state: boolean, section: JSX.Element) => {
|
28 |
+
return state ? section : null;
|
29 |
+
};
|
30 |
+
|
31 |
+
return (
|
32 |
+
<>
|
33 |
+
<DescriptionSection selectedPoi={selectedPoi} />
|
34 |
+
{renderSection(
|
35 |
+
isDemographicsSection,
|
36 |
+
<DemographicsSection selectedPoi={selectedPoi} />
|
37 |
+
)}
|
38 |
+
{renderSection(
|
39 |
+
isFootfallSection,
|
40 |
+
<FootfallSection selectedPoi={selectedPoi} />
|
41 |
+
)}
|
42 |
+
{renderSection(isModeSection, <ModeSection selectedPoi={selectedPoi} />)}
|
43 |
+
{renderSection(
|
44 |
+
isInformationSection,
|
45 |
+
<InformationSection selectedPoi={selectedPoi} />
|
46 |
+
)}
|
47 |
+
{renderSection(
|
48 |
+
isTraficSection,
|
49 |
+
<TraficSection selectedPoi={selectedPoi} />
|
50 |
+
)}
|
51 |
+
</>
|
52 |
+
);
|
53 |
+
};
|
54 |
+
|
55 |
+
export default GraphSections;
|
src/constants/main.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
export const DEMOGRAPHICS_SECTION = 1;
|
src/data/data.ts
ADDED
The diff for this file is too large to render.
See raw diff
|
|
src/declarations.d.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
declare module "*.png" {
|
2 |
+
const value: string;
|
3 |
+
export default value;
|
4 |
+
}
|
src/index.tsx
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React from "react";
|
2 |
+
import ReactDOM from "react-dom/client";
|
3 |
+
import App from "./App";
|
4 |
+
import store from "./redux/store/store";
|
5 |
+
import { Provider } from "react-redux";
|
6 |
+
import "./styles/global/index.css";
|
7 |
+
import "./assets/fonts/fonts.css";
|
8 |
+
// import * as dotenv from "dotenv";
|
9 |
+
|
10 |
+
// dotenv.config();
|
11 |
+
|
12 |
+
const root = ReactDOM.createRoot(
|
13 |
+
document.getElementById("root") as HTMLElement
|
14 |
+
);
|
15 |
+
root.render(
|
16 |
+
<Provider store={store}>
|
17 |
+
<App />
|
18 |
+
</Provider>
|
19 |
+
);
|
src/redux/actions/poiActions.ts
ADDED
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createAction } from "@reduxjs/toolkit";
|
2 |
+
import { Poi } from "../types/Poi";
|
3 |
+
|
4 |
+
export const fetchPoiRequest = createAction("poi/fetchPoiRequest");
|
5 |
+
export const fetchPoiSuccess = createAction<Poi[]>("poi/fetchPoiSuccess");
|
6 |
+
export const fetchPoiFailure = createAction<string>("poi/fetchPoiFailure");
|
src/redux/reducers/poiReducer.ts
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createReducer } from "@reduxjs/toolkit";
|
2 |
+
import { Poi } from "../types/Poi";
|
3 |
+
import {
|
4 |
+
fetchPoiRequest,
|
5 |
+
fetchPoiSuccess,
|
6 |
+
fetchPoiFailure,
|
7 |
+
} from "../actions/poiActions";
|
8 |
+
|
9 |
+
interface PoiState {
|
10 |
+
data: Poi[] | null;
|
11 |
+
loading: boolean;
|
12 |
+
error: string | null;
|
13 |
+
}
|
14 |
+
|
15 |
+
const initialState: PoiState = {
|
16 |
+
data: null,
|
17 |
+
loading: false,
|
18 |
+
error: null,
|
19 |
+
};
|
20 |
+
|
21 |
+
const poiReducer = createReducer(initialState, (builder) => {
|
22 |
+
builder
|
23 |
+
.addCase(fetchPoiRequest, (state) => {
|
24 |
+
state.loading = true;
|
25 |
+
state.error = null;
|
26 |
+
})
|
27 |
+
.addCase(fetchPoiSuccess, (state, action) => {
|
28 |
+
state.loading = false;
|
29 |
+
state.data = action.payload;
|
30 |
+
})
|
31 |
+
.addCase(fetchPoiFailure, (state, action) => {
|
32 |
+
state.loading = false;
|
33 |
+
state.error = action.payload;
|
34 |
+
});
|
35 |
+
});
|
36 |
+
|
37 |
+
export default poiReducer;
|
src/redux/sagas/poiSaga.ts
ADDED
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { put, takeLatest, call } from "redux-saga/effects";
|
2 |
+
import axios, { AxiosResponse, AxiosError, AxiosRequestConfig } from "axios";
|
3 |
+
import {
|
4 |
+
fetchPoiRequest,
|
5 |
+
fetchPoiSuccess,
|
6 |
+
fetchPoiFailure,
|
7 |
+
} from "../actions/poiActions";
|
8 |
+
import { Poi } from "../types/Poi";
|
9 |
+
|
10 |
+
function* fetchPoiData() {
|
11 |
+
try {
|
12 |
+
const config: AxiosRequestConfig = {
|
13 |
+
headers: {
|
14 |
+
key: "", // no more credits
|
15 |
+
},
|
16 |
+
};
|
17 |
+
|
18 |
+
const response: AxiosResponse = yield call(() =>
|
19 |
+
axios.get("https://api.seiki.co/v1/pois", config)
|
20 |
+
);
|
21 |
+
const data = response.data as Poi[];
|
22 |
+
yield put(fetchPoiSuccess(data));
|
23 |
+
} catch (error) {
|
24 |
+
const errorMessage = (error as AxiosError).message || "An error occurred.";
|
25 |
+
yield put(fetchPoiFailure(errorMessage));
|
26 |
+
}
|
27 |
+
}
|
28 |
+
|
29 |
+
export function* watchFetchPoi() {
|
30 |
+
yield takeLatest(fetchPoiRequest.type, fetchPoiData);
|
31 |
+
}
|
src/redux/store/store.ts
ADDED
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { configureStore } from "@reduxjs/toolkit";
|
2 |
+
import createSagaMiddleware from "redux-saga";
|
3 |
+
import poiReducer from "../reducers/poiReducer";
|
4 |
+
import { watchFetchPoi } from "../sagas/poiSaga";
|
5 |
+
|
6 |
+
const sagaMiddleware = createSagaMiddleware();
|
7 |
+
|
8 |
+
const store = configureStore({
|
9 |
+
reducer: {
|
10 |
+
poi: poiReducer,
|
11 |
+
},
|
12 |
+
middleware: [sagaMiddleware],
|
13 |
+
});
|
14 |
+
|
15 |
+
sagaMiddleware.run(watchFetchPoi);
|
16 |
+
|
17 |
+
export type RootState = ReturnType<typeof store.getState>;
|
18 |
+
|
19 |
+
export default store;
|
src/redux/types/Poi.ts
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface Poi {
|
2 |
+
id: string;
|
3 |
+
lat: number;
|
4 |
+
lng: number;
|
5 |
+
address: string;
|
6 |
+
ts_created: string;
|
7 |
+
ts_updated: string;
|
8 |
+
error: string | null;
|
9 |
+
state: string | null;
|
10 |
+
data: {
|
11 |
+
footfall: {
|
12 |
+
avg_daily_number: number;
|
13 |
+
daytype_hour_distribution: {
|
14 |
+
day_type: string;
|
15 |
+
hour: number;
|
16 |
+
percentage: number;
|
17 |
+
}[];
|
18 |
+
week_distribution: {
|
19 |
+
indice_base_100: number;
|
20 |
+
week: number;
|
21 |
+
}[];
|
22 |
+
};
|
23 |
+
mode: {
|
24 |
+
soft_mode: number;
|
25 |
+
vehicle: number;
|
26 |
+
};
|
27 |
+
origin_top_10: {
|
28 |
+
commune: string;
|
29 |
+
percentage: number;
|
30 |
+
}[];
|
31 |
+
section: {
|
32 |
+
avg_speed: number;
|
33 |
+
commune: string;
|
34 |
+
department: string;
|
35 |
+
geometry: string;
|
36 |
+
iris: string;
|
37 |
+
length: number;
|
38 |
+
max_speed: number;
|
39 |
+
name: string;
|
40 |
+
nbr_direction: number;
|
41 |
+
region: string;
|
42 |
+
};
|
43 |
+
socio_demography: {
|
44 |
+
age: string;
|
45 |
+
gender: string;
|
46 |
+
percentage: number;
|
47 |
+
social_group: string;
|
48 |
+
}[];
|
49 |
+
traffic: {
|
50 |
+
avg_daily_flow: number;
|
51 |
+
daytype_hour_distribution: {
|
52 |
+
day_type: string;
|
53 |
+
hour: number;
|
54 |
+
percentage: number;
|
55 |
+
}[];
|
56 |
+
};
|
57 |
+
trip_purpose: {
|
58 |
+
percentage: number;
|
59 |
+
trip_purpose_group: string;
|
60 |
+
}[];
|
61 |
+
};
|
62 |
+
}
|
src/sections/demographics/index.tsx
ADDED
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from "react";
|
2 |
+
import ParentSize from "@visx/responsive/lib/components/ParentSize";
|
3 |
+
import PieChart from "../../components/charts/pieChart/Pie";
|
4 |
+
import { Poi } from "../../redux/types/Poi";
|
5 |
+
import LegendChart from "../../components/generalInformation/legend/Legend";
|
6 |
+
import CloudWord from "../../components/charts/wordCloudChart/CloudWord";
|
7 |
+
import { characterizeSocioDemographicData } from "../../algorithm/WordCloud";
|
8 |
+
|
9 |
+
type DemographicsSectionProps = {
|
10 |
+
selectedPoi: Poi;
|
11 |
+
};
|
12 |
+
|
13 |
+
const DemographicsSection: React.FC<DemographicsSectionProps> = ({
|
14 |
+
selectedPoi,
|
15 |
+
}) => {
|
16 |
+
return (
|
17 |
+
<div className="demographicsSection">
|
18 |
+
<div className="mainDemoPie">
|
19 |
+
<div className="mainDemoPieTitle">Total Socio Demographics</div>
|
20 |
+
<ParentSize>
|
21 |
+
{({ width, height }) => (
|
22 |
+
<PieChart
|
23 |
+
data={selectedPoi?.data.socio_demography}
|
24 |
+
width={width}
|
25 |
+
height={height}
|
26 |
+
flag={0}
|
27 |
+
/>
|
28 |
+
)}
|
29 |
+
</ParentSize>
|
30 |
+
</div>
|
31 |
+
<div className="otherDemoPieWrapper">
|
32 |
+
<div className="menAndWomenPies">
|
33 |
+
<div className="menPie">
|
34 |
+
<div className="weekDistributionTitle">
|
35 |
+
Masculine Socio Demographics
|
36 |
+
</div>
|
37 |
+
<ParentSize>
|
38 |
+
{({ width, height }) => (
|
39 |
+
<PieChart
|
40 |
+
data={selectedPoi?.data.socio_demography.filter(
|
41 |
+
(item) => item.gender.toUpperCase() === "MALE"
|
42 |
+
)}
|
43 |
+
width={width}
|
44 |
+
height={height}
|
45 |
+
flag={1}
|
46 |
+
/>
|
47 |
+
)}
|
48 |
+
</ParentSize>
|
49 |
+
</div>
|
50 |
+
<div className="womenPie">
|
51 |
+
<div className="weekDistributionTitle">
|
52 |
+
Feminine Socio Demographics
|
53 |
+
</div>
|
54 |
+
<ParentSize>
|
55 |
+
{({ width, height }) => (
|
56 |
+
<PieChart
|
57 |
+
data={selectedPoi?.data.socio_demography.filter(
|
58 |
+
(item) => item.gender.toUpperCase() === "FEMALE"
|
59 |
+
)}
|
60 |
+
width={width}
|
61 |
+
height={height}
|
62 |
+
flag={2}
|
63 |
+
/>
|
64 |
+
)}
|
65 |
+
</ParentSize>
|
66 |
+
</div>
|
67 |
+
</div>
|
68 |
+
<div className="LegendChart">
|
69 |
+
<div className="wordCloud">
|
70 |
+
<ParentSize>
|
71 |
+
{({ width, height }) => (
|
72 |
+
<LegendChart width={width} height={height} />
|
73 |
+
)}
|
74 |
+
</ParentSize>
|
75 |
+
</div>
|
76 |
+
<div className="wordCloud">
|
77 |
+
<ParentSize>
|
78 |
+
{({ width, height }) => (
|
79 |
+
<CloudWord
|
80 |
+
demographicWords={characterizeSocioDemographicData(
|
81 |
+
selectedPoi?.data.socio_demography
|
82 |
+
)}
|
83 |
+
width={width}
|
84 |
+
height={height - 15}
|
85 |
+
/>
|
86 |
+
)}
|
87 |
+
</ParentSize>
|
88 |
+
</div>
|
89 |
+
</div>
|
90 |
+
</div>
|
91 |
+
</div>
|
92 |
+
);
|
93 |
+
};
|
94 |
+
|
95 |
+
export default DemographicsSection;
|
src/sections/description/index.tsx
ADDED
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from "react";
|
2 |
+
import { Poi } from "../../redux/types/Poi";
|
3 |
+
import { convertModeObjectToArray } from "../../utils/convertModeObjectToArray";
|
4 |
+
import { FaWalking } from "react-icons/fa";
|
5 |
+
import { AiFillCar } from "react-icons/ai";
|
6 |
+
import { formatDate } from "../../utils/formatDate";
|
7 |
+
import { FaLocationDot } from "react-icons/fa6";
|
8 |
+
import { TbWorldLatitude, TbWorldLongitude } from "react-icons/tb";
|
9 |
+
import { BiTimeFive } from "react-icons/bi";
|
10 |
+
|
11 |
+
type DemographicsSectionProps = {
|
12 |
+
selectedPoi: Poi;
|
13 |
+
};
|
14 |
+
const DescriptionSection: React.FC<DemographicsSectionProps> = ({
|
15 |
+
selectedPoi,
|
16 |
+
}) => {
|
17 |
+
return (
|
18 |
+
<div className="sectionBasicInfoOne">
|
19 |
+
<div className="basicInfoLabelsOne">
|
20 |
+
<FaLocationDot style={{ fontSize: "25px", marginRight: "10px" }} />
|
21 |
+
{selectedPoi ? selectedPoi?.address : "unknown"}
|
22 |
+
</div>
|
23 |
+
<div className="basicInfoLabelsOne">
|
24 |
+
<TbWorldLatitude style={{ fontSize: "25px", marginRight: "10px" }} />
|
25 |
+
{selectedPoi ? selectedPoi?.lat : "unknown"}
|
26 |
+
</div>
|
27 |
+
<div className="basicInfoLabelsOne">
|
28 |
+
<TbWorldLongitude style={{ fontSize: "25px", marginRight: "10px" }} />
|
29 |
+
{selectedPoi ? selectedPoi?.lng : "unknown"}
|
30 |
+
</div>
|
31 |
+
<div className="basicInfoLabelsOne">
|
32 |
+
<BiTimeFive style={{ fontSize: "25px", marginRight: "10px" }} />
|
33 |
+
{selectedPoi ? formatDate(selectedPoi?.ts_updated) : "unknown"}
|
34 |
+
</div>
|
35 |
+
</div>
|
36 |
+
);
|
37 |
+
};
|
38 |
+
|
39 |
+
export default DescriptionSection;
|
src/sections/drawer/index.tsx
ADDED
@@ -0,0 +1,303 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from "react";
|
2 |
+
import seiki from "../../assets/images/seiki.png";
|
3 |
+
import { BsSearch, BsChevronDown } from "react-icons/bs";
|
4 |
+
import { FaMapLocationDot, FaLocationDot } from "react-icons/fa6";
|
5 |
+
import { AiFillCar, AiFillDatabase } from "react-icons/ai";
|
6 |
+
import { RiDashboardFill } from "react-icons/ri";
|
7 |
+
import { IoFootstepsSharp, IoStatsChartSharp } from "react-icons/io5";
|
8 |
+
import { FaBusAlt, FaInfoCircle, FaMapMarkedAlt } from "react-icons/fa";
|
9 |
+
import { Poi } from "../../redux/types/Poi";
|
10 |
+
import FootfallSection from "../footfall";
|
11 |
+
|
12 |
+
type DrawerSectionProps = {
|
13 |
+
poiList: Poi[];
|
14 |
+
setSelectedPoi: React.Dispatch<React.SetStateAction<Poi>>;
|
15 |
+
isFootfallSection: boolean;
|
16 |
+
setFootfallSection: React.Dispatch<React.SetStateAction<boolean>>;
|
17 |
+
isModeSection: boolean;
|
18 |
+
setModeSection: React.Dispatch<React.SetStateAction<boolean>>;
|
19 |
+
isDemographicsSection: boolean;
|
20 |
+
setDemographicsSection: React.Dispatch<React.SetStateAction<boolean>>;
|
21 |
+
isInformationSection: boolean;
|
22 |
+
setInformationSection: React.Dispatch<React.SetStateAction<boolean>>;
|
23 |
+
isTraficSection: boolean;
|
24 |
+
setTraficSection: React.Dispatch<React.SetStateAction<boolean>>;
|
25 |
+
isMapView: boolean;
|
26 |
+
setIsMapView: React.Dispatch<React.SetStateAction<boolean>>;
|
27 |
+
};
|
28 |
+
|
29 |
+
const DrawerSection: React.FC<DrawerSectionProps> = ({
|
30 |
+
poiList,
|
31 |
+
setSelectedPoi,
|
32 |
+
isFootfallSection,
|
33 |
+
setFootfallSection,
|
34 |
+
isModeSection,
|
35 |
+
setModeSection,
|
36 |
+
isDemographicsSection,
|
37 |
+
setDemographicsSection,
|
38 |
+
isInformationSection,
|
39 |
+
setInformationSection,
|
40 |
+
isTraficSection,
|
41 |
+
setTraficSection,
|
42 |
+
isMapView,
|
43 |
+
setIsMapView,
|
44 |
+
}) => {
|
45 |
+
const [open, setOpen] = useState(true);
|
46 |
+
const [submenuOpen, setSubmenuOpen] = useState(false);
|
47 |
+
|
48 |
+
const Menus = [
|
49 |
+
{
|
50 |
+
title: "Points of interest",
|
51 |
+
icon: <FaLocationDot />,
|
52 |
+
submenu: true,
|
53 |
+
submenuItems: poiList,
|
54 |
+
},
|
55 |
+
{
|
56 |
+
title: "Footfall",
|
57 |
+
icon: <IoFootstepsSharp />,
|
58 |
+
},
|
59 |
+
{
|
60 |
+
title: "Mode",
|
61 |
+
icon: <AiFillCar />,
|
62 |
+
},
|
63 |
+
{
|
64 |
+
title: "Demographics",
|
65 |
+
icon: <IoStatsChartSharp />,
|
66 |
+
},
|
67 |
+
{
|
68 |
+
title: "Section info",
|
69 |
+
icon: <FaInfoCircle />,
|
70 |
+
},
|
71 |
+
{
|
72 |
+
title: "Trafic data",
|
73 |
+
icon: <AiFillDatabase />,
|
74 |
+
},
|
75 |
+
];
|
76 |
+
|
77 |
+
return (
|
78 |
+
<div
|
79 |
+
className={`p-5 pt-8 ${open ? "w-72" : "w-20"} duration-300 relative`}
|
80 |
+
style={{ height: "100%", backgroundColor: "#191F20" }}
|
81 |
+
>
|
82 |
+
<div className="inline-flex">
|
83 |
+
<img
|
84 |
+
src={seiki}
|
85 |
+
width={40}
|
86 |
+
height={100}
|
87 |
+
className={`text-4xl rounded cursor-pointer block float-left mr-4 duration-500 ${
|
88 |
+
open && "rotate-[360deg]"
|
89 |
+
}`}
|
90 |
+
onClick={() => setOpen(!open)}
|
91 |
+
/>
|
92 |
+
<h1
|
93 |
+
className={`text-white origin-left font-medium text-2xl duration-300 ${
|
94 |
+
!open && "scale-0"
|
95 |
+
}`}
|
96 |
+
style={{ fontFamily: "airbnb_semi_bold", fontSize: 35 }}
|
97 |
+
>
|
98 |
+
seiki
|
99 |
+
</h1>
|
100 |
+
</div>
|
101 |
+
|
102 |
+
<div
|
103 |
+
className={`flex items-center rounded-md bg-light-white mt-6 ${
|
104 |
+
!open ? "px-2.5" : "px-4"
|
105 |
+
} py-2`}
|
106 |
+
>
|
107 |
+
<BsSearch
|
108 |
+
className={`text-white text-lg block float-left cursor-pointer ${
|
109 |
+
open && "mr-2"
|
110 |
+
}`}
|
111 |
+
/>
|
112 |
+
<input
|
113 |
+
type={"search"}
|
114 |
+
placeholder="Search"
|
115 |
+
className={`text-base bg-transparent w-full text-white focus:outline-none ${
|
116 |
+
!open && "hidden"
|
117 |
+
}`}
|
118 |
+
/>
|
119 |
+
</div>
|
120 |
+
|
121 |
+
<ul className="pt-2">
|
122 |
+
<li
|
123 |
+
className={`text-gray-300 text-sm flex items-center gap-x-4 cursor-pointer p-2
|
124 |
+
hover:bg-light-white rounded-md mb-2`}
|
125 |
+
>
|
126 |
+
<span className="text-2xl block float-left">
|
127 |
+
<FaLocationDot />
|
128 |
+
</span>
|
129 |
+
<span
|
130 |
+
className={`text-base font-medium flex-1 duration-200 ${
|
131 |
+
!open && "hidden"
|
132 |
+
}`}
|
133 |
+
style={{ fontFamily: "airbnb_light" }}
|
134 |
+
>
|
135 |
+
Points of interest
|
136 |
+
</span>
|
137 |
+
{open && (
|
138 |
+
<BsChevronDown
|
139 |
+
className={`${submenuOpen && "rotate-180"}`}
|
140 |
+
onClick={() => {
|
141 |
+
setSubmenuOpen(!submenuOpen);
|
142 |
+
}}
|
143 |
+
/>
|
144 |
+
)}
|
145 |
+
</li>
|
146 |
+
{submenuOpen && open && (
|
147 |
+
<ul>
|
148 |
+
{Menus[0].submenuItems ? (
|
149 |
+
Menus[0].submenuItems.map((submenuItem, index) => (
|
150 |
+
<li
|
151 |
+
onClick={() => {
|
152 |
+
setSelectedPoi(submenuItem);
|
153 |
+
}}
|
154 |
+
key={index}
|
155 |
+
className="text-gray-300 text-sm flex items-center gap-x-4 cursor-pointer p-2 px-5
|
156 |
+
hover:bg-light-white rounded-md"
|
157 |
+
style={{ fontFamily: "airbnb_light" }}
|
158 |
+
>
|
159 |
+
{submenuItem.address}
|
160 |
+
</li>
|
161 |
+
))
|
162 |
+
) : (
|
163 |
+
<></>
|
164 |
+
)}
|
165 |
+
</ul>
|
166 |
+
)}
|
167 |
+
<li
|
168 |
+
className={`text-gray-300 text-sm flex items-center gap-x-4 cursor-pointer p-2
|
169 |
+
hover:bg-light-white rounded-md mb-2 ${
|
170 |
+
isFootfallSection ? "" : "bg-light-white"
|
171 |
+
}`}
|
172 |
+
onClick={() => {
|
173 |
+
setFootfallSection(!isFootfallSection);
|
174 |
+
}}
|
175 |
+
>
|
176 |
+
<span className="text-2xl block float-left">
|
177 |
+
<IoFootstepsSharp />
|
178 |
+
</span>
|
179 |
+
<span
|
180 |
+
className={`text-base font-medium flex-1 duration-200 ${
|
181 |
+
!open && "hidden"
|
182 |
+
}`}
|
183 |
+
style={{ fontFamily: "airbnb_light" }}
|
184 |
+
>
|
185 |
+
Footfall
|
186 |
+
</span>
|
187 |
+
</li>
|
188 |
+
|
189 |
+
<li
|
190 |
+
className={`text-gray-300 text-sm flex items-center gap-x-4 cursor-pointer p-2
|
191 |
+
hover:bg-light-white rounded-md mb-2 ${
|
192 |
+
isModeSection ? "" : "bg-light-white"
|
193 |
+
}`}
|
194 |
+
onClick={() => {
|
195 |
+
setModeSection(!isModeSection);
|
196 |
+
}}
|
197 |
+
>
|
198 |
+
<span className="text-2xl block float-left">
|
199 |
+
<AiFillCar />
|
200 |
+
</span>
|
201 |
+
<span
|
202 |
+
className={`text-base font-medium flex-1 duration-200 ${
|
203 |
+
!open && "hidden"
|
204 |
+
}`}
|
205 |
+
style={{ fontFamily: "airbnb_light" }}
|
206 |
+
>
|
207 |
+
Mode
|
208 |
+
</span>
|
209 |
+
</li>
|
210 |
+
|
211 |
+
<li
|
212 |
+
className={`text-gray-300 text-sm flex items-center gap-x-4 cursor-pointer p-2
|
213 |
+
hover:bg-light-white rounded-md mb-2 ${
|
214 |
+
isDemographicsSection ? "" : "bg-light-white"
|
215 |
+
}`}
|
216 |
+
onClick={() => {
|
217 |
+
setDemographicsSection(!isDemographicsSection);
|
218 |
+
}}
|
219 |
+
>
|
220 |
+
<span className="text-2xl block float-left">
|
221 |
+
<IoStatsChartSharp />
|
222 |
+
</span>
|
223 |
+
<span
|
224 |
+
className={`text-base font-medium flex-1 duration-200 ${
|
225 |
+
!open && "hidden"
|
226 |
+
}`}
|
227 |
+
style={{ fontFamily: "airbnb_light" }}
|
228 |
+
>
|
229 |
+
Demographics
|
230 |
+
</span>
|
231 |
+
</li>
|
232 |
+
|
233 |
+
<li
|
234 |
+
className={`text-gray-300 text-sm flex items-center gap-x-4 cursor-pointer p-2
|
235 |
+
hover:bg-light-white rounded-md mb-2 ${
|
236 |
+
isInformationSection ? "" : "bg-light-white"
|
237 |
+
}`}
|
238 |
+
onClick={() => {
|
239 |
+
setInformationSection(!isInformationSection);
|
240 |
+
}}
|
241 |
+
>
|
242 |
+
<span className="text-2xl block float-left">
|
243 |
+
<FaInfoCircle />
|
244 |
+
</span>
|
245 |
+
<span
|
246 |
+
className={`text-base font-medium flex-1 duration-200 ${
|
247 |
+
!open && "hidden"
|
248 |
+
}`}
|
249 |
+
style={{ fontFamily: "airbnb_light" }}
|
250 |
+
>
|
251 |
+
Section info
|
252 |
+
</span>
|
253 |
+
</li>
|
254 |
+
|
255 |
+
<li
|
256 |
+
className={`text-gray-300 text-sm flex items-center gap-x-4 cursor-pointer p-2
|
257 |
+
hover:bg-light-white rounded-md mb-2 ${
|
258 |
+
isTraficSection ? "" : "bg-light-white"
|
259 |
+
}`}
|
260 |
+
onClick={() => {
|
261 |
+
setTraficSection(!isTraficSection);
|
262 |
+
}}
|
263 |
+
>
|
264 |
+
<span className="text-2xl block float-left">
|
265 |
+
<AiFillDatabase />
|
266 |
+
</span>
|
267 |
+
<span
|
268 |
+
className={`text-base font-medium flex-1 duration-200 ${
|
269 |
+
!open && "hidden"
|
270 |
+
}`}
|
271 |
+
style={{ fontFamily: "airbnb_light" }}
|
272 |
+
>
|
273 |
+
Trafic data
|
274 |
+
</span>
|
275 |
+
</li>
|
276 |
+
|
277 |
+
<li
|
278 |
+
className={`text-gray-300 text-sm flex items-center gap-x-4 cursor-pointer p-2
|
279 |
+
hover:bg-light-white rounded-md mb-2 ${
|
280 |
+
isTraficSection ? "" : "bg-light-white"
|
281 |
+
}`}
|
282 |
+
onClick={() => {
|
283 |
+
setIsMapView(!isMapView);
|
284 |
+
}}
|
285 |
+
>
|
286 |
+
<span className="text-2xl block float-left">
|
287 |
+
<FaMapMarkedAlt />
|
288 |
+
</span>
|
289 |
+
<span
|
290 |
+
className={`text-base font-medium flex-1 duration-200 ${
|
291 |
+
!open && "hidden"
|
292 |
+
}`}
|
293 |
+
style={{ fontFamily: "airbnb_light" }}
|
294 |
+
>
|
295 |
+
Map View
|
296 |
+
</span>
|
297 |
+
</li>
|
298 |
+
</ul>
|
299 |
+
</div>
|
300 |
+
);
|
301 |
+
};
|
302 |
+
|
303 |
+
export default DrawerSection;
|
src/sections/footfall/index.tsx
ADDED
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from "react";
|
2 |
+
import { Poi } from "../../redux/types/Poi";
|
3 |
+
import { formatNumber } from "../../utils/formatNumber";
|
4 |
+
import { IoFootstepsSharp } from "react-icons/io5";
|
5 |
+
import ParentSize from "@visx/responsive/lib/components/ParentSize";
|
6 |
+
import BrushChart from "../../components/charts/brushChart/AreaChart";
|
7 |
+
import DayTypeGraph from "../../components/charts/tripeChart/DayTypeGraph";
|
8 |
+
|
9 |
+
type FootfallSectionProps = {
|
10 |
+
selectedPoi: Poi;
|
11 |
+
};
|
12 |
+
|
13 |
+
const FootfallSection: React.FC<FootfallSectionProps> = ({ selectedPoi }) => {
|
14 |
+
return (
|
15 |
+
<div className="sectionBasicInfo">
|
16 |
+
<div className="footfallLeftWrapper">
|
17 |
+
<div className="averageFootfall">
|
18 |
+
<div className="weekDistributionTitle">Average Footfall</div>
|
19 |
+
<div className="averageFootfallWrapper">
|
20 |
+
{selectedPoi
|
21 |
+
? formatNumber(selectedPoi?.data.footfall.avg_daily_number)
|
22 |
+
: "17 400"}
|
23 |
+
<IoFootstepsSharp style={{ marginLeft: "2%", fontSize: "50px" }} />
|
24 |
+
</div>
|
25 |
+
</div>
|
26 |
+
<div className="weekDistribution">
|
27 |
+
<div className="weekDistributionTitle">
|
28 |
+
Footfall Week Distribution
|
29 |
+
</div>
|
30 |
+
<ParentSize>
|
31 |
+
{({ width, height }) => (
|
32 |
+
<BrushChart
|
33 |
+
data={selectedPoi?.data.footfall.week_distribution}
|
34 |
+
compact={false}
|
35 |
+
width={width}
|
36 |
+
height={height}
|
37 |
+
/>
|
38 |
+
)}
|
39 |
+
</ParentSize>
|
40 |
+
</div>
|
41 |
+
</div>
|
42 |
+
<div className="basicInfoMap">
|
43 |
+
<div className="weekDistributionTitle">
|
44 |
+
Footfall Day Type Hour distribution
|
45 |
+
</div>
|
46 |
+
<ParentSize>
|
47 |
+
{({ width, height }) => (
|
48 |
+
<DayTypeGraph
|
49 |
+
data={selectedPoi?.data.footfall.daytype_hour_distribution}
|
50 |
+
width={width}
|
51 |
+
height={height}
|
52 |
+
/>
|
53 |
+
)}
|
54 |
+
</ParentSize>
|
55 |
+
</div>
|
56 |
+
</div>
|
57 |
+
);
|
58 |
+
};
|
59 |
+
|
60 |
+
export default FootfallSection;
|
src/sections/information/index.tsx
ADDED
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { SiZenn } from "react-icons/si";
|
2 |
+
import { FaDirections } from "react-icons/fa";
|
3 |
+
import { BsSpeedometer2, BsSpeedometer, BsPinMapFill } from "react-icons/bs";
|
4 |
+
import { BiSolidCity, BiSolidRename } from "react-icons/bi";
|
5 |
+
import { FaMapLocationDot } from "react-icons/fa6";
|
6 |
+
import { Poi } from "../../redux/types/Poi";
|
7 |
+
import ParentSize from "@visx/responsive/lib/components/ParentSize";
|
8 |
+
import SimpleChart from "../../components/charts/singleChart/SimpleChart";
|
9 |
+
|
10 |
+
type InformationSectionProps = {
|
11 |
+
selectedPoi: Poi;
|
12 |
+
};
|
13 |
+
|
14 |
+
const InformationSection: React.FC<InformationSectionProps> = ({
|
15 |
+
selectedPoi,
|
16 |
+
}) => {
|
17 |
+
const streetSpecs = [
|
18 |
+
{
|
19 |
+
label: "Average speed",
|
20 |
+
icon: <BsSpeedometer2 style={{ fontSize: "25px" }} />,
|
21 |
+
value: selectedPoi
|
22 |
+
? selectedPoi?.data.section?.avg_speed.toString().slice(0, 4)
|
23 |
+
: "n/a" + "km/h",
|
24 |
+
},
|
25 |
+
{
|
26 |
+
label: "Commune",
|
27 |
+
icon: <BiSolidCity style={{ fontSize: "25px" }} />,
|
28 |
+
value: selectedPoi ? selectedPoi?.data.section?.commune : "n/a",
|
29 |
+
},
|
30 |
+
{
|
31 |
+
label: "Department",
|
32 |
+
icon: <FaMapLocationDot style={{ fontSize: "25px" }} />,
|
33 |
+
value: selectedPoi ? selectedPoi?.data.section?.department : "n/a",
|
34 |
+
},
|
35 |
+
{
|
36 |
+
label: "Length",
|
37 |
+
icon: <SiZenn style={{ fontSize: "25px" }} />,
|
38 |
+
value: selectedPoi ? selectedPoi?.data.section?.length : "n/a" + "m",
|
39 |
+
},
|
40 |
+
{
|
41 |
+
label: "Max speed",
|
42 |
+
icon: <BsSpeedometer style={{ fontSize: "25px" }} />,
|
43 |
+
value: selectedPoi
|
44 |
+
? selectedPoi?.data.section?.max_speed
|
45 |
+
: "n/a" + "km/h",
|
46 |
+
},
|
47 |
+
{
|
48 |
+
label: "Name",
|
49 |
+
icon: <BiSolidRename style={{ fontSize: "25px" }} />,
|
50 |
+
value: (
|
51 |
+
<p style={{ fontSize: "15px", marginTop: "5%" }}>
|
52 |
+
{selectedPoi ? selectedPoi?.data.section?.name : "n/a"}
|
53 |
+
</p>
|
54 |
+
),
|
55 |
+
},
|
56 |
+
{
|
57 |
+
label: "Directions",
|
58 |
+
icon: <FaDirections style={{ fontSize: "25px" }} />,
|
59 |
+
value: selectedPoi ? selectedPoi?.data.section?.nbr_direction : "n/a",
|
60 |
+
},
|
61 |
+
{
|
62 |
+
label: "Region",
|
63 |
+
icon: <BsPinMapFill style={{ fontSize: "25px" }} />,
|
64 |
+
value: selectedPoi ? selectedPoi?.data.section?.region : "n/a",
|
65 |
+
},
|
66 |
+
];
|
67 |
+
return (
|
68 |
+
<div className="blockSection">
|
69 |
+
<div className="blockInformation">
|
70 |
+
<div className="weekDistributionTitle">Section Information</div>
|
71 |
+
{streetSpecs.map((object, index) => (
|
72 |
+
<div className="blockInformationBackground">
|
73 |
+
<div className="blockInformationContent">
|
74 |
+
<p
|
75 |
+
style={{
|
76 |
+
width: "60%",
|
77 |
+
display: "flex",
|
78 |
+
flexDirection: "row",
|
79 |
+
alignItems: "center",
|
80 |
+
justifyContent: "space-around",
|
81 |
+
}}
|
82 |
+
>
|
83 |
+
{object.label}
|
84 |
+
{object.icon}
|
85 |
+
</p>
|
86 |
+
<p>{object.value}</p>
|
87 |
+
</div>
|
88 |
+
</div>
|
89 |
+
))}
|
90 |
+
</div>
|
91 |
+
<div className="blockSimpleChart">
|
92 |
+
<div className="weekDistributionTitle">Top 10 Trafic Origin</div>
|
93 |
+
<ParentSize>
|
94 |
+
{({ width, height }) => (
|
95 |
+
<SimpleChart
|
96 |
+
data={selectedPoi?.data.origin_top_10}
|
97 |
+
width={width}
|
98 |
+
height={height}
|
99 |
+
/>
|
100 |
+
)}
|
101 |
+
</ParentSize>
|
102 |
+
</div>
|
103 |
+
</div>
|
104 |
+
);
|
105 |
+
};
|
106 |
+
|
107 |
+
export default InformationSection;
|
src/sections/map/control-panel.tsx
ADDED
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import React, { useState, useEffect } from "react";
|
2 |
+
import { fromJS, Map } from "immutable";
|
3 |
+
import MAP_STYLE from "./map-style-basic-v8.json";
|
4 |
+
|
5 |
+
const defaultMapStyle: Map<string, any> = fromJS(MAP_STYLE);
|
6 |
+
const defaultLayers = defaultMapStyle.get("layers");
|
7 |
+
|
8 |
+
const categories: string[] = [
|
9 |
+
"labels",
|
10 |
+
"roads",
|
11 |
+
"buildings",
|
12 |
+
"parks",
|
13 |
+
"water",
|
14 |
+
"background",
|
15 |
+
];
|
16 |
+
|
17 |
+
// Layer id patterns by category
|
18 |
+
const layerSelector: { [key: string]: RegExp } = {
|
19 |
+
background: /background/,
|
20 |
+
water: /water/,
|
21 |
+
parks: /park/,
|
22 |
+
buildings: /building/,
|
23 |
+
roads: /bridge|road|tunnel/,
|
24 |
+
labels: /label|place|poi/,
|
25 |
+
};
|
26 |
+
|
27 |
+
// Layer color class by type
|
28 |
+
const colorClass: { [key: string]: string } = {
|
29 |
+
line: "line-color",
|
30 |
+
fill: "fill-color",
|
31 |
+
background: "background-color",
|
32 |
+
symbol: "text-color",
|
33 |
+
};
|
34 |
+
|
35 |
+
function getMapStyle({ visibility, color }: MapStyleOptions): Map<string, any> {
|
36 |
+
const layers = defaultLayers
|
37 |
+
.filter((layer: any) => {
|
38 |
+
const id = layer.get("id");
|
39 |
+
return categories.every(
|
40 |
+
(name) => visibility[name] || !layerSelector[name].test(id)
|
41 |
+
);
|
42 |
+
})
|
43 |
+
.map((layer: any) => {
|
44 |
+
const id = layer.get("id");
|
45 |
+
const type = layer.get("type");
|
46 |
+
const category = categories.find((name) => layerSelector[name].test(id));
|
47 |
+
if (category && colorClass[type]) {
|
48 |
+
return layer.setIn(["paint", colorClass[type]], color[category]);
|
49 |
+
}
|
50 |
+
return layer;
|
51 |
+
});
|
52 |
+
|
53 |
+
return defaultMapStyle.set("layers", layers);
|
54 |
+
}
|
55 |
+
|
56 |
+
interface MapStyleOptions {
|
57 |
+
visibility: { [key: string]: boolean };
|
58 |
+
color: { [key: string]: string };
|
59 |
+
}
|
60 |
+
|
61 |
+
interface StyleControlsProps {
|
62 |
+
onChange: (style: Map<string, any>) => void;
|
63 |
+
}
|
64 |
+
|
65 |
+
function StyleControls(props: StyleControlsProps) {
|
66 |
+
const [visibility, setVisibility] = useState<any>({
|
67 |
+
water: true,
|
68 |
+
parks: true,
|
69 |
+
buildings: true,
|
70 |
+
roads: true,
|
71 |
+
labels: true,
|
72 |
+
background: true,
|
73 |
+
});
|
74 |
+
|
75 |
+
const [color, setColor] = useState<any>({
|
76 |
+
water: "#DBE2E6",
|
77 |
+
parks: "#E6EAE9",
|
78 |
+
buildings: "#c0c0c8",
|
79 |
+
roads: "#ffffff",
|
80 |
+
labels: "#78888a",
|
81 |
+
background: "#EBF0F0",
|
82 |
+
});
|
83 |
+
|
84 |
+
useEffect(() => {
|
85 |
+
props.onChange(getMapStyle({ visibility, color }));
|
86 |
+
}, [visibility, color]);
|
87 |
+
|
88 |
+
const onColorChange = (name: string, value: string) => {
|
89 |
+
setColor({ ...color, [name]: value });
|
90 |
+
};
|
91 |
+
|
92 |
+
const onVisibilityChange = (name: string, value: boolean) => {
|
93 |
+
setVisibility({ ...visibility, [name]: value });
|
94 |
+
};
|
95 |
+
|
96 |
+
return (
|
97 |
+
<div className="control-panel">
|
98 |
+
<h3>Dynamic Styling</h3>
|
99 |
+
<hr />
|
100 |
+
{categories.map((name) => (
|
101 |
+
<div
|
102 |
+
key={name}
|
103 |
+
className="input"
|
104 |
+
style={{
|
105 |
+
display: "flex",
|
106 |
+
flexDirection: "row",
|
107 |
+
justifyContent: "space-between",
|
108 |
+
alignItems: "center",
|
109 |
+
}}
|
110 |
+
>
|
111 |
+
<label>{name}</label>
|
112 |
+
<input
|
113 |
+
type="checkbox"
|
114 |
+
checked={visibility[name]}
|
115 |
+
onChange={(evt) => onVisibilityChange(name, evt.target.checked)}
|
116 |
+
/>
|
117 |
+
<input
|
118 |
+
type="color"
|
119 |
+
value={color[name]}
|
120 |
+
disabled={!visibility[name]}
|
121 |
+
onChange={(evt) => onColorChange(name, evt.target.value)}
|
122 |
+
/>
|
123 |
+
</div>
|
124 |
+
))}
|
125 |
+
</div>
|
126 |
+
);
|
127 |
+
}
|
128 |
+
|
129 |
+
export default React.memo(StyleControls);
|
src/sections/map/index.tsx
ADDED
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from "react";
|
2 |
+
import { Poi } from "../../redux/types/Poi";
|
3 |
+
import { convertModeObjectToArray } from "../../utils/convertModeObjectToArray";
|
4 |
+
import { FaWalking } from "react-icons/fa";
|
5 |
+
import { AiFillCar } from "react-icons/ai";
|
6 |
+
import { Map, Marker } from "react-map-gl";
|
7 |
+
import ControlPanel from "./control-panel";
|
8 |
+
import pin from "../../assets/images/pin.png";
|
9 |
+
|
10 |
+
type DemographicsSectionProps = {
|
11 |
+
selectedPoi: Poi;
|
12 |
+
};
|
13 |
+
const MapSection: React.FC<DemographicsSectionProps> = ({ selectedPoi }) => {
|
14 |
+
const [mapStyle, setMapStyle] = useState<any>(null);
|
15 |
+
|
16 |
+
return (
|
17 |
+
<div
|
18 |
+
style={{
|
19 |
+
width: "100%",
|
20 |
+
height: "100%",
|
21 |
+
}}
|
22 |
+
>
|
23 |
+
<Map
|
24 |
+
initialViewState={{
|
25 |
+
latitude: selectedPoi.lat,
|
26 |
+
longitude: selectedPoi.lng,
|
27 |
+
zoom: 15.5,
|
28 |
+
}}
|
29 |
+
mapStyle={mapStyle && mapStyle.toJS()}
|
30 |
+
styleDiffing
|
31 |
+
mapboxAccessToken={
|
32 |
+
"pk.eyJ1Ijoibm9lbm9sdWFsIiwiYSI6ImNsbXUwbGNvODA5a3Iya3Fmc202OHU2MW8ifQ.6h8Y00MzBQFDlYPtxw-_Bg"
|
33 |
+
}
|
34 |
+
>
|
35 |
+
<Marker latitude={selectedPoi.lat} longitude={selectedPoi.lng}>
|
36 |
+
{/* You can customize the pin marker here */}
|
37 |
+
<img
|
38 |
+
src={pin}
|
39 |
+
alt="Custom Marker"
|
40 |
+
width={40} // Adjust the width and height as needed
|
41 |
+
height={40}
|
42 |
+
/>
|
43 |
+
</Marker>
|
44 |
+
</Map>
|
45 |
+
<div
|
46 |
+
style={{
|
47 |
+
width: "12%",
|
48 |
+
position: "absolute",
|
49 |
+
top: "5%",
|
50 |
+
left: "85%",
|
51 |
+
padding: "1%",
|
52 |
+
borderRadius: "10px",
|
53 |
+
backgroundColor: "white",
|
54 |
+
fontFamily: "airbnb_regular",
|
55 |
+
border: "1px solid #3C3C3C",
|
56 |
+
}}
|
57 |
+
>
|
58 |
+
<ControlPanel onChange={setMapStyle} />
|
59 |
+
</div>
|
60 |
+
</div>
|
61 |
+
);
|
62 |
+
};
|
63 |
+
|
64 |
+
export default MapSection;
|
src/sections/map/map-style-basic-v8.json
ADDED
@@ -0,0 +1,866 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"version": 8,
|
3 |
+
"name": "Basic",
|
4 |
+
"metadata": {
|
5 |
+
"mapbox:autocomposite": true
|
6 |
+
},
|
7 |
+
"sources": {
|
8 |
+
"mapbox": {
|
9 |
+
"url": "mapbox://mapbox.mapbox-streets-v7",
|
10 |
+
"type": "vector"
|
11 |
+
}
|
12 |
+
},
|
13 |
+
"sprite": "mapbox://sprites/mapbox/basic-v8",
|
14 |
+
"glyphs": "mapbox://fonts/mapbox/{fontstack}/{range}.pbf",
|
15 |
+
"layers": [
|
16 |
+
{
|
17 |
+
"id": "background",
|
18 |
+
"type": "background",
|
19 |
+
"paint": {
|
20 |
+
"background-color": "#dedede"
|
21 |
+
},
|
22 |
+
"interactive": true
|
23 |
+
},
|
24 |
+
{
|
25 |
+
"id": "landuse_overlay_national_park",
|
26 |
+
"type": "fill",
|
27 |
+
"source": "mapbox",
|
28 |
+
"source-layer": "landuse_overlay",
|
29 |
+
"filter": [
|
30 |
+
"==",
|
31 |
+
"class",
|
32 |
+
"national_park"
|
33 |
+
],
|
34 |
+
"paint": {
|
35 |
+
"fill-color": "#d2edae",
|
36 |
+
"fill-opacity": 0.75
|
37 |
+
},
|
38 |
+
"interactive": true
|
39 |
+
},
|
40 |
+
{
|
41 |
+
"id": "landuse_park",
|
42 |
+
"type": "fill",
|
43 |
+
"source": "mapbox",
|
44 |
+
"source-layer": "landuse",
|
45 |
+
"filter": [
|
46 |
+
"==",
|
47 |
+
"class",
|
48 |
+
"park"
|
49 |
+
],
|
50 |
+
"paint": {
|
51 |
+
"fill-color": "#d2edae"
|
52 |
+
},
|
53 |
+
"interactive": true
|
54 |
+
},
|
55 |
+
{
|
56 |
+
"id": "waterway",
|
57 |
+
"type": "line",
|
58 |
+
"source": "mapbox",
|
59 |
+
"source-layer": "waterway",
|
60 |
+
"filter": [
|
61 |
+
"all",
|
62 |
+
[
|
63 |
+
"==",
|
64 |
+
"$type",
|
65 |
+
"LineString"
|
66 |
+
],
|
67 |
+
[
|
68 |
+
"in",
|
69 |
+
"class",
|
70 |
+
"river",
|
71 |
+
"canal"
|
72 |
+
]
|
73 |
+
],
|
74 |
+
"paint": {
|
75 |
+
"line-color": "#a0cfdf",
|
76 |
+
"line-width": {
|
77 |
+
"base": 1.4,
|
78 |
+
"stops": [
|
79 |
+
[
|
80 |
+
8,
|
81 |
+
0.5
|
82 |
+
],
|
83 |
+
[
|
84 |
+
20,
|
85 |
+
15
|
86 |
+
]
|
87 |
+
]
|
88 |
+
}
|
89 |
+
},
|
90 |
+
"interactive": true
|
91 |
+
},
|
92 |
+
{
|
93 |
+
"id": "water",
|
94 |
+
"type": "fill",
|
95 |
+
"source": "mapbox",
|
96 |
+
"source-layer": "water",
|
97 |
+
"paint": {
|
98 |
+
"fill-color": "#a0cfdf"
|
99 |
+
},
|
100 |
+
"interactive": true
|
101 |
+
},
|
102 |
+
{
|
103 |
+
"id": "building",
|
104 |
+
"type": "fill",
|
105 |
+
"source": "mapbox",
|
106 |
+
"source-layer": "building",
|
107 |
+
"paint": {
|
108 |
+
"fill-color": "#d6d6d6"
|
109 |
+
},
|
110 |
+
"interactive": true
|
111 |
+
},
|
112 |
+
{
|
113 |
+
"interactive": true,
|
114 |
+
"layout": {
|
115 |
+
"line-cap": "butt",
|
116 |
+
"line-join": "miter"
|
117 |
+
},
|
118 |
+
"filter": [
|
119 |
+
"all",
|
120 |
+
[
|
121 |
+
"==",
|
122 |
+
"$type",
|
123 |
+
"LineString"
|
124 |
+
],
|
125 |
+
[
|
126 |
+
"all",
|
127 |
+
[
|
128 |
+
"in",
|
129 |
+
"class",
|
130 |
+
"motorway_link",
|
131 |
+
"street",
|
132 |
+
"street_limited",
|
133 |
+
"service",
|
134 |
+
"track",
|
135 |
+
"pedestrian",
|
136 |
+
"path",
|
137 |
+
"link"
|
138 |
+
],
|
139 |
+
[
|
140 |
+
"==",
|
141 |
+
"structure",
|
142 |
+
"tunnel"
|
143 |
+
]
|
144 |
+
]
|
145 |
+
],
|
146 |
+
"type": "line",
|
147 |
+
"source": "mapbox",
|
148 |
+
"id": "tunnel_minor",
|
149 |
+
"paint": {
|
150 |
+
"line-color": "#efefef",
|
151 |
+
"line-width": {
|
152 |
+
"base": 1.55,
|
153 |
+
"stops": [
|
154 |
+
[
|
155 |
+
4,
|
156 |
+
0.25
|
157 |
+
],
|
158 |
+
[
|
159 |
+
20,
|
160 |
+
30
|
161 |
+
]
|
162 |
+
]
|
163 |
+
},
|
164 |
+
"line-dasharray": [
|
165 |
+
0.36,
|
166 |
+
0.18
|
167 |
+
]
|
168 |
+
},
|
169 |
+
"source-layer": "road"
|
170 |
+
},
|
171 |
+
{
|
172 |
+
"interactive": true,
|
173 |
+
"layout": {
|
174 |
+
"line-cap": "butt",
|
175 |
+
"line-join": "miter"
|
176 |
+
},
|
177 |
+
"filter": [
|
178 |
+
"all",
|
179 |
+
[
|
180 |
+
"==",
|
181 |
+
"$type",
|
182 |
+
"LineString"
|
183 |
+
],
|
184 |
+
[
|
185 |
+
"all",
|
186 |
+
[
|
187 |
+
"in",
|
188 |
+
"class",
|
189 |
+
"motorway",
|
190 |
+
"primary",
|
191 |
+
"secondary",
|
192 |
+
"tertiary",
|
193 |
+
"trunk"
|
194 |
+
],
|
195 |
+
[
|
196 |
+
"==",
|
197 |
+
"structure",
|
198 |
+
"tunnel"
|
199 |
+
]
|
200 |
+
]
|
201 |
+
],
|
202 |
+
"type": "line",
|
203 |
+
"source": "mapbox",
|
204 |
+
"id": "tunnel_major",
|
205 |
+
"paint": {
|
206 |
+
"line-color": "#fff",
|
207 |
+
"line-width": {
|
208 |
+
"base": 1.4,
|
209 |
+
"stops": [
|
210 |
+
[
|
211 |
+
6,
|
212 |
+
0.5
|
213 |
+
],
|
214 |
+
[
|
215 |
+
20,
|
216 |
+
30
|
217 |
+
]
|
218 |
+
]
|
219 |
+
},
|
220 |
+
"line-dasharray": [
|
221 |
+
0.28,
|
222 |
+
0.14
|
223 |
+
]
|
224 |
+
},
|
225 |
+
"source-layer": "road"
|
226 |
+
},
|
227 |
+
{
|
228 |
+
"interactive": true,
|
229 |
+
"layout": {
|
230 |
+
"line-cap": "round",
|
231 |
+
"line-join": "round"
|
232 |
+
},
|
233 |
+
"filter": [
|
234 |
+
"all",
|
235 |
+
[
|
236 |
+
"==",
|
237 |
+
"$type",
|
238 |
+
"LineString"
|
239 |
+
],
|
240 |
+
[
|
241 |
+
"all",
|
242 |
+
[
|
243 |
+
"in",
|
244 |
+
"class",
|
245 |
+
"motorway_link",
|
246 |
+
"street",
|
247 |
+
"street_limited",
|
248 |
+
"service",
|
249 |
+
"track",
|
250 |
+
"pedestrian",
|
251 |
+
"path",
|
252 |
+
"link"
|
253 |
+
],
|
254 |
+
[
|
255 |
+
"in",
|
256 |
+
"structure",
|
257 |
+
"none",
|
258 |
+
"ford"
|
259 |
+
]
|
260 |
+
]
|
261 |
+
],
|
262 |
+
"type": "line",
|
263 |
+
"source": "mapbox",
|
264 |
+
"id": "road_minor",
|
265 |
+
"paint": {
|
266 |
+
"line-color": "#efefef",
|
267 |
+
"line-width": {
|
268 |
+
"base": 1.55,
|
269 |
+
"stops": [
|
270 |
+
[
|
271 |
+
4,
|
272 |
+
0.25
|
273 |
+
],
|
274 |
+
[
|
275 |
+
20,
|
276 |
+
30
|
277 |
+
]
|
278 |
+
]
|
279 |
+
}
|
280 |
+
},
|
281 |
+
"source-layer": "road"
|
282 |
+
},
|
283 |
+
{
|
284 |
+
"interactive": true,
|
285 |
+
"layout": {
|
286 |
+
"line-cap": "round",
|
287 |
+
"line-join": "round"
|
288 |
+
},
|
289 |
+
"filter": [
|
290 |
+
"all",
|
291 |
+
[
|
292 |
+
"==",
|
293 |
+
"$type",
|
294 |
+
"LineString"
|
295 |
+
],
|
296 |
+
[
|
297 |
+
"all",
|
298 |
+
[
|
299 |
+
"in",
|
300 |
+
"class",
|
301 |
+
"motorway",
|
302 |
+
"primary",
|
303 |
+
"secondary",
|
304 |
+
"tertiary",
|
305 |
+
"trunk"
|
306 |
+
],
|
307 |
+
[
|
308 |
+
"in",
|
309 |
+
"structure",
|
310 |
+
"none",
|
311 |
+
"ford"
|
312 |
+
]
|
313 |
+
]
|
314 |
+
],
|
315 |
+
"type": "line",
|
316 |
+
"source": "mapbox",
|
317 |
+
"id": "road_major",
|
318 |
+
"paint": {
|
319 |
+
"line-color": "#fff",
|
320 |
+
"line-width": {
|
321 |
+
"base": 1.4,
|
322 |
+
"stops": [
|
323 |
+
[
|
324 |
+
6,
|
325 |
+
0.5
|
326 |
+
],
|
327 |
+
[
|
328 |
+
20,
|
329 |
+
30
|
330 |
+
]
|
331 |
+
]
|
332 |
+
}
|
333 |
+
},
|
334 |
+
"source-layer": "road"
|
335 |
+
},
|
336 |
+
{
|
337 |
+
"interactive": true,
|
338 |
+
"layout": {
|
339 |
+
"line-cap": "butt",
|
340 |
+
"line-join": "miter"
|
341 |
+
},
|
342 |
+
"filter": [
|
343 |
+
"all",
|
344 |
+
[
|
345 |
+
"==",
|
346 |
+
"$type",
|
347 |
+
"LineString"
|
348 |
+
],
|
349 |
+
[
|
350 |
+
"all",
|
351 |
+
[
|
352 |
+
"in",
|
353 |
+
"class",
|
354 |
+
"motorway_link",
|
355 |
+
"street",
|
356 |
+
"street_limited",
|
357 |
+
"service",
|
358 |
+
"track",
|
359 |
+
"pedestrian",
|
360 |
+
"path",
|
361 |
+
"link"
|
362 |
+
],
|
363 |
+
[
|
364 |
+
"==",
|
365 |
+
"structure",
|
366 |
+
"bridge"
|
367 |
+
]
|
368 |
+
]
|
369 |
+
],
|
370 |
+
"type": "line",
|
371 |
+
"source": "mapbox",
|
372 |
+
"id": "bridge_minor case",
|
373 |
+
"paint": {
|
374 |
+
"line-color": "#dedede",
|
375 |
+
"line-width": {
|
376 |
+
"base": 1.6,
|
377 |
+
"stops": [
|
378 |
+
[
|
379 |
+
12,
|
380 |
+
0.5
|
381 |
+
],
|
382 |
+
[
|
383 |
+
20,
|
384 |
+
10
|
385 |
+
]
|
386 |
+
]
|
387 |
+
},
|
388 |
+
"line-gap-width": {
|
389 |
+
"base": 1.55,
|
390 |
+
"stops": [
|
391 |
+
[
|
392 |
+
4,
|
393 |
+
0.25
|
394 |
+
],
|
395 |
+
[
|
396 |
+
20,
|
397 |
+
30
|
398 |
+
]
|
399 |
+
]
|
400 |
+
}
|
401 |
+
},
|
402 |
+
"source-layer": "road"
|
403 |
+
},
|
404 |
+
{
|
405 |
+
"interactive": true,
|
406 |
+
"layout": {
|
407 |
+
"line-cap": "butt",
|
408 |
+
"line-join": "miter"
|
409 |
+
},
|
410 |
+
"filter": [
|
411 |
+
"all",
|
412 |
+
[
|
413 |
+
"==",
|
414 |
+
"$type",
|
415 |
+
"LineString"
|
416 |
+
],
|
417 |
+
[
|
418 |
+
"all",
|
419 |
+
[
|
420 |
+
"in",
|
421 |
+
"class",
|
422 |
+
"motorway",
|
423 |
+
"primary",
|
424 |
+
"secondary",
|
425 |
+
"tertiary",
|
426 |
+
"trunk"
|
427 |
+
],
|
428 |
+
[
|
429 |
+
"==",
|
430 |
+
"structure",
|
431 |
+
"bridge"
|
432 |
+
]
|
433 |
+
]
|
434 |
+
],
|
435 |
+
"type": "line",
|
436 |
+
"source": "mapbox",
|
437 |
+
"id": "bridge_major case",
|
438 |
+
"paint": {
|
439 |
+
"line-color": "#dedede",
|
440 |
+
"line-width": {
|
441 |
+
"base": 1.6,
|
442 |
+
"stops": [
|
443 |
+
[
|
444 |
+
12,
|
445 |
+
0.5
|
446 |
+
],
|
447 |
+
[
|
448 |
+
20,
|
449 |
+
10
|
450 |
+
]
|
451 |
+
]
|
452 |
+
},
|
453 |
+
"line-gap-width": {
|
454 |
+
"base": 1.55,
|
455 |
+
"stops": [
|
456 |
+
[
|
457 |
+
4,
|
458 |
+
0.25
|
459 |
+
],
|
460 |
+
[
|
461 |
+
20,
|
462 |
+
30
|
463 |
+
]
|
464 |
+
]
|
465 |
+
}
|
466 |
+
},
|
467 |
+
"source-layer": "road"
|
468 |
+
},
|
469 |
+
{
|
470 |
+
"interactive": true,
|
471 |
+
"layout": {
|
472 |
+
"line-cap": "round",
|
473 |
+
"line-join": "round"
|
474 |
+
},
|
475 |
+
"filter": [
|
476 |
+
"all",
|
477 |
+
[
|
478 |
+
"==",
|
479 |
+
"$type",
|
480 |
+
"LineString"
|
481 |
+
],
|
482 |
+
[
|
483 |
+
"all",
|
484 |
+
[
|
485 |
+
"in",
|
486 |
+
"class",
|
487 |
+
"motorway_link",
|
488 |
+
"street",
|
489 |
+
"street_limited",
|
490 |
+
"service",
|
491 |
+
"track",
|
492 |
+
"pedestrian",
|
493 |
+
"path",
|
494 |
+
"link"
|
495 |
+
],
|
496 |
+
[
|
497 |
+
"==",
|
498 |
+
"structure",
|
499 |
+
"bridge"
|
500 |
+
]
|
501 |
+
]
|
502 |
+
],
|
503 |
+
"type": "line",
|
504 |
+
"source": "mapbox",
|
505 |
+
"id": "bridge_minor",
|
506 |
+
"paint": {
|
507 |
+
"line-color": "#efefef",
|
508 |
+
"line-width": {
|
509 |
+
"base": 1.55,
|
510 |
+
"stops": [
|
511 |
+
[
|
512 |
+
4,
|
513 |
+
0.25
|
514 |
+
],
|
515 |
+
[
|
516 |
+
20,
|
517 |
+
30
|
518 |
+
]
|
519 |
+
]
|
520 |
+
}
|
521 |
+
},
|
522 |
+
"source-layer": "road"
|
523 |
+
},
|
524 |
+
{
|
525 |
+
"interactive": true,
|
526 |
+
"layout": {
|
527 |
+
"line-cap": "round",
|
528 |
+
"line-join": "round"
|
529 |
+
},
|
530 |
+
"filter": [
|
531 |
+
"all",
|
532 |
+
[
|
533 |
+
"==",
|
534 |
+
"$type",
|
535 |
+
"LineString"
|
536 |
+
],
|
537 |
+
[
|
538 |
+
"all",
|
539 |
+
[
|
540 |
+
"in",
|
541 |
+
"class",
|
542 |
+
"motorway",
|
543 |
+
"primary",
|
544 |
+
"secondary",
|
545 |
+
"tertiary",
|
546 |
+
"trunk"
|
547 |
+
],
|
548 |
+
[
|
549 |
+
"==",
|
550 |
+
"structure",
|
551 |
+
"bridge"
|
552 |
+
]
|
553 |
+
]
|
554 |
+
],
|
555 |
+
"type": "line",
|
556 |
+
"source": "mapbox",
|
557 |
+
"id": "bridge_major",
|
558 |
+
"paint": {
|
559 |
+
"line-color": "#fff",
|
560 |
+
"line-width": {
|
561 |
+
"base": 1.4,
|
562 |
+
"stops": [
|
563 |
+
[
|
564 |
+
6,
|
565 |
+
0.5
|
566 |
+
],
|
567 |
+
[
|
568 |
+
20,
|
569 |
+
30
|
570 |
+
]
|
571 |
+
]
|
572 |
+
}
|
573 |
+
},
|
574 |
+
"source-layer": "road"
|
575 |
+
},
|
576 |
+
{
|
577 |
+
"interactive": true,
|
578 |
+
"layout": {
|
579 |
+
"line-cap": "round",
|
580 |
+
"line-join": "round"
|
581 |
+
},
|
582 |
+
"filter": [
|
583 |
+
"all",
|
584 |
+
[
|
585 |
+
"==",
|
586 |
+
"$type",
|
587 |
+
"LineString"
|
588 |
+
],
|
589 |
+
[
|
590 |
+
"all",
|
591 |
+
[
|
592 |
+
"<=",
|
593 |
+
"admin_level",
|
594 |
+
2
|
595 |
+
],
|
596 |
+
[
|
597 |
+
"==",
|
598 |
+
"maritime",
|
599 |
+
0
|
600 |
+
]
|
601 |
+
]
|
602 |
+
],
|
603 |
+
"type": "line",
|
604 |
+
"source": "mapbox",
|
605 |
+
"id": "admin_country",
|
606 |
+
"paint": {
|
607 |
+
"line-color": "#8b8a8a",
|
608 |
+
"line-width": {
|
609 |
+
"base": 1.3,
|
610 |
+
"stops": [
|
611 |
+
[
|
612 |
+
3,
|
613 |
+
0.5
|
614 |
+
],
|
615 |
+
[
|
616 |
+
22,
|
617 |
+
15
|
618 |
+
]
|
619 |
+
]
|
620 |
+
}
|
621 |
+
},
|
622 |
+
"source-layer": "admin"
|
623 |
+
},
|
624 |
+
{
|
625 |
+
"interactive": true,
|
626 |
+
"minzoom": 5,
|
627 |
+
"layout": {
|
628 |
+
"icon-image": "{maki}-11",
|
629 |
+
"text-offset": [
|
630 |
+
0,
|
631 |
+
0.5
|
632 |
+
],
|
633 |
+
"text-field": "{name_en}",
|
634 |
+
"text-font": [
|
635 |
+
"Open Sans Semibold",
|
636 |
+
"Arial Unicode MS Bold"
|
637 |
+
],
|
638 |
+
"text-max-width": 8,
|
639 |
+
"text-anchor": "top",
|
640 |
+
"text-size": 11,
|
641 |
+
"icon-size": 1
|
642 |
+
},
|
643 |
+
"filter": [
|
644 |
+
"all",
|
645 |
+
[
|
646 |
+
"==",
|
647 |
+
"$type",
|
648 |
+
"Point"
|
649 |
+
],
|
650 |
+
[
|
651 |
+
"all",
|
652 |
+
[
|
653 |
+
"==",
|
654 |
+
"scalerank",
|
655 |
+
1
|
656 |
+
],
|
657 |
+
[
|
658 |
+
"==",
|
659 |
+
"localrank",
|
660 |
+
1
|
661 |
+
]
|
662 |
+
]
|
663 |
+
],
|
664 |
+
"type": "symbol",
|
665 |
+
"source": "mapbox",
|
666 |
+
"id": "poi_label",
|
667 |
+
"paint": {
|
668 |
+
"text-color": "#666",
|
669 |
+
"text-halo-width": 1,
|
670 |
+
"text-halo-color": "rgba(255,255,255,0.75)",
|
671 |
+
"text-halo-blur": 1
|
672 |
+
},
|
673 |
+
"source-layer": "poi_label"
|
674 |
+
},
|
675 |
+
{
|
676 |
+
"interactive": true,
|
677 |
+
"layout": {
|
678 |
+
"symbol-placement": "line",
|
679 |
+
"text-field": "{name_en}",
|
680 |
+
"text-font": [
|
681 |
+
"Open Sans Semibold",
|
682 |
+
"Arial Unicode MS Bold"
|
683 |
+
],
|
684 |
+
"text-transform": "uppercase",
|
685 |
+
"text-letter-spacing": 0.1,
|
686 |
+
"text-size": {
|
687 |
+
"base": 1.4,
|
688 |
+
"stops": [
|
689 |
+
[
|
690 |
+
10,
|
691 |
+
8
|
692 |
+
],
|
693 |
+
[
|
694 |
+
20,
|
695 |
+
14
|
696 |
+
]
|
697 |
+
]
|
698 |
+
}
|
699 |
+
},
|
700 |
+
"filter": [
|
701 |
+
"all",
|
702 |
+
[
|
703 |
+
"==",
|
704 |
+
"$type",
|
705 |
+
"LineString"
|
706 |
+
],
|
707 |
+
[
|
708 |
+
"in",
|
709 |
+
"class",
|
710 |
+
"motorway",
|
711 |
+
"primary",
|
712 |
+
"secondary",
|
713 |
+
"tertiary",
|
714 |
+
"trunk"
|
715 |
+
]
|
716 |
+
],
|
717 |
+
"type": "symbol",
|
718 |
+
"source": "mapbox",
|
719 |
+
"id": "road_major_label",
|
720 |
+
"paint": {
|
721 |
+
"text-color": "#666",
|
722 |
+
"text-halo-color": "rgba(255,255,255,0.75)",
|
723 |
+
"text-halo-width": 2
|
724 |
+
},
|
725 |
+
"source-layer": "road_label"
|
726 |
+
},
|
727 |
+
{
|
728 |
+
"interactive": true,
|
729 |
+
"minzoom": 8,
|
730 |
+
"layout": {
|
731 |
+
"text-field": "{name_en}",
|
732 |
+
"text-font": [
|
733 |
+
"Open Sans Semibold",
|
734 |
+
"Arial Unicode MS Bold"
|
735 |
+
],
|
736 |
+
"text-max-width": 6,
|
737 |
+
"text-size": {
|
738 |
+
"stops": [
|
739 |
+
[
|
740 |
+
6,
|
741 |
+
12
|
742 |
+
],
|
743 |
+
[
|
744 |
+
12,
|
745 |
+
16
|
746 |
+
]
|
747 |
+
]
|
748 |
+
}
|
749 |
+
},
|
750 |
+
"filter": [
|
751 |
+
"all",
|
752 |
+
[
|
753 |
+
"==",
|
754 |
+
"$type",
|
755 |
+
"Point"
|
756 |
+
],
|
757 |
+
[
|
758 |
+
"in",
|
759 |
+
"type",
|
760 |
+
"town",
|
761 |
+
"village",
|
762 |
+
"hamlet",
|
763 |
+
"suburb",
|
764 |
+
"neighbourhood",
|
765 |
+
"island"
|
766 |
+
]
|
767 |
+
],
|
768 |
+
"type": "symbol",
|
769 |
+
"source": "mapbox",
|
770 |
+
"id": "place_label_other",
|
771 |
+
"paint": {
|
772 |
+
"text-color": "#666",
|
773 |
+
"text-halo-color": "rgba(255,255,255,0.75)",
|
774 |
+
"text-halo-width": 1,
|
775 |
+
"text-halo-blur": 1
|
776 |
+
},
|
777 |
+
"source-layer": "place_label"
|
778 |
+
},
|
779 |
+
{
|
780 |
+
"interactive": true,
|
781 |
+
"layout": {
|
782 |
+
"text-field": "{name_en}",
|
783 |
+
"text-font": [
|
784 |
+
"Open Sans Bold",
|
785 |
+
"Arial Unicode MS Bold"
|
786 |
+
],
|
787 |
+
"text-max-width": 10,
|
788 |
+
"text-size": {
|
789 |
+
"stops": [
|
790 |
+
[
|
791 |
+
3,
|
792 |
+
12
|
793 |
+
],
|
794 |
+
[
|
795 |
+
8,
|
796 |
+
16
|
797 |
+
]
|
798 |
+
]
|
799 |
+
}
|
800 |
+
},
|
801 |
+
"maxzoom": 16,
|
802 |
+
"filter": [
|
803 |
+
"all",
|
804 |
+
[
|
805 |
+
"==",
|
806 |
+
"$type",
|
807 |
+
"Point"
|
808 |
+
],
|
809 |
+
[
|
810 |
+
"==",
|
811 |
+
"type",
|
812 |
+
"city"
|
813 |
+
]
|
814 |
+
],
|
815 |
+
"type": "symbol",
|
816 |
+
"source": "mapbox",
|
817 |
+
"id": "place_label_city",
|
818 |
+
"paint": {
|
819 |
+
"text-color": "#666",
|
820 |
+
"text-halo-color": "rgba(255,255,255,0.75)",
|
821 |
+
"text-halo-width": 1,
|
822 |
+
"text-halo-blur": 1
|
823 |
+
},
|
824 |
+
"source-layer": "place_label"
|
825 |
+
},
|
826 |
+
{
|
827 |
+
"interactive": true,
|
828 |
+
"layout": {
|
829 |
+
"text-field": "{name_en}",
|
830 |
+
"text-font": [
|
831 |
+
"Open Sans Regular",
|
832 |
+
"Arial Unicode MS Regular"
|
833 |
+
],
|
834 |
+
"text-max-width": 10,
|
835 |
+
"text-size": {
|
836 |
+
"stops": [
|
837 |
+
[
|
838 |
+
3,
|
839 |
+
14
|
840 |
+
],
|
841 |
+
[
|
842 |
+
8,
|
843 |
+
22
|
844 |
+
]
|
845 |
+
]
|
846 |
+
}
|
847 |
+
},
|
848 |
+
"maxzoom": 12,
|
849 |
+
"filter": [
|
850 |
+
"==",
|
851 |
+
"$type",
|
852 |
+
"Point"
|
853 |
+
],
|
854 |
+
"type": "symbol",
|
855 |
+
"source": "mapbox",
|
856 |
+
"id": "country_label",
|
857 |
+
"paint": {
|
858 |
+
"text-color": "#666",
|
859 |
+
"text-halo-color": "rgba(255,255,255,0.75)",
|
860 |
+
"text-halo-width": 1,
|
861 |
+
"text-halo-blur": 1
|
862 |
+
},
|
863 |
+
"source-layer": "country_label"
|
864 |
+
}
|
865 |
+
]
|
866 |
+
}
|
src/sections/mode/index.tsx
ADDED
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from "react";
|
2 |
+
import { Poi } from "../../redux/types/Poi";
|
3 |
+
import { convertModeObjectToArray } from "../../utils/convertModeObjectToArray";
|
4 |
+
import { FaWalking } from "react-icons/fa";
|
5 |
+
import { AiFillCar } from "react-icons/ai";
|
6 |
+
|
7 |
+
type DemographicsSectionProps = {
|
8 |
+
selectedPoi: Poi;
|
9 |
+
};
|
10 |
+
const ModeSection: React.FC<DemographicsSectionProps> = ({ selectedPoi }) => {
|
11 |
+
return (
|
12 |
+
//isSelected ?
|
13 |
+
<div className="modeSection">
|
14 |
+
<div className="sectionModeDivider">
|
15 |
+
<div className="weekDistributionTitle">People walking</div>
|
16 |
+
<div className="averageModeWrapper">
|
17 |
+
{selectedPoi
|
18 |
+
? convertModeObjectToArray(selectedPoi?.data.mode)[0]
|
19 |
+
: "20%"}
|
20 |
+
<FaWalking style={{ marginLeft: "2%", fontSize: "50px" }} />
|
21 |
+
</div>
|
22 |
+
</div>
|
23 |
+
<div className="sectionModeDivider">
|
24 |
+
<div className="weekDistributionTitle">People using vehicles</div>
|
25 |
+
<div className="averageModeWrapper">
|
26 |
+
{selectedPoi
|
27 |
+
? convertModeObjectToArray(selectedPoi?.data.mode)[1]
|
28 |
+
: "20%"}
|
29 |
+
<AiFillCar style={{ marginLeft: "2%", fontSize: "50px" }} />
|
30 |
+
</div>
|
31 |
+
</div>
|
32 |
+
</div>
|
33 |
+
//) : ( <></>
|
34 |
+
);
|
35 |
+
};
|
36 |
+
|
37 |
+
export default ModeSection;
|
src/sections/trafic/index.tsx
ADDED
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { useState, useEffect } from "react";
|
2 |
+
import { Poi } from "../../redux/types/Poi";
|
3 |
+
import { formatNumber } from "../../utils/formatNumber";
|
4 |
+
import { FaBusAlt } from "react-icons/fa";
|
5 |
+
import ParentSize from "@visx/responsive/lib/components/ParentSize";
|
6 |
+
import DayTypeGraph from "../../components/charts/tripeChart/DayTypeGraph";
|
7 |
+
import SimpleChartTripPurpose from "../../components/charts/singleChart/SimpleChartTripPurpose";
|
8 |
+
import BrushChart from "../../components/charts/brushChart/AreaChart";
|
9 |
+
|
10 |
+
type TraficSectionProps = {
|
11 |
+
selectedPoi: Poi;
|
12 |
+
};
|
13 |
+
|
14 |
+
const TraficSection: React.FC<TraficSectionProps> = ({ selectedPoi }) => {
|
15 |
+
return (
|
16 |
+
//) isSelected ?
|
17 |
+
<>
|
18 |
+
<div className="sectionTrafic">
|
19 |
+
<div className="footfallLeftWrapper">
|
20 |
+
<div className="averageFootfall">
|
21 |
+
<div className="weekDistributionTitle">
|
22 |
+
Average Trafic Daily Flow
|
23 |
+
</div>
|
24 |
+
<div className="averageFootfallWrapper">
|
25 |
+
{selectedPoi
|
26 |
+
? formatNumber(selectedPoi?.data.traffic.avg_daily_flow)
|
27 |
+
: "17 400"}
|
28 |
+
<FaBusAlt style={{ marginLeft: "2%", fontSize: "40px" }} />
|
29 |
+
</div>
|
30 |
+
</div>
|
31 |
+
<div className="weekDistribution">
|
32 |
+
<div className="weekDistributionTitle">
|
33 |
+
Trafic Week Distribution
|
34 |
+
</div>
|
35 |
+
<ParentSize>
|
36 |
+
{({ width, height }) => (
|
37 |
+
<BrushChart
|
38 |
+
data={selectedPoi?.data.footfall.week_distribution}
|
39 |
+
compact={false}
|
40 |
+
width={width}
|
41 |
+
height={height}
|
42 |
+
/>
|
43 |
+
)}
|
44 |
+
</ParentSize>
|
45 |
+
</div>
|
46 |
+
</div>
|
47 |
+
<div className="basicInfoMap">
|
48 |
+
<div className="weekDistributionTitle">
|
49 |
+
Trafic Day Type Hour distribution
|
50 |
+
</div>
|
51 |
+
<ParentSize>
|
52 |
+
{({ width, height }) => (
|
53 |
+
<DayTypeGraph
|
54 |
+
data={selectedPoi?.data.traffic.daytype_hour_distribution}
|
55 |
+
width={width}
|
56 |
+
height={height}
|
57 |
+
/>
|
58 |
+
)}
|
59 |
+
</ParentSize>
|
60 |
+
</div>
|
61 |
+
</div>
|
62 |
+
<div className="tripPurposeChart">
|
63 |
+
<div className="weekDistributionTitle">Trip Purpose</div>
|
64 |
+
<ParentSize>
|
65 |
+
{({ width, height }) => (
|
66 |
+
<SimpleChartTripPurpose
|
67 |
+
data={selectedPoi?.data.trip_purpose}
|
68 |
+
width={width}
|
69 |
+
height={height}
|
70 |
+
/>
|
71 |
+
)}
|
72 |
+
</ParentSize>
|
73 |
+
</div>
|
74 |
+
</>
|
75 |
+
//) : ( <></>
|
76 |
+
);
|
77 |
+
};
|
78 |
+
|
79 |
+
export default TraficSection;
|
src/styles/global/App.css
ADDED
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.rootContent {
|
2 |
+
display: flex;
|
3 |
+
height: 100vh;
|
4 |
+
width: 100vw;
|
5 |
+
overflow: hidden;
|
6 |
+
}
|
7 |
+
.mainContent {
|
8 |
+
flex: 1;
|
9 |
+
overflow-y: auto;
|
10 |
+
width: 100%;
|
11 |
+
display: flex;
|
12 |
+
justify-content: flex-start;
|
13 |
+
align-items: center;
|
14 |
+
flex-direction: column;
|
15 |
+
}
|
16 |
+
|
17 |
+
.sectionBasicInfo {
|
18 |
+
width: 100%;
|
19 |
+
display: flex;
|
20 |
+
background-color: white;
|
21 |
+
flex-direction: row;
|
22 |
+
justify-content: space-between;
|
23 |
+
padding: 2%;
|
24 |
+
border-bottom: 1px solid #d1d1d1;
|
25 |
+
}
|
26 |
+
|
27 |
+
.sectionTrafic {
|
28 |
+
width: 100%;
|
29 |
+
display: flex;
|
30 |
+
background-color: white;
|
31 |
+
flex-direction: row;
|
32 |
+
justify-content: space-between;
|
33 |
+
padding: 2%;
|
34 |
+
}
|
35 |
+
|
36 |
+
.sectionBasicInfoOne {
|
37 |
+
width: 100%;
|
38 |
+
display: flex;
|
39 |
+
flex-direction: row;
|
40 |
+
justify-content: space-between;
|
41 |
+
padding: 2%;
|
42 |
+
padding-left: 10%;
|
43 |
+
padding-right: 10%;
|
44 |
+
border-bottom: 1px solid #d1d1d1; /* Add a thin bottom border */
|
45 |
+
}
|
46 |
+
|
47 |
+
.basicInfoLabelsOne {
|
48 |
+
background-color: white; /* Change background color to white */
|
49 |
+
font-family: "airbnb_semi_bold";
|
50 |
+
color: #3e3e3e;
|
51 |
+
font-size: 20px;
|
52 |
+
width: auto;
|
53 |
+
height: 10vh;
|
54 |
+
padding: 2%;
|
55 |
+
display: flex;
|
56 |
+
justify-content: space-evenly;
|
57 |
+
align-items: center;
|
58 |
+
border-radius: 20px; /* Add rounded borders */
|
59 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
60 |
+
}
|
61 |
+
|
62 |
+
.footfallLeftWrapper {
|
63 |
+
display: flex;
|
64 |
+
flex-direction: column;
|
65 |
+
justify-content: space-between;
|
66 |
+
height: 70vh;
|
67 |
+
width: 49%;
|
68 |
+
}
|
69 |
+
|
70 |
+
.averageFootfallWrapper {
|
71 |
+
display: flex;
|
72 |
+
flex-direction: row;
|
73 |
+
justify-content: center;
|
74 |
+
align-items: center;
|
75 |
+
width: auto;
|
76 |
+
position: sticky;
|
77 |
+
color: white;
|
78 |
+
font-family: "airbnb_extra_bold";
|
79 |
+
font-size: 60px;
|
80 |
+
}
|
81 |
+
|
82 |
+
.averageModeWrapper {
|
83 |
+
display: flex;
|
84 |
+
flex-direction: row;
|
85 |
+
justify-content: center;
|
86 |
+
align-items: center;
|
87 |
+
width: auto;
|
88 |
+
color: white;
|
89 |
+
font-family: "airbnb_extra_bold";
|
90 |
+
font-size: 60px;
|
91 |
+
}
|
92 |
+
|
93 |
+
.averageFootfallWrapperText {
|
94 |
+
font-family: "airbnb_light";
|
95 |
+
margin-top: 15px;
|
96 |
+
margin-left: 10px;
|
97 |
+
font-size: 20px;
|
98 |
+
}
|
99 |
+
|
100 |
+
.averageFootfall {
|
101 |
+
background: linear-gradient(to bottom, #858585, #000000);
|
102 |
+
width: 100%;
|
103 |
+
height: 14vh;
|
104 |
+
display: flex;
|
105 |
+
flex-direction: column;
|
106 |
+
border-radius: 20px; /* Add rounded borders */
|
107 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
108 |
+
}
|
109 |
+
|
110 |
+
.parentWidthFootfall {
|
111 |
+
display: flex;
|
112 |
+
justify-content: center;
|
113 |
+
align-items: center;
|
114 |
+
width: 100%;
|
115 |
+
height: 10vh;
|
116 |
+
border-bottom-left-radius: 14px;
|
117 |
+
border-bottom-right-radius: 14px;
|
118 |
+
}
|
119 |
+
|
120 |
+
.blockInformationBackground {
|
121 |
+
background: linear-gradient(to bottom, #4b4b4b, #000000);
|
122 |
+
display: flex;
|
123 |
+
justify-content: center;
|
124 |
+
align-items: center;
|
125 |
+
width: 90%;
|
126 |
+
height: 6.5vh;
|
127 |
+
border-radius: 14px;
|
128 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
129 |
+
}
|
130 |
+
|
131 |
+
.weekDistribution {
|
132 |
+
background-color: white; /* Change background color to white */
|
133 |
+
width: 100%;
|
134 |
+
height: 53vh;
|
135 |
+
display: flex;
|
136 |
+
flex-direction: column;
|
137 |
+
border-radius: 20px; /* Add rounded borders */
|
138 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
139 |
+
}
|
140 |
+
|
141 |
+
.weekDistributionTitle {
|
142 |
+
width: 100%;
|
143 |
+
height: 4vh;
|
144 |
+
display: flex;
|
145 |
+
justify-content: center;
|
146 |
+
align-items: center;
|
147 |
+
font-family: "airbnb_extra_bold";
|
148 |
+
font-size: 17px;
|
149 |
+
background-color: white;
|
150 |
+
color: #4b4b4b;
|
151 |
+
border-top-left-radius: 14px;
|
152 |
+
border-top-right-radius: 14px;
|
153 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
154 |
+
}
|
155 |
+
|
156 |
+
.basicInfoMap {
|
157 |
+
background-color: white; /* Change background color to white */
|
158 |
+
width: 49%;
|
159 |
+
height: 70vh;
|
160 |
+
display: flex;
|
161 |
+
flex-direction: column;
|
162 |
+
border-radius: 20px; /* Add rounded borders */
|
163 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
164 |
+
}
|
165 |
+
|
166 |
+
.modeSection {
|
167 |
+
width: 100%;
|
168 |
+
display: flex;
|
169 |
+
background-color: white; /* Change background color to white */
|
170 |
+
flex-direction: row;
|
171 |
+
justify-content: space-between;
|
172 |
+
padding: 2%;
|
173 |
+
border-bottom: 1px solid #d1d1d1; /* Add a thin bottom border */
|
174 |
+
}
|
175 |
+
|
176 |
+
.sectionModeDivider {
|
177 |
+
background: linear-gradient(to bottom, #858585, #000000);
|
178 |
+
width: 49%;
|
179 |
+
height: 14vh;
|
180 |
+
display: flex;
|
181 |
+
flex-direction: column;
|
182 |
+
border-radius: 20px; /* Add rounded borders */
|
183 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
184 |
+
}
|
185 |
+
|
186 |
+
.sectionModeWrapper {
|
187 |
+
display: flex;
|
188 |
+
flex-direction: row;
|
189 |
+
justify-content: space-between;
|
190 |
+
align-items: center;
|
191 |
+
position: absolute;
|
192 |
+
width: auto;
|
193 |
+
color: white;
|
194 |
+
font-family: "airbnb_extra_bold";
|
195 |
+
left: 70%;
|
196 |
+
font-size: 60px;
|
197 |
+
}
|
198 |
+
|
199 |
+
.demographicsSection {
|
200 |
+
width: 100%;
|
201 |
+
display: flex;
|
202 |
+
background-color: white; /* Change background color to white */
|
203 |
+
flex-direction: row;
|
204 |
+
justify-content: space-between;
|
205 |
+
padding: 2%;
|
206 |
+
border-bottom: 1px solid #d1d1d1; /* Add a thin bottom border */
|
207 |
+
}
|
208 |
+
|
209 |
+
.mainDemoPie {
|
210 |
+
background-color: white; /* Change background color to white */
|
211 |
+
width: 49%;
|
212 |
+
height: 73vh;
|
213 |
+
display: flex;
|
214 |
+
flex-direction: column;
|
215 |
+
}
|
216 |
+
|
217 |
+
.mainDemoPieTitle {
|
218 |
+
width: 100%;
|
219 |
+
height: 7vh;
|
220 |
+
display: flex;
|
221 |
+
justify-content: center;
|
222 |
+
align-items: center;
|
223 |
+
font-family: "airbnb_extra_bold";
|
224 |
+
font-size: 20px;
|
225 |
+
background-color: white;
|
226 |
+
color: #4b4b4b;
|
227 |
+
border-top-left-radius: 14px;
|
228 |
+
border-top-right-radius: 14px;
|
229 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
230 |
+
}
|
231 |
+
|
232 |
+
.otherDemoPieWrapper {
|
233 |
+
width: 49%;
|
234 |
+
height: 73vh;
|
235 |
+
display: flex;
|
236 |
+
flex-direction: column;
|
237 |
+
justify-content: space-between;
|
238 |
+
border-radius: 20px; /* Add rounded borders */
|
239 |
+
}
|
240 |
+
|
241 |
+
.wordCloud {
|
242 |
+
width: 49%;
|
243 |
+
height: 32vh;
|
244 |
+
border-radius: 20px; /* Add rounded borders */
|
245 |
+
background-color: white;
|
246 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
247 |
+
}
|
248 |
+
|
249 |
+
.menAndWomenPies {
|
250 |
+
display: flex;
|
251 |
+
flex-direction: row;
|
252 |
+
justify-content: space-between;
|
253 |
+
border-radius: 20px; /* Add rounded borders */
|
254 |
+
}
|
255 |
+
|
256 |
+
.LegendChart {
|
257 |
+
display: flex;
|
258 |
+
flex-direction: row;
|
259 |
+
justify-content: space-between;
|
260 |
+
border-radius: 20px; /* Add rounded borders */
|
261 |
+
}
|
262 |
+
|
263 |
+
.menPie {
|
264 |
+
width: 49%;
|
265 |
+
height: 33vh;
|
266 |
+
border-radius: 20px; /* Add rounded borders */
|
267 |
+
background-color: white;
|
268 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
269 |
+
}
|
270 |
+
|
271 |
+
.womenPie {
|
272 |
+
width: 49%;
|
273 |
+
height: 33vh;
|
274 |
+
border-radius: 20px; /* Add rounded borders */
|
275 |
+
background-color: white;
|
276 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
277 |
+
}
|
278 |
+
|
279 |
+
.blockSection {
|
280 |
+
width: 100%;
|
281 |
+
height: 80vh;
|
282 |
+
display: flex;
|
283 |
+
flex-direction: row;
|
284 |
+
justify-content: space-between;
|
285 |
+
padding: 2%;
|
286 |
+
border-bottom: 1px solid #d1d1d1; /* Add a thin bottom border */
|
287 |
+
}
|
288 |
+
|
289 |
+
.blockInformation {
|
290 |
+
width: 25%;
|
291 |
+
height: 68vh;
|
292 |
+
display: flex;
|
293 |
+
flex-direction: column;
|
294 |
+
justify-content: space-between;
|
295 |
+
align-items: center;
|
296 |
+
border-radius: 20px; /* Add rounded borders */
|
297 |
+
background-color: white;
|
298 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
299 |
+
padding-bottom: 10px;
|
300 |
+
}
|
301 |
+
|
302 |
+
.blockSimpleChart {
|
303 |
+
width: 73%;
|
304 |
+
height: 63vh;
|
305 |
+
border-radius: 20px; /* Add rounded borders */
|
306 |
+
background-color: pink;
|
307 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2); /* Add shadow */
|
308 |
+
}
|
309 |
+
|
310 |
+
.tripPurposeChart {
|
311 |
+
width: 95%;
|
312 |
+
height: 63vh;
|
313 |
+
border-radius: 20px; /* Add rounded borders */
|
314 |
+
padding-bottom: 5%;
|
315 |
+
}
|
316 |
+
|
317 |
+
.blockInformationContent {
|
318 |
+
padding: 0.8%;
|
319 |
+
padding-right: 5%;
|
320 |
+
display: flex;
|
321 |
+
flex-direction: row;
|
322 |
+
justify-content: space-between;
|
323 |
+
width: 100%;
|
324 |
+
font-family: "airbnb_semi_bold";
|
325 |
+
font-size: 20px;
|
326 |
+
color: white;
|
327 |
+
}
|
328 |
+
|
329 |
+
@media (max-width: 1200px) {
|
330 |
+
.sectionBasicInfoOne {
|
331 |
+
width: 100%;
|
332 |
+
display: flex;
|
333 |
+
flex-direction: row;
|
334 |
+
justify-content: space-between;
|
335 |
+
padding: 2%;
|
336 |
+
padding-left: 10%;
|
337 |
+
padding-right: 10%;
|
338 |
+
border-bottom: 1px solid #d1d1d1;
|
339 |
+
}
|
340 |
+
.basicInfoLabelsOne {
|
341 |
+
background-color: white;
|
342 |
+
font-family: "airbnb_semi_bold";
|
343 |
+
color: #3e3e3e;
|
344 |
+
font-size: 12px;
|
345 |
+
width: 20%;
|
346 |
+
height: 10vh;
|
347 |
+
padding: 2%;
|
348 |
+
display: flex;
|
349 |
+
justify-content: space-evenly;
|
350 |
+
align-items: center;
|
351 |
+
border-radius: 20px;
|
352 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
|
353 |
+
}
|
354 |
+
}
|
355 |
+
|
356 |
+
@media (min-width: 1200px) and (max-width: 1800px) {
|
357 |
+
.sectionBasicInfoOne {
|
358 |
+
width: 100%;
|
359 |
+
display: flex;
|
360 |
+
flex-direction: row;
|
361 |
+
justify-content: space-between;
|
362 |
+
padding: 2%;
|
363 |
+
padding-left: 10%;
|
364 |
+
padding-right: 10%;
|
365 |
+
border-bottom: 1px solid #d1d1d1;
|
366 |
+
}
|
367 |
+
.basicInfoLabelsOne {
|
368 |
+
background-color: white;
|
369 |
+
font-family: "airbnb_semi_bold";
|
370 |
+
color: #3e3e3e;
|
371 |
+
font-size: 14px;
|
372 |
+
width: 20%;
|
373 |
+
height: 10vh;
|
374 |
+
padding: 2%;
|
375 |
+
display: flex;
|
376 |
+
justify-content: space-evenly;
|
377 |
+
align-items: center;
|
378 |
+
border-radius: 20px;
|
379 |
+
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
|
380 |
+
}
|
381 |
+
}
|
src/styles/global/customScrollBar.css
ADDED
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
::-webkit-scrollbar {
|
2 |
+
width: 6px; /* Width of the scrollbar track */
|
3 |
+
}
|
4 |
+
|
5 |
+
/* Define the scrollbar thumb (the draggable part) */
|
6 |
+
::-webkit-scrollbar-thumb {
|
7 |
+
background: #cfcfcf; /* Color of the thumb */
|
8 |
+
border-radius: 6px; /* Rounded corners of the thumb */
|
9 |
+
}
|
10 |
+
|
11 |
+
/* Define the scrollbar track on hover */
|
12 |
+
::-webkit-scrollbar-track:hover {
|
13 |
+
background: #aaa; /* Color of the track on hover */
|
14 |
+
}
|
15 |
+
|
16 |
+
/* Define the scrollbar thumb on hover */
|
17 |
+
::-webkit-scrollbar-thumb:hover {
|
18 |
+
background: #555; /* Color of the thumb on hover */
|
19 |
+
}
|
20 |
+
|
21 |
+
/* Define the scrollbar corner (between vertical and horizontal scrollbars) */
|
22 |
+
::-webkit-scrollbar-corner {
|
23 |
+
background: transparent; /* Color of the scrollbar corner */
|
24 |
+
}
|
src/styles/global/index.css
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
@tailwind base;
|
2 |
+
@tailwind components;
|
3 |
+
@tailwind utilities;
|
4 |
+
|
5 |
+
html,
|
6 |
+
body {
|
7 |
+
height: 100%; /* Ensure full viewport height */
|
8 |
+
width: 100vw;
|
9 |
+
overflow: hidden; /* Prevent body scrolling */
|
10 |
+
|
11 |
+
/* Add your background color or other styles here */
|
12 |
+
}
|
13 |
+
|
14 |
+
/* Define a container for your React app */
|
15 |
+
#root {
|
16 |
+
display: flex;
|
17 |
+
height: 100%; /* Ensure full viewport height */
|
18 |
+
width: 100vw;
|
19 |
+
overflow: hidden; /* Prevent container scrolling */
|
20 |
+
}
|
src/styles/sections/demographics/index.css
ADDED
File without changes
|
src/styles/sections/description/index.css
ADDED
File without changes
|
src/styles/sections/drawer/index.css
ADDED
File without changes
|
src/styles/sections/footfall/index.css
ADDED
File without changes
|
src/styles/sections/information/index.css
ADDED
File without changes
|