(WIP)
ToDo:
- pan
- zoom
change the scale dynamically- log scale
- tool tip
(WIP)
ToDo:
| { | |
| "times": ["2020-09-21 16:40:00", "2020-09-22 17:45:00", "2020-09-23 18:50:00", "2020-09-24 19:55:00", "2020-09-25 21:00:00"], | |
| "A": [3, 10, -3, 6, 2], | |
| "B": [3.5, -10.3, -3.2, -5.6, 2.9], | |
| "C": [13, 8.7, -13, -1.6, 12.9] | |
| } |
| <!DOCTYPE html> | |
| <html lang="jp"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta | |
| name="viewport" | |
| content="width=device-width, initial-scale=1, shrink-to-fit=no" | |
| /> | |
| <title>D3 time series graph with jQuery UI Dialog</title> | |
| <!-- stylesheet --> | |
| <link | |
| rel="stylesheet" | |
| href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" | |
| /> | |
| <link | |
| rel="stylesheet" | |
| href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" | |
| /> | |
| <link rel="stylesheet" href="/resources/demos/style.css" /> | |
| <!-- javascript library --> | |
| <script src="https://code.jquery.com/jquery-3.5.1.min.js"></script> | |
| <script | |
| src="https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js" | |
| async | |
| ></script> | |
| <script | |
| src="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js" | |
| async | |
| ></script> | |
| <script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script> | |
| <script src="https://d3js.org/d3.v6.min.js" async></script> | |
| </head> | |
| <body> | |
| <h1>Test: D3 Graph in Dialog</h1> | |
| <div id="dialog" title="Basic dialog"> | |
| <!-- Initialize a select button --> | |
| <select id="selectButton"></select> | |
| <!-- Create a div where the graph will take place --> | |
| <div id="d3_dataviz"></div> | |
| </div> | |
| <button id="opener" class="btn btn-primary btn-lg">Open Dialog</button> | |
| <!-- read script --> | |
| <script src="./main.js"></script> | |
| </body> | |
| </html> |
| // *** | |
| // variables ------------------ | |
| let readData; // for DEBUG | |
| let readDataFormated; // for DEBUG | |
| const dataPath = './data.json'; | |
| const DialogSize = { | |
| width: 800, | |
| height: 600, | |
| }; | |
| const margin = { | |
| top: 10, | |
| bottom: 30, | |
| left: 30, | |
| right: 50, | |
| }; | |
| const graphSize = { | |
| width: DialogSize.width - margin.left - margin.right, | |
| height: DialogSize.height - margin.top - margin.bottom, | |
| }; | |
| // functions | |
| const plotGraphD3 = (json) => { | |
| // parseDate | |
| // CAUTION: this process will override original object. | |
| const dataTimeFormated = Object.assign(json, { | |
| times: json.times.map((x) => { | |
| return d3.timeParse('%Y-%m-%d %H:%M:%S')(x); | |
| }), | |
| }); | |
| // make data | |
| const dataKeys = Object.keys(dataTimeFormated); | |
| const dataSize = dataTimeFormated[dataKeys[0]].length; | |
| const data = []; | |
| for (let i = 0; i < dataSize; i++) { | |
| const record = {}; | |
| dataKeys.forEach((key) => { | |
| record[key] = dataTimeFormated[key][i]; | |
| }); | |
| data.push(record); | |
| } | |
| console.log(data); | |
| readDataFormated = data; | |
| // append the svg | |
| const svg = d3 | |
| .select('#d3_dataviz') | |
| .append('svg') | |
| .attr('width', DialogSize.width) | |
| .attr('height', DialogSize.height) | |
| .append('g') | |
| .attr('transform', `translate(${margin.left}, ${margin.top})`); | |
| // button | |
| const allGroup = ['A', 'B', 'C']; | |
| d3.select('#selectButton') | |
| .selectAll('myOptions') | |
| .data(allGroup) | |
| .enter() | |
| .append('option') | |
| .text((d) => { | |
| return d; | |
| }) | |
| .attr('value', (d) => { | |
| return d; | |
| }); | |
| // x-axis | |
| console.log( | |
| d3.extent(data, (d) => { | |
| return d.times; | |
| }) | |
| ); // DEBUG | |
| const x = d3 | |
| .scaleTime() | |
| .domain( | |
| d3.extent(data, (d) => { | |
| return d.times; | |
| }) | |
| ) | |
| .range([0, graphSize.width]); | |
| svg | |
| .append('g') | |
| .attr('transform', `translate(0, ${graphSize.height})`) | |
| .attr('id', 'axisX') | |
| .call(d3.axisBottom(x)); | |
| // y-axis | |
| const y = d3.scaleLinear().domain([-15, 15]).range([graphSize.height, 0]); | |
| svg.append('g').attr('id', 'axisY').call(d3.axisLeft(y)); | |
| // draw line | |
| const line = svg | |
| .append('g') | |
| .append('path') | |
| .attr('class', 'high') | |
| .datum(data) | |
| .attr( | |
| 'd', | |
| d3 | |
| .line() | |
| .x(function (d) { | |
| return x(d.times); | |
| }) | |
| .y(function (d) { | |
| return y(d.A); | |
| }) | |
| ) | |
| .attr('stroke', 'black') | |
| .attr('stroke-width', 4) | |
| .attr('fill', 'none'); | |
| // draw dots | |
| const dot = svg | |
| .selectAll('circle') | |
| .data(data) | |
| .enter() | |
| .append('circle') | |
| .attr('cx', function (d) { | |
| return x(d.times); | |
| }) | |
| .attr('cy', function (d) { | |
| return y(d.A); | |
| }) | |
| .attr('r', 3) | |
| .style('fill', '#0000ff'); | |
| // implement update | |
| const update = (selectedGroup) => { | |
| const dataFilter = data.map((d) => { | |
| return { times: d.times, value: d[selectedGroup] }; | |
| }); | |
| console.log(dataFilter); | |
| // update axis | |
| d3.select('#axisX').remove(); | |
| d3.select('#axisY').remove(); | |
| x.domain( | |
| d3.extent(dataFilter, (d) => { | |
| return d.times; | |
| }) | |
| ); | |
| y.domain([ | |
| d3.min(dataFilter, (d) => { | |
| return d.value; | |
| }), | |
| d3.max(dataFilter, (d) => { | |
| return d.value; | |
| }), | |
| ]); | |
| svg | |
| .append('g') | |
| .attr('id', 'axisX') | |
| .attr('transform', `translate(0, ${graphSize.height})`) | |
| .call(d3.axisBottom(x)); | |
| svg.append('g').attr('id', 'axisY').call(d3.axisLeft(y)); | |
| // Give these new data to update | |
| line | |
| .datum(dataFilter) | |
| .transition() | |
| .duration(1000) | |
| .attr( | |
| 'd', | |
| d3 | |
| .line() | |
| .x((d) => { | |
| return x(d.times); | |
| }) | |
| .y((d) => { | |
| return y(d.value); | |
| }) | |
| ); | |
| dot | |
| .data(dataFilter) | |
| .transition() | |
| .duration(1000) | |
| .attr('cx', (d) => { | |
| return x(d.times); | |
| }) | |
| .attr('cy', (d) => { | |
| return y(d.value); | |
| }); | |
| }; | |
| // When the button is changed, run the updateChart function | |
| d3.select('#selectButton').on('change', (event) => { | |
| // recover the option that has been chosen | |
| const selectedOption = d3.select(event.currentTarget).property('value'); | |
| // run the updateChart function with this selected option | |
| console.log(selectedOption); // for DEBUG | |
| update(selectedOption); | |
| }); | |
| }; | |
| const loadAndSetData = () => { | |
| const query = {}; | |
| fetch(dataPath + '?' + new URLSearchParams(query)) | |
| .then((a) => (a.ok ? a.json() : null)) | |
| .then((json) => { | |
| readData = JSON.parse(JSON.stringify(json)); | |
| plotGraphD3(json); | |
| }) | |
| .catch((error) => { | |
| console.error('Error:', error); | |
| }); | |
| return { message: 'Done.' }; | |
| }; | |
| // actions ------------------- | |
| // dialog | |
| $('#dialog').dialog({ | |
| autoOpen: false, | |
| modal: false, | |
| title: 'D3 TimeSeries Graph Sample', | |
| width: DialogSize.width, | |
| height: DialogSize.height, | |
| }); | |
| // button(opener) | |
| $('#opener').on('click', () => { | |
| // clear inner dialog | |
| $('#dialog') | |
| .empty() | |
| .append('<select id="selectButton"></select>') | |
| .append('<div id="d3_dataviz"></div>'); | |
| // load data & call d3 | |
| const result = loadAndSetData(); | |
| console.log(result); | |
| // open dialog | |
| $('#dialog').dialog('open'); | |
| }); |