Prechádzať zdrojové kódy

Add wordCloud feature

Tatiana Inama 6 rokov pred
rodič
commit
be2b8d28fc
3 zmenil súbory, kde vykonal 223 pridanie a 7 odobranie
  1. 150 0
      package-lock.json
  2. 1 0
      package.json
  3. 72 7
      src/Admin.tsx

+ 150 - 0
package-lock.json

@@ -1314,6 +1314,11 @@
       "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz",
       "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
     },
+    "@popperjs/core": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.4.2.tgz",
+      "integrity": "sha512-JlGTGRYHC2QK+DDbePyXdBdooxFq2+noLfWpRqJtkxcb/oYWzOF0kcbfvvbWrwevCC1l6hLUg1wHYT+ona5BWQ=="
+    },
     "@sheerun/mutationobserver-shim": {
       "version": "0.3.3",
       "resolved": "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz",
@@ -4233,6 +4238,104 @@
         "type": "^1.0.1"
       }
     },
+    "d3-array": {
+      "version": "2.4.0",
+      "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.4.0.tgz",
+      "integrity": "sha512-KQ41bAF2BMakf/HdKT865ALd4cgND6VcIztVQZUTt0+BH3RWy6ZYnHghVXf6NFjt2ritLr8H1T8LreAAlfiNcw=="
+    },
+    "d3-cloud": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/d3-cloud/-/d3-cloud-1.2.5.tgz",
+      "integrity": "sha512-4s2hXZgvs0CoUIw31oBAGrHt9Kt/7P9Ik5HIVzISFiWkD0Ga2VLAuO/emO/z1tYIpE7KG2smB4PhMPfFMJpahw==",
+      "requires": {
+        "d3-dispatch": "^1.0.3"
+      }
+    },
+    "d3-color": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
+      "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
+    },
+    "d3-dispatch": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
+      "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA=="
+    },
+    "d3-ease": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.6.tgz",
+      "integrity": "sha512-SZ/lVU7LRXafqp7XtIcBdxnWl8yyLpgOmzAk0mWBI9gXNzLDx5ybZgnRbH9dN/yY5tzVBqCQ9avltSnqVwessQ=="
+    },
+    "d3-format": {
+      "version": "1.4.4",
+      "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.4.tgz",
+      "integrity": "sha512-TWks25e7t8/cqctxCmxpUuzZN11QxIA7YrMbram94zMQ0PXjE4LVIMe/f6a4+xxL8HQ3OsAFULOINQi1pE62Aw=="
+    },
+    "d3-interpolate": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
+      "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==",
+      "requires": {
+        "d3-color": "1"
+      }
+    },
+    "d3-scale": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-3.2.1.tgz",
+      "integrity": "sha512-huz5byJO/6MPpz6Q8d4lg7GgSpTjIZW/l+1MQkzKfu2u8P6hjaXaStOpmyrD6ymKoW87d2QVFCKvSjLwjzx/rA==",
+      "requires": {
+        "d3-array": "1.2.0 - 2",
+        "d3-format": "1",
+        "d3-interpolate": "^1.2.0",
+        "d3-time": "1",
+        "d3-time-format": "2"
+      }
+    },
+    "d3-scale-chromatic": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz",
+      "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==",
+      "requires": {
+        "d3-color": "1",
+        "d3-interpolate": "1"
+      }
+    },
+    "d3-selection": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.1.tgz",
+      "integrity": "sha512-BTIbRjv/m5rcVTfBs4AMBLKs4x8XaaLkwm28KWu9S2vKNqXkXt2AH2Qf0sdPZHjFxcWg/YL53zcqAz+3g4/7PA=="
+    },
+    "d3-time": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
+      "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
+    },
+    "d3-time-format": {
+      "version": "2.2.3",
+      "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.2.3.tgz",
+      "integrity": "sha512-RAHNnD8+XvC4Zc4d2A56Uw0yJoM7bsvOlJR33bclxq399Rak/b9bhvu/InjxdWhPtkgU53JJcleJTGkNRnN6IA==",
+      "requires": {
+        "d3-time": "1"
+      }
+    },
+    "d3-timer": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz",
+      "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw=="
+    },
+    "d3-transition": {
+      "version": "1.3.2",
+      "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz",
+      "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==",
+      "requires": {
+        "d3-color": "1",
+        "d3-dispatch": "1",
+        "d3-ease": "1",
+        "d3-interpolate": "1",
+        "d3-selection": "^1.1.0",
+        "d3-timer": "1"
+      }
+    },
     "damerau-levenshtein": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz",
