From 796e06b88be3475e6233b543711201d7e128e566 Mon Sep 17 00:00:00 2001 From: Nikhil Ramchandani Date: Wed, 18 Sep 2024 20:11:06 -0400 Subject: [PATCH 1/3] new workflow --- frontend/package-lock.json | 38 ++++++++++ frontend/package.json | 4 + .../LocationsDropDown/LocationsDropDown.tsx | 73 ++++++++++++------- .../src/pages/LocationPage/LocationsPage.tsx | 62 ++++++++++++---- jest.config.js | 4 +- 5 files changed, 134 insertions(+), 47 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 19bf058..126a422 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -30,10 +30,12 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^29.5.4", + "@types/lodash": "^4.17.7", "@types/node": "^20.6.0", "@types/react": "^18.2.25", "@types/react-dom": "^18.2.7", "@types/react-redux": "^7.1.26", + "@types/react-window": "^1.8.8", "@types/redux-logger": "^3.0.9", "antd": "^5.4.2", "axios": "^1.6.1", @@ -45,6 +47,7 @@ "google-map-react": "^2.2.0", "graphql": "^16.6.0", "konva": "^9.2.0", + "lodash": "^4.17.21", "moment": "^2.29.4", "pigeon-maps": "^0.21.3", "pigeon-maps-cluster": "^1.0.8", @@ -59,6 +62,7 @@ "react-tabs": "^6.0.2", "react-toastify": "^10.0.5", "react-tooltip": "^5.10.0", + "react-window": "^1.8.10", "redux-logger": "^3.0.6", "serve": "^14.2.1", "supercluster": "^8.0.1", @@ -7938,6 +7942,11 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==" + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -8042,6 +8051,14 @@ "@types/react": "*" } }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/redux-logger": { "version": "3.0.12", "resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.12.tgz", @@ -19774,6 +19791,11 @@ "node": ">= 4.0.0" } }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -23624,6 +23646,22 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-window": { + "version": "1.8.10", + "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", + "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", + "dependencies": { + "@babel/runtime": "^7.0.0", + "memoize-one": ">=3.1.1 <6" + }, + "engines": { + "node": ">8.0.0" + }, + "peerDependencies": { + "react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 9eb362e..45959b7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -25,10 +25,12 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "@types/jest": "^29.5.4", + "@types/lodash": "^4.17.7", "@types/node": "^20.6.0", "@types/react": "^18.2.25", "@types/react-dom": "^18.2.7", "@types/react-redux": "^7.1.26", + "@types/react-window": "^1.8.8", "@types/redux-logger": "^3.0.9", "antd": "^5.4.2", "axios": "^1.6.1", @@ -40,6 +42,7 @@ "google-map-react": "^2.2.0", "graphql": "^16.6.0", "konva": "^9.2.0", + "lodash": "^4.17.21", "moment": "^2.29.4", "pigeon-maps": "^0.21.3", "pigeon-maps-cluster": "^1.0.8", @@ -54,6 +57,7 @@ "react-tabs": "^6.0.2", "react-toastify": "^10.0.5", "react-tooltip": "^5.10.0", + "react-window": "^1.8.10", "redux-logger": "^3.0.6", "serve": "^14.2.1", "supercluster": "^8.0.1", diff --git a/frontend/src/components/LocationsDropDown/LocationsDropDown.tsx b/frontend/src/components/LocationsDropDown/LocationsDropDown.tsx index 0e509bd..24b0d94 100644 --- a/frontend/src/components/LocationsDropDown/LocationsDropDown.tsx +++ b/frontend/src/components/LocationsDropDown/LocationsDropDown.tsx @@ -1,13 +1,12 @@ -import React, { useContext } from "react"; +import React, { useContext, useMemo } from "react"; import { MenuProps } from "antd"; import { Menu } from "antd"; import { LocationContext } from "../../contexts/location_context"; import { Locations } from "../../__generated__/graphql"; import { Card, CardContent } from "@mui/material"; -import { Input } from "antd"; // Import Input for search bar - - - +import { Input } from "antd"; +import { debounce } from "lodash"; // Import lodash debounce +import { FixedSizeList as List, ListChildComponentProps } from 'react-window'; // Import react-window for virtualization type MenuItem = Required["items"][number]; @@ -18,39 +17,57 @@ interface LocationsDropDownProps { const LocationsDropDown: React.FC = ({ selectedLocation, setSelectedLocation }) => { const { locationsData } = useContext(LocationContext)!; - const [searchTerm, setSearchTerm] = React.useState(""); // State for search term + const [searchTerm, setSearchTerm] = React.useState(""); // Add explicit string type + // Debounced search term update + const debouncedSetSearchTerm = debounce((value: string) => setSearchTerm(value), 300); + // Memoized filtered locations + const filteredItems: Locations[] = useMemo(() => { + return locationsData?.filter(location => + location.value.toLowerCase().includes(searchTerm.toLowerCase()) + ) + .sort((a, b) => b.articles.length - a.articles.length) || []; // Sort by article count descending + }, [locationsData, searchTerm]); - const filteredItems: MenuItem[] = locationsData?.filter(location => - location.value.toLowerCase().includes(searchTerm.toLowerCase()) - ).map(location => ({ - key: location.value, - label: `${location.value} - ${location.articles.length} articles`, - onClick: () => { - setSelectedLocation(location); - }, - })) || []; + // Virtualized List Row Component + const Row = ({ index, style }: ListChildComponentProps) => { + const location = filteredItems[index]; + const isSelected = selectedLocation?.value === location.value; + return ( +
setSelectedLocation(location)} + > + {location.value} - {location.articles.length} articles +
+ ); + }; return ( setSearchTerm(e.target.value)} // Update search term - style={{ marginBottom: '10px' }} // Spacing for search bar - /> - debouncedSetSearchTerm(e.target.value)} + style={{ marginBottom: '10px' }} /> + + {Row} + - ); + ); }; -export default LocationsDropDown; \ No newline at end of file +export default LocationsDropDown; diff --git a/frontend/src/pages/LocationPage/LocationsPage.tsx b/frontend/src/pages/LocationPage/LocationsPage.tsx index a8f059a..0a1d769 100644 --- a/frontend/src/pages/LocationPage/LocationsPage.tsx +++ b/frontend/src/pages/LocationPage/LocationsPage.tsx @@ -11,14 +11,31 @@ import { useOrganization, useUser } from "@clerk/clerk-react"; import { minDate, maxDate } from "../../App"; import { Article } from '../../__generated__/graphql'; import ArticleCard from "../../components/ArticleCard/ArticleCard"; +import TopicCount from '../../components/TopicCount/TopicCount'; +import NeighborhoodDemographicsBoard from "../../components/NeighborhoodDemoBoard/NeighborhoodDemoBoard"; +import { TractContext } from "../../contexts/tract_context"; // Import TractContext - +const getMostCommonTract = (articles: Article[]) => { + const tractCount: { [key: string]: number } = {}; + + articles.forEach(article => { + article.tracts.forEach(tract => { + tractCount[tract] = (tractCount[tract] || 0) + 1; + }); + }); + + return Object.keys(tractCount).reduce((a, b) => + tractCount[a] > tractCount[b] ? a : b + ); +}; const LocationsPage: React.FC = () => { const { locationsData, queryLocationsData } = React.useContext(LocationContext)!; + const { queryTractDataType } = React.useContext(TractContext)!; // Get query function from TractContext + const [selectedLocation, setSelectedLocation] = useState(null); @@ -85,25 +102,23 @@ const LocationsPage: React.FC = () => { params.set('location', selectedLocation?.value as string); navigate({ search: params.toString() }); } - const newLocationArticles = articleData2.filter(article => - selectedLocation.articles.includes(article.content_id) - ); - setLocationArticles(newLocationArticles); + const newLocationArticles = articleData2.filter(article => + selectedLocation.articles.includes(article.content_id) + ); + setLocationArticles(newLocationArticles); + const mostCommonTract = getMostCommonTract(newLocationArticles); + queryTractDataType("TRACT_DATA", { + dateFrom: parseInt(minDate.format("YYYYMMDD")), + dateTo: parseInt(maxDate.format("YYYYMMDD")), + tract: mostCommonTract, + }); + + } },[selectedLocation, articleData2]); - React.useEffect(() => { - console.log("articleData",articleData) - console.log("articleData2",articleData2) - - }, [articleData,articleData2]); - - React.useEffect(() => { - - console.log(locationArticles) - }, [locationArticles]); @@ -133,8 +148,23 @@ const LocationsPage: React.FC = () => { {locationArticles.length > 0 && ( - +
+
+
+

