diff --git a/src/main/java/ru/krotarnya/diasync/service/DemoCarbsDataGenerator.java b/src/main/java/ru/krotarnya/diasync/service/DemoCarbsDataGenerator.java new file mode 100644 index 0000000..7ac583e --- /dev/null +++ b/src/main/java/ru/krotarnya/diasync/service/DemoCarbsDataGenerator.java @@ -0,0 +1,35 @@ +package ru.krotarnya.diasync.service; + +import java.time.Duration; +import java.time.Instant; +import org.springframework.stereotype.Component; +import ru.krotarnya.diasync.model.Carbs; +import ru.krotarnya.diasync.model.DataPoint; + +@Component +public class DemoCarbsDataGenerator extends DemoDataGenerator { + private static final Duration BASE_PERIOD = Duration.ofMinutes(30); + private static final Duration PERIOD_STD_DEVIATION = Duration.ofSeconds(10); + private static final double MAX_GRAMS = 20.0; + + @Override + protected Duration basePeriod() { + return BASE_PERIOD; + } + + @Override + protected Duration periodStdDev() { + return PERIOD_STD_DEVIATION; + } + + @Override + protected DataPoint generate(Instant timestamp) { + return DataPoint.builder() + .userId(userId()) + .timestamp(timestamp) + .carbs(Carbs.builder() + .grams(random().nextDouble() * MAX_GRAMS) + .build()) + .build(); + } +} diff --git a/src/main/resources/static/kiosk/kiosk.js b/src/main/resources/static/kiosk/kiosk.js index 0d3c69f..efce1ab 100644 --- a/src/main/resources/static/kiosk/kiosk.js +++ b/src/main/resources/static/kiosk/kiosk.js @@ -33,10 +33,33 @@ const PERIOD_MS = (() => { })(); const ctx = document.getElementById('bg').getContext('2d'); -Chart.register(Chart.registry.getPlugin('annotation'), centerTextPlugin); +const carbsTextPlugin = { + id: 'carbsText', + afterDraw(chart) { + const {ctx, chartArea, scales} = chart; + ctx.save(); + ctx.fillStyle = 'yellow'; + ctx.strokeStyle = '#000'; + ctx.lineWidth = 3; + ctx.textAlign = 'center'; + ctx.textBaseline = 'bottom'; + ctx.font = 'bold 24px Audiowide, sans-serif'; + visibleCarbs.forEach(pt => { + const x = scales.x.getPixelForValue(new Date(pt.x)); + const g = Math.round(pt.grams); + ctx.strokeText(`${g}g`, x, chartArea.bottom - 4); + ctx.fillText(`${g}g`, x, chartArea.bottom - 4); + }); + ctx.restore(); + } +}; + +Chart.register(Chart.registry.getPlugin('annotation'), centerTextPlugin, carbsTextPlugin); const sensorPoints = []; const manualPoints = []; +const carbsPoints = []; +let visibleCarbs = []; let lastTimestamp = Date.now(); function applyCalib(mgdl, cal) { @@ -77,6 +100,13 @@ function initChart() { backgroundColor: 'red', borderColor: 'white', borderWidth: radius / 2 + }, + { + label: 'Carbs', + data: [], + pointRadius: 0, + pointHitRadius: 10, + yAxisID: 'carbsAxis' } ] }, @@ -87,7 +117,8 @@ function initChart() { interaction: {mode: 'nearest', intersect: true}, scales: { x: {display: false, type: 'time', min: () => Date.now() - PERIOD_MS, max: () => Date.now()}, - y: {display: false} + y: {display: false}, + carbsAxis: {display: false, min: 0, max: 1} }, plugins: { legend: {display: false}, @@ -105,6 +136,8 @@ function initChart() { `Slope: ${d.calibration?.slope?.toFixed(3) || '-'}`, `Intercept: ${d.calibration?.intercept?.toFixed(3) || '-'}` ]; + } else if (d.grams !== undefined) { + return [`Carbs: ${d.grams.toFixed(1)}g`]; } else { return [`Manual: ${UNIT === 'mgdl' ? Math.round(d.y) : (d.y / 18).toFixed(1)} ${UNIT}`]; } @@ -136,10 +169,12 @@ function updateChart() { const recentSensor = sensorPoints.filter(p => new Date(p.x).getTime() >= now - PERIOD_MS); const recentManual = manualPoints.filter(p => new Date(p.x).getTime() >= now - PERIOD_MS); + visibleCarbs = carbsPoints.filter(p => new Date(p.x).getTime() >= now - PERIOD_MS); chart.data.datasets[0].data = recentSensor; chart.data.datasets[0].backgroundColor = recentSensor.map(p => p.backgroundColor); chart.data.datasets[1].data = recentManual; + chart.data.datasets[2].data = visibleCarbs.map(p => ({x: p.x, y: 0, grams: p.grams})); if (recentSensor.length || recentManual.length) { const allY = [...recentSensor.map(p => p.mgdl), ...recentManual.map(p => p.y)]; @@ -164,6 +199,7 @@ function loadInitial() { timestamp sensorGlucose{mgdl sensorId calibration{slope intercept}} manualGlucose{mgdl} + carbs{grams} }}`; fetch('/graphql', { method: 'POST', @@ -191,12 +227,16 @@ function loadInitial() { if (mg && mg.mgdl != null) { manualPoints.push({x: ts, y: mg.mgdl}); } + if (pt.carbs && pt.carbs.grams != null) { + carbsPoints.push({x: ts, grams: pt.carbs.grams}); + } }); const last = pts.at(-1)?.sensorGlucose; if (last?.mgdl != null) { updateDisplay(last.mgdl, last.calibration); } + updateChart(); }) .catch(err => console.error('Failed to load data:', err)); } @@ -211,6 +251,7 @@ function startSubscription() { timestamp sensorGlucose{mgdl sensorId calibration{slope intercept}} manualGlucose{mgdl} + carbs{grams} }}`; client.subscribe({query: subQ}, { next({data}) { @@ -232,6 +273,11 @@ function startSubscription() { if (mg && mg.mgdl != null) { manualPoints.push({x: ts, y: mg.mgdl}); } + const cb = data.onDataPointAdded.carbs; + if (cb && cb.grams != null) { + carbsPoints.push({x: ts, grams: cb.grams}); + } + updateChart(); }, error: () => setTimeout(startSubscription, 3000), complete: () => setTimeout(startSubscription, 3000)