Skip to content

Commit

Permalink
Merge pull request #14 from CropWatchDevelopment/develop
Browse files Browse the repository at this point in the history
updated chart to highcharts
  • Loading branch information
CropWatchDevelopment authored Apr 22, 2024
2 parents de747e2 + 9195726 commit 772a447
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 121 deletions.
23 changes: 23 additions & 0 deletions src/lib/actions/highcharts.action.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Highcharts from 'highcharts';

export default (node: HTMLElement, config: Highcharts.Options) => {
const redraw = true;
const oneToOne = true;
const chart = Highcharts.chart(node, config);

const resizeObserver = new ResizeObserver(() => {
chart.reflow();
});

resizeObserver.observe(node);

return {
update(config: Highcharts.Options) {
chart.update(config, redraw, oneToOne);
},
destroy() {
resizeObserver.disconnect();
chart.destroy();
}
};
};
97 changes: 97 additions & 0 deletions src/lib/charts/highcharts/timeseries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { browser } from '$app/environment';
import Highcharts from 'highcharts';
import moment from 'moment';

export const HighChartsTimeSeriesChart = (data: any[], name: string = '') => {
return {
chart: {
zoomType: 'x'
},
title: {
text: name,
align: 'left'
},
subtitle: {
text: browser && document.ontouchstart === undefined ?
'Click and drag in the plot area to zoom in' : 'Pinch the chart to zoom in',
align: 'left'
},
xAxis: {
type: 'datetime',
title: {
enabled: true,
text: 'Month/Day'
},
labels: {
formatter: function(): any {
return moment(this.value).format('MMM-DD').toString();
},
}
},
yAxis: [{ // Primary yAxis
labels: {
format: '{value}°C',
style: {
color: 'red'
}
},
title: {
text: 'Temperature',
style: {
color: 'red'
}
}
}, { // Secondary yAxis
title: {
text: 'Humidity',
style: {
color: 'blue'
}
},
labels: {
format: '{value} %',
style: {
color: 'blue'
}
},
opposite: true
}],
legend: {
enabled: false
},
tooltip: {
borderColor: '#2c3e50',
shared: true,
formatter: function () {
return '<b>Time: ' + moment(this.x).format('hh:mm a').toString() + '</b><br/>' + name + ': ' + this.y + '°C';
}
},
plotOptions: {
area: {
fillColor: {
linearGradient: {
x1: 0,
y1: 0,
x2: 0,
y2: 1
},
stops: [
[0, Highcharts.getOptions().colors[0]],
[1, Highcharts.color(Highcharts.getOptions().colors[0]).setOpacity(0).get('rgba')]
]
},
marker: {
radius: 2
},
lineWidth: 1,
states: {
hover: {
lineWidth: 1
}
},
threshold: null
}
},
series: data,
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export async function load({ params }) {
.select('*')
.eq('dev_eui', params.sensor_eui)
.gte('created_at', moment().subtract(1, 'days').toISOString())
.limit(30)
.order('created_at', { ascending: false })
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import CWStatCard from '$lib/components/stat-card/CWStatCard.svelte';
import { mdiGauge, mdiMoleculeCo2, mdiThermometer, mdiWater } from '@mdi/js';
import { scaleBand, scaleTime } from 'd3-scale';
import Highcharts from '$lib/actions/highcharts.action';
import { HighChartsTimeSeriesChart } from '$lib/charts/highcharts/timeseries';
import {
Axis,
Bars,
Expand All @@ -17,54 +19,85 @@
} from 'layerchart';
import { Avatar, Card, Header, PeriodType, formatDate, Icon, RangeSlider } from 'svelte-ux';
import moment from 'moment';
import { onMount } from 'svelte';
import { browser } from '$app/environment';
export let sensor;
const tempData = sensor.sensor.data.map((d: any) => [
new Date(d.created_at).valueOf(),
d.temperatureC
]);
const humidityData = sensor.sensor.data.map((d: any) => [
new Date(d.created_at).valueOf(),
d.humidity
]);
let config = HighChartsTimeSeriesChart(
[
{
type: 'line',
yAxis: 0,
name: 'Temperature',
color: 'red',
data: tempData
},
{
type: 'line',
yAxis: 1,
name: 'Humidity',
color: 'blue',
data: humidityData
}
],
'Temperature'
);
// Assuming sensor.sensor.data is sorted by created_at in ascending order
// If not, you may need to sort it first
let earliestData = sensor.sensor.data.at(-1);
let latestData = sensor.sensor.data.at(0);
// If not, you may need to sort it first
let earliestData = sensor.sensor.data.at(-1);
let latestData = sensor.sensor.data.at(0);
// Convert earliest and latest data points to moment objects for consistency
let earliestTime = moment(earliestData.created_at);
let latestTime = moment(latestData.created_at);
let earliestTime = moment(earliestData.created_at);
let latestTime = moment(latestData.created_at);
// Calculate the maximum hours difference between the earliest and latest data points
let maxHours = latestTime.diff(earliestTime, 'minutes');
let maxHours = latestTime.diff(earliestTime, 'minutes');
// Use the maximum hours as the range to ensure all data points are considered
// The range is set from 0 to maxHours to cover the full range of your data
// The range is set from 0 to maxHours to cover the full range of your data
let value = [0, maxHours];
$: TempData = sensor.sensor.data.filter((f) => {
const createdAt = moment(f.created_at);
return isWithinInterval(createdAt.toDate(), {
return isWithinInterval(createdAt.toDate(), {
start: moment().subtract(value[0], 'minutes').toDate(), // More hours ago, based on the maxHours
end: moment().subtract(value[1], 'minutes').toDate() // Fewer hours ago, more recent
});
});
end: moment().subtract(value[1], 'minutes').toDate() // Fewer hours ago, more recent
});
});
</script>

<div class="grid grid-cols-1 mt-10 gap-4 mb-2">
<CWStatCard
icon={mdiMoleculeCo2}
title="{$_('Detail.Air Temperature')}"
title={$_('Detail.Air Temperature')}
value={latestData.temperatureC}
optimal={24.33}
notation="°c"
counterStartTime={subSeconds(sensor.sensor.data?.at(0).created_at, 0)}
/>
<CWStatCard
icon={mdiThermometer}
title="{$_('Detail.Relative Humidity')}"
title={$_('Detail.Relative Humidity')}
value={latestData.humidity}
optimal={24.33}
notation="%"
counterStartTime={subSeconds(sensor.sensor.data?.at(0).created_at, 0)}
/>
<CWStatCard
icon={mdiWater}
title="{$_('Detail.VPD')}"
title={$_('Detail.VPD')}
value={latestData.vpd}
optimal={25}
notation="kPa"
Expand All @@ -73,7 +106,7 @@
{#if latestData.dew !== null}
<CWStatCard
icon={mdiGauge}
title="{$_('Detail.Dew Point')}"
title={$_('Detail.Dew Point')}
value={latestData.dewPointC}
optimal={25}
notation="°c"
Expand All @@ -83,116 +116,18 @@
</div>

<Card>
<Header title="{$_('Detail.Temperature History')}" subheading="{$_('Detail.Air Temperature Over Time')}" slot="header">
<Header
title={$_('Detail.Temperature History')}
subheading={$_('Detail.Air Temperature Over Time')}
slot="header"
>
<div slot="avatar">
<Avatar class="bg-primary text-primary-content font-bold">
<Icon data={mdiMoleculeCo2} />
</Avatar>
</div>
</Header>
<div class="h-[300px] p-4 border rounded">
<Chart
data={TempData.map((d) => {
return {
date: new Date(d.created_at),
value: d.temperatureC
};
})}
x="date"
xScale={scaleBand().padding(0.4)}
y="value"
yDomain={[0, null]}
yNice={4}
padding={{ left: 16, bottom: 24 }}
>
<Svg>
<Axis placement="left" grid rule />
<Axis
placement="bottom"
format={(d) => formatDate(d, PeriodType.DayTime, { variant: 'short' })}
rule
/>
<Bars radius={4} strokeWidth={1} class="fill-primary" />
</Svg>
</Chart>
<RangeSlider bind:value min={0} max={maxHours}/>
<div class="chart" use:Highcharts={config} />
</div>
</Card>

<div class="grid grid-cols-1 md:grid-cols-2 gap-2 mt-2">
<Card>
<Header title="{$_('Detail.Relative Humidity History')}" subheading="{$_('Detail.Humidity Over Time')}" slot="header">
<div slot="avatar">
<Avatar class="bg-primary text-primary-content font-bold">
<Icon data={mdiThermometer} />
</Avatar>
</div>
</Header>
<div class="h-[300px] p-4 border rounded">
<Chart
data={TempData.map((d) => {
return {
date: new Date(d.created_at),
value: d.humidity
};
})}
x="date"
xScale={scaleTime()}
y="value"
yDomain={[0, null]}
yNice
padding={{ left: 16, bottom: 24 }}
>
<Svg>
<Axis placement="left" grid rule />
<Axis
placement="bottom"
format={(d) => formatDate(d, PeriodType.Day, { variant: 'short' })}
rule
/>
<Spline class="stroke-2 stroke-primary" />
<Labels format="integer" />
</Svg>
</Chart>
<RangeSlider bind:value min={0} max={maxHours}/>
</div>
</Card>

<Card>
<Header title="{$_('Detail.Dew Point History')}" subheading="{$_('Detail.Dew Point Over Time')}" slot="header">
<div slot="avatar">
<Avatar class="bg-primary text-primary-content font-bold">
<Icon data={mdiWater} />
</Avatar>
</div>
</Header>
<div class="h-[300px] p-4 border rounded">
<Chart
data={TempData.map((d) => {
return {
date: new Date(d.created_at),
value: d.dewPointC
};
})}
x="date"
xScale={scaleTime()}
y="value"
yDomain={[0, null]}
yNice
padding={{ left: 16, bottom: 24 }}
>
<Svg>
<Axis placement="left" grid rule />
<Axis
placement="bottom"
format={(d) => formatDate(d, PeriodType.Day, { variant: 'short' })}
rule
/>
<Spline class="stroke-2 stroke-primary" />
<Labels format="integer" />
</Svg>
</Chart>
<RangeSlider bind:value min={0} max={maxHours}/>
</div>
</Card>
</div>

0 comments on commit 772a447

Please sign in to comment.