@@ -8477,6 +8580,16 @@
       "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz",
       "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0="
     },
+    "lodash.clonedeep": {
+      "version": "4.5.0",
+      "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+      "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8="
+    },
+    "lodash.debounce": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
+      "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168="
+    },
     "lodash.memoize": {
       "version": "4.1.2",
       "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -11369,6 +11482,25 @@
         "react-lifecycles-compat": "^3.0.4"
       }
     },
+    "react-wordcloud": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/react-wordcloud/-/react-wordcloud-1.2.3.tgz",
+      "integrity": "sha512-pRanqs6w6O0uFisG7KRP7qzs0ae4nYABZ//f8HDKMgZbnN/s7xz7I8zGk843kGdCNUD3/rh7xqC0gNv9/t89LQ==",
+      "requires": {
+        "d3-array": "^2.4.0",
+        "d3-cloud": "^1.2.5",
+        "d3-dispatch": "^1.0.6",
+        "d3-scale": "^3.2.1",
+        "d3-scale-chromatic": "^1.5.0",
+        "d3-selection": "1.4.1",
+        "d3-transition": "^1.3.2",
+        "lodash.clonedeep": "^4.5.0",
+        "lodash.debounce": "^4.0.8",
+        "resize-observer-polyfill": "^1.5.1",
+        "seedrandom": "^3.0.5",
+        "tippy.js": "^6.2.0"
+      }
+    },
     "reactstrap": {
       "version": "8.4.1",
       "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-8.4.1.tgz",
@@ -11664,6 +11796,11 @@
       "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
       "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
     },
+    "resize-observer-polyfill": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+      "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
+    },
     "resolve": {
       "version": "1.15.0",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz",
@@ -11963,6 +12100,11 @@
         "ajv-keywords": "^3.4.1"
       }
     },
+    "seedrandom": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
+      "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
+    },
     "select-hose": {
       "version": "2.0.0",
       "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -13114,6 +13256,14 @@
       "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
       "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
     },
+    "tippy.js": {
+      "version": "6.2.3",
+      "resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.2.3.tgz",
+      "integrity": "sha512-MzqHMrr2C0IC8ZUnG5kLQPxonWJ7V+Usqiy2W5b+dCvAfousio0mA85h+Ea5wRq94AQGd8mbFGeciRgkP+F+7w==",
+      "requires": {
+        "@popperjs/core": "^2.3.2"
+      }
+    },
     "tmp": {
       "version": "0.0.33",
       "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",

+ 1 - 0
package.json

@@ -29,6 +29,7 @@
     "react-paper-bindings": "^2.0.0",
     "react-router-dom": "^5.1.2",
     "react-scripts": "3.4.0",
+    "react-wordcloud": "^1.2.3",
     "reactstrap": "^8.4.1",
     "typescript": "^3.8.3"
   },

+ 72 - 7
src/Admin.tsx

@@ -1,11 +1,13 @@
-import React, { useState } from 'react';
+import React, { useState, RefObject } from 'react';
 import { Container, Badge, FormGroup, Label, Input, Button, Collapse, Table, ListGroup, ListGroupItem } from 'reactstrap';
 import Paper, { Path, PaperScope, Point, Raster, Color } from 'paper';
-import { without, append, isNil, identity, includes, is } from 'ramda';
+import { without, append, isNil, identity, includes, is, descend, prop, sort, take } from 'ramda';
 import Axios from 'axios';
 import VALUES, { AgeVal, GenderVal, NonNativeVal, Filters, FilterVal, FilterStatus, EducationVal } from './services';
 import FileDownload from 'js-file-download';
 import map from './merseyside-nobg-white.png';
+import ReactWordcloud from 'react-wordcloud'
+
 import './Admin.css';
 
 const BACKEND = process.env.REACT_APP_BACKEND || '/backend';
@@ -65,6 +67,11 @@ type CanvasData = {
   label: paper.PointText
 };
 
