diff --git a/package-lock.json b/package-lock.json index dbade1e..7932e07 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1715,6 +1715,12 @@ "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", "dev": true }, + "@types/lodash": { + "version": "4.14.159", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.159.tgz", + "integrity": "sha512-gF7A72f7WQN33DpqOWw9geApQPh4M3PxluMtaHxWHXEGSN12/WbcEk/eNSqWNQcQhF66VSZ06vCF94CrHwXJDg==", + "dev": true + }, "@types/node": { "version": "12.0.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.10.tgz", @@ -8808,9 +8814,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash._reinterpolate": { "version": "3.0.0", diff --git a/package.json b/package.json index 43bc154..969e7f2 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "gh-pages": "^2.0.0", "html2canvas": "^1.0.0-alpha.12", "jquery": "^3.3.1", + "lodash": "^4.17.19", "lodash.debounce": "^4.0.8", "node": "^11.12.0", "query-string": "^6.6.0", @@ -45,6 +46,7 @@ "homepage": "https://danielacorner.github.io/pave__react", "devDependencies": { "@types/jest": "^24.0.13", + "@types/lodash": "^4.14.159", "@types/node": "^12.0.7", "@types/react": "^16.8.19", "@types/react-dom": "^16.8.4", diff --git a/src/App.tsx b/src/App.tsx index 32904d6..bddd9a2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,32 +1,40 @@ -import { createMuiTheme } from '@material-ui/core/styles'; +import { createMuiTheme } from "@material-ui/core/styles"; // import Navbar from './components/Navbar'; -import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider'; -import React, { useState } from 'react'; -import './App.css'; -import ContextProvider from './components/Context/ContextProvider'; -import AppWithContext from './AppWithContext'; -import { AddToHomeScreenBanner } from './components/AddToHomeScreenBanner'; -import { PictogramClipPathsDefs } from './components/Viz/PictogramClipPathsDefs'; -import { BeforeInstallPromptEvent } from './types'; +import MuiThemeProvider from "@material-ui/core/styles/MuiThemeProvider"; +import React, { useState } from "react"; +import "./App.css"; +import AppWithContext from "./AppWithContext"; +import { AddToHomeScreenBanner } from "./components/AddToHomeScreenBanner"; +import { PictogramClipPathsDefs } from "./components/Viz/PictogramClipPathsDefs"; +import { BeforeInstallPromptEvent } from "./types"; +import useStore from "./components/store"; +import { useMount } from "./utils/constants"; -export const brightGreen = '#49ac52'; +export const brightGreen = "#49ac52"; const theme = createMuiTheme({ palette: { primary: { main: brightGreen, - contrastText: '#fff', + contrastText: "#fff", }, - secondary: { main: '#64b5f6' }, + secondary: { main: "#64b5f6" }, contrastThreshold: 3, }, }); function App() { const [deferredPrompt, setDeferredPrompt] = useState( - null as null | BeforeInstallPromptEvent, + null as null | BeforeInstallPromptEvent ); + const initializeClusterCenters = useStore( + (state) => state.initializeClusterCenters + ); + + useMount(() => { + initializeClusterCenters(); + }); - window.addEventListener('beforeinstallprompt', (event: any) => { + window.addEventListener("beforeinstallprompt", (event: any) => { // Prevent Chrome 67 and earlier from automatically showing the prompt event.preventDefault(); // Stash the event so it can be triggered later. @@ -34,16 +42,14 @@ function App() { }); return ( - - - - - + + + ); } diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index bc1abfd..63a2003 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -1,11 +1,5 @@ // import queryString from 'query-string' -import React, { - useContext, - useState, - useEffect, - useRef, - useCallback, -} from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import useMediaQuery from "@material-ui/core/useMediaQuery"; import styled from "styled-components/macro"; import { @@ -15,15 +9,13 @@ import { SKILLS_LOGI, SKILLS_COMP, SKILLS_MATH, -} from "../utils/constants"; -import { ControlsContext } from "./Context/ContextProvider"; -import FiltersPanel from "./Controls/FiltersPanel"; -import SortPanel, { STUDY, SALARY, STUDY_LABEL, SALARY_LABEL, -} from "./Controls/SortPanel"; +} from "../utils/constants"; +import FiltersPanel from "./Controls/FiltersPanel"; +import SortPanel from "./Controls/SortPanel"; import InfoDrawer from "./Viz/InfoDrawer"; import Tooltip from "./Viz/Tooltip"; import Viz from "./Viz/Viz"; @@ -33,6 +25,7 @@ import FORCE from "./FORCE"; import { useWindowSize } from "./useWindowSize"; import { NAV_HEIGHT } from "./Nav/Navbar"; import ContainerDimensions from "react-container-dimensions"; +import useStore from "./store"; const AppLayoutStyles = styled.div` position: relative; @@ -109,14 +102,16 @@ const AppLayout = () => { x: { displayName: STUDY, dataLabel: STUDY_LABEL }, y: { displayName: SALARY, dataLabel: SALARY_LABEL }, }); - const { state, handleResize, restartSimulation } = useContext( - ControlsContext - ); + const getRadiusScale = useStore((state) => state.getRadiusScale); + const zScale = useStore((state) => state.zScale); + const uniqueClusterValues = useStore((state) => state.uniqueClusterValues); + const sortedByValue = useStore((state) => state.sortedByValue); + const handleResize = useStore((state) => state.handleResize); + const restartSimulation = useStore((state) => state.restartSimulation); const [isExpanded, setIsExpanded] = useState( INITIAL_SUBSKILL_FILTERS_EXPANDED_STATE ); - const { getRadiusScale, zScale, uniqueClusterValues, sortedByValue } = state; const [isGraphView, setIsGraphView] = useState(false); @@ -222,7 +217,7 @@ const AppLayout = () => { }); startTooltipActive(); }, - [innerHeight, startTooltipActive] + [innerHeight] ); const onClickNode = useCallback((event: Event, datum: any) => { diff --git a/src/components/Context/ContextProvider.jsx b/src/components/Context/ContextProvider.jsx index 48c4ee5..9361743 100644 --- a/src/components/Context/ContextProvider.jsx +++ b/src/components/Context/ContextProvider.jsx @@ -1,351 +1,352 @@ -import * as d3 from 'd3'; -import debounce from 'lodash.debounce'; -import React, { Component } from 'react'; -import NOCData from '../../assets/NOC-data'; -import FORCE from '../FORCE'; -import { INDUSTRY, WORKERS } from '../Controls/SortPanel'; -const NOCDataProcessed = NOCData.map(d => { - d.name = d.job; - return d; -}); - -export const ControlsContext = React.createContext(); - -export const $ = element => document.querySelector(element); // jQuerify - -const getGraphContainerDims = () => { - const graphContainer = $('#graphContainer'); - return graphContainer - ? graphContainer.getBoundingClientRect() - : { width: window.innerWidth, height: window.innerHeight * 0.8 }; -}; - -// TODO: switch to hooks -class ContextProvider extends Component { - constructor(props) { - super(props); - this.state = { - originalData: NOCDataProcessed, - nodes: NOCDataProcessed, - filters: { - skillsLang: 0, - skillsLogi: 0, - skillsMath: 0, - skillsComp: 0, - s1DataAnalysis: 0, - s2DecisionMaking: 0, - s3FindingInformation: 0, - s4JobTaskPlanningandOrganizing: 0, - s5MeasurementandCalculation: 0, - s6MoneyMath: 0, - s7NumericalEstimation: 0, - s8OralCommunication: 0, - s9ProblemSolving: 0, - s10Reading: 0, - s11SchedulingorBudgetingandAccounting: 0, - s12DigitalTechnology: 0, - s13DocumentUse: 0, - s14Writing: 0, - s15CriticalThinking: 0, - }, - radiusSelector: WORKERS, - clusterSelector: INDUSTRY, - getRadiusScale: () => { - const radii = NOCData.map(d => d[this.state.radiusSelector]); - const radiusRange = [5, 50]; - return d3 - .scaleSqrt() // square root scale because radius of a circle - .domain([d3.min(radii), d3.max(radii)]) - .range(radiusRange); - }, - uniqueClusterValues: NOCData.map(d => d[INDUSTRY]).filter( - (value, index, self) => self.indexOf(value) === index, - ), - clusterCenters: [], - sortedByValue: false, - colouredByValue: false, - svgBBox: 0, - zScale: d3.scaleOrdinal(d3.schemeCategory10), - isOffsetTop: false, - offsetTop: 0, - }; - } - - componentDidMount() { - const { - clusterCenters, - radiusSelector, - clusterSelector, - uniqueClusterValues, - } = this.state; - - // create clusters arrays - NOCData.forEach(d => { - const cluster = uniqueClusterValues.indexOf(d[clusterSelector]) + 1; - // add to clusters array if it doesn't exist or the radius is larger than any other radius in the cluster - if ( - !clusterCenters[cluster] || - d[radiusSelector] > clusterCenters[cluster][radiusSelector] - ) { - clusterCenters[cluster] = d; - // todo: emit new cluster centers to/from context - this.setState({ clusterCenters: clusterCenters }); - } - }); - - this.setState({ - zScale: d3.scaleOrdinal(d3.schemeCategory10), - }); - window.addEventListener('resize', this.handleResize); - setTimeout(this.handleResize, 1500); - - // tranlate nodes to center - - const { width, height } = getGraphContainerDims(); - - const nodesG = $('#nodesG'); - if (nodesG) { - nodesG.style.transform = `translate(${+width / 2}px,${+height / 2}px)`; - } - } - - componentWillUnmount() { - window.removeEventListener('resize', this.handleResize); - } - - // componentDidUpdate(nextProps, nextState) {} - - shouldComponentUpdate(nextProps, nextState) { - return ( - this.state.nodes !== nextState.nodes || - this.state.sortedByValue !== nextState.sortedByValue - ); - } - - getOffsetTop() { - const graph = $('#graphContainer'); - const graphRect = graph && graph.getBoundingClientRect(); - const nodesG = $('#nodesG'); - const nodesRect = nodesG && nodesG.getBoundingClientRect(); - - const newOffsetTop = - this.state.sortedByValue && nodesRect.bottom > 0.975 * graphRect.bottom - ? -(nodesRect.bottom - 0.975 * graphRect.bottom) - : 0; - - this.setState({ - offsetTop: this.state.isOffsetTop ? this.state.offsetTop : newOffsetTop, - isOffsetTop: this.state.isOffsetTop || newOffsetTop !== 0, - }); - - return this.state.isOffsetTop ? this.state.offsetTop : newOffsetTop; - } - - getTranslate() { - const { width, height } = getGraphContainerDims(); - return `${+width / 2}px,${+height / 2 + this.getOffsetTop()}px`; - } - - getScale() { - if (FORCE.isGraphView) { - return 0.36; - } - // resize the graph container to fit the screen - const { width, height } = getGraphContainerDims(); - - // zoom in until you hit the edge of... - const windowConstrainingLength = Math.min(width, height); // constrain by the smaller length - - const nodesG = $('#nodesG'); - const nodesBB = nodesG - ? nodesG.getBBox() - : { width: window.innerWidth - 10, height: window.innerHeight * 0.8 }; - // constrain the maximum nodes length - const nodesConstrainedLength = Math.max(nodesBB.width, nodesBB.height); - - // bugfix: zooming in because initial nodesConstrainedLength = 100, and doesn't resize correctly when browser focus isn't on this tab - if ( - nodesConstrainedLength === 100 && - this.state.nodes.length === this.state.originalData.length - ) { - return 0.95; - } - - const scaleRatio = windowConstrainingLength / nodesConstrainedLength; - - return scaleRatio * 0.95; // zoom out a little extra - } - - handleResize = debounce(() => { - if (FORCE.isGraphView) { - return; - } - const newScale = this.getScale(); - const graphContainer = $('#graphContainer'); - const svgBBox = graphContainer - ? graphContainer.getBoundingClientRect() - : { height: window.innerHeight * 0.8 }; - this.setState({ - svgBBox, - }); - - // resize size legend scale - d3.selectAll('.sizeCircle') - .style('transform', `scale(${newScale})`) - .style('opacity', 1 / newScale); - d3.selectAll('.size').style( - 'padding-bottom', - `${Math.min(Math.max(newScale - 1, 0) * 40, svgBBox.height / 5)}px`, - ); - - // scale up the spaces between the y axis ticks - d3.select('.yAxis').style('transform', `scaleY(${newScale})`); - d3.selectAll('.yAxis .tick').style('transform', `scaleY(${1 / newScale})`); - - // translate the nodes group into the middle and scale to fit - const nodesG = $('#nodesG'); - if (nodesG) { - nodesG.style.transform = `translate(${this.getTranslate()}) scale(${newScale})`; - } - }, 150); - - filteredNodes = () => { - // filter the dataset according to the slider state - const { filters, originalData } = this.state; - const filterKeys = Object.keys(filters); - const numFilters = filterKeys.length; - const numNodes = originalData.length; - - const filtered = []; - for (let i = 0; i < numNodes; i++) { - const node = originalData[i]; - let keep = true; - // for each filter variable - for (let i = 0; i < numFilters; i++) { - const filterVar = filterKeys[i]; // 'skillsLang', 'skillsMath'... - // filter out the node if less than the slider value - if (node[filterVar] < filters[filterVar]) { - keep = false; - } - } - keep && filtered.push(node); - } - return filtered; - }; - - filterNodes = () => { - // pause the simulation if running - !FORCE.paused && FORCE.stopSimulation(); - - this.setState({ - nodes: this.filteredNodes(), - }); - }; - - restartSimulation = () => { - this.setState({ - nodes: this.filteredNodes(), - }); - const isStrongForce = this.state.sortedByValue && this.state.sortedType; - const isMediumForce = this.state.sortedByValue; - setTimeout(() => { - FORCE.restartSimulation(this.state.nodes, isStrongForce, isMediumForce); - }, 200); - setTimeout(() => { - this.handleResize(); - }, 1500); - }; - - handleFilterChange = (filter, value) => { - this.setState( - { - filters: { ...this.state.filters, [filter]: value }, - }, - this.filterNodes, - ); - }; - handleFilterMouseup = () => { - this.setState({ - isOffsetTop: false, // recalculate offsetTop after each filter - // offsetTop: 0, - }); - if (FORCE.isGraphView) { - return; - } - setTimeout(() => { - // TODO: instead of actually moving the filters, could set the background fill instead? - // set all filters to new minima on mouseup - // let newMinima = {}; - // Object.keys(this.state.filters).forEach(filter => { - // newMinima[filter] = Math.min(...this.state.nodes.map(d => d[filter])); - // }); - // restart the simulation - // this.setState({ filters: newMinima }, - this.restartSimulation(); - // ); - }, 0); - }; - resetFilters = () => { - const filtersReset = this.state.filters; - Object.keys(this.state.filters).map(key => (filtersReset[key] = 0)); - this.setState( - { - filters: filtersReset, - offsetTop: 0, - isOffsetTop: false, - }, - () => { - this.filterNodes(); - this.restartSimulation(); - }, - ); - }; - sortByValue = (value, doSort = false) => { - const { radiusSelector, getRadiusScale } = this.state; - const newSortedValue = this.state.sortedByValue && !doSort ? false : value; - this.setState({ sortedByValue: newSortedValue }); - FORCE.applySortForces({ - sortByValue: newSortedValue, - getRadiusScale, - radiusSelector, - }); - setTimeout(this.handleResize, 2000); - }; - setCurrentColor = value => { - this.setState({ colouredByValue: value }); - }; - colourByValue = value => { - if (!this.state.colouredByValue) { - this.setState({ colouredByValue: value }); - FORCE.colourByValue({ doColour: true, value }); - } else { - this.setState({ colouredByValue: false }); - FORCE.colourByValue({ doColour: false, value: null }); - } - }; - render() { - return ( - this.setState({ radiusSelector: x }), - setClusterSelector: x => this.setState({ clusterSelector: x }), - handleFilterChange: this.handleFilterChange, - handleResize: this.handleResize, - resetFilters: this.resetFilters, - restartSimulation: this.restartSimulation, - handleFilterMouseup: this.handleFilterMouseup, - setNodes: nodes => this.setState({ nodes: nodes }), - sortByValue: this.sortByValue, - colourByValue: this.colourByValue, - setCurrentColor: this.setCurrentColor, - getScale: this.getScale, - }} - > - {this.props.children} - - ); - } -} - -export default ContextProvider; +export {}; +// import * as d3 from 'd3'; +// import debounce from 'lodash.debounce'; +// import React, { Component } from 'react'; +// import NOCData from '../../assets/NOC-data'; +// import FORCE from '../FORCE'; +// import { INDUSTRY, WORKERS } from '../Controls/SortPanel'; +// const NOCDataProcessed = NOCData.map(d => { +// d.name = d.job; +// return d; +// }); + +// export const ControlsContext = React.createContext(); + +// export const $ = element => document.querySelector(element); // jQuerify + +// const getGraphContainerDims = () => { +// const graphContainer = $('#graphContainer'); +// return graphContainer +// ? graphContainer.getBoundingClientRect() +// : { width: window.innerWidth, height: window.innerHeight * 0.8 }; +// }; + +// // TODO: switch to hooks +// class ContextProvider extends Component { +// constructor(props) { +// super(props); +// this.state = { +// originalData: NOCDataProcessed, +// nodes: NOCDataProcessed, +// filters: { +// skillsLang: 0, +// skillsLogi: 0, +// skillsMath: 0, +// skillsComp: 0, +// s1DataAnalysis: 0, +// s2DecisionMaking: 0, +// s3FindingInformation: 0, +// s4JobTaskPlanningandOrganizing: 0, +// s5MeasurementandCalculation: 0, +// s6MoneyMath: 0, +// s7NumericalEstimation: 0, +// s8OralCommunication: 0, +// s9ProblemSolving: 0, +// s10Reading: 0, +// s11SchedulingorBudgetingandAccounting: 0, +// s12DigitalTechnology: 0, +// s13DocumentUse: 0, +// s14Writing: 0, +// s15CriticalThinking: 0, +// }, +// radiusSelector: WORKERS, +// clusterSelector: INDUSTRY, +// getRadiusScale: () => { +// const radii = NOCData.map(d => d[this.state.radiusSelector]); +// const radiusRange = [5, 50]; +// return d3 +// .scaleSqrt() // square root scale because radius of a circle +// .domain([d3.min(radii), d3.max(radii)]) +// .range(radiusRange); +// }, +// uniqueClusterValues: NOCData.map(d => d[INDUSTRY]).filter( +// (value, index, self) => self.indexOf(value) === index, +// ), +// clusterCenters: [], +// sortedByValue: false, +// colouredByValue: false, +// svgBBox: 0, +// zScale: d3.scaleOrdinal(d3.schemeCategory10), +// isOffsetTop: false, +// offsetTop: 0, +// }; +// } + +// componentDidMount() { +// const { +// clusterCenters, +// radiusSelector, +// clusterSelector, +// uniqueClusterValues, +// } = this.state; + +// // create clusters arrays +// NOCData.forEach(d => { +// const cluster = uniqueClusterValues.indexOf(d[clusterSelector]) + 1; +// // add to clusters array if it doesn't exist or the radius is larger than any other radius in the cluster +// if ( +// !clusterCenters[cluster] || +// d[radiusSelector] > clusterCenters[cluster][radiusSelector] +// ) { +// clusterCenters[cluster] = d; +// // todo: emit new cluster centers to/from context +// this.setState({ clusterCenters: clusterCenters }); +// } +// }); + +// this.setState({ +// zScale: d3.scaleOrdinal(d3.schemeCategory10), +// }); +// window.addEventListener('resize', this.handleResize); +// setTimeout(this.handleResize, 1500); + +// // tranlate nodes to center + +// const { width, height } = getGraphContainerDims(); + +// const nodesG = $('#nodesG'); +// if (nodesG) { +// nodesG.style.transform = `translate(${+width / 2}px,${+height / 2}px)`; +// } +// } + +// componentWillUnmount() { +// window.removeEventListener('resize', this.handleResize); +// } + +// // componentDidUpdate(nextProps, nextState) {} + +// shouldComponentUpdate(nextProps, nextState) { +// return ( +// this.state.nodes !== nextState.nodes || +// this.state.sortedByValue !== nextState.sortedByValue +// ); +// } + +// getOffsetTop() { +// const graph = $('#graphContainer'); +// const graphRect = graph && graph.getBoundingClientRect(); +// const nodesG = $('#nodesG'); +// const nodesRect = nodesG && nodesG.getBoundingClientRect(); + +// const newOffsetTop = +// this.state.sortedByValue && nodesRect.bottom > 0.975 * graphRect.bottom +// ? -(nodesRect.bottom - 0.975 * graphRect.bottom) +// : 0; + +// this.setState({ +// offsetTop: this.state.isOffsetTop ? this.state.offsetTop : newOffsetTop, +// isOffsetTop: this.state.isOffsetTop || newOffsetTop !== 0, +// }); + +// return this.state.isOffsetTop ? this.state.offsetTop : newOffsetTop; +// } + +// getTranslate() { +// const { width, height } = getGraphContainerDims(); +// return `${+width / 2}px,${+height / 2 + this.getOffsetTop()}px`; +// } + +// getScale() { +// if (FORCE.isGraphView) { +// return 0.36; +// } +// // resize the graph container to fit the screen +// const { width, height } = getGraphContainerDims(); + +// // zoom in until you hit the edge of... +// const windowConstrainingLength = Math.min(width, height); // constrain by the smaller length + +// const nodesG = $('#nodesG'); +// const nodesBB = nodesG +// ? nodesG.getBBox() +// : { width: window.innerWidth - 10, height: window.innerHeight * 0.8 }; +// // constrain the maximum nodes length +// const nodesConstrainedLength = Math.max(nodesBB.width, nodesBB.height); + +// // bugfix: zooming in because initial nodesConstrainedLength = 100, and doesn't resize correctly when browser focus isn't on this tab +// if ( +// nodesConstrainedLength === 100 && +// this.state.nodes.length === this.state.originalData.length +// ) { +// return 0.95; +// } + +// const scaleRatio = windowConstrainingLength / nodesConstrainedLength; + +// return scaleRatio * 0.95; // zoom out a little extra +// } + +// handleResize = debounce(() => { +// if (FORCE.isGraphView) { +// return; +// } +// const newScale = this.getScale(); +// const graphContainer = $('#graphContainer'); +// const svgBBox = graphContainer +// ? graphContainer.getBoundingClientRect() +// : { height: window.innerHeight * 0.8 }; +// this.setState({ +// svgBBox, +// }); + +// // resize size legend scale +// d3.selectAll('.sizeCircle') +// .style('transform', `scale(${newScale})`) +// .style('opacity', 1 / newScale); +// d3.selectAll('.size').style( +// 'padding-bottom', +// `${Math.min(Math.max(newScale - 1, 0) * 40, svgBBox.height / 5)}px`, +// ); + +// // scale up the spaces between the y axis ticks +// d3.select('.yAxis').style('transform', `scaleY(${newScale})`); +// d3.selectAll('.yAxis .tick').style('transform', `scaleY(${1 / newScale})`); + +// // translate the nodes group into the middle and scale to fit +// const nodesG = $('#nodesG'); +// if (nodesG) { +// nodesG.style.transform = `translate(${this.getTranslate()}) scale(${newScale})`; +// } +// }, 150); + +// filteredNodes = () => { +// // filter the dataset according to the slider state +// const { filters, originalData } = this.state; +// const filterKeys = Object.keys(filters); +// const numFilters = filterKeys.length; +// const numNodes = originalData.length; + +// const filtered = []; +// for (let i = 0; i < numNodes; i++) { +// const node = originalData[i]; +// let keep = true; +// // for each filter variable +// for (let i = 0; i < numFilters; i++) { +// const filterVar = filterKeys[i]; // 'skillsLang', 'skillsMath'... +// // filter out the node if less than the slider value +// if (node[filterVar] < filters[filterVar]) { +// keep = false; +// } +// } +// keep && filtered.push(node); +// } +// return filtered; +// }; + +// filterNodes = () => { +// // pause the simulation if running +// !FORCE.paused && FORCE.stopSimulation(); + +// this.setState({ +// nodes: this.filteredNodes(), +// }); +// }; + +// restartSimulation = () => { +// this.setState({ +// nodes: this.filteredNodes(), +// }); +// const isStrongForce = this.state.sortedByValue && this.state.sortedType; +// const isMediumForce = this.state.sortedByValue; +// setTimeout(() => { +// FORCE.restartSimulation(this.state.nodes, isStrongForce, isMediumForce); +// }, 200); +// setTimeout(() => { +// this.handleResize(); +// }, 1500); +// }; + +// handleFilterChange = (filter, value) => { +// this.setState( +// { +// filters: { ...this.state.filters, [filter]: value }, +// }, +// this.filterNodes, +// ); +// }; +// handleFilterMouseup = () => { +// this.setState({ +// isOffsetTop: false, // recalculate offsetTop after each filter +// // offsetTop: 0, +// }); +// if (FORCE.isGraphView) { +// return; +// } +// setTimeout(() => { +// // TODO: instead of actually moving the filters, could set the background fill instead? +// // set all filters to new minima on mouseup +// // let newMinima = {}; +// // Object.keys(this.state.filters).forEach(filter => { +// // newMinima[filter] = Math.min(...this.state.nodes.map(d => d[filter])); +// // }); +// // restart the simulation +// // this.setState({ filters: newMinima }, +// this.restartSimulation(); +// // ); +// }, 0); +// }; +// resetFilters = () => { +// const filtersReset = this.state.filters; +// Object.keys(this.state.filters).map(key => (filtersReset[key] = 0)); +// this.setState( +// { +// filters: filtersReset, +// offsetTop: 0, +// isOffsetTop: false, +// }, +// () => { +// this.filterNodes(); +// this.restartSimulation(); +// }, +// ); +// }; +// sortByValue = (value, doSort = false) => { +// const { radiusSelector, getRadiusScale } = this.state; +// const newSortedValue = this.state.sortedByValue && !doSort ? false : value; +// this.setState({ sortedByValue: newSortedValue }); +// FORCE.applySortForces({ +// sortByValue: newSortedValue, +// getRadiusScale, +// radiusSelector, +// }); +// setTimeout(this.handleResize, 2000); +// }; +// setCurrentColor = value => { +// this.setState({ colouredByValue: value }); +// }; +// colourByValue = value => { +// if (!this.state.colouredByValue) { +// this.setState({ colouredByValue: value }); +// FORCE.colourByValue({ doColour: true, value }); +// } else { +// this.setState({ colouredByValue: false }); +// FORCE.colourByValue({ doColour: false, value: null }); +// } +// }; +// render() { +// return ( +// this.setState({ radiusSelector: x }), +// setClusterSelector: x => this.setState({ clusterSelector: x }), +// handleFilterChange: this.handleFilterChange, +// handleResize: this.handleResize, +// resetFilters: this.resetFilters, +// restartSimulation: this.restartSimulation, +// handleFilterMouseup: this.handleFilterMouseup, +// setNodes: nodes => this.setState({ nodes: nodes }), +// sortByValue: this.sortByValue, +// colourByValue: this.colourByValue, +// setCurrentColor: this.setCurrentColor, +// getScale: this.getScale, +// }} +// > +// {this.props.children} +// +// ); +// } +// } + +// export default ContextProvider; diff --git a/src/components/Controls/FilterSlider.jsx b/src/components/Controls/FilterSlider.jsx index 54b1a93..1b0a723 100644 --- a/src/components/Controls/FilterSlider.jsx +++ b/src/components/Controls/FilterSlider.jsx @@ -1,10 +1,10 @@ -import { Collapse, IconButton } from '@material-ui/core'; -import Tooltip from '@material-ui/core/Tooltip'; -import Typography from '@material-ui/core/Typography'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import Slider from '@material-ui/lab/Slider'; -import React, { useContext, useState } from 'react'; -import styled from 'styled-components/macro'; +import { Collapse, IconButton } from "@material-ui/core"; +import Tooltip from "@material-ui/core/Tooltip"; +import Typography from "@material-ui/core/Typography"; +import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; +import Slider from "@material-ui/lab/Slider"; +import React, { useState } from "react"; +import styled from "styled-components/macro"; import { FILTER_RANGE, FILTER_TITLE, @@ -12,8 +12,8 @@ import { SLIDER_WIDTH_MD, SUBSKILL_FILTER_TITLES, SLIDER_TOOLTIP_TEXT, -} from '../../utils/constants'; -import { ControlsContext } from '../Context/ContextProvider'; +} from "../../utils/constants"; +import useStore from "../store"; const LabelAndSliderStyles = styled.div` background: white; @@ -67,7 +67,7 @@ const LabelAndSliderStyles = styled.div` `; const MinMax = ({ visible, title }) => ( -
+
Min
Max
@@ -77,25 +77,26 @@ const MAX_SUBSKILL_VALUE = 10; const SubskillFilters = ({ filterVar, onMouseUp }) => { const subskillFilters = SUBSKILL_FILTER_TITLES(filterVar); - const context = useContext(ControlsContext); + const filters = useStore((state) => state.filters); + const handleFilterChange = useStore((state) => state.handleFilterChange); const [minMaxVisible, setMinMaxVisible] = useState(false); return (
- {subskillFilters.map(subskill => ( + {subskillFilters.map((subskill) => (
{subskill.title} { - context.handleFilterChange(subskill.dataLabel, value); + handleFilterChange(subskill.dataLabel, value); }} onMouseUp={onMouseUp} onTouchEnd={onMouseUp} @@ -136,10 +137,10 @@ const FilterSlider = ({ {FILTER_TITLE(filterVar)} - + setIsExpanded({ ...isExpanded, [filterVar]: !filterIsExpanded }) diff --git a/src/components/Controls/FiltersPanel.jsx b/src/components/Controls/FiltersPanel.jsx index fc37d51..ea95fe5 100644 --- a/src/components/Controls/FiltersPanel.jsx +++ b/src/components/Controls/FiltersPanel.jsx @@ -1,8 +1,8 @@ -import React, { useContext } from 'react'; -import styled from 'styled-components/macro'; -import { SLIDER_WIDTH_LG, SLIDER_WIDTH_MD } from '../../utils/constants'; -import { ControlsContext } from '../Context/ContextProvider'; -import FilterSlider from './FilterSlider'; +import React from "react"; +import styled from "styled-components/macro"; +import { SLIDER_WIDTH_LG, SLIDER_WIDTH_MD } from "../../utils/constants"; +import FilterSlider from "./FilterSlider"; +import useStore from "../store"; const FiltersPanelStyles = styled.div` margin: 10px 0px 0px 6px; @@ -35,21 +35,21 @@ const FiltersPanelStyles = styled.div` `; const FiltersPanel = ({ filterVariables, isExpanded, setIsExpanded }) => { - const { handleFilterMouseup, handleFilterChange, state } = useContext( - ControlsContext, - ); + const handleFilterMouseup = useStore((state) => state.handleFilterMouseup); + const handleFilterChange = useStore((state) => state.handleFilterChange); + const filters = useStore((state) => state.filters); return (
- {filterVariables.map(filterVar => ( + {filterVariables.map((filterVar) => ( { + value={filters[filterVar]} + onChange={(value) => { handleFilterChange(filterVar, value); }} onMouseUp={handleFilterMouseup} diff --git a/src/components/Controls/GraphViewButton.tsx b/src/components/Controls/GraphViewButton.tsx index c7795f1..d41437f 100644 --- a/src/components/Controls/GraphViewButton.tsx +++ b/src/components/Controls/GraphViewButton.tsx @@ -1,22 +1,28 @@ -import React from 'react'; -import { Switch } from '@material-ui/core'; -import styled from 'styled-components/macro'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import Tooltip from '@material-ui/core/Tooltip'; -import { MenuItem, Select } from '@material-ui/core'; -import { AUTOMATION_RISK, WORKERS, SALARY, STUDY, INDUSTRY } from './SortPanel'; -import { getDatalabelMap } from '../../utils/constants'; +import React from "react"; +import { Switch } from "@material-ui/core"; +import styled from "styled-components/macro"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Tooltip from "@material-ui/core/Tooltip"; +import { MenuItem, Select } from "@material-ui/core"; +import { + AUTOMATION_RISK, + WORKERS, + SALARY, + STUDY, + INDUSTRY, + getDatalabelMap, +} from "../../utils/constants"; export const VariablePickerMenu = ({ value, onChange, isIndustry = false }) => ( { document.querySelector('[name="snapshotLink"]').select(); - document.execCommand('copy'); + document.execCommand("copy"); handleClose(); }} > @@ -175,18 +178,18 @@ const SnapshotsPanel = () => { {/* Delete Snapshot */} { - context.handleDeleteSnapshot(ss.id); + handleDeleteSnapshot(ss.id); handleClose(); }} > Delete Snapshot - + diff --git a/src/components/Controls/SortPanel.jsx b/src/components/Controls/SortPanel.jsx index c099791..ca9fa1f 100644 --- a/src/components/Controls/SortPanel.jsx +++ b/src/components/Controls/SortPanel.jsx @@ -1,28 +1,24 @@ -import Switch from '@material-ui/core/Switch'; -import Button from '@material-ui/core/Button'; -import FormControlLabel from '@material-ui/core/FormControlLabel'; -import Tooltip from '@material-ui/core/Tooltip'; -import RestoreIcon from '@material-ui/icons/RestoreRounded'; -import React, { useContext, useState } from 'react'; -import { ControlsContext } from '../Context/ContextProvider'; -import FORCE from '../FORCE'; -import GraphViewButton, { VariablePickerMenu } from './GraphViewButton'; -import { SortPanelStyles } from './SortPanelStyles'; -import { deactivateGraphView } from '../Viz/graphViewUtils'; -import { INITIAL_SUBSKILL_FILTERS_EXPANDED_STATE } from '../AppLayout'; - -export const AUTOMATION_RISK = 'automationRisk'; -export const AUTOMATION_RISK_LABEL = 'automationRisk'; -export const COLOUR_BY_VALUE = 'colourByValue'; -export const SORT_BY_VALUE = 'sortByValue'; -export const WORKERS = 'workers'; -export const SALARY = 'salary'; -export const STUDY = 'study'; -export const INDUSTRY = 'industry'; -export const WORKERS_LABEL = 'workers'; -export const SALARY_LABEL = 'salaryMed'; -export const STUDY_LABEL = 'yearsStudy'; -export const INDUSTRY_LABEL = 'industry'; +import Switch from "@material-ui/core/Switch"; +import Button from "@material-ui/core/Button"; +import FormControlLabel from "@material-ui/core/FormControlLabel"; +import Tooltip from "@material-ui/core/Tooltip"; +import RestoreIcon from "@material-ui/icons/RestoreRounded"; +import React, { useState } from "react"; +import FORCE from "../FORCE"; +import GraphViewButton, { VariablePickerMenu } from "./GraphViewButton"; +import { SortPanelStyles } from "./SortPanelStyles"; +import { deactivateGraphView } from "../Viz/graphViewUtils"; +import { INITIAL_SUBSKILL_FILTERS_EXPANDED_STATE } from "../AppLayout"; +import useStore from "../store"; +import { + AUTOMATION_RISK, + COLOUR_BY_VALUE, + SORT_BY_VALUE, + WORKERS, + SALARY, + STUDY, + INDUSTRY, +} from "../../utils/constants"; const getTooltipText = (value, type) => { if (value === WORKERS) { @@ -40,24 +36,24 @@ const getTooltipText = (value, type) => { return (
- {type === COLOUR_BY_VALUE ? 'Colour' : 'Sort'} the circles by risk + {type === COLOUR_BY_VALUE ? "Colour" : "Sort"} the circles by risk that the job will be replaced by machine work.
{type === COLOUR_BY_VALUE ? (
- Once coloured, look for{' '} - green circles to + Once coloured, look for{" "} + green circles to find jobs which won{"'"}t be taken over by machines for a long time. Darker green means lower risk.
- Avoid high-risk{' '} - red circles. + Avoid high-risk{" "} + red circles.
) : (
- Once sorted, look{' '} - higher up to find jobs + Once sorted, look{" "} + higher up to find jobs which won{"'"}t be taken over by machines for a long time. Avoid jobs lower down, which are at increased risk.
@@ -68,21 +64,21 @@ const getTooltipText = (value, type) => { return (
- {type === COLOUR_BY_VALUE ? 'Colour' : 'Sort'} by how much money is + {type === COLOUR_BY_VALUE ? "Colour" : "Sort"} by how much money is made by the average worker.
{type === COLOUR_BY_VALUE ? (
- Once coloured, look for{' '} - + Once coloured, look for{" "} + dark green circles - {' '} + {" "} to find jobs which make more money.
) : (
- Once sorted, look{' '} - higher up to find jobs + Once sorted, look{" "} + higher up to find jobs with higher salaries.
)} @@ -92,24 +88,24 @@ const getTooltipText = (value, type) => { return (
- {type === COLOUR_BY_VALUE ? 'Colour' : 'Sort'} by years of study after + {type === COLOUR_BY_VALUE ? "Colour" : "Sort"} by years of study after high school for the average person working this job (not necessarily how many are required for the job).
{type === COLOUR_BY_VALUE ? (
- Once coloured, look for{' '} - dark blue circles{' '} - to find jobs which require more study, or{' '} - + Once coloured, look for{" "} + dark blue circles{" "} + to find jobs which require more study, or{" "} + light blue circles - {' '} + {" "} for jobs which require less study.
) : (
- Once sorted, looking{' '} - higher up you'll find + Once sorted, looking{" "} + higher up you'll find jobs that take a little longer to prepare for.
)} @@ -120,7 +116,7 @@ const getTooltipText = (value, type) => { return (
- {type === COLOUR_BY_VALUE ? 'Colour' : 'Sort'} by job industry, or + {type === COLOUR_BY_VALUE ? "Colour" : "Sort"} by job industry, or groups of related jobs.
@@ -144,14 +140,13 @@ const SortPanel = ({ const [activeSwitches, setActiveSwitches] = useState([]); const [valueToColourBy, setValueToColourBy] = useState(INDUSTRY); const [valueToSortBy, setValueToSortBy] = useState(WORKERS); - const { - sortByValue, - colourByValue, - restartSimulation, - resetFilters, - setCurrentColor, - state, - } = useContext(ControlsContext); + + const sortByValue = useStore((state) => state.sortByValue); + const colourByValue = useStore((state) => state.colourByValue); + const restartSimulation = useStore((state) => state.restartSimulation); + const resetFilters = useStore((state) => state.resetFilters); + const setCurrentColor = useStore((state) => state.setCurrentColor); + const filters = useStore((state) => state.filters); const handleSort = ({ toggleActivated, @@ -164,7 +159,7 @@ const SortPanel = ({ if (!activeSwitches.includes(SORT_BY_VALUE)) { setActiveSwitches([...activeSwitches, SORT_BY_VALUE]); } else { - setActiveSwitches(activeSwitches.filter(d => d !== SORT_BY_VALUE)); + setActiveSwitches(activeSwitches.filter((d) => d !== SORT_BY_VALUE)); } } if (toggleActivated === COLOUR_BY_VALUE) { @@ -173,7 +168,7 @@ const SortPanel = ({ if (!activeSwitches.includes(COLOUR_BY_VALUE)) { setActiveSwitches([...activeSwitches, COLOUR_BY_VALUE]); } else { - setActiveSwitches(activeSwitches.filter(d => d !== COLOUR_BY_VALUE)); + setActiveSwitches(activeSwitches.filter((d) => d !== COLOUR_BY_VALUE)); } } }; @@ -187,9 +182,7 @@ const SortPanel = ({ colourByValue(valueToColourBy); } // if any filter is active, reset them - if ( - Object.values(state.filters).reduce((tally, cur) => tally + cur, 0) > 0 - ) { + if (Object.values(filters).reduce((tally, cur) => tally + cur, 0) > 0) { resetFilters(); } // if any subskill slider panel is open, close them @@ -233,13 +226,13 @@ const SortPanel = ({
Sort {activeSwitches && activeSwitches.includes(SORT_BY_VALUE) - ? 'ed' - : ''}{' '} - by{' '} + ? "ed" + : ""}{" "} + by{" "}
{ + onChange={(event) => { setValueToSortBy(event.target.value); if (activeSwitches.includes(SORT_BY_VALUE)) { sortByValue(event.target.value, true); @@ -255,7 +248,7 @@ const SortPanel = ({ title={getTooltipText(valueToColourBy, COLOUR_BY_VALUE)} > Colour {activeSwitches && activeSwitches.includes(COLOUR_BY_VALUE) - ? 'ed' - : ''}{' '} - by{' '} + ? "ed" + : ""}{" "} + by{" "}
{ + onChange={(event) => { setValueToColourBy(event.target.value); if (activeSwitches.includes(COLOUR_BY_VALUE)) { FORCE.colourByValue({ @@ -297,7 +290,10 @@ const SortPanel = ({ />