Last active
February 6, 2019 16:30
-
-
Save belst/67ef05a4cb4b0efadb3c3cf7c1c895a6 to your computer and use it in GitHub Desktop.
Visualization of env data
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <meta http-equiv="X-UA-Compatible" content="ie=edge"> | |
| <title>Weather API</title> | |
| <style> | |
| body { | |
| margin: 0 auto; | |
| } | |
| svg { | |
| margin: 20px auto; | |
| display: block; | |
| } | |
| path { | |
| clip-path: url(#clip); | |
| } | |
| rect { | |
| pointer-events: all; | |
| } | |
| .valbox { | |
| margin-left: 5px; | |
| padding: 5px; | |
| background-color: #efefef; | |
| opacity: 0.7; | |
| border-radius: 4px; | |
| border: 1px solid white; | |
| } | |
| .valbox ul { | |
| margin: 0; | |
| padding: 0; | |
| } | |
| .valbox li { | |
| list-style-type: none; | |
| } | |
| .symbol { | |
| clip-path: none; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <svg width="960" height="500"> | |
| <foreignObject style="pointer-events: none"> | |
| <div class="valbox" style="display: inline-block; z-index: -5; pointer-events: none;"> | |
| <ul> | |
| <li id="temp"></li> | |
| <li id="press"></li> | |
| <li id="humid"></li> | |
| </ul> | |
| <div class="clearfix" ></div> | |
| </div> | |
| </foreignObject> | |
| </svg> | |
| <script src="https://d3js.org/d3.v5.min.js"></script> | |
| <script> | |
| Number.prototype.clamp = function(min, max) { | |
| return Math.min(Math.max(this, min), max); | |
| }; | |
| const binarySearch = (value, array, accessor) => { | |
| let l = 0; | |
| let u = array.length - 1; | |
| let c = accessor; | |
| if (!c) { | |
| c = i => i; | |
| } | |
| while (l <= u) { | |
| const m = Math.floor((l + u) / 2); | |
| if (c(array[m]) < c(value)) { | |
| l = m + 1; | |
| } else if (c(array[m]) > c(value)) { | |
| u = m - 1; | |
| } else { | |
| return m | |
| } | |
| } | |
| l = l.clamp(0, array.length - 1); | |
| u = u.clamp(0, array.length - 1); | |
| if (Math.abs(c(array[u]) - c(value)) < Math.abs(c(array[l]) - c(value))) { | |
| return u; | |
| } else { | |
| return l; | |
| } | |
| } | |
| (async () => { | |
| const svg = d3.select('svg'), | |
| margin = {top: 40, right: 20, bottom: 110, left: 135}, | |
| margin2 = {top: 430, right: 20, bottom: 30, left: 135}, | |
| width = +svg.attr('width') - margin.left - margin.right, | |
| height = +svg.attr('height') - margin.top - margin.bottom, | |
| height2 = +svg.attr('height') - margin2.top - margin2.bottom; | |
| svg.insert('defs', 'foreignObject').append('clipPath') | |
| .attr('id', 'clip') | |
| .append('rect') | |
| .attr('width', width) | |
| .attr('height', height); | |
| const yScales = { | |
| humid: d3.scaleLinear().range([height, 0]), | |
| temp: d3.scaleLinear().range([height, 0]), | |
| press: d3.scaleLinear().range([height, 0]), | |
| }; | |
| const y2Scales = { | |
| humid: d3.scaleLinear().range([height2, 0]), | |
| temp: d3.scaleLinear().range([height2, 0]), | |
| press: d3.scaleLinear().range([height2, 0]), | |
| }; | |
| const x = d3.scaleTime().range([0, width]), | |
| x2 = x.copy(); | |
| const z = d3.scaleOrdinal().range(d3.schemeCategory10) | |
| .domain(['humid', 'temp', 'press']); | |
| const shape = d3.scaleOrdinal().range(d3.symbols) | |
| .domain(['humid', 'temp', 'press']); | |
| const symbol = d3.symbol(); | |
| const tempAxis = d3.axisLeft(yScales.temp).ticks(10, 's'), | |
| humidAxis = d3.axisLeft(yScales.humid).ticks(10, 's'), | |
| pressAxis = d3.axisLeft(yScales.press).ticks(10, 's'); | |
| const brushed = () => { | |
| const s = d3.event.selection || x2.range(); | |
| data = overview; | |
| x.domain(s.map(x2.invert, x2)); | |
| focus.selectAll('.line').remove(); | |
| let focusl = focus.selectAll('.line') | |
| .data(data) | |
| .enter().append('g') | |
| .attr('class', 'line'); | |
| //focusl = focusl.merge(focus.selectAll('.line').data(data)); | |
| focusl.append('path') | |
| .attr('class', 'line') | |
| .attr('d', d => area(x, yScales[d.id])(d.values)) | |
| .style('fill', d => z(d.id)) | |
| .style('opacity', 0.3); | |
| focusl.append('path') | |
| .attr('class', 'line') | |
| .attr('d', d => line(x, yScales[d.id])(d.values)) | |
| .style('stroke', d => z(d.id)) | |
| .style('fill', 'none'); | |
| focus.select('.axis--x').call(d3.axisBottom(x)); | |
| } | |
| const endfn = async () => { | |
| const s = d3.event.selection || x2.range(); | |
| [start, end] = s.map(x2.invert, x2); | |
| url.searchParams.set('start', start.toISOString()); | |
| url.searchParams.set('end', end.toISOString()); | |
| data = mapdata(await d3.json(url)); | |
| x.domain([start, end]); | |
| focus.selectAll('.line').remove(); | |
| let focusl = focus.selectAll('.line') | |
| .data(data) | |
| .enter().append('g') | |
| .attr('class', 'line'); | |
| focusl.append('path') | |
| .attr('d', d => area(x, yScales[d.id])(d.values)) | |
| .style('fill', d => z(d.id)) | |
| .style('stroke', 'none') | |
| .style('opacity', 0.3); | |
| focusl.append('path') | |
| .attr('d', d => line(x, yScales[d.id])(d.values)) | |
| .style('stroke', d => z(d.id)) | |
| .style('fill', 'none'); | |
| focusl.append('path') | |
| .attr('class', 'symbol') | |
| .attr('opacity', '0') | |
| .style('fill', d => z(d.id)) | |
| .attr('d', d => symbol.type(shape(d.id))()); | |
| focus.select('.axis--x').call(d3.axisBottom(x)); | |
| } | |
| const brush = d3.brushX() | |
| .extent([[0,0], [width, height2]]) | |
| .on('brush', brushed) | |
| .on('end', endfn); | |
| const line = (x, y) => d3.line() | |
| .curve(d3.curveLinear) | |
| .x(d => x(d.time)) | |
| .y(d => y(d.value)); | |
| const area = (x, y) => d3.area() | |
| .curve(d3.curveLinear) | |
| .x(d => x(d.time)) | |
| .y0(d => y(d.value + d.stddev)) | |
| .y1(d => y(d.value - d.stddev)); | |
| textoffset = (x) => { | |
| const l = hovertext.node().getComputedTextLength(); | |
| return Math.min(Math.max(x - l / 2, 0), width - l); | |
| }, | |
| boxoffset = (x, y) => { | |
| let yoffset = hoverbox.node().clientHeight; | |
| yoffset = Math.min(Math.max(y - yoffset / 2, 0), height - yoffset); | |
| let vx = x; | |
| if (vx + hoverbox.node().offsetWidth > width) { | |
| vx = vx - hoverbox.node().offsetWidth - 10; // hardcoded 10 = margin * 2 | |
| } | |
| return [vx, yoffset]; | |
| } | |
| focusenter = () => { | |
| hoverline.attr('opacity', '1'); | |
| hovertext.attr('opacity', '1'); | |
| svg.select('foreignObject').attr('opacity', '1'); | |
| focus.selectAll('.symbol').attr('opacity', '1'); | |
| }, | |
| focusleave = () => { | |
| hoverline.attr('opacity', '0'); | |
| hovertext.attr('opacity', '0'); | |
| svg.select('foreignObject').attr('opacity', '0'); | |
| focus.selectAll('.symbol').attr('opacity', '0'); | |
| }, | |
| focusmove = () => { | |
| let [xc, yc] = d3.mouse(hoverrect.node()); | |
| let i = binarySearch({time: x.invert(xc) }, data[0].values, v => v.time); | |
| xc = x(data[0].values[i].time); | |
| hoverline.attr('x1', xc); | |
| hoverline.attr('x2', xc); | |
| hovertext.text(data[0].values[i].time); | |
| hovertext.attr('x', textoffset(xc)); | |
| const tmpv = data[0].values[i]; | |
| const tmph = data[1].values[i]; | |
| const tmpp = data[2].values[i]; | |
| hoverbox.select('#temp') | |
| .style('color', z('temp')) | |
| .text(`Temperature: ${tmpv.value.toFixed(2)} °C (σ: ${tmpv.stddev.toFixed(2)})`); | |
| hoverbox.select('#humid') | |
| .style('color', z('humid')) | |
| .text(`Humidity: ${tmph.value.toFixed(2)} % (σ: ${tmph.stddev.toFixed(2)})`); | |
| hoverbox.select('#press') | |
| .style('color', z('press')) | |
| .text(`Pressure: ${tmpp.value.toFixed(2)} hPa (σ: ${tmpp.stddev.toFixed(2)})`); | |
| const [bx, by] = boxoffset(xc, yc); | |
| const tmp = svg.select('foreignObject'); | |
| tmp.attr('x', bx); | |
| tmp.attr('y', by); | |
| d3.selectAll('.symbol') | |
| .attr('transform', d => `translate(${xc}, ${yScales[d.id](d.values[i].value)})`) | |
| }; | |
| const focus = svg.insert('g', 'foreignObject') | |
| .attr('class', 'focus') | |
| .attr('transform', `translate(${margin.left}, ${margin.top})`); | |
| const context = svg.insert('g', 'foreignObject') | |
| .attr('class', 'context') | |
| .attr('transform', `translate(${margin2.left}, ${margin2.top})`); | |
| const hoverrect = svg.insert('rect', 'foreignObject') | |
| .attr('transform', `translate(${margin.left}, ${margin.top})`) | |
| .attr('width', width) | |
| .attr('height', height) | |
| .attr('fill', 'none') | |
| .attr('opactiy', '0') | |
| .on('mouseenter', focusenter) | |
| .on('mouseleave', focusleave) | |
| .on('mousemove', focusmove); | |
| const hoverline = focus.append('line') | |
| .attr('y1', '0') | |
| .attr('y2', height) | |
| .attr('opacity', '0') | |
| .style('stroke-width', '1px') | |
| .style('stroke', 'black'); | |
| const hovertext = focus.append('text') | |
| .attr('dy', '-0.1em') | |
| .attr('opacity', '0'); | |
| const hoverbox = svg.select('foreignObject') | |
| .attr('width', width) | |
| .attr('height', height) | |
| .attr('transform', `translate(${margin.left}, ${margin.top})`) | |
| .select('.valbox'); | |
| // const hoverbox = svg.select('.valbox') | |
| // .attr('transform', `translate(${margin.left}, ${margin.top})`); | |
| let start = new Date(0); | |
| let end = new Date(); | |
| let url = new URL('https://weather.totally.rip/api/v2/timespan.php'); | |
| url.searchParams.append('start', start.toISOString()); | |
| url.searchParams.append('end', end.toISOString()); | |
| url.searchParams.append('limit', width); | |
| const mapdata = d => { | |
| const ret = [ | |
| { id: 'temp', values: [] }, | |
| { id: 'humid', values: [] }, | |
| { id: 'press', values: [] } | |
| ]; | |
| for (v of d.data) { | |
| ret[0].values.push({ | |
| id: 'temp', | |
| time: new Date(v.time), | |
| value: +v.temp_avg, | |
| stddev: +v.temp_stddev | |
| }); | |
| ret[1].values.push({ | |
| id: 'humid', | |
| time: new Date(v.time), | |
| value: +v.humidity_avg, | |
| stddev: +v.humidity_stddev | |
| }); | |
| ret[2].values.push({ | |
| id: 'press', | |
| time: new Date(v.time), | |
| value: +v.pressure_avg, | |
| stddev: +v.pressure_stddev | |
| }); | |
| } | |
| return ret; | |
| }; | |
| tempaxis = g => g.attr('transform', `translate(${margin.left},${margin.top})`) | |
| .call(tempAxis) | |
| .append('text') | |
| .attr('dy', '-1em') | |
| .attr('x', '-45px') | |
| .style('fill', z('temp')) | |
| .style('text-anchor', 'start') | |
| .text('Temp (°C)'); | |
| humidaxis = g => g.attr('transform', `translate(${90},${margin.top})`) | |
| .call(humidAxis) | |
| .append('text') | |
| .attr('dy', '-2em') | |
| .attr('y', 0) | |
| .attr('x', '-45px') | |
| .style('fill', z('humid')) | |
| .style('text-anchor', 'start') | |
| .text('Humidity %'); | |
| pressaxis = g => g.attr('transform', `translate(${45},${margin.top})`) | |
| .call(pressAxis) | |
| .append('text') | |
| .attr('dy', '-1em') | |
| .attr('x', '-45px') | |
| .style('fill', z('press')) | |
| .style('text-anchor', 'start') | |
| .text('Pressure (hPa)'); | |
| let data = mapdata(await d3.json(url)); | |
| const overview = data; | |
| x.domain([d3.min(data[0].values, d => d.time), new Date()]); | |
| x2.domain(x.domain()); | |
| yScales.temp.domain([d3.min(data[0].values, d => d.value - d.stddev), d3.max(data[0].values, d => d.value + d.stddev)]).nice(); | |
| yScales.humid.domain([d3.min(data[1].values, d => d.value - d.stddev), d3.max(data[1].values, d => d.value + d.stddev)]).nice(); | |
| yScales.press.domain([d3.min(data[2].values, d => d.value - d.stddev), d3.max(data[2].values, d => d.value + d.stddev)]).nice(); | |
| y2Scales.temp.domain(yScales.temp.domain()); | |
| y2Scales.humid.domain(yScales.humid.domain()); | |
| y2Scales.press.domain(yScales.press.domain()); | |
| tempaxis(svg.append('g')); | |
| humidaxis(svg.append('g')); | |
| pressaxis(svg.append('g')); | |
| context.append('g') | |
| .attr('class', 'axis axis--x') | |
| .attr('transform', `translate(0, ${height2})`) | |
| .call(d3.axisBottom(x2)); | |
| focus.append('g') | |
| .attr('class', 'axis axis--x') | |
| .attr('transform', `translate(0, ${height})`) | |
| .call(d3.axisBottom(x)); | |
| const contextl = context.selectAll('.line') | |
| .data(overview) | |
| .enter().append('g') | |
| .attr('class', 'line'); | |
| contextl.append('path') | |
| .attr('d', d => line(x2, y2Scales[d.id])(d.values)) | |
| .style('stroke', d => z(d.id)) | |
| .style('fill', 'none'); | |
| context.append('g') | |
| .attr('class', 'brush') | |
| .call(brush) | |
| .call(brush.move, x.range()); | |
| })(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment