diff --git a/package-lock.json b/package-lock.json index fe338f8..9061d52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "bootstrap": "^5.2.3", + "d3": "^7.8.5", "leaflet": "^1.9.4", "openai": "^3.3.0", "python-shell": "^5.0.0", @@ -7207,6 +7208,46 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/d3": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.5.tgz", + "integrity": "sha512-JgoahDG51ncUfJu6wX/1vWQEqOflgXyl4MaHqlcSruTez7yhaRKR9i8VjjcQGeS2en/jnFivXuaIMnseMMt0XA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-array": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", @@ -7218,6 +7259,40 @@ "node": ">=12" } }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-color": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", @@ -7226,6 +7301,80 @@ "node": ">=12" } }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -7234,6 +7383,30 @@ "node": ">=12" } }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-format": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", @@ -7242,6 +7415,25 @@ "node": ">=12" } }, + "node_modules/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -7261,6 +7453,30 @@ "node": ">=12" } }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-scale": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", @@ -7276,6 +7492,26 @@ "node": ">=12" } }, + "node_modules/d3-scale-chromatic": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz", + "integrity": "sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", @@ -7317,6 +7553,39 @@ "node": ">=12" } }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -7493,6 +7762,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delaunator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.0.tgz", + "integrity": "sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw==", + "dependencies": { + "robust-predicates": "^3.0.0" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -16389,6 +16666,11 @@ "dev": true, "optional": true }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/rollup": { "version": "2.79.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.1.tgz", @@ -16461,6 +16743,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/rxjs": { "version": "7.8.1", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", @@ -17919,19 +18206,6 @@ "is-typedarray": "^1.0.0" } }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } - }, "node_modules/unbox-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", diff --git a/package.json b/package.json index 866406a..2e8889a 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", "bootstrap": "^5.2.3", + "d3": "^7.8.5", "leaflet": "^1.9.4", "openai": "^3.3.0", "python-shell": "^5.0.0", diff --git a/scripts/sim.py b/scripts/sim.py index ee3a991..96d7d57 100644 --- a/scripts/sim.py +++ b/scripts/sim.py @@ -72,7 +72,7 @@ def run_sim_once(): command = input() # print(f"(Python Script): Received the following input from hidden renderer: {command}") - if command == 'run_sim': + if command.split(' ')[0] == 'run_sim': # expected: command = "run_sim" + " " + JSON String of SimArgs from front-end # TODO: May want to create a thread run_sim_once() print("simulation_run_complete") diff --git a/src/App.css b/src/App.css index 7d82568..6cb2e86 100644 --- a/src/App.css +++ b/src/App.css @@ -17,7 +17,7 @@ } .container { - max-width: '100%' //This will make container to take screen width + max-width: '100%'; /* This will make container to take screen width */ } @@ -38,8 +38,12 @@ #centerRow { /* background-color: white; */ - padding-right: 25px; + /* padding-right: 25px; */ padding-top: 2px; + display: flex; + flex-direction: column; + align-items: center; + row-gap: 2vh; } #rightRow { @@ -48,18 +52,23 @@ height: 100vh; } + + /* SIMULATION ARGUMENTS SECTION */ /* ----------------------------------- */ /* ----------------------------------- */ #fireSimButton { - margin-top: 7px; + margin-top: 20px; width: 150px; height: 50px; + margin-left: auto; + margin-right: auto; + margin-bottom: 20px; } #simArgSection { - margin-top: 20px; + /* margin-top: 20px; */ } .simArgDiv { @@ -72,46 +81,63 @@ border: 1px solid grey; } +#valueTable { + margin-top: 20px; +} + /* Stat Bar Styles */ /* ----------------------------------- */ /* ----------------------------------- */ -.statUL { - list-style-type: none; +#statDiv { padding: 20px; margin: auto; - width: 450px; + width: 85%; + height: 90vh; + } -.statUL li { +#graphBox { outline-style: solid; margin: 10px; - margin-left: 0px; - padding: 15px; - padding-left: 0px; + padding: 20px; font-size: 15px; background-color: #022D36; - + height: 100%; } - .graphTitle { font-weight: bold; font-size: 20px; } -.toggleStats { +#toggleStats { color: white; margin: auto; border: 1px solid grey; - + margin-top: 20px; } .selectDiv { } +.graph_card { + display: flex; + justify-content: center; + align-items: center; +} + +.hover_tooltip { + position: absolute; + background-color: white; + color: black; + padding: 5px; + border: 1px solid #ccc; + border-radius: 5px; + z-index: 99; +} /* Zoom Chart Styling */ /* ----------------------------------- */ @@ -131,6 +157,7 @@ .plot-container { /* position: relative; */ + /* padding: 20px; */ } .chartButton { @@ -139,8 +166,6 @@ } - - /* Map styling */ /* ----------------------------------- */ /* ----------------------------------- */ diff --git a/src/App.js b/src/App.js index 0ab6e96..926a9e8 100644 --- a/src/App.js +++ b/src/App.js @@ -105,7 +105,8 @@ class App extends Component { startSim = () => { console.log("RE-RUNNING SIMULATION") - window.port.postMessage('run_sim') + console.log(this.state.simArgs) + window.port.postMessage('run_sim' + ' ' + JSON.stringify(this.state.simArgs)) // send sim args as a json string as a part of the command this.setState({ loading: true, displayMap: false, @@ -161,7 +162,7 @@ class App extends Component { this.setState({ ExtraGraphs: typeof value === 'string' ? value.split(',') : value, }); - console.log(typeof value === 'string' ? value.split(',') : value,); + } @@ -170,7 +171,7 @@ class App extends Component { value: this.state.rightCol, onChange: (e) => { if(this.state.rightCol === 'stats') { - this.setState({rightCol:'bot'}) + this.setState({rightCol:'map'}) } else { this.setState({rightCol: 'stats'}) } @@ -186,10 +187,11 @@ class App extends Component { json={this.state.json} handleChange={this.handleChangeSelect} Select={this.state.ExtraGraphs} + ExtraGraphs={this.state.ExtraGraphs} /> ); } else { - return(); + return( ); } } return ( @@ -197,14 +199,18 @@ class App extends Component { - -
Graphs
-
Bot
-
- {statProvider()} - - - + + + + + + +
Graphs
+
Map
+
+ +
+ - - - + + {statProvider()}
diff --git a/src/Subcomponents/Graph.js b/src/Subcomponents/Graph.js new file mode 100644 index 0000000..47b4509 --- /dev/null +++ b/src/Subcomponents/Graph.js @@ -0,0 +1,264 @@ +import * as d3 from "d3"; +import React, {useEffect} from "react"; + +export default function Graph(props) { + + // TODO: Data tick values are not the corresponding simulation output ticks since simulation outputs have been shortened. + // TODO: Hunt down the ellusive bug. The hover functionality sometimes randomly disappears. Sometimes reappearing shortly + // and sometimes staying gone for good. This isn't a huge problem but one day should be fixed + + const createGraph = () => { + // data array will store datapoints for the selected graphs + let data = []; + let colors = ['#fcba03', "#9405fa", "#cc0808", "#3bc708"] + + // initializing data array to have the correct size and a tick value at each point. + props.data['speed_kmh'].forEach((value, tick) => { + data[tick] = {tick: tick} + }) + + props.graphs.forEach((dataName) => { + let values1 = props.data[dataName]; + values1.forEach((d, index) => { + data[index][dataName] = d; + }); + }); + + // leftMargin is set dynamically to leave enough space for multiple y-axis + var leftMargin = props.graphs.length * 37.5; + // set the dimensions and margins of the graph + var margin = { top: 30, right: 50, bottom: 50, left: leftMargin }; + + // Get the width of the "graphBox" div + var graphBox = document.getElementById('graphBox'); + var width = graphBox.clientWidth - margin.left - margin.right; + var height = graphBox.clientHeight - margin.top - margin.bottom; + + // Clean up previous graph before creating a new one + d3.select('#graphBox svg').remove(); + + // append the svg object to the body of the page + var svg = d3.select("#graphBox").append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", `translate(${margin.left}, ${margin.top})`); + + // Initialize x and yto the proper number of pixels + var x = d3.scaleLinear().range([0, width]); + var y = d3.scaleLinear().range([height, 0]); + + // Initialize x-domain + x.domain(d3.extent(data, (d) => { return d.tick; })); + var xAxis = svg.append("g") + .attr("transform", `translate(0, ${height})`) + .call(d3.axisBottom(x)); + + // Add a clipPath: everything out of this area won't be drawn. + var clip = svg.append("defs").append("svg:clipPath") + .attr("id", "clip") + .append("svg:rect") + .attr("width", width ) + .attr("height", height ) + .attr("x", 0) + .attr("y", 0); + + + + /** Hover Functionality **/ + + // Tooltip element that will be shown on hover + const hover_tooltip = d3.select("body").append("div") + .attr("class", "hover_tooltip") + .style("opacity", 0); + + // Define lines to create a crosshair that follows the mouse + const hoverLineVertical = svg.append("line") + .attr("class", "hover-line") + .style("stroke", "#666") + .style("stroke-width", "2px") + .style("stroke-dasharray", "3,3") + .attr("x1", 0) + .attr("x2", 0) + .attr("y1", 0) + .attr("y2", height) + .style("opacity", 0); + + const hoverLineHorizontal = svg.append("line") + .attr("class", "hover-line") + .style("stroke", "#666") + .style("stroke-width", "2px") + .style("stroke-dasharray", "3,3") + .attr("x1", 0) + .attr("x2", width) + .attr("y1", 0) + .attr("y2", 0) + .style("opacity", 0); + + + + /** Zoom Functionality **/ + + var idleTimeout + function idled() { idleTimeout = null; } + + // This function updates the chart based on the selected x-domain of the brush object. + function updateChart(event, d) { + // What are the selected boundaries? + let extent = event.selection; + + // number of ticks on the x-axis + let maxWidth = props.data["speed_kmh"].length; + + // If no selection, back to the initial coordinate. Otherwise, update X axis domain + if (!extent) { + if (!idleTimeout) return (idleTimeout = setTimeout(idled, 350)); // This allows waiting a little bit + x.domain([0, maxWidth]); + } else { + x.domain([x.invert(extent[0]), x.invert(extent[1])]); + line.select(".brush").call(brush.move, null); // This removes the grey brush area as soon as the selection has been done + } + + // Update axis and line position + xAxis.transition().duration(1000).call(d3.axisBottom(x)); + + // Update each line separately + props.graphs.forEach((dataName, index) => { + let yScale = d3.scaleLinear().range([height, 0]); // Separate y-scale for each line + + yScale.domain([d3.min(data, (d) => d[dataName]), d3.max(data, (d) => d[dataName])]); + + let lineSelection = line.select(`.line-${index + 1}`); + lineSelection + .transition() + .duration(1000) + .attr("d", d3.line() + .x(function (d) { return x(d.tick); }) + .y(function (d) { return yScale(d[dataName]); }) + ); + }) + } + + // Add brushing + var brush = d3.brushX() + .extent([[0,0], [width,height]]) + .on("end", (event, d) => {updateChart(event, d)}) + + // Create the line variable: where both the line and the brush take place + var line = svg.append('g') + .attr("clip-path", "url(#clip)") + + // Add the brushing + line + .append("g") + .attr("class", "brush") + .call(brush); + + let graphNumber = 0; + props.graphs.forEach((dataName) => { + var color = colors[graphNumber]; + y.domain([d3.min(data, (d) => {return d[dataName]}), d3.max(data, (d) => { return d[dataName]; })]); + + // Append the y-axis to the far left and shift it over some distance + var YAxis = svg.append("g") + .attr("transform", `translate(-${graphNumber * 40}, 0)`) // Move to the left and shift it by 40 units + .call(d3.axisLeft(y)); + + YAxis.selectAll("text") + .style("fill", color); + + // create the line + var valueLine = d3.line() + .x((d) => { return x(d.tick); }) + .y((d) => { return y(d[dataName]); }); + + // add the line to the line object. Also introduce hover functionality on the line + line.append("path") + .data([data]) + .attr("class", `line-${graphNumber+1}`) + .attr("fill", "none") + .attr("stroke", color) + .attr("stroke-width", 2) // change this to change thickness of graph lines + .attr("d", valueLine) + .on("mouseover", () => { + // show tooltip and horizontal line + hover_tooltip.style("opacity", 1); + hoverLineVertical.style("opacity", 1); + hoverLineHorizontal.style("opacity", 1); + }) + .on("mouseout", () => { + // make tooltip and horizontal line invisible + hover_tooltip.style("opacity", 0); + hoverLineVertical.style("opacity", 0); + hoverLineHorizontal.style("opacity", 0); + }) + .on("mousemove", (event) => { + // Calculate the corresponding data point based on the mouse position + const xPosition = d3.pointer(event)[0]; + const xPositionInverted = x.invert(xPosition); + const i = d3.bisectLeft(data.map(d => d.tick), xPositionInverted, 1); + const dataPoint = data[i - 1]; + + // Update tooltip text and position + let tooltip_str = ""; + Object.keys(dataPoint).forEach((key) => { + tooltip_str += key + ": " + (Math.round(dataPoint[key] * 100000) / 100000) + "
"; + }); + hover_tooltip.html(tooltip_str) + .style("left", (event.pageX + 10) + "px") + .style("top", (event.pageY - 30) + "px"); + + // Update crosshair position + const yPosition = d3.pointer(event)[1]; + hoverLineHorizontal.attr("y1", yPosition).attr("y2", yPosition); + hoverLineVertical.attr("x1", xPosition).attr("x2", xPosition); + }); + + // These are the dots and labels for the legend at the top of the graph + svg.selectAll("mydots") + .data([data]) + .enter() + .append("circle") + .attr("cx", d => 160 * graphNumber - (props.graphs.length * 40) + 40) // Adjust the x position to stack horizontally + .attr("cy", -20) // Keep the vertical position constant + .attr("r", 7) + .style("fill", color); + + // Add one dot in the legend for each name. + svg.selectAll("mylabels") + .data([data]) + .enter() + .append("text") + .attr("x", d => 160 * graphNumber + 13 - (props.graphs.length * 40) + 40) // Adjust the x position to stack horizontally + .attr("y", -20) // Keep the vertical position constant + .style("fill", color) + .text(dataName) + .attr("text-anchor", "left") + .style("alignment-baseline", "middle"); + + graphNumber++; + }) + } + + useEffect(() => { + // Clean up previous graph before creating a new one + createGraph(); + + + // Listenser to remake/rerender the graph when the size of the window changes + // Add event listener for window resize + const handleResize = () => { + createGraph(); + }; + + window.addEventListener('resize', handleResize); + + // Remove the event listener when the component unmounts + return () => { + window.removeEventListener('resize', handleResize); + }; + + }, [props.graphs]); + + return(<>); +} \ No newline at end of file diff --git a/src/Subcomponents/MultiSelect.js b/src/Subcomponents/MultiSelect.js index 261e577..98d4c5e 100644 --- a/src/Subcomponents/MultiSelect.js +++ b/src/Subcomponents/MultiSelect.js @@ -38,9 +38,10 @@ export default function MultiSelect(props) { label="Graph To Display" onChange={handleChange} > - State of Charge - Speed - Distances + State of Charge + Speed + Distances + Delta Energy diff --git a/src/Subcomponents/Stats.js b/src/Subcomponents/Stats.js index 59b1b9b..0412c09 100644 --- a/src/Subcomponents/Stats.js +++ b/src/Subcomponents/Stats.js @@ -1,52 +1,41 @@ -import React, { Component } from 'react'; -import ZoomChart from './ZoomChart' -import secondsToDhms from "../HelperFunctions/TimeString" - +import React, { useEffect } from 'react'; +import ZoomChart from './ZoomChart'; +import Graph from './Graph'; +import secondsToDhms from '../HelperFunctions/TimeString'; import '../App.css'; -import loading from "../Images/loading.gif" +import loading from '../Images/loading.gif'; import MultiSelect from './MultiSelect'; -class Stats extends Component { - createGraph(arrayName) { - return( - - ) - } - - render() { - let returnString = () => { - let emptyString = "NO DATA..." - if(this.props.loading){ - return loading - } else { - if (this.props.json["empty"] === undefined){ - return( -
-
    -
  • {"distance traveled: " + Math.round(this.props.json["distance_travelled"]) +" km"}
  • -
  • {"time taken: "}
    {secondsToDhms(this.props.json["time_taken"])}
  • -
  • {"final SOC: " + Math.round(this.props.json["final_soc"])}
  • -
  • {this.createGraph("speed_kmh")}
  • -
  • {this.createGraph("distances")}
  • -
  • {this.createGraph("state_of_charge")}
  • - {/*
  • {this.createGraph("influx_soc")}
  • */} -
+const Stats = (props) => { + const createGraph = (arrayName) => { + return ; + }; -
- ); - } else { - return
{emptyString}
; - } - } - } - - return( -
- - {returnString()} + const returnString = () => { + let emptyString = 'NO DATA...'; + if (props.loading) { + return loading; + } else { + if (props.json['empty'] === undefined) { + return ( +
+
+
+
); + } else { + return
{emptyString}
; + } } -} + }; + + return ( +
+ + {returnString()} +
+ ); +}; export default Stats; diff --git a/src/Subcomponents/ValueTable.js b/src/Subcomponents/ValueTable.js index ca20fa7..a9a7d07 100644 --- a/src/Subcomponents/ValueTable.js +++ b/src/Subcomponents/ValueTable.js @@ -12,7 +12,7 @@ class ValueTable extends Component { const { currentValues, expectedValues } = this.props; return ( - + @@ -27,7 +27,7 @@ class ValueTable extends Component { {Object.keys(currentValues).map(key => ( - + {/* Round to 4 decimal places */} ))} diff --git a/src/Subcomponents/ZoomChart.js b/src/Subcomponents/ZoomChart.js index ecf4121..157113c 100644 --- a/src/Subcomponents/ZoomChart.js +++ b/src/Subcomponents/ZoomChart.js @@ -1,13 +1,13 @@ import React, { useState } from "react"; -import { LineChart, Line, ReferenceArea, XAxis, YAxis } from "recharts"; +import { LineChart, Line, ReferenceArea, XAxis, YAxis, Tooltip } from "recharts"; import '../App.css'; const MIN_ZOOM = 5; // adjust based on your data const DEFAULT_ZOOM = { x1: null, y1: null, x2: null, y2: null }; -const CHART_WIDTH = 400; +const CHART_WIDTH = 380; const Normalize = (min, max, dataset) => { @@ -96,6 +96,18 @@ export default function ZoomChart(props) { } } + const CustomTooltip = ({ active, payload, label }) => { + if (active && payload && payload.length) { + return ( +
+

{`(${Math.round(label * 100) / 100} , ${Math.round(payload[0].value * 100) / 100})`}

{/* (x, y) pair rounded to 2 decimal places */} +
+ ); + } + + return null; + }; + return (
{props.name}
@@ -112,6 +124,7 @@ export default function ZoomChart(props) { type="number" dataKey="x" domain={["auto", "auto"]} + tick={{fontSize: 13}} stroke="white" /> + +
); diff --git a/src/index.js b/src/index.js index d563c0f..7e22409 100644 --- a/src/index.js +++ b/src/index.js @@ -6,9 +6,7 @@ import reportWebVitals from './reportWebVitals'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - - - + ); // If you want to start measuring performance in your app, pass a function diff --git a/src/telemetry_data.json b/src/telemetry_data.json index ba58f26..029aa91 100644 --- a/src/telemetry_data.json +++ b/src/telemetry_data.json @@ -1 +1 @@ -"{\n \"_time\": \"2023-09-30T22:32:50Z\",\n \"bad_hall_sequence\": 0.0,\n \"bridge_pwm_flag\": 0.0,\n \"bus_current\": 0.0,\n \"bus_current_flag\": 0.0,\n \"bus_voltage\": 105.158732096354,\n \"bus_voltage_lower_limit_flag\": 0.0,\n \"bus_voltage_upper_limit_flag\": 0.0,\n \"config_read_error\": 0.0,\n \"current_setpoint\": 0.0,\n \"dc_bus_overvoltage\": 0.0,\n \"desired_velocity\": 0.0,\n \"heat_sink_temp_flag\": 0.0,\n \"heatsink_temperature\": 23.0060119628906,\n \"hw_overcurrent\": 0.0,\n \"motor_current_flag\": 0.0,\n \"motor_temperature\": 29.7648010253906,\n \"motor_velocity\": 0.0,\n \"phase_a_current\": 0.246654238019671,\n \"phase_b_current\": 0.306003434317453,\n \"sw_overcurrent\": 0.0,\n \"undervoltage_lockout\": 0.0,\n \"vehicle_velocity\": 0.0,\n \"velocity_flag\": 0.0,\n \"watchdog_reset\": 0.0\n}" \ No newline at end of file +"{}" \ No newline at end of file
{key}{currentValues[key]}{Math.round(currentValues[key] * 1000) / 1000}{expectedValues[key]}