├── js-client ├── index.css ├── index.html └── index.js └── react-client ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.js ├── App.test.js ├── BarChart.js ├── PolarArea.js ├── RadarChart.js ├── ScatterPlot.js ├── index.js └── reportWebVitals.js └── yarn.lock /js-client/index.css: -------------------------------------------------------------------------------- 1 | .barchart { 2 | font-size: 9px; 3 | line-height: 15px; 4 | table-layout: fixed; 5 | text-align: center; 6 | width: 100%; 7 | height: 226px; 8 | } 9 | 10 | .barchart tr:nth-child(1) td { 11 | vertical-align: bottom; 12 | height: 200px; 13 | } 14 | 15 | .barchart .bar { 16 | background: #0d4aa5; 17 | padding: 10px 2px 0; 18 | } 19 | 20 | .barchart .label { 21 | background-color: black; 22 | margin-top: -30px; 23 | padding: 0 3px; 24 | color: white; 25 | border-radius: 4px; 26 | } 27 | 28 | .minwidth { 29 | width: 500px; 30 | } -------------------------------------------------------------------------------- /js-client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Vanilla JS Data Visualization Demo 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 18 | 22 | 26 | 30 | 34 | 38 | 42 | 46 | 50 | 54 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
15 | 50 16 |
17 |
19 | 5000 20 |
21 |
23 | 5 24 |
25 |
27 | 5 28 |
29 |
31 | 50 32 |
33 |
35 | 50 36 |
37 |
39 | 5 40 |
41 |
43 | 5 44 |
45 |
47 | 50 48 |
49 |
51 | 50 52 |
53 |
55 | 5 56 |
57 |
0001020304050607080910
73 |
74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /js-client/index.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | { 3 | "year": "2010", 4 | "amount": 134 5 | }, 6 | { 7 | "year": "2011", 8 | "amount": 156 9 | }, 10 | { 11 | "year": "2012", 12 | "amount": 190 13 | }, 14 | { 15 | "year": "2013", 16 | "amount": 100 17 | }, 18 | { 19 | "year": "2014", 20 | "amount": 138 21 | }, 22 | { 23 | "year": "2015", 24 | "amount": 165 25 | }, 26 | { 27 | "year": "2016", 28 | "amount": 84 29 | }, 30 | { 31 | "year": "2017", 32 | "amount": 122 33 | }, 34 | { 35 | "year": "2018", 36 | "amount": 167 37 | }, 38 | { 39 | "year": "2019", 40 | "amount": 198 41 | }, 42 | { 43 | "year": "2020", 44 | "amount": 176 45 | } 46 | ] 47 | 48 | async function callApis() { 49 | const users = await fetch("https://reqres.in/api/users") 50 | const products = await fetch("https://fakestoreapi.com/products") 51 | const posts = await fetch('https://jsonplaceholder.typicode.com/posts') 52 | 53 | await Promise.all([users, products, posts]).then(responses => { 54 | return responses.map(res => res.json()) 55 | }).then(data => console.log(data)) 56 | } 57 | 58 | callApis() 59 | 60 | fetch('https://jsonplaceholder.typicode.com/posts') 61 | .then(async (response) => { 62 | fetchedData = await response.json() 63 | var svg = d3.select("#data-vis-container") 64 | 65 | var margin = 200, 66 | width = svg.attr("width") - margin, 67 | height = svg.attr("height") - margin 68 | 69 | svg.append("text") 70 | .attr("transform", "translate(100,0)") 71 | .attr("x", 50) 72 | .attr("y", 50) 73 | .attr("font-size", "24px") 74 | .text("Example fluctuations") 75 | 76 | var xScale = d3.scaleBand().range([0, width]).padding(0.4), 77 | yScale = d3.scaleLinear().range([height, 0]); 78 | 79 | var g = svg.append("g") 80 | .attr("transform", "translate(" + 100 + "," + 100 + ")"); 81 | 82 | xScale.domain(fetchedData.map(function (d) { return d.userId; })); 83 | yScale.domain([0, d3.max(fetchedData, function (d) { return d.id; })]); 84 | 85 | g.append("g") 86 | .attr("transform", "translate(0," + height + ")") 87 | .call(d3.axisBottom(xScale)) 88 | .append("text") 89 | .attr("y", height - 250) 90 | .attr("x", width - 100) 91 | .attr("text-anchor", "end") 92 | .attr("stroke", "black") 93 | .text("Year"); 94 | 95 | g.append("g") 96 | .call(d3.axisLeft(yScale).tickFormat(function (d) { 97 | return "*" + d; 98 | }) 99 | .ticks(10)) 100 | .append("text") 101 | .attr("transform", "rotate(-90)") 102 | .attr("y", 6) 103 | .attr("dy", "-5.1em") 104 | .attr("text-anchor", "end") 105 | .attr("stroke", "black") 106 | .text("Amount"); 107 | 108 | g.selectAll(".bar") 109 | .data(fetchedData) 110 | .enter().append("rect") 111 | .attr("class", "bar") 112 | .attr("x", function (d) { return xScale(d.userId); }) 113 | .attr("y", function (d) { return yScale(d.id); }) 114 | .attr("width", xScale.bandwidth()) 115 | .attr("height", function (d) { return height - yScale(d.id); }) 116 | .attr("fill", "#0d4aa5"); 117 | }) 118 | 119 | -------------------------------------------------------------------------------- /react-client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /react-client/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits.\ 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `yarn eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 35 | 36 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 39 | 40 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `yarn build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /react-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.11.4", 7 | "@testing-library/react": "^11.1.0", 8 | "@testing-library/user-event": "^12.1.10", 9 | "@visx/group": "^2.1.0", 10 | "@visx/mock-data": "^2.1.2", 11 | "@visx/scale": "^2.2.2", 12 | "@visx/shape": "^2.2.2", 13 | "chart.js": "^3.7.0", 14 | "d3": "^7.1.1", 15 | "faker": "^6.6.6", 16 | "react": "^17.0.2", 17 | "react-chartjs-2": "^4.0.1", 18 | "react-dom": "^17.0.2", 19 | "react-scripts": "4.0.3", 20 | "web-vitals": "^1.0.1" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /react-client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flippedcoder/data-vis/920887f5ebad9670de69f48c77d9ad77c76f054b/react-client/public/favicon.ico -------------------------------------------------------------------------------- /react-client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /react-client/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flippedcoder/data-vis/920887f5ebad9670de69f48c77d9ad77c76f054b/react-client/public/logo192.png -------------------------------------------------------------------------------- /react-client/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flippedcoder/data-vis/920887f5ebad9670de69f48c77d9ad77c76f054b/react-client/public/logo512.png -------------------------------------------------------------------------------- /react-client/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /react-client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /react-client/src/App.js: -------------------------------------------------------------------------------- 1 | import { RadarChart } from './RadarChart'; 2 | import PolarAreaChart from './PolarArea'; 3 | import ScatterPlot from './ScatterPlot'; 4 | import BarChart from './BarChart'; 5 | import { useEffect } from 'react'; 6 | 7 | const rawData = [ 8 | { group: 'Captain America', axis: 'Intelligence', value: 3, description: 'only human' }, 9 | { group: 'Captain America', axis: 'Strength', value: 3, description: 'only human' }, 10 | { group: 'Captain America', axis: 'Speed', value: 2, description: 'only human' }, 11 | { group: 'Captain America', axis: 'Durability', value: 3, description: 'only human' }, 12 | { group: 'Captain America', axis: 'Energy', value: 1, description: 'only human' }, 13 | { group: 'Captain America', axis: 'Fighting Skills', value: 6, description: 'able to judge combat decisively' }, 14 | { group: 'Iron Man', axis: 'Intelligence', value: 6, description: 'Smart entreprenuer' }, 15 | { group: 'Iron Man', axis: 'Strength', value: 6, description: 'Powered by his suit' }, 16 | { group: 'Iron Man', axis: 'Speed', value: 5, description: 'rocket boosters' }, 17 | { group: 'Iron Man', axis: 'Durability', value: 6, description: 'tough durable material' }, 18 | { group: 'Iron Man', axis: 'Energy', value: 6, description: '' }, 19 | { group: 'Iron Man', axis: 'Fighting Skills', value: 4, description: '' }, 20 | { group: 'Hulk', axis: 'Intelligence', value: 6, description: 'Scientist brilliance' }, 21 | { group: 'Hulk', axis: 'Strength', value: 7, description: 'Insanely strong' }, 22 | { group: 'Hulk', axis: 'Speed', value: 3, description: 'clumsy' }, 23 | { group: 'Hulk', axis: 'Durability', value: 7, description: 'Close to industructible' }, 24 | { group: 'Hulk', axis: 'Energy', value: 1, description: '' }, 25 | { group: 'Hulk', axis: 'Fighting Skills', value: 4, description: 'great at SMASHING' }, 26 | { group: 'Thor', axis: 'Intelligence', value: 2, description: 'not too bright' }, 27 | { group: 'Thor', axis: 'Strength', value: 7, description: 'god-like strength' }, 28 | { group: 'Thor', axis: 'Speed', value: 7, description: 'god-like speed' }, 29 | { group: 'Thor', axis: 'Durability', value: 6, description: 'god-like durability' }, 30 | { group: 'Thor', axis: 'Energy', value: 6, description: '' }, 31 | { group: 'Thor', axis: 'Fighting Skills', value: 4, description: 'quite low for a god???' }, 32 | ] 33 | 34 | const shapeData = (input) => { 35 | let data = []; 36 | let groups = []; // track unique groups 37 | input.forEach(function (record) { 38 | let group = record.group; 39 | if (groups.indexOf(group) < 0) { 40 | groups.push(group); // push to unique groups tracking 41 | data.push({ // push group node in data 42 | group: group, 43 | axes: [] 44 | }); 45 | }; 46 | data.forEach(function (d) { 47 | if (d.group === record.group) { // push record data into right group in data 48 | d.axes.push({ 49 | axis: record.axis, 50 | value: parseInt(record.value), 51 | description: record.description 52 | }); 53 | } 54 | }); 55 | }); 56 | return data; 57 | } 58 | 59 | function App() { 60 | // const radarData = shapeData(rawData) 61 | 62 | // useEffect(() => { 63 | // RadarChart.draw('#radar', radarData) 64 | // }, [radarData]) 65 | 66 | return ( 67 | <> 68 | {/*

