nolual commited on
Commit
0c20ea8
1 Parent(s): 17bd1ee

Upload 55 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. src/App.tsx +79 -0
  2. src/algorithm/DemographicsPieByGender.ts +39 -0
  3. src/algorithm/DemographicsPieBySocialGroup.ts +24 -0
  4. src/algorithm/WordCloud.ts +98 -0
  5. src/assets/fonts/AirbnbCereal_W_Bd.otf +0 -0
  6. src/assets/fonts/AirbnbCereal_W_Bk.otf +0 -0
  7. src/assets/fonts/AirbnbCereal_W_Blk.otf +0 -0
  8. src/assets/fonts/AirbnbCereal_W_Lt.otf +0 -0
  9. src/assets/fonts/AirbnbCereal_W_Md.otf +0 -0
  10. src/assets/fonts/AirbnbCereal_W_XBd.otf +0 -0
  11. src/assets/fonts/fonts.css +29 -0
  12. src/assets/images/pin.png +0 -0
  13. src/assets/images/seiki.png +0 -0
  14. src/components/charts/brushChart/Area.tsx +94 -0
  15. src/components/charts/brushChart/AreaChart.tsx +245 -0
  16. src/components/charts/pieChart/Pie.tsx +278 -0
  17. src/components/charts/singleChart/SimpleChart.tsx +141 -0
  18. src/components/charts/singleChart/SimpleChartTripPurpose.tsx +141 -0
  19. src/components/charts/tripeChart/DayTypeGraph.tsx +204 -0
  20. src/components/charts/wordCloudChart/CloudWord.tsx +114 -0
  21. src/components/generalInformation/dropdown/DropDown.tsx +78 -0
  22. src/components/generalInformation/legend/Legend.tsx +343 -0
  23. src/components/main/GraphSections.tsx +55 -0
  24. src/constants/main.ts +1 -0
  25. src/data/data.ts +0 -0
  26. src/declarations.d.ts +4 -0
  27. src/index.tsx +19 -0
  28. src/redux/actions/poiActions.ts +6 -0
  29. src/redux/reducers/poiReducer.ts +37 -0
  30. src/redux/sagas/poiSaga.ts +31 -0
  31. src/redux/store/store.ts +19 -0
  32. src/redux/types/Poi.ts +62 -0
  33. src/sections/demographics/index.tsx +95 -0
  34. src/sections/description/index.tsx +39 -0
  35. src/sections/drawer/index.tsx +303 -0
  36. src/sections/footfall/index.tsx +60 -0
  37. src/sections/information/index.tsx +107 -0
  38. src/sections/map/control-panel.tsx +129 -0
  39. src/sections/map/index.tsx +64 -0
  40. src/sections/map/map-style-basic-v8.json +866 -0
  41. src/sections/mode/index.tsx +37 -0
  42. src/sections/trafic/index.tsx +79 -0
  43. src/styles/global/App.css +381 -0
  44. src/styles/global/customScrollBar.css +24 -0
  45. src/styles/global/index.css +20 -0
  46. src/styles/sections/demographics/index.css +0 -0
  47. src/styles/sections/description/index.css +0 -0
  48. src/styles/sections/drawer/index.css +0 -0
  49. src/styles/sections/footfall/index.css +0 -0
  50. 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