diff --git a/nest_frontend/LocalizationStrings.js b/nest_frontend/LocalizationStrings.js
index 40d9d9e..481f91f 100644
--- a/nest_frontend/LocalizationStrings.js
+++ b/nest_frontend/LocalizationStrings.js
@@ -46,7 +46,7 @@ export default {
repoEdit: "Modifica repository",
menuActive: "Le tue repository attive",
menuArchived: "Le tue repository archiviate",
- emptyMenu: "Non c'è nulla qui",
+ emptyMenu: "Non c'è nulla qui.",
delete: "Elimina",
archive: "Archivia",
edit: "Modifica",
@@ -117,7 +117,7 @@ export default {
repoEdit: "Edit repository",
menuActive: "Your active repositories",
menuArchived: "Your archived repositories",
- emptyMenu: "There's nothing here",
+ emptyMenu: "There's nothing here.",
delete: "Delete",
archive: "Archive",
edit: "Edit",
@@ -188,7 +188,7 @@ export default {
repoEdit: "Muokkaa arkistoa",
menuActive: "Aktiiviset arkistosi",
menuArchived: "Arkistoidut arkistosi",
- emptyMenu: "Täällä ei ole mitään",
+ emptyMenu: "Täällä ei ole mitään.",
delete: "Poista",
archive: "Arkistoi",
edit: "Muokkaa",
diff --git a/nest_frontend/components/base/BoxMap.js b/nest_frontend/components/base/BoxMap.js
index 5dda737..1ba1ad8 100644
--- a/nest_frontend/components/base/BoxMap.js
+++ b/nest_frontend/components/base/BoxMap.js
@@ -4,7 +4,7 @@ import BoxFull from "./BoxFull"
import { MapContainer, TileLayer } from "react-leaflet"
-export default function BoxMap({ header, setMap, startingPosition, startingZoom, button, children, additions, ...props }) {
+export default function BoxMap({ header, setMap, startingPosition = { lat: 41.89309, lng: 12.48289 }, startingZoom = 3, button, children, ...props }) {
return (
- {additions}
+ {children}
{button}
diff --git a/nest_frontend/components/interactive/BoxWordcloud.js b/nest_frontend/components/base/BoxWordcloud.js
similarity index 67%
rename from nest_frontend/components/interactive/BoxWordcloud.js
rename to nest_frontend/components/base/BoxWordcloud.js
index d8e5177..a74cf3d 100644
--- a/nest_frontend/components/interactive/BoxWordcloud.js
+++ b/nest_frontend/components/base/BoxWordcloud.js
@@ -1,7 +1,7 @@
-import React, { useContext } from "react"
+import React from "react"
import BoxFull from "../base/BoxFull"
import ReactWordcloud from "@steffo/nest-react-wordcloud"
-import ContextLanguage from "../../contexts/ContextLanguage"
+import Style from "./BoxWordcloud.module.css"
/**
@@ -12,19 +12,17 @@ import ContextLanguage from "../../contexts/ContextLanguage"
* @returns {JSX.Element}
* @constructor
*/
-export default function BoxWordcloud({ words, props }) {
- const { strings } = useContext(ContextLanguage)
-
+export default function BoxWordcloud({ words, ...props }) {
return (
-
-
+
+
}
- startingPosition={STARTING_POSITION}
- startingZoom={STARTING_ZOOM}
setMap={setMap}
button={
}
else if(repositories.length === 0) {
- contents = {strings.emptyMenu}
+ contents =
}
else {
contents = repositories.map(repo => (
diff --git a/nest_frontend/components/interactive/BoxRepositoryTweets.js b/nest_frontend/components/interactive/BoxRepositoryTweets.js
index d5e32fa..166f65a 100644
--- a/nest_frontend/components/interactive/BoxRepositoryTweets.js
+++ b/nest_frontend/components/interactive/BoxRepositoryTweets.js
@@ -1,14 +1,23 @@
import React from "react"
import BoxFullScrollable from "../base/BoxFullScrollable"
import SummaryTweet from "./SummaryTweet"
+import Empty from "./Empty"
export default function BoxRepositoryTweets({ tweets, ...props }) {
// TODO: Translate this
+ let content
+ if(tweets.length === 0) {
+ content =
+ }
+ else {
+ content = tweets.map(tweet => )
+ }
+
return (
- {tweets.map(tweet => )}
+ {content}
)
}
diff --git a/nest_frontend/components/interactive/BoxVisualizationGraph.js b/nest_frontend/components/interactive/BoxVisualizationGraph.js
new file mode 100644
index 0000000..fbcf990
--- /dev/null
+++ b/nest_frontend/components/interactive/BoxVisualizationGraph.js
@@ -0,0 +1,14 @@
+import React from "react"
+import BoxFull from "../base/BoxFull"
+
+
+export default function BoxVisualizationGraph({ children, className, ...props }) {
+ // TODO: translate this
+ // TODO: implement this
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/nest_frontend/components/interactive/BoxVisualizationMap.js b/nest_frontend/components/interactive/BoxVisualizationMap.js
new file mode 100644
index 0000000..96f7f8a
--- /dev/null
+++ b/nest_frontend/components/interactive/BoxVisualizationMap.js
@@ -0,0 +1,40 @@
+import React, { useContext } from "react"
+import BoxMap from "../base/BoxMap"
+import ContextLanguage from "../../contexts/ContextLanguage"
+import { Marker, Popup } from "react-leaflet"
+
+
+const locationRegex = /[{](?[0-9.]+),(?[0-9.]+)[}]/
+
+export default function BoxVisualizationMap({ tweets, ...props }) {
+ // TODO: translate this
+ const {strings} = useContext(ContextLanguage)
+
+ console.debug(tweets)
+ const markers = tweets.filter(tweet => tweet.location).map(tweet => {
+ const match = locationRegex.exec(tweet.location)
+ if(!match) {
+ console.error("No match for location ", tweet.location)
+ return null
+ }
+ const {lat, lng} = match.groups
+ return (
+
+
+
+ {tweet["content"]}
+
+
+ — @{tweet["poster"]}
+
+
+
+ )
+ })
+
+ return (
+
+ {markers}
+
+ )
+}
diff --git a/nest_frontend/components/interactive/BoxVisualizationStats.js b/nest_frontend/components/interactive/BoxVisualizationStats.js
new file mode 100644
index 0000000..404bbce
--- /dev/null
+++ b/nest_frontend/components/interactive/BoxVisualizationStats.js
@@ -0,0 +1,77 @@
+import React, { useMemo } from "react"
+import FormLabelled from "../base/FormLabelled"
+import FormLabel from "../base/formparts/FormLabel"
+import BoxFullScrollable from "../base/BoxFullScrollable"
+import tokenizeTweetWords from "../../utils/tokenizeTweetWords"
+
+
+export default function BoxVisualizationStats({ tweets, totalTweetCount, ...props }) {
+
+ const words = useMemo(
+ () => tokenizeTweetWords(tweets),
+ [tweets]
+ )
+
+ const tweetCount = tweets.length
+ const tweetPct = tweetCount / totalTweetCount * 100
+ const tweetLocationCount = tweets.filter(tweet => tweet.location).length
+ const tweetLocationPct = tweetLocationCount / tweetCount * 100
+ const tweetContent = tweets.filter(tweet => tweet.content)
+ const tweetContentCount = tweetContent.length
+ const tweetContentPct = tweetContentCount / tweetCount * 100
+ const wordCount = words.map(word => word.value).reduce((a, b) => a+b)
+ const mostPopularWord = words.sort((wa, wb) => {
+ if(wa.value > wb.value) return -1
+ if(wa.value < wb.value) return 1
+ return 0
+ })[0].text
+ const users = [...new Set(tweets.map(tweet => tweet.poster))]
+ const usersCount = users.length
+
+ // TODO: tweets with picture count
+ // TODO: tweets with picture pct
+
+ // TODO: translate this
+ return (
+
+
+
+ {totalTweetCount}
+
+
+ {tweetCount}
+
+
+ {tweetPct.toFixed(2)}%
+
+
+ {tweetLocationCount}
+
+
+ {tweetLocationPct.toFixed(2)}%
+
+
+ {tweetContentCount}
+
+
+ {tweetContentPct.toFixed(2)}%
+
+
+ {wordCount}
+
+
+ {mostPopularWord}
+
+
+ 🚧
+
+
+ 🚧
+
+
+ {usersCount}
+
+
+
+ )
+}
diff --git a/nest_frontend/components/interactive/BoxVisualizationWordcloud.js b/nest_frontend/components/interactive/BoxVisualizationWordcloud.js
new file mode 100644
index 0000000..6806ff2
--- /dev/null
+++ b/nest_frontend/components/interactive/BoxVisualizationWordcloud.js
@@ -0,0 +1,18 @@
+import React, { useContext, useMemo } from "react"
+import BoxWordcloud from "../base/BoxWordcloud"
+import ContextLanguage from "../../contexts/ContextLanguage"
+import tokenizeTweetWords from "../../utils/tokenizeTweetWords"
+
+
+export default function BoxVisualizationWordcloud({ tweets = [], ...props }) {
+ const {strings} = useContext(ContextLanguage)
+
+ const words = useMemo(
+ () => tokenizeTweetWords(tweets),
+ [tweets]
+ )
+
+ return (
+
+ )
+}
diff --git a/nest_frontend/components/interactive/Empty.js b/nest_frontend/components/interactive/Empty.js
new file mode 100644
index 0000000..8393748
--- /dev/null
+++ b/nest_frontend/components/interactive/Empty.js
@@ -0,0 +1,15 @@
+import React, { useContext } from "react"
+import Style from "./Empty.module.css"
+import classNames from "classnames"
+import ContextLanguage from "../../contexts/ContextLanguage"
+
+
+export default function Empty({ children, className, ...props }) {
+ const { strings } = useContext(ContextLanguage)
+
+ return (
+
+ {strings.emptyMenu}
+
+ )
+}
diff --git a/nest_frontend/components/interactive/Empty.module.css b/nest_frontend/components/interactive/Empty.module.css
new file mode 100644
index 0000000..558041f
--- /dev/null
+++ b/nest_frontend/components/interactive/Empty.module.css
@@ -0,0 +1,3 @@
+.Empty {
+ opacity: 0.5;
+}
diff --git a/nest_frontend/components/interactive/SummaryTweet.js b/nest_frontend/components/interactive/SummaryTweet.js
index 5f64122..2b45ac7 100644
--- a/nest_frontend/components/interactive/SummaryTweet.js
+++ b/nest_frontend/components/interactive/SummaryTweet.js
@@ -1,15 +1,18 @@
import React from "react"
import SummaryBase from "../base/summary/SummaryBase"
import SummaryLeft from "../base/summary/SummaryLeft"
-import { faComment, faMapPin } from "@fortawesome/free-solid-svg-icons"
+import { faComment, faLocationArrow, faMapMarker, faMapMarkerAlt, faMapPin } from "@fortawesome/free-solid-svg-icons"
import SummaryText from "../base/summary/SummaryText"
import SummaryRight from "../base/summary/SummaryRight"
export default function SummaryTweet({ tweet, ...props }) {
let icon
- if(tweet.place) {
- icon = faMapPin
+ if(tweet["location"]) {
+ icon = faMapMarkerAlt
+ }
+ else if(tweet["place"]) {
+ icon = faLocationArrow
}
else {
icon = faComment
@@ -19,9 +22,9 @@ export default function SummaryTweet({ tweet, ...props }) {
window.open(`https://twitter.com/${tweet.poster}/status/${tweet.snowflake}`)}
+ title={`@${tweet["poster"]}`}
+ subtitle={new Date(tweet["insert_time"]).toLocaleString()}
+ onClick={() => window.open(`https://twitter.com/${tweet["poster"]}/status/${tweet["snowflake"]}`)}
/>
{tweet.content}
diff --git a/nest_frontend/components/providers/RepositoryViewer.js b/nest_frontend/components/providers/RepositoryViewer.js
deleted file mode 100644
index 7d0fa6d..0000000
--- a/nest_frontend/components/providers/RepositoryViewer.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import React from "react"
-import Style from "./RepositoryViewer.module.css"
-import classNames from "classnames"
-import ContextRepositoryViewer from "../../contexts/ContextRepositoryViewer"
-
-
-export default function RepositoryViewer({
- id,
- }) {
- return (
-
-
-
-
-
- )
-}
diff --git a/nest_frontend/hooks/useRepositoryViewer.js b/nest_frontend/hooks/useRepositoryViewer.js
deleted file mode 100644
index f4ae358..0000000
--- a/nest_frontend/hooks/useRepositoryViewer.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import { useContext } from "react"
-import ContextRepositoryViewer from "../contexts/ContextRepositoryViewer"
-
-
-/**
- * Hook to quickly use {@link ContextRepositoryViewer}.
- */
-export default function useRepositoryViewer() {
- const context = useContext(ContextRepositoryViewer)
- if(!context) {
- throw new Error("This component must be placed inside a RepositoryViewer.")
- }
- return context
-}
diff --git a/nest_frontend/routes/PageRepository.js b/nest_frontend/routes/PageRepository.js
index 3386b2a..1aa2833 100644
--- a/nest_frontend/routes/PageRepository.js
+++ b/nest_frontend/routes/PageRepository.js
@@ -1,8 +1,7 @@
-import React, { useMemo, useState } from "react"
+import React, { useContext, useMemo, useState } from "react"
import Style from "./PageRepository.module.css"
import classNames from "classnames"
import BoxRepositoryTweets from "../components/interactive/BoxRepositoryTweets"
-import BoxWordcloud from "../components/interactive/BoxWordcloud"
import BoxHeader from "../components/base/BoxHeader"
import PickerVisualization from "../components/interactive/PickerVisualization"
import PickerFilter from "../components/interactive/PickerFilter"
@@ -12,10 +11,17 @@ import { faFolder, faFolderOpen, faTrash } from "@fortawesome/free-solid-svg-ico
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import { useParams } from "react-router"
import Loading from "../components/base/Loading"
+import BoxVisualizationStats from "../components/interactive/BoxVisualizationStats"
+import BoxVisualizationGraph from "../components/interactive/BoxVisualizationGraph"
+import BoxVisualizationMap from "../components/interactive/BoxVisualizationMap"
+import BoxVisualizationWordcloud from "../components/interactive/BoxVisualizationWordcloud"
+import BoxFull from "../components/base/BoxFull"
+import ContextLanguage from "../contexts/ContextLanguage"
export default function PageRepository({ className, ...props }) {
const {id} = useParams()
+ const {strings} = useContext(ContextLanguage)
const [visualizationTab, setVisualizationTab] = useState("wordcloud")
const [addFilterTab, setAddFilterTab] = useState("hashtag")
@@ -46,36 +52,6 @@ export default function PageRepository({ className, ...props }) {
)
const tweets = tweetsBv.resources && tweetsBv.error ? [] : tweetsBv.resources
- const words = useMemo(
- () => {
- let preprocessedWords = {}
- for(const tweet of tweets) {
- if(!tweet.content) {
- continue
- }
- for(const word of tweet.content.toLowerCase().split(/\s+/)) {
- if(!preprocessedWords.hasOwnProperty(word)) {
- preprocessedWords[word] = 0
- }
- preprocessedWords[word] += 1
- }
- }
-
- let processedWords = []
- for(const word in preprocessedWords) {
- if(!preprocessedWords.hasOwnProperty(word)) {
- continue
- }
- processedWords.push({
- text: word,
- value: preprocessedWords[word]
- })
- }
- return processedWords
- },
- [tweets]
- )
-
let contents;
if(!repositoryBr.firstLoad || !tweetsBv.firstLoad) {
contents = <>
@@ -85,8 +61,6 @@ export default function PageRepository({ className, ...props }) {
>
}
else if(repository === null) {
- console.debug("repositoryBr: ", repositoryBr, ", tweetsBv: ", tweetsBv)
-
// TODO: Translate this!
contents = <>
@@ -111,17 +85,44 @@ export default function PageRepository({ className, ...props }) {
setTab={setVisualizationTab}
/>
{visualizationTab === "wordcloud" ?
-
- : null}
+
+ : null}
+ {visualizationTab === "histogram" ?
+
+ : null}
+ {visualizationTab === "map" ?
+
+ : null}
+ {visualizationTab === "stats" ?
+
+ : null}
+
+
+ {strings.notImplemented}
+
+
+
+ {strings.notImplemented}
+
>
}
diff --git a/nest_frontend/utils/tokenizeTweetWords.js b/nest_frontend/utils/tokenizeTweetWords.js
new file mode 100644
index 0000000..ce48ff8
--- /dev/null
+++ b/nest_frontend/utils/tokenizeTweetWords.js
@@ -0,0 +1,35 @@
+import sw from "stopword"
+
+
+const stopwords = [...sw.it, ...sw.en, "rt"]
+
+
+export default function(tweets = {}) {
+ let preprocessedWords = {}
+ for(const tweet of tweets) {
+ if(!tweet.content) {
+ continue
+ }
+ for(const word of tweet.content.toLowerCase().split(/\s+/)) {
+ if(stopwords.includes(word)) continue
+ if(word.startsWith("https://")) continue
+
+ if(!preprocessedWords.hasOwnProperty(word)) {
+ preprocessedWords[word] = 0
+ }
+ preprocessedWords[word] += 1
+ }
+ }
+
+ let processedWords = []
+ for(const word in preprocessedWords) {
+ if(!preprocessedWords.hasOwnProperty(word)) {
+ continue
+ }
+ processedWords.push({
+ text: word,
+ value: preprocessedWords[word]
+ })
+ }
+ return processedWords
+}
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 946b637..598406a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,6 +28,7 @@
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"serve": "^11.3.2",
+ "stopword": "^1.0.7",
"web-vitals": "^1.1.1"
},
"devDependencies": {
@@ -18917,6 +18918,11 @@
"node": ">=0.10.0"
}
},
+ "node_modules/stopword": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/stopword/-/stopword-1.0.7.tgz",
+ "integrity": "sha512-KKPM5LGeulAxoNrg384AfNSoUvphG87sgj/vTHOkJ5M5U7hO99XA6Sl0HBhtCBv17Tv3kQPLj+5fCt7cadjXWQ=="
+ },
"node_modules/stream-browserify": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
@@ -37540,6 +37546,11 @@
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
},
+ "stopword": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/stopword/-/stopword-1.0.7.tgz",
+ "integrity": "sha512-KKPM5LGeulAxoNrg384AfNSoUvphG87sgj/vTHOkJ5M5U7hO99XA6Sl0HBhtCBv17Tv3kQPLj+5fCt7cadjXWQ=="
+ },
"stream-browserify": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
diff --git a/package.json b/package.json
index e677b62..9dee233 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"serve": "^11.3.2",
+ "stopword": "^1.0.7",
"web-vitals": "^1.1.1"
},
"main": "nest_frontend/index.js",