Weather dashboard
https://natronics.org/weather/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
363 lines
12 KiB
363 lines
12 KiB
var d3 = require('d3')
|
|
var SunCalc = require('suncalc')
|
|
|
|
const lat = 39.9243509
|
|
const lon = -75.1696126
|
|
const leftMargin = 75
|
|
|
|
class Chart {
|
|
constructor(canvas, width, height, offset, margin, data, yrange) {
|
|
this.canvas = canvas
|
|
this.width = width
|
|
this.height = height
|
|
this.offset = offset
|
|
this.margin = margin
|
|
this.data = data
|
|
|
|
this.top = offset + margin.top
|
|
this.bottom = offset + height - margin.bottom
|
|
|
|
this.yRange = d3.scaleLinear()
|
|
.domain(yrange)
|
|
.range([this.bottom, this.top])
|
|
this.xRange = d3.scaleTime()
|
|
.domain([data.beginTime, data.endTime])
|
|
.range([margin.left, width - margin.right])
|
|
}
|
|
|
|
draw_yAxisLine() {
|
|
this.canvas.append('line').attr('class', 'axis')
|
|
.attr('x1', this.margin.left).attr('y1', this.top)
|
|
.attr('x2', this.margin.left).attr('y2', this.bottom)
|
|
}
|
|
|
|
draw_xAxisLineTop() {
|
|
this.canvas.append('line').attr('class', 'axis')
|
|
.attr('x1', this.margin.left).attr('y1', this.top)
|
|
.attr('x2', this.width - this.margin.right).attr('y2', this.top)
|
|
}
|
|
|
|
draw_xAxisLineBottom() {
|
|
this.canvas.append('line').attr('class', 'axis')
|
|
.attr('x1', this.margin.left).attr('y1', this.bottom)
|
|
.attr('x2', this.width - this.margin.right).attr('y2', this.bottom)
|
|
}
|
|
|
|
draw_yAxisTitle(title) {
|
|
const middle = this.top + ((this.height) / 2.0)
|
|
const x = 15
|
|
this.canvas.append('text').attr('class', 'axis-title')
|
|
.attr('text-anchor', 'middle').attr('dominant-baseline', 'middle')
|
|
.attr('transform', 'rotate(-90,' + x + ',' + middle + ')')
|
|
.attr('x', x).attr('y', middle)
|
|
.text(title)
|
|
}
|
|
|
|
set_yAxisTic(value, label) {
|
|
this.canvas.append('line').attr('class', 'axis')
|
|
.attr('x1', this.margin.left - 4).attr('y1', this.yRange(value))
|
|
.attr('x2', this.margin.left).attr('y2', this.yRange(value))
|
|
this.canvas.append('text').attr('class', 'axis-label')
|
|
.attr('text-anchor', 'end').attr('dominant-baseline', 'middle')
|
|
.attr('x', this.margin.left - 6).attr('y', this.yRange(value))
|
|
.text(label)
|
|
}
|
|
|
|
set_xAxisTic(value, label, anchor) {
|
|
const x = this.xRange(value)
|
|
this.canvas.append('line').attr('class', 'axis')
|
|
.attr('x1', x).attr('y1', this.bottom)
|
|
.attr('x2', x).attr('y2', this.bottom + 2)
|
|
this.canvas.append('text').attr('class', 'axis-label')
|
|
.attr('text-anchor', anchor).attr('dominant-baseline', 'hanging')
|
|
.attr('x', x).attr('y', this.bottom + 1)
|
|
.text(label)
|
|
}
|
|
|
|
draw_yGrid(value, style) {
|
|
this.canvas.append('line').attr('class', style)
|
|
.attr('x1', this.xRange(value)).attr('y1', this.offset)
|
|
.attr('x2', this.xRange(value)).attr('y2', this.height - this.margin.bottom)
|
|
}
|
|
|
|
draw_xGrid(value, style) {
|
|
this.canvas.append('line').attr('class', style)
|
|
.attr('x1', this.margin.left).attr('y1', this.yRange(value))
|
|
.attr('x2', this.width).attr('y2', this.yRange(value))
|
|
}
|
|
|
|
draw_box(x1, x2, style) {
|
|
const x = this.xRange(x1)
|
|
const width = this.xRange(x2) - x
|
|
this.canvas.append('rect').attr('class', style)
|
|
.attr('x', x).attr('y', this.offset)
|
|
.attr('width', width)
|
|
.attr('height', this.height - this.offset - this.margin.bottom)
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Draw days to show time
|
|
*/
|
|
export function render_calendar(canvas, width, height, offset, data) {
|
|
|
|
const margin = {
|
|
top: 0,
|
|
left: leftMargin,
|
|
bottom: 0,
|
|
right: 0
|
|
}
|
|
|
|
const xRange = d3.scaleTime()
|
|
.domain([data.beginTime, data.endTime])
|
|
.range([margin.left, width])
|
|
|
|
const calendar = canvas.append('g').attr('id', 'calendar')
|
|
const weekdays = calendar.append('g').attr('id', 'weekdays')
|
|
const dates = calendar.append('g').attr('id', 'dates')
|
|
|
|
let noons = []
|
|
let days = []
|
|
const year = data.beginTime.getFullYear()
|
|
const month = data.beginTime.getMonth()
|
|
|
|
const n_days = Math.ceil((data.endTime- data.beginTime) / (1000 * 60 * 60 * 24))
|
|
for (var day = data.beginTime.getDate(); day <= (data.beginTime.getDate() + n_days); day++) {
|
|
const dayBegin = new Date(year, month, day, 0, 1, 0)
|
|
const noon = new Date(year, month, day, 12, 0, 0)
|
|
noons.push(noon)
|
|
days.push(dayBegin)
|
|
}
|
|
|
|
weekdays.selectAll('text').data(noons).enter()
|
|
.append('text')
|
|
.attr('text-anchor', 'middle').attr('dominant-baseline', 'alphabetic')
|
|
.attr('x', function(d) {return xRange(d)})
|
|
.attr('y', height - 5)
|
|
.text(function (d) {
|
|
return d.toLocaleDateString('en-US', {weekday: 'long'})
|
|
})
|
|
|
|
dates.selectAll('text').data(days).enter()
|
|
.append('text')
|
|
.attr('text-anchor', 'begin').attr('dominant-baseline', 'hanging')
|
|
.attr('x', function(d) {return xRange(d) + 3})
|
|
.attr('y', 0)
|
|
.text(function (d) {
|
|
return d.toLocaleDateString('en-US', {day: 'numeric'})
|
|
})
|
|
dates.selectAll('line').data(days).enter()
|
|
.append('line').attr('class', 'axis')
|
|
.attr('x1', function(d) {return xRange(d)}).attr('y1', 0)
|
|
.attr('x2', function(d) {return xRange(d)}).attr('y2', height)
|
|
|
|
calendar.append('line').attr('class', 'axis')
|
|
.attr('x1', margin.left).attr('y1', height)
|
|
.attr('x2', width - margin.right).attr('y2', height)
|
|
}
|
|
|
|
|
|
/**
|
|
* Draw sky brightness bars
|
|
*/
|
|
export function render_nights(canvas, width, height, offset, data) {
|
|
const margin = {
|
|
top: 0,
|
|
left: leftMargin,
|
|
bottom: 0,
|
|
right: 0
|
|
}
|
|
|
|
const xRange = d3.scaleTime()
|
|
.domain([data.beginTime, data.endTime])
|
|
.range([margin.left, width])
|
|
|
|
const nights = canvas.append('g').attr('id', 'nights')
|
|
|
|
const draw_box = function (x1, x2, style) {
|
|
const x = xRange(x1)
|
|
const width = xRange(x2) - x
|
|
nights.append('rect').attr('class', style)
|
|
.attr('x', x).attr('y', offset)
|
|
.attr('width', width)
|
|
.attr('height', height - margin.bottom)
|
|
}
|
|
|
|
const year = data.beginTime.getFullYear()
|
|
const month = data.beginTime.getMonth()
|
|
|
|
const n_days = Math.ceil((data.endTime- data.beginTime) / (1000 * 60 * 60 * 24))
|
|
for (var day = data.beginTime.getDate(); day <= (data.beginTime.getDate() + n_days); day++) {
|
|
const midnight = new Date(year, month, day, 0, 0, 0)
|
|
const sunEphem = SunCalc.getTimes(new Date(year, month, day, 12, 0, 0), lat, lon)
|
|
draw_box(midnight, sunEphem.nauticalDawn, 'night')
|
|
draw_box(sunEphem.nauticalDawn, sunEphem.sunrise, 'twilight')
|
|
draw_box(sunEphem.sunset, sunEphem.nauticalDusk, 'twilight')
|
|
draw_box(sunEphem.nauticalDusk, new Date(year, month, day, 24, 0, 0), 'night')
|
|
nights.append('line').attr('class', 'midnight')
|
|
.attr('x1', xRange(midnight)).attr('y1', offset)
|
|
.attr('x2', xRange(midnight)).attr('y2', height + offset)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sun and moon chart
|
|
*/
|
|
export function render_astronomy(canvas, width, height, offset, data) {
|
|
const margin = {
|
|
top: 0,
|
|
left: leftMargin,
|
|
bottom: 12,
|
|
right: 0
|
|
}
|
|
const yrange = [0, 80 * (Math.PI/180)]
|
|
const chart = new Chart(canvas, width, height, offset, margin, data, yrange)
|
|
|
|
const x = chart.xRange(data.beginTime)
|
|
const box_width = chart.xRange(data.endTime) - x
|
|
canvas.append('rect').attr('class', 'blank')
|
|
.attr('x', x).attr('y', offset + height - margin.bottom)
|
|
.attr('width', box_width)
|
|
.attr('height', margin.bottom)
|
|
|
|
let sunData = []
|
|
const year = data.beginTime.getFullYear()
|
|
const month = data.beginTime.getMonth()
|
|
|
|
const n_days = Math.ceil((data.endTime- data.beginTime) / (1000 * 60 * 60 * 24))
|
|
for (var day = data.beginTime.getDate(); day <= (data.beginTime.getDate() + n_days); day++) {
|
|
const midnight = new Date(year, month, day, 0, 0, 0)
|
|
|
|
// Solar Ephemeris
|
|
let todaysSun = []
|
|
const sunEphem = SunCalc.getTimes(new Date(year, month, day, 2, 0, 0), lat, lon)
|
|
todaysSun.push({
|
|
'date': sunEphem.sunrise,
|
|
'alt': 0
|
|
})
|
|
chart.set_xAxisTic(
|
|
sunEphem.sunrise,
|
|
sunEphem.sunrise.toLocaleTimeString('en-US', {hour12: true, hour: 'numeric', minute: '2-digit'}).replace('AM',''),
|
|
'end'
|
|
)
|
|
chart.set_xAxisTic(
|
|
sunEphem.sunset,
|
|
sunEphem.sunset.toLocaleTimeString('en-US', {hour12: true, hour: 'numeric', minute: '2-digit'}).replace('PM',''),
|
|
'begining'
|
|
)
|
|
for (var hour = 0; hour < 24; hour++) {
|
|
const datetime = new Date(year, month, day, hour, 0, 0)
|
|
const sun = SunCalc.getPosition(datetime, lat, lon)
|
|
|
|
if (sun.altitude > 0) {
|
|
todaysSun.push({
|
|
'date': datetime,
|
|
'alt': sun.altitude
|
|
})
|
|
}
|
|
}
|
|
todaysSun.push({
|
|
'date': sunEphem.sunset,
|
|
'alt': 0
|
|
})
|
|
sunData.push(todaysSun)
|
|
}
|
|
|
|
const line = d3.line()
|
|
.x(function(d) { return chart.xRange(d.date) })
|
|
.y(function(d) { return chart.yRange(d.alt) })
|
|
.curve(d3.curveBasis)
|
|
|
|
const sunView = canvas.append('g').attr('id', 'sun')
|
|
sunView.selectAll('path').data(sunData).enter()
|
|
.append("path")
|
|
.attr('class', 'sun')
|
|
.attr('d', line)
|
|
|
|
chart.set_yAxisTic( 0, '0°')
|
|
chart.set_yAxisTic(26.6 * (Math.PI/180), '27°')
|
|
chart.draw_xGrid( 26.6 * (Math.PI/180), 'guide')
|
|
chart.set_yAxisTic(75 * (Math.PI/180), '75°')
|
|
chart.draw_xAxisLineBottom()
|
|
}
|
|
|
|
|
|
/**
|
|
* Temperature chart
|
|
*/
|
|
export function render_temperature(canvas, width, height, offset, data) {
|
|
const margin = {
|
|
top: 0,
|
|
left: leftMargin,
|
|
bottom: 5,
|
|
right: 0
|
|
}
|
|
const yrange = [-15, 45]
|
|
const chart = new Chart(canvas, width, height, offset, margin, data, yrange)
|
|
|
|
const line = d3.line()
|
|
.x(function(d) { return chart.xRange(d.date) })
|
|
.y(function(d) { return chart.yRange(d.temp) })
|
|
.curve(d3.curveBasis)
|
|
|
|
chart.draw_yAxisTitle('Temperature')
|
|
chart.set_yAxisTic(-10, '-10 °C')
|
|
chart.draw_xGrid( -10, 'guide')
|
|
chart.set_yAxisTic( 0, '0 °C')
|
|
chart.draw_xGrid( 0, 'zero')
|
|
chart.set_yAxisTic( 12, '12 °C')
|
|
chart.draw_xGrid( 12, 'guide')
|
|
chart.set_yAxisTic( 22, '22 °C')
|
|
chart.draw_xGrid( 22, 'guide')
|
|
chart.set_yAxisTic( 30, '30 °C')
|
|
chart.draw_xGrid( 30, 'hot')
|
|
chart.set_yAxisTic( 40, '40 °C')
|
|
chart.draw_xGrid( 40, 'guide')
|
|
|
|
canvas.append('path')
|
|
.data([data.temperature])
|
|
.attr('class', 'data')
|
|
.attr('d', line)
|
|
|
|
//chart.draw_yAxisLine()
|
|
//chart.draw_xAxisLineTop()
|
|
chart.draw_xAxisLineBottom()
|
|
}
|
|
|
|
export function render_clouds(canvas, width, height, offset, data) {
|
|
const margin = {
|
|
top: 0,
|
|
left: leftMargin,
|
|
bottom: 10,
|
|
right: 0
|
|
}
|
|
const yrange = [0, 100]
|
|
const chart = new Chart(canvas, width, height, offset, margin, data, yrange)
|
|
|
|
const cloudLine = d3.line()
|
|
.x(function(d) { return chart.xRange(d.date) })
|
|
.y(function(d) { return chart.yRange(d.coverage) })
|
|
.curve(d3.curveBasis)
|
|
|
|
const precipLine = d3.line()
|
|
.x(function(d) { return chart.xRange(d.date) })
|
|
.y(function(d) { return chart.yRange(d.probability) })
|
|
|
|
chart.set_yAxisTic( 0, '0%')
|
|
chart.set_yAxisTic( 50, '50%')
|
|
chart.draw_xGrid( 50, 'guide')
|
|
chart.set_yAxisTic(100, '100%')
|
|
chart.draw_xGrid( 100, 'guide')
|
|
|
|
canvas.append('path')
|
|
.data([data.clouds])
|
|
.attr('class', 'clouds')
|
|
.attr('d', cloudLine)
|
|
canvas.append('path')
|
|
.data([data.precip])
|
|
.attr('class', 'precip')
|
|
.attr('d', precipLine)
|
|
|
|
chart.draw_xAxisLineBottom()
|
|
}
|