mirror of
https://github.com/pds-nest/nest.git
synced 2024-11-22 04:54:18 +00:00
✨ Implement analysis visualization
This commit is contained in:
parent
b58e49b5b3
commit
b471fc17c0
21 changed files with 294 additions and 108 deletions
|
@ -46,7 +46,7 @@ export default {
|
||||||
repoEdit: "Modifica repository",
|
repoEdit: "Modifica repository",
|
||||||
menuActive: "Le tue repository attive",
|
menuActive: "Le tue repository attive",
|
||||||
menuArchived: "Le tue repository archiviate",
|
menuArchived: "Le tue repository archiviate",
|
||||||
emptyMenu: "Non c'è nulla qui",
|
emptyMenu: "Non c'è nulla qui.",
|
||||||
delete: "Elimina",
|
delete: "Elimina",
|
||||||
archive: "Archivia",
|
archive: "Archivia",
|
||||||
edit: "Modifica",
|
edit: "Modifica",
|
||||||
|
@ -117,7 +117,7 @@ export default {
|
||||||
repoEdit: "Edit repository",
|
repoEdit: "Edit repository",
|
||||||
menuActive: "Your active repositories",
|
menuActive: "Your active repositories",
|
||||||
menuArchived: "Your archived repositories",
|
menuArchived: "Your archived repositories",
|
||||||
emptyMenu: "There's nothing here",
|
emptyMenu: "There's nothing here.",
|
||||||
delete: "Delete",
|
delete: "Delete",
|
||||||
archive: "Archive",
|
archive: "Archive",
|
||||||
edit: "Edit",
|
edit: "Edit",
|
||||||
|
@ -188,7 +188,7 @@ export default {
|
||||||
repoEdit: "Muokkaa arkistoa",
|
repoEdit: "Muokkaa arkistoa",
|
||||||
menuActive: "Aktiiviset arkistosi",
|
menuActive: "Aktiiviset arkistosi",
|
||||||
menuArchived: "Arkistoidut arkistosi",
|
menuArchived: "Arkistoidut arkistosi",
|
||||||
emptyMenu: "Täällä ei ole mitään",
|
emptyMenu: "Täällä ei ole mitään.",
|
||||||
delete: "Poista",
|
delete: "Poista",
|
||||||
archive: "Arkistoi",
|
archive: "Arkistoi",
|
||||||
edit: "Muokkaa",
|
edit: "Muokkaa",
|
||||||
|
|
|
@ -4,7 +4,7 @@ import BoxFull from "./BoxFull"
|
||||||
import { MapContainer, TileLayer } from "react-leaflet"
|
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 (
|
return (
|
||||||
<BoxFull
|
<BoxFull
|
||||||
header={header}
|
header={header}
|
||||||
|
@ -21,7 +21,7 @@ export default function BoxMap({ header, setMap, startingPosition, startingZoom,
|
||||||
attribution='(c) <a href="https://osm.org/copyright">OpenStreetMap contributors</a>'
|
attribution='(c) <a href="https://osm.org/copyright">OpenStreetMap contributors</a>'
|
||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
/>
|
/>
|
||||||
{additions}
|
{children}
|
||||||
<div className={"leaflet-top leaflet-right"}>
|
<div className={"leaflet-top leaflet-right"}>
|
||||||
<div className={"leaflet-control"}>
|
<div className={"leaflet-control"}>
|
||||||
{button}
|
{button}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useContext } from "react"
|
import React from "react"
|
||||||
import BoxFull from "../base/BoxFull"
|
import BoxFull from "../base/BoxFull"
|
||||||
import ReactWordcloud from "@steffo/nest-react-wordcloud"
|
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}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function BoxWordcloud({ words, props }) {
|
export default function BoxWordcloud({ words, ...props }) {
|
||||||
const { strings } = useContext(ContextLanguage)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxFull header={strings.wordcloud} {...props}>
|
<BoxFull {...props}>
|
||||||
<div style={{ "width": "100%", "height": "100%" }}>
|
<div className={Style.WordcloudContainer}>
|
||||||
<ReactWordcloud
|
<ReactWordcloud
|
||||||
options={{
|
options={{
|
||||||
colors: [
|
colors: [
|
||||||
"var(--fg-primary)",
|
"var(--fg-primary)",
|
||||||
],
|
],
|
||||||
fontFamily: "Bree Serif",
|
fontFamily: "Bree Serif",
|
||||||
fontSizes: [16, 48],
|
fontSizes: [8, 64],
|
||||||
size: undefined,
|
size: undefined,
|
||||||
deterministic: true,
|
deterministic: true,
|
||||||
}}
|
}}
|
4
nest_frontend/components/base/BoxWordcloud.module.css
Normal file
4
nest_frontend/components/base/BoxWordcloud.module.css
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.WordcloudContainer {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
.SummaryLeft {
|
.SummaryLeft {
|
||||||
min-width: 175px;
|
width: 260px;
|
||||||
width: 275px;
|
flex-shrink: 0;
|
||||||
|
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-areas:
|
grid-template-areas:
|
||||||
|
|
|
@ -8,9 +8,6 @@ import ContextLanguage from "../../contexts/ContextLanguage"
|
||||||
import BoxMap from "../base/BoxMap"
|
import BoxMap from "../base/BoxMap"
|
||||||
|
|
||||||
|
|
||||||
const STARTING_POSITION = { lat: 41.89309, lng: 12.48289 }
|
|
||||||
const STARTING_ZOOM = 3
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://wiki.openstreetmap.org/wiki/Zoom_levels
|
* https://wiki.openstreetmap.org/wiki/Zoom_levels
|
||||||
*/
|
*/
|
||||||
|
@ -47,8 +44,8 @@ const MPIXEL = [
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function BoxConditionMap({ ...props }) {
|
export default function BoxConditionMap({ ...props }) {
|
||||||
const [position, setPosition] = useState(STARTING_POSITION)
|
const [position, setPosition] = useState()
|
||||||
const [zoom, setZoom] = useState(STARTING_ZOOM)
|
const [zoom, setZoom] = useState()
|
||||||
const [map, setMap] = useState(null)
|
const [map, setMap] = useState(null)
|
||||||
const { addCondition } = useRepositoryEditor()
|
const { addCondition } = useRepositoryEditor()
|
||||||
const { strings } = useContext(ContextLanguage)
|
const { strings } = useContext(ContextLanguage)
|
||||||
|
@ -92,7 +89,6 @@ export default function BoxConditionMap({ ...props }) {
|
||||||
"COORDINATES",
|
"COORDINATES",
|
||||||
`< ${radius} ${position.lat} ${position.lng}`,
|
`< ${radius} ${position.lat} ${position.lng}`,
|
||||||
))
|
))
|
||||||
setPosition(STARTING_POSITION)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -106,8 +102,6 @@ export default function BoxConditionMap({ ...props }) {
|
||||||
{strings.byZone}
|
{strings.byZone}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
startingPosition={STARTING_POSITION}
|
|
||||||
startingZoom={STARTING_ZOOM}
|
|
||||||
setMap={setMap}
|
setMap={setMap}
|
||||||
button={
|
button={
|
||||||
<ButtonIconOnly
|
<ButtonIconOnly
|
||||||
|
|
|
@ -3,6 +3,7 @@ import BoxFullScrollable from "../base/BoxFullScrollable"
|
||||||
import Loading from "../base/Loading"
|
import Loading from "../base/Loading"
|
||||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||||
import SummaryRepository from "./SummaryRepository"
|
import SummaryRepository from "./SummaryRepository"
|
||||||
|
import Empty from "./Empty"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,14 +22,12 @@ import SummaryRepository from "./SummaryRepository"
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function BoxRepositories({ repositories, view, archive, edit, destroy, loading, running, className, ...props }) {
|
export default function BoxRepositories({ repositories, view, archive, edit, destroy, loading, running, className, ...props }) {
|
||||||
const { strings } = useContext(ContextLanguage)
|
|
||||||
|
|
||||||
let contents
|
let contents
|
||||||
if(loading) {
|
if(loading) {
|
||||||
contents = <Loading/>
|
contents = <Loading/>
|
||||||
}
|
}
|
||||||
else if(repositories.length === 0) {
|
else if(repositories.length === 0) {
|
||||||
contents = <i>{strings.emptyMenu}</i>
|
contents = <Empty/>
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
contents = repositories.map(repo => (
|
contents = repositories.map(repo => (
|
||||||
|
|
|
@ -1,14 +1,23 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import BoxFullScrollable from "../base/BoxFullScrollable"
|
import BoxFullScrollable from "../base/BoxFullScrollable"
|
||||||
import SummaryTweet from "./SummaryTweet"
|
import SummaryTweet from "./SummaryTweet"
|
||||||
|
import Empty from "./Empty"
|
||||||
|
|
||||||
|
|
||||||
export default function BoxRepositoryTweets({ tweets, ...props }) {
|
export default function BoxRepositoryTweets({ tweets, ...props }) {
|
||||||
// TODO: Translate this
|
// TODO: Translate this
|
||||||
|
|
||||||
|
let content
|
||||||
|
if(tweets.length === 0) {
|
||||||
|
content = <Empty/>
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
content = tweets.map(tweet => <SummaryTweet key={tweet["snowflake"]} tweet={tweet}/>)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BoxFullScrollable header={"Tweets"} {...props}>
|
<BoxFullScrollable header={"Tweets"} {...props}>
|
||||||
{tweets.map(tweet => <SummaryTweet key={tweet.snowflake} tweet={tweet}/>)}
|
{content}
|
||||||
</BoxFullScrollable>
|
</BoxFullScrollable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
<BoxFull header={"Hourly graph"} {...props}>
|
||||||
|
{children}
|
||||||
|
</BoxFull>
|
||||||
|
)
|
||||||
|
}
|
40
nest_frontend/components/interactive/BoxVisualizationMap.js
Normal file
40
nest_frontend/components/interactive/BoxVisualizationMap.js
Normal file
|
@ -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 = /[{](?<lat>[0-9.]+),(?<lng>[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 (
|
||||||
|
<Marker key={tweet["snowflake"]} position={[Number.parseFloat(lat), Number.parseFloat(lng)]}>
|
||||||
|
<Popup>
|
||||||
|
<p>
|
||||||
|
{tweet["content"]}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
— <a href={`https://twitter.com/${tweet["poster"]}/status/${tweet["snowflake"]}`}>@{tweet["poster"]}</a>
|
||||||
|
</p>
|
||||||
|
</Popup>
|
||||||
|
</Marker>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BoxMap header={"Map"} {...props}>
|
||||||
|
{markers}
|
||||||
|
</BoxMap>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<BoxFullScrollable header={"Stats"} {...props}>
|
||||||
|
<FormLabelled>
|
||||||
|
<FormLabel text={"Total tweets"}>
|
||||||
|
<b>{totalTweetCount}</b>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"Displayed tweets"}>
|
||||||
|
<b>{tweetCount}</b>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"% of displayed tweets"}>
|
||||||
|
<b>{tweetPct.toFixed(2)}%</b>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"Tweets with location"}>
|
||||||
|
<b>{tweetLocationCount}</b>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"% of tweets with location"}>
|
||||||
|
<b>{tweetLocationPct.toFixed(2)}%</b>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"Tweets with content"}>
|
||||||
|
<b>{tweetContentCount}</b>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"% of tweets with content"}>
|
||||||
|
<b>{tweetContentPct.toFixed(2)}%</b>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"Word count"}>
|
||||||
|
<b>{wordCount}</b>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"Most popular word"}>
|
||||||
|
<b>{mostPopularWord}</b>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"Tweets with image"}>
|
||||||
|
<b>🚧</b>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"% of tweets with image"}>
|
||||||
|
<b>🚧</b>
|
||||||
|
</FormLabel>
|
||||||
|
<FormLabel text={"Users count"}>
|
||||||
|
<b>{usersCount}</b>
|
||||||
|
</FormLabel>
|
||||||
|
</FormLabelled>
|
||||||
|
</BoxFullScrollable>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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 (
|
||||||
|
<BoxWordcloud header={strings.wordcloud} words={words} {...props}/>
|
||||||
|
)
|
||||||
|
}
|
15
nest_frontend/components/interactive/Empty.js
Normal file
15
nest_frontend/components/interactive/Empty.js
Normal file
|
@ -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 (
|
||||||
|
<i className={classNames(Style.Empty, className)} {...props}>
|
||||||
|
{strings.emptyMenu}
|
||||||
|
</i>
|
||||||
|
)
|
||||||
|
}
|
3
nest_frontend/components/interactive/Empty.module.css
Normal file
3
nest_frontend/components/interactive/Empty.module.css
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
.Empty {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
|
@ -1,15 +1,18 @@
|
||||||
import React from "react"
|
import React from "react"
|
||||||
import SummaryBase from "../base/summary/SummaryBase"
|
import SummaryBase from "../base/summary/SummaryBase"
|
||||||
import SummaryLeft from "../base/summary/SummaryLeft"
|
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 SummaryText from "../base/summary/SummaryText"
|
||||||
import SummaryRight from "../base/summary/SummaryRight"
|
import SummaryRight from "../base/summary/SummaryRight"
|
||||||
|
|
||||||
|
|
||||||
export default function SummaryTweet({ tweet, ...props }) {
|
export default function SummaryTweet({ tweet, ...props }) {
|
||||||
let icon
|
let icon
|
||||||
if(tweet.place) {
|
if(tweet["location"]) {
|
||||||
icon = faMapPin
|
icon = faMapMarkerAlt
|
||||||
|
}
|
||||||
|
else if(tweet["place"]) {
|
||||||
|
icon = faLocationArrow
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
icon = faComment
|
icon = faComment
|
||||||
|
@ -19,9 +22,9 @@ export default function SummaryTweet({ tweet, ...props }) {
|
||||||
<SummaryBase {...props}>
|
<SummaryBase {...props}>
|
||||||
<SummaryLeft
|
<SummaryLeft
|
||||||
icon={icon}
|
icon={icon}
|
||||||
title={`@${tweet.poster}`}
|
title={`@${tweet["poster"]}`}
|
||||||
subtitle={tweet.place}
|
subtitle={new Date(tweet["insert_time"]).toLocaleString()}
|
||||||
onClick={() => window.open(`https://twitter.com/${tweet.poster}/status/${tweet.snowflake}`)}
|
onClick={() => window.open(`https://twitter.com/${tweet["poster"]}/status/${tweet["snowflake"]}`)}
|
||||||
/>
|
/>
|
||||||
<SummaryText>
|
<SummaryText>
|
||||||
{tweet.content}
|
{tweet.content}
|
||||||
|
|
|
@ -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 (
|
|
||||||
<ContextRepositoryViewer.Provider
|
|
||||||
value={{
|
|
||||||
id,
|
|
||||||
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={classNames(Style.RepositoryViewer, className)}>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</ContextRepositoryViewer.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -1,8 +1,7 @@
|
||||||
import React, { useMemo, useState } from "react"
|
import React, { useContext, useMemo, useState } from "react"
|
||||||
import Style from "./PageRepository.module.css"
|
import Style from "./PageRepository.module.css"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import BoxRepositoryTweets from "../components/interactive/BoxRepositoryTweets"
|
import BoxRepositoryTweets from "../components/interactive/BoxRepositoryTweets"
|
||||||
import BoxWordcloud from "../components/interactive/BoxWordcloud"
|
|
||||||
import BoxHeader from "../components/base/BoxHeader"
|
import BoxHeader from "../components/base/BoxHeader"
|
||||||
import PickerVisualization from "../components/interactive/PickerVisualization"
|
import PickerVisualization from "../components/interactive/PickerVisualization"
|
||||||
import PickerFilter from "../components/interactive/PickerFilter"
|
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 {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||||
import { useParams } from "react-router"
|
import { useParams } from "react-router"
|
||||||
import Loading from "../components/base/Loading"
|
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 }) {
|
export default function PageRepository({ className, ...props }) {
|
||||||
const {id} = useParams()
|
const {id} = useParams()
|
||||||
|
const {strings} = useContext(ContextLanguage)
|
||||||
|
|
||||||
const [visualizationTab, setVisualizationTab] = useState("wordcloud")
|
const [visualizationTab, setVisualizationTab] = useState("wordcloud")
|
||||||
const [addFilterTab, setAddFilterTab] = useState("hashtag")
|
const [addFilterTab, setAddFilterTab] = useState("hashtag")
|
||||||
|
@ -46,36 +52,6 @@ export default function PageRepository({ className, ...props }) {
|
||||||
)
|
)
|
||||||
const tweets = tweetsBv.resources && tweetsBv.error ? [] : tweetsBv.resources
|
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;
|
let contents;
|
||||||
if(!repositoryBr.firstLoad || !tweetsBv.firstLoad) {
|
if(!repositoryBr.firstLoad || !tweetsBv.firstLoad) {
|
||||||
contents = <>
|
contents = <>
|
||||||
|
@ -85,8 +61,6 @@ export default function PageRepository({ className, ...props }) {
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
else if(repository === null) {
|
else if(repository === null) {
|
||||||
console.debug("repositoryBr: ", repositoryBr, ", tweetsBv: ", tweetsBv)
|
|
||||||
|
|
||||||
// TODO: Translate this!
|
// TODO: Translate this!
|
||||||
contents = <>
|
contents = <>
|
||||||
<BoxHeader className={Style.Header}>
|
<BoxHeader className={Style.Header}>
|
||||||
|
@ -111,17 +85,44 @@ export default function PageRepository({ className, ...props }) {
|
||||||
setTab={setVisualizationTab}
|
setTab={setVisualizationTab}
|
||||||
/>
|
/>
|
||||||
{visualizationTab === "wordcloud" ?
|
{visualizationTab === "wordcloud" ?
|
||||||
<BoxWordcloud
|
<BoxVisualizationWordcloud
|
||||||
className={Style.Wordcloud}
|
className={Style.Wordcloud}
|
||||||
words={words}
|
tweets={tweets}
|
||||||
/>
|
/>
|
||||||
: null}
|
: null}
|
||||||
|
{visualizationTab === "histogram" ?
|
||||||
|
<BoxVisualizationGraph
|
||||||
|
className={Style.Wordcloud}
|
||||||
|
tweets={tweets}
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
|
{visualizationTab === "map" ?
|
||||||
|
<BoxVisualizationMap
|
||||||
|
className={Style.Wordcloud}
|
||||||
|
tweets={tweets}
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
|
{visualizationTab === "stats" ?
|
||||||
|
<BoxVisualizationStats
|
||||||
|
className={Style.Wordcloud}
|
||||||
|
tweets={tweets}
|
||||||
|
totalTweetCount={tweets.length}
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
|
|
||||||
<PickerFilter
|
<PickerFilter
|
||||||
className={Style.FilterPicker}
|
className={Style.FilterPicker}
|
||||||
currentTab={addFilterTab}
|
currentTab={addFilterTab}
|
||||||
setTab={setAddFilterTab}
|
setTab={setAddFilterTab}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<BoxFull header={strings.notImplemented} className={Style.Filters}>
|
||||||
|
{strings.notImplemented}
|
||||||
|
</BoxFull>
|
||||||
|
|
||||||
|
<BoxFull header={strings.notImplemented} className={Style.AddFilter}>
|
||||||
|
{strings.notImplemented}
|
||||||
|
</BoxFull>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
35
nest_frontend/utils/tokenizeTweetWords.js
Normal file
35
nest_frontend/utils/tokenizeTweetWords.js
Normal file
|
@ -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
|
||||||
|
}
|
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -28,6 +28,7 @@
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"serve": "^11.3.2",
|
"serve": "^11.3.2",
|
||||||
|
"stopword": "^1.0.7",
|
||||||
"web-vitals": "^1.1.1"
|
"web-vitals": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -18917,6 +18918,11 @@
|
||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/stream-browserify": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz",
|
||||||
"integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks="
|
"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": {
|
"stream-browserify": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz",
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"react-router-dom": "^5.2.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-scripts": "4.0.3",
|
"react-scripts": "4.0.3",
|
||||||
"serve": "^11.3.2",
|
"serve": "^11.3.2",
|
||||||
|
"stopword": "^1.0.7",
|
||||||
"web-vitals": "^1.1.1"
|
"web-vitals": "^1.1.1"
|
||||||
},
|
},
|
||||||
"main": "nest_frontend/index.js",
|
"main": "nest_frontend/index.js",
|
||||||
|
|
Loading…
Reference in a new issue