Radar Chart

69 |
70 | 71 | */} 72 | 73 | 74 | ) 75 | } 76 | 77 | export default App; -------------------------------------------------------------------------------- /react-client/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /react-client/src/BarChart.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Group } from "@visx/group"; 3 | import { Bar } from "@visx/shape"; 4 | import { scaleLinear, scaleBand } from "@visx/scale"; 5 | 6 | const data = [{ 7 | "year": "2010", 8 | "amount": 134 9 | }, 10 | { 11 | "year": "2011", 12 | "amount": 156 13 | }, 14 | { 15 | "year": "2012", 16 | "amount": 190 17 | }, 18 | { 19 | "year": "2013", 20 | "amount": 100 21 | }, 22 | { 23 | "year": "2014", 24 | "amount": 138 25 | }, 26 | { 27 | "year": "2015", 28 | "amount": 165 29 | }, 30 | { 31 | "year": "2016", 32 | "amount": 84 33 | }, 34 | { 35 | "year": "2017", 36 | "amount": 122 37 | }, 38 | { 39 | "year": "2018", 40 | "amount": 167 41 | }, 42 | { 43 | "year": "2019", 44 | "amount": 198 45 | }, 46 | { 47 | "year": "2020", 48 | "amount": 176 49 | } 50 | ]; 51 | 52 | const width = 500; 53 | const height = 300; 54 | const margin = { top: 20, bottom: 20, left: 20, right: 20 }; 55 | const xMax = width - margin.left - margin.right; 56 | const yMax = height - margin.top - margin.bottom; 57 | 58 | const x = (d) => d.year; 59 | const y = (d) => +d.amount; 60 | 61 | const xScale = scaleBand({ 62 | range: [0, xMax], 63 | round: true, 64 | domain: data.map(x), 65 | padding: 0.4 66 | }); 67 | 68 | const yScale = scaleLinear({ 69 | range: [yMax, 0], 70 | round: true, 71 | domain: [0, Math.max(...data.map(y))] 72 | }); 73 | 74 | const compose = (scale, accessor) => (data) => scale(accessor(data)); 75 | const xPoint = compose(xScale, x); 76 | const yPoint = compose(yScale, y); 77 | 78 | function BarChart() { 79 | return ( 80 | 81 | {data.map((d, i) => { 82 | const barHeight = yMax - yPoint(d); 83 | return ( 84 | 85 | 92 | 93 | ); 94 | })} 95 | 96 | ); 97 | } 98 | 99 | export default BarChart -------------------------------------------------------------------------------- /react-client/src/PolarArea.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Chart as ChartJS, 4 | RadialLinearScale, 5 | ArcElement, 6 | Tooltip, 7 | Legend, 8 | } from 'chart.js'; 9 | import { PolarArea } from 'react-chartjs-2'; 10 | 11 | ChartJS.register(RadialLinearScale, ArcElement, Tooltip, Legend); 12 | 13 | export const data = { 14 | labels: ['Red', 'Blue', 'Yellow', 'Green', 'Purple', 'Orange'], 15 | datasets: [ 16 | { 17 | label: '# of Votes', 18 | data: [12, 19, 3, 5, 2, 3], 19 | backgroundColor: [ 20 | 'rgba(255, 99, 132, 0.5)', 21 | 'rgba(54, 162, 235, 0.5)', 22 | 'rgba(255, 206, 86, 0.5)', 23 | 'rgba(75, 192, 192, 0.5)', 24 | 'rgba(153, 102, 255, 0.5)', 25 | 'rgba(255, 159, 64, 0.5)', 26 | ], 27 | borderWidth: 1, 28 | }, 29 | ], 30 | }; 31 | 32 | export default function PolarAreaChart() { 33 | return ; 34 | } 35 | -------------------------------------------------------------------------------- /react-client/src/RadarChart.js: -------------------------------------------------------------------------------- 1 | import * as d3 from 'd3'; 2 | 3 | export const RadarChart = { 4 | draw: function(id, data, options) { 5 | // add touch to mouseover and mouseout 6 | let over = "ontouchstart" in window ? "touchstart" : "mouseover"; 7 | let out = "ontouchstart" in window ? "touchend" : "mouseout"; 8 | 9 | /** Initiate default configuration parameters and vis object 10 | * 11 | **/ 12 | // initiate default config 13 | const w = 300; 14 | const h = 300; 15 | 16 | const config = { 17 | w: w, 18 | h: h, 19 | facet: false, 20 | levels: 5, 21 | levelScale: 0.85, 22 | labelScale: 1.0, 23 | facetPaddingScale: 2.5, 24 | maxValue: 0, 25 | radians: 2 * Math.PI, 26 | polygonAreaOpacity: 0.3, 27 | polygonStrokeOpacity: 1, 28 | polygonPointSize: 4, 29 | legendBoxSize: 10, 30 | translateX: w / 4, 31 | translateY: h / 4, 32 | paddingX: w, 33 | paddingY: h, 34 | colors: d3.scaleOrdinal(d3.schemeCategory10), 35 | showLevels: true, 36 | showLevelsLabels: true, 37 | showAxesLabels: true, 38 | showAxes: true, 39 | showLegend: true, 40 | showVertices: true, 41 | showPolygons: true 42 | }; 43 | 44 | // initiate main vis component 45 | let vis = { 46 | svg: null, 47 | tooltip: null, 48 | levels: null, 49 | axis: null, 50 | vertices: null, 51 | legend: null, 52 | allAxis: null, 53 | total: null, 54 | radius: null 55 | }; 56 | 57 | // feed user configuration options 58 | if ("undefined" !== typeof options) { 59 | for (let i in options) { 60 | if ("undefined" !== typeof options[i]) { 61 | config[i] = options[i]; 62 | } 63 | } 64 | } 65 | 66 | render(data); // render the visualization 67 | 68 | /** helper functions 69 | * 70 | * @function: render: render the visualization 71 | * @function: updateConfig: update configuration parameters 72 | * @function: buildVis: build visualization using the other build helper functions 73 | * @function: buildVisComponents: build main vis components 74 | * @function: buildLevels: build "spiderweb" levels 75 | * @function: buildLevelsLabels: build out the levels labels 76 | * @function: buildAxes: builds out the axes 77 | * @function: buildAxesLabels: builds out the axes labels 78 | * @function: buildCoordinates: builds [x, y] coordinates of polygon vertices. 79 | * @function: buildPolygons: builds out the polygon areas of the dataset 80 | * @function: buildVertices: builds out the polygon vertices of the dataset 81 | * @function: buildLegend: builds out the legend 82 | **/ 83 | 84 | // render the visualization 85 | function render(data) { 86 | // remove existing svg if exists 87 | d3.select(id).selectAll("svg").remove(); 88 | updateConfig(); 89 | 90 | if (config.facet) { 91 | data.forEach(function(d, i) { 92 | buildVis([d]); // build svg for each data group 93 | 94 | // override colors 95 | vis.svg.selectAll(".polygon-areas") 96 | .attr("stroke", config.colors(i)) 97 | .attr("fill", config.colors(i)); 98 | vis.svg.selectAll(".polygon-vertices") 99 | .attr("fill", config.colors(i)); 100 | vis.svg.selectAll(".legend-tiles") 101 | .attr("fill", config.colors(i)); 102 | }); 103 | } else { 104 | buildVis(data); // build svg 105 | } 106 | } 107 | 108 | // update configuration parameters 109 | function updateConfig() { 110 | // adjust config parameters 111 | config.maxValue = Math.max(config.maxValue, d3.max(data, function(d) { 112 | return d3.max(d.axes, function(o) { return o.value; }); 113 | })); 114 | config.w *= config.levelScale; 115 | config.h *= config.levelScale; 116 | config.paddingX = config.w * config.levelScale; 117 | config.paddingY = config.h * config.levelScale; 118 | 119 | // if facet required: 120 | if (config.facet) { 121 | config.w /= data.length; 122 | config.h /= data.length; 123 | config.paddingX /= (data.length / config.facetPaddingScale); 124 | config.paddingY /= (data.length / config.facetPaddingScale); 125 | config.polygonPointSize *= Math.pow(0.9, data.length); 126 | } 127 | } 128 | 129 | //build visualization using the other build helper functions 130 | function buildVis(data) { 131 | buildVisComponents(data); 132 | buildCoordinates(data); 133 | if (config.showLevels) buildLevels(); 134 | if (config.showLevelsLabels) buildLevelsLabels(); 135 | if (config.showAxes) buildAxes(); 136 | if (config.showAxesLabels) buildAxesLabels(); 137 | if (config.showLegend) buildLegend(data); 138 | if (config.showVertices) buildVertices(data); 139 | if (config.showPolygons) buildPolygons(data); 140 | } 141 | 142 | // build main vis components 143 | function buildVisComponents(data) { 144 | // update vis parameters 145 | vis.allAxis = data[0].axes.map(function(i, j) { return i.axis; }); 146 | vis.totalAxes = vis.allAxis.length; 147 | vis.radius = Math.min(config.w / 2, config.h / 2); 148 | 149 | // create main vis svg 150 | vis.svg = d3.select(id) 151 | .append("svg").classed("svg-vis", true) 152 | .attr("width", config.w + config.paddingX) 153 | .attr("height", config.h + config.paddingY) 154 | .append("svg:g") 155 | .attr("transform", "translate(" + config.translateX + "," + config.translateY + ")");; 156 | 157 | // create verticesTooltip 158 | d3.select("body") 159 | .append("div").classed("verticesTooltip", true) 160 | .attr("opacity", 0) 161 | .style({ 162 | "position": "absolute", 163 | "color": "black", 164 | "font-size": "10px", 165 | "width": "100px", 166 | "height": "auto", 167 | "padding": "5px", 168 | "border": "2px solid gray", 169 | "border-radius": "5px", 170 | "pointer-events": "none", 171 | "opacity": "0", 172 | "background": "#f4f4f4" 173 | }); 174 | 175 | vis.verticesTooltip = d3.select(".verticesTooltip") 176 | 177 | // create levels 178 | vis.levels = vis.svg.selectAll(".levels") 179 | .append("svg:g").classed("levels", true); 180 | 181 | // create axes 182 | vis.axes = vis.svg.selectAll(".axes") 183 | .append("svg:g").classed("axes", true); 184 | 185 | // create vertices 186 | vis.vertices = vis.svg.selectAll(".vertices"); 187 | 188 | //Initiate Legend 189 | vis.legend = vis.svg.append("svg:g").classed("legend", true) 190 | .attr("height", config.h / 2) 191 | .attr("width", config.w / 2) 192 | .attr("transform", "translate(" + 0 + ", " + 1.1 * config.h + ")"); 193 | } 194 | 195 | // builds out the levels of the spiderweb 196 | function buildLevels() { 197 | for (let level = 0; level < config.levels; level++) { 198 | let levelFactor = vis.radius * ((level + 1) / config.levels); 199 | 200 | // build level-lines 201 | vis.levels 202 | .data(vis.allAxis).enter() 203 | .append("svg:line").classed("level-lines", true) 204 | .attr("x1", function(d, i) { return levelFactor * (1 - Math.sin(i * config.radians / vis.totalAxes)); }) 205 | .attr("y1", function(d, i) { return levelFactor * (1 - Math.cos(i * config.radians / vis.totalAxes)); }) 206 | .attr("x2", function(d, i) { return levelFactor * (1 - Math.sin((i + 1) * config.radians / vis.totalAxes)); }) 207 | .attr("y2", function(d, i) { return levelFactor * (1 - Math.cos((i + 1) * config.radians / vis.totalAxes)); }) 208 | .attr("transform", "translate(" + (config.w / 2 - levelFactor) + ", " + (config.h / 2 - levelFactor) + ")") 209 | .attr("stroke", "gray") 210 | .attr("stroke-width", "0.5px"); 211 | } 212 | } 213 | 214 | // builds out the levels labels 215 | function buildLevelsLabels() { 216 | for (let level = 0; level < config.levels; level++) { 217 | let levelFactor = vis.radius * ((level + 1) / config.levels); 218 | 219 | // build level-labels 220 | vis.levels 221 | .data([1]).enter() 222 | .append("svg:text").classed("level-labels", true) 223 | .text((config.maxValue * (level + 1) / config.levels).toFixed(2)) 224 | .attr("x", function(d) { return levelFactor * (1 - Math.sin(0)); }) 225 | .attr("y", function(d) { return levelFactor * (1 - Math.cos(0)); }) 226 | .attr("transform", "translate(" + (config.w / 2 - levelFactor + 5) + ", " + (config.h / 2 - levelFactor) + ")") 227 | .attr("fill", "gray") 228 | .attr("font-family", "sans-serif") 229 | .attr("font-size", 10 * config.labelScale + "px"); 230 | } 231 | } 232 | 233 | // builds out the axes 234 | function buildAxes() { 235 | vis.axes 236 | .data(vis.allAxis).enter() 237 | .append("svg:line").classed("axis-lines", true) 238 | .attr("x1", config.w / 2) 239 | .attr("y1", config.h / 2) 240 | .attr("x2", function(d, i) { return config.w / 2 * (1 - Math.sin(i * config.radians / vis.totalAxes)); }) 241 | .attr("y2", function(d, i) { return config.h / 2 * (1 - Math.cos(i * config.radians / vis.totalAxes)); }) 242 | .attr("stroke", "grey") 243 | .attr("stroke-width", "1px"); 244 | } 245 | 246 | // builds out the axes labels 247 | function buildAxesLabels() { 248 | vis.axes 249 | .data(vis.allAxis).enter() 250 | .append("svg:text").classed("axis-labels", true) 251 | .text(function(d) { return d; }) 252 | .attr("text-anchor", "middle") 253 | .attr("x", function(d, i) { return config.w / 2 * (1 - 1.3 * Math.sin(i * config.radians / vis.totalAxes)); }) 254 | .attr("y", function(d, i) { return config.h / 2 * (1 - 1.1 * Math.cos(i * config.radians / vis.totalAxes)); }) 255 | .attr("font-family", "sans-serif") 256 | .attr("font-size", 11 * config.labelScale + "px"); 257 | } 258 | 259 | // builds [x, y] coordinates of polygon vertices. 260 | function buildCoordinates(data) { 261 | data.forEach(function(group) { 262 | group.axes.forEach(function(d, i) { 263 | d.coordinates = { // [x, y] coordinates 264 | x: config.w / 2 * (1 - (parseFloat(Math.max(d.value, 0)) / config.maxValue) * Math.sin(i * config.radians / vis.totalAxes)), 265 | y: config.h / 2 * (1 - (parseFloat(Math.max(d.value, 0)) / config.maxValue) * Math.cos(i * config.radians / vis.totalAxes)) 266 | }; 267 | }); 268 | }); 269 | } 270 | 271 | // builds out the polygon vertices of the dataset 272 | function buildVertices(data) { 273 | data.forEach(function(group, g) { 274 | vis.vertices 275 | .data(group.axes).enter() 276 | .append("svg:circle").classed("polygon-vertices", true) 277 | .attr("r", config.polygonPointSize) 278 | .attr("cx", function(d, i) { return d.coordinates.x; }) 279 | .attr("cy", function(d, i) { return d.coordinates.y; }) 280 | .attr("fill", config.colors(g)) 281 | .on(over, verticesTooltipShow) 282 | .on(out, verticesTooltipHide); 283 | }); 284 | } 285 | 286 | // builds out the polygon areas of the dataset 287 | function buildPolygons(data) { 288 | vis.vertices 289 | .data(data).enter() 290 | .append("svg:polygon").classed("polygon-areas", true) 291 | .attr("points", function(group) { // build verticesString for each group 292 | let verticesString = ""; 293 | group.axes.forEach(function(d) { verticesString += d.coordinates.x + "," + d.coordinates.y + " "; }); 294 | return verticesString; 295 | }) 296 | .attr("stroke-width", "2px") 297 | .attr("stroke", function(d, i) { return config.colors(i); }) 298 | .attr("fill", function(d, i) { return config.colors(i); }) 299 | .attr("fill-opacity", config.polygonAreaOpacity) 300 | .attr("stroke-opacity", config.polygonStrokeOpacity) 301 | .on(over, function(d) { 302 | vis.svg.selectAll(".polygon-areas") // fade all other polygons out 303 | .transition(250) 304 | .attr("fill-opacity", 0.1) 305 | .attr("stroke-opacity", 0.1); 306 | d3.select(this) // focus on active polygon 307 | .transition(250) 308 | .attr("fill-opacity", 0.7) 309 | .attr("stroke-opacity", config.polygonStrokeOpacity); 310 | }) 311 | .on(out, function() { 312 | d3.selectAll(".polygon-areas") 313 | .transition(250) 314 | .attr("fill-opacity", config.polygonAreaOpacity) 315 | .attr("stroke-opacity", 1); 316 | }); 317 | } 318 | 319 | // builds out the legend 320 | function buildLegend(data) { 321 | //Create legend squares 322 | vis.legend.selectAll(".legend-tiles") 323 | .data(data).enter() 324 | .append("svg:rect").classed("legend-tiles", true) 325 | .attr("x", config.w - config.paddingX / 2) 326 | .attr("y", function(d, i) { return i * 2 * config.legendBoxSize; }) 327 | .attr("width", config.legendBoxSize) 328 | .attr("height", config.legendBoxSize) 329 | .attr("fill", function(d, g) { return config.colors(g); }); 330 | 331 | //Create text next to squares 332 | vis.legend.selectAll(".legend-labels") 333 | .data(data).enter() 334 | .append("svg:text").classed("legend-labels", true) 335 | .attr("x", config.w - config.paddingX / 2 + (1.5 * config.legendBoxSize)) 336 | .attr("y", function(d, i) { return i * 2 * config.legendBoxSize; }) 337 | .attr("dy", 0.07 * config.legendBoxSize + "em") 338 | .attr("font-size", 11 * config.labelScale + "px") 339 | .attr("fill", "gray") 340 | .text(function(d) { 341 | return d.group; 342 | }); 343 | } 344 | 345 | // show tooltip of vertices 346 | function verticesTooltipShow(event) { 347 | vis.verticesTooltip.style("opacity", 0.9) 348 | .html(`Value: ${data.value}
349 | Description: ${data.description}
`) 350 | } 351 | 352 | // hide tooltip of vertices 353 | function verticesTooltipHide() { 354 | vis.verticesTooltip.style("opacity", 0); 355 | } 356 | } 357 | }; -------------------------------------------------------------------------------- /react-client/src/ScatterPlot.js: -------------------------------------------------------------------------------- 1 | import { 2 | Chart as ChartJS, 3 | LinearScale, 4 | PointElement, 5 | LineElement, 6 | Tooltip, 7 | Legend, 8 | } from 'chart.js'; 9 | import { Scatter } from 'react-chartjs-2'; 10 | 11 | ChartJS.register(LinearScale, PointElement, LineElement, Tooltip, Legend); 12 | 13 | export const options = { 14 | scales: { 15 | y: { 16 | beginAtZero: true, 17 | }, 18 | }, 19 | }; 20 | 21 | export const data = { 22 | labels: [-56, 74, -25, 25, 68, -47, 1, -10, 38], 23 | datasets: [ 24 | { 25 | label: 'This dataset', 26 | data: [6, 74, 25, -25, -68, 47, -1, 10, -38], 27 | backgroundColor: 'rgba(255, 99, 132, 1)', 28 | }, 29 | ], 30 | }; 31 | 32 | export default function ScatterPlot() { 33 | return ; 34 | } 35 | -------------------------------------------------------------------------------- /react-client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | import reportWebVitals from './reportWebVitals'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | // If you want to start measuring performance in your app, pass a function 14 | // to log results (for example: reportWebVitals(console.log)) 15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 16 | reportWebVitals(); 17 | -------------------------------------------------------------------------------- /react-client/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | --------------------------------------------------------------------------------