+type WordCloud = {
+  text: string,
+  value: number
+};
+
 type AdminState = {
   original: StateData[],
   data: StateData[],
@@ -73,6 +80,7 @@ type AdminState = {
   focusedResponse?: StateData,
   path?: paper.Path,
   selectedArea?: paper.Item[],
+  wordCloud: WordCloud[],
   focusedDrawResponse?: {
     shape: paper.Item,
     label: paper.TextItem
@@ -123,13 +131,17 @@ const mkPath = (pathData: string, i: number, scale: number, data: ShapeData): pa
 
 class Admin extends React.Component<{}, AdminState> {
 
+  wordCloudRef: RefObject<unknown>;
+
   constructor(props: any) {
     super(props);
+    this.wordCloudRef = React.createRef();
     this.state = {
       height: 100,
       canvas: undefined,
       original: [],
       data: [],
+      wordCloud: [],
       focusedResponse: undefined,
       path: undefined,
       selectedArea: undefined,
@@ -172,11 +184,45 @@ class Admin extends React.Component<{}, AdminState> {
         item.visible = true;
       });
       this.setState({
-        selectedArea: items || []
+        selectedArea: items || [],
+        wordCloud: this.mkWords(items)
       })
     }
   }
 
+  mkWords = (items: paper.Item[]) => {
+    const wordCounter = items.reduce<{[key: string]: number}>((words, item) => {
+      if (item.data && item.data.associations) {
+        item.data.associations.forEach((assoc: string) => {
+          if (assoc.trim && assoc !== '') {
+            const word = assoc.trim().toLowerCase();
+            words[word] = words[word] ? words[word] + 1 : 1;
+          }
+        })
+      }
+      return words;
+    }, {});
+    const keys = Object.keys(wordCounter);
+    const wordCloud = keys.map(word => {
+      return {
+        text: word,
+        value: wordCounter[word]
+      }
+    });
+    return take(10, sort(descend(prop('value')), wordCloud));
+  }
+
+  mkWordCloud = (words: {[key:string]: number}) => {
+    const keys = Object.keys(words);
+    const wordCloud = keys.map(word => {
+      return {
+        text: word,
+        value: words[word]
+      }
+    });
+    return take(10, sort(descend(prop('value')), wordCloud));
+  }
+
   toggleDrawings = (data: StateData[], visibility: boolean) => {
     data.forEach(result => {
       result.canvas.forEach(({ path, label }) => {
@@ -328,12 +374,14 @@ class Admin extends React.Component<{}, AdminState> {
     this.setState({
       path: undefined,
       selectedArea: undefined,
-      focusedDrawResponse: undefined
+      focusedDrawResponse: undefined,
+      wordCloud: [],
     })
   }
 
   hideDrawing = (item: paper.Item) => {
     const { selectedArea } = this.state;
+
     if (selectedArea) {
       const newSelection = selectedArea.filter(i => {
         if (item.id === i.id) {
@@ -344,7 +392,8 @@ class Admin extends React.Component<{}, AdminState> {
         }
       });
       this.setState({
-        selectedArea: newSelection
+        selectedArea: newSelection,
+        wordCloud: this.mkWords(newSelection)
       })
     }
   }
@@ -449,8 +498,22 @@ class Admin extends React.Component<{}, AdminState> {
             {
               this.state.selectedArea ? (
                 <>
-                  <div>
-                    
+                  <div className="vom-word-cloud">
+                    <ReactWordcloud 
+                      options={{
+                        colors: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b'],
+                        enableTooltip: true,
+                        deterministic: false,
+                        fontFamily: 'impact',
+                        fontSizes: [12, 40],
+                        fontStyle: 'normal',
+                        fontWeight: 'normal',
+                        padding: 1,
+                        rotations: 1,
+                        rotationAngles: [0, 90],
+                      }}
+                      words={this.state.wordCloud}
+                    />
                   </div>
                   <SelectedAreaTable items={this.state.selectedArea}></SelectedAreaTable>
                 </>
@@ -464,6 +527,7 @@ class Admin extends React.Component<{}, AdminState> {
                   <h4>Showing:</h4>
                   <ViewResponse response={this.state.focusedResponse}/>
                 </div>
+
               </div>
             ) : null
           }
@@ -979,4 +1043,5 @@ const ViewResponse: React.FunctionComponent<{response: StateData}> = ({ response
   )
 }
 
+
 export default Admin;