Topics Count

+ +
+
+

Demographics

+ +
+
+

Articles

+ +
+ )} + )} diff --git a/jest.config.js b/jest.config.js index cdda2ef..d8925c9 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,10 +3,8 @@ module.exports = { '^.+\\.(js|jsx|ts|tsx)$': 'babel-jest', }, transformIgnorePatterns: [ - 'node_modules/(?!(axios|@mui|x-charts|d3-shape)/)', // Explicitly transform certain node_modules + 'node_modules/(?!(axios|@mui|x-charts|d3-shape|use-supercluster)/)', '/node_modules/(?!@mui/x-charts|@mui/material|@babel/runtime|d3-(color|format|interpolate|scale|shape|time|time-format|path|array)|internmap)', - 'node_modules/(?!(use-supercluster)/)', - ], moduleNameMapper: { From 40bd62a2b8778d98ce7847ecf2be322704b1bc91 Mon Sep 17 00:00:00 2001 From: Nikhil Ramchandani Date: Wed, 18 Sep 2024 20:13:48 -0400 Subject: [PATCH 2/3] topic count module --- .../src/components/TopicCount/TopicCount.tsx | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 frontend/src/components/TopicCount/TopicCount.tsx diff --git a/frontend/src/components/TopicCount/TopicCount.tsx b/frontend/src/components/TopicCount/TopicCount.tsx new file mode 100644 index 0000000..980746c --- /dev/null +++ b/frontend/src/components/TopicCount/TopicCount.tsx @@ -0,0 +1,86 @@ +import React, { useState } from "react"; +import { Article } from "../../__generated__/graphql"; +import { Button, Modal, Box, Typography, Card, CardContent } from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import { List, ListItem, ListItemText } from "@mui/material"; // Add List imports + + + +interface TopicCountProps { + articles: Article[]; +} + +const TopicCount: React.FC = ({ articles }) => { + const [open, setOpen] = useState(false); + const topicCounts: Record = {}; + const navigate = useNavigate(); + + // Count occurrences of each topic + articles.forEach((article) => { + const topic = article.openai_labels; + if (topic) { + topicCounts[topic] = (topicCounts[topic] || 0) + 1; + } + }); + + // Get top 5 topics + const topTopics = Object.entries(topicCounts) + .sort(([, countA], [, countB]) => countB - countA) + .slice(0, 5); + + // Handle modal open/close + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + // Navigate to topic page + const handleTopicClick = (topic: string) => { + navigate(`/Topics?topic=${encodeURIComponent(topic)}`); + }; + + return ( + + + {/* Use List component */} + {topTopics.map(([topic, count]) => ( + handleTopicClick(topic)} style={{ cursor: 'pointer', color:'#1E90FF' }}> + + + ))} + + + + + + + All Topic Counts + + {/* Use flexbox for layout */} + {Object.entries(topicCounts) + .sort(([, countA], [, countB]) => countB - countA) // Sort by count in descending order + .map(([topic, count]) => ( + handleTopicClick(topic)} style={{ cursor: 'pointer', width: '23%', color:'#1E90FF' }}> + + + ))} + + + + + + + ); +} +export default TopicCount; \ No newline at end of file From 5809b4c0952db89685819f520eaa47f3623fac2c Mon Sep 17 00:00:00 2001 From: Nikhil Ramchandani Date: Wed, 18 Sep 2024 20:19:39 -0400 Subject: [PATCH 3/3] test frontend fix --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index d8925c9..9008221 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,7 +4,7 @@ module.exports = { }, transformIgnorePatterns: [ 'node_modules/(?!(axios|@mui|x-charts|d3-shape|use-supercluster)/)', - '/node_modules/(?!@mui/x-charts|@mui/material|@babel/runtime|d3-(color|format|interpolate|scale|shape|time|time-format|path|array)|internmap)', + '/node_modules/(?!@mui/x-charts|@mui/material|@babel/runtime|d3-(color|format|interpolate|scale|shape|time|time-format|path|array)|internmap|use-supercluster)', ], moduleNameMapper: {