Graph update and little noe ui tweaks

This commit is contained in:
Max committed 2023-07-23 23:13:28 +00:00
1 parent d3acd62688
commit c61f0c0198
12 files changed
+1341 -356

No files matched your search

+201 -222
View File
@@ -1,4 +1,4 @@
<style>
<style scoped>
div.no-padding {
padding: 10px 0 40px !important;
box-sizing: border-box;
@@ -233,16 +233,26 @@
}
}
.an-graph {
background: #fefefe82;
.days-ago-display {
font-size: 0.7em;
}
.input-grid {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
justify-content: flex-start;
}
.bottom.aligned.row {
flex: 1 1 auto;
}
</style>
<template>
<div class="squire-box no-padding" ref="scrollcontainer">
<!-- intro modal -->
<modal v-if="!noteId" :click-out-close="false">
<div class="ui segment">
<div class="ui center aligned middle aligned grid">
@@ -273,10 +283,10 @@
</div>
<!-- section 1 entry -->
<section class="swipe-section">
<section class="swipe-section" v-if="true">
<!-- <delete-button v-if="noteId > 0" class="ui small button" :note-id="noteId" /> -->
<div class="ui small centered dividing header">
Metric Tracking Bet
Metric Tracking
<span class="sub header"><i class="small lock icon"></i>All data Encrypted. Only accessible by you.</span>
</div>
@@ -314,10 +324,18 @@
<!-- data input -->
<div class="ui basic segment">
<div class="ui very compact grid">
<div class="ui twelve wide middle aligned column">
<div class="four wide column"></div>
<div class="ui eight wide middle aligned center aligned column">
<div class="ui header">
Entry for {{ calendar.monthName }},
<span :class="{'loading-day-title':loadingDay}">{{ calendar.today }}</span>
<span :class="{'loading-day-title':loadingDay}">
{{ calendar.monthName }}
{{ calendar.today }}, {{ calendar.dayName }}
<span class="days-ago-display">
<!-- <span v-if="calendar.daysAgo == 0">Today</span> -->
<span v-if="calendar.daysAgo == 1"><br>{{ calendar.daysAgo }} day ago</span>
<span v-if="calendar.daysAgo > 1"><br>{{ calendar.daysAgo }} days ago</span>
</span>
</span>
</div>
</div>
<div class="ui four wide right aligned middle aligned column">
@@ -339,77 +357,89 @@
<div class="ui form" :class="{'loading-day':loadingDay}">
<draggable v-model="fields" class="ui compact grid" ghost-class="ghost" @end="onDragEnd" handle=".draggable">
<div v-for="field in fields" :key="field"
:class="userFields[field]?.width ? userFields[field]?.width+' wide column':'eight wide column'">
<!-- field label display -->
<div class="ui very compact grid">
<div class="ui sixteen wide center aligned column">
<i :class="`${getFieldColor(field)} ${getFieldIcon(field)} icon`"></i>
<b>{{ userFields[field]?.label }}</b>
<span>{{ field }}</span>
</div>
</div>
:class="userFields[field]?.width ? userFields[field]?.width+' wide stretched column':'eight wide stretched column'">
<!-- float -->
<div v-if="userFields[field]?.type == 'float'" class="ui fluid input">
<input type="text" :placeholder="userFields[field]?.label" v-on:keyup="e => saveField(field, e.target.value)" :value="openDay[field]">
</div>
<!-- range -->
<div v-if="userFields[field]?.type == 'shortRange'">
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui button">1</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui button">2</div>
<div :class="{green:(openDay[field] == 3)}" v-on:click="saveField(field, 3)" class="ui button">3</div>
<div :class="{green:(openDay[field] == 4)}" v-on:click="saveField(field, 4)" class="ui button">4</div>
<div :class="{green:(openDay[field] == 5)}" v-on:click="saveField(field, 5)" class="ui button">5</div>
</div>
<!-- text area -->
<div v-if="userFields[field]?.type == 'text'">
<textarea rows="3" v-on:keyup="e => saveField(field, e.target.value, 2000)" :value="openDay[field]">
</textarea>
</div>
<!-- boolean -->
<div v-if="userFields[field]?.type == 'boolean'">
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui button">Yes</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui button">No</div>
</div>
<div v-if="['sex','period','mucus','pms'].includes(userFields[field]?.type)">
<div class="option-buttons">
<div :class="{green:(openDay[field] == key)}" v-on:click="saveField(field, key)" class="ui compact button" v-for="(item,key) in fieldTypes[userFields[field]?.type].split(',')">{{ item }}</div>
</div>
</div>
<div v-if="userFields[field]?.type == 'custom'">
<div class="option-buttons">
<div
v-for="(item, value) in userFields[field]?.customOptions.split(',')"
v-on:click="saveField(field, value)"
:class="{green:(openDay[field] == value) && openDay[field] !== ''}"
class="ui compact button">
{{ item.trim() }}
<div class="ui very compact grid input-grid">
<!-- field label display -->
<div class="row">
<div class="ui sixteen wide column">
<b><i :class="`${getFieldColor(field)} ${getFieldIcon(field)} icon`"></i>
{{ userFields[field]?.label }}</b>
<!-- <span>{{ field }}</span> -->
</div>
</div>
</div>
<!-- input display -->
<div class="row">
<div class="sixteen wide column">
<div class="ui very compact grid">
<div class="ui six wide column">
<span v-on:click="editField(field)">
<i class="clickable grey edit outline icon"></i>
</span>
<!-- float -->
<div v-if="userFields[field]?.type == 'float'" class="ui fluid input">
<input type="text" :placeholder="userFields[field]?.label" v-on:keyup="e => saveField(field, e.target.value)" :value="openDay[field]">
</div>
<!-- range -->
<div v-if="userFields[field]?.type == 'shortRange'">
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui button">1</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui button">2</div>
<div :class="{green:(openDay[field] == 3)}" v-on:click="saveField(field, 3)" class="ui button">3</div>
<div :class="{green:(openDay[field] == 4)}" v-on:click="saveField(field, 4)" class="ui button">4</div>
<div :class="{green:(openDay[field] == 5)}" v-on:click="saveField(field, 5)" class="ui button">5</div>
</div>
<!-- text area -->
<div v-if="userFields[field]?.type == 'text'">
<textarea rows="3" v-on:keyup="e => saveField(field, e.target.value, 2000)" :value="openDay[field]">
</textarea>
</div>
<!-- boolean -->
<div v-if="userFields[field]?.type == 'boolean'">
<div class="option-buttons">
<div :class="{green:(openDay[field] == 1)}" v-on:click="saveField(field, 1)" class="ui compact button">Yes</div>
<div :class="{green:(openDay[field] == 2)}" v-on:click="saveField(field, 2)" class="ui compact button">No</div>
</div>
</div>
<div v-if="['sex','period','mucus','pms'].includes(userFields[field]?.type)">
<div class="option-buttons">
<div :class="{green:(openDay[field] == key)}" v-on:click="saveField(field, key)" class="ui compact button" v-for="(item,key) in fieldTypesDef[userFields[field]?.type].split(',')">{{ item }}</div>
</div>
</div>
<div v-if="userFields[field]?.type == 'custom'">
<div class="option-buttons">
<div
v-for="(item, value) in userFields[field]?.customOptions.split(',')"
v-on:click="saveField(field, value)"
:class="{green:(openDay[field] == value) && openDay[field] !== ''}"
class="ui compact button">
{{ item.trim() }}
</div>
</div>
</div>
</div>
</div>
<div class="four wide center aligned column draggable">
<span>
<i class="grey grip lines icon"></i>
</span>
</div>
<div class="ui six wide right aligned column">
<span v-if="openDay[field] !== ''" v-on:click="saveField(field, '')">
<i class="grey clickable reply icon"></i>
</span>
<div class="bottom aligned row">
<div class="ui six wide column">
<span v-on:click="editField(field)">
<i class="clickable grey edit outline icon"></i>
</span>
</div>
<div class="four wide center aligned column draggable">
<span>
<i class="grey grip lines icon"></i>
</span>
</div>
<div class="ui six wide right aligned column">
<span v-if="openDay[field] !== ''" v-on:click="saveField(field, '')">
<i class="grey clickable reply icon"></i>
</span>
</div>
</div>
</div>
</div>
@@ -420,7 +450,7 @@
</section>
<!-- section 2 analysis -->
<section class="swipe-section">
<section class="swipe-section" v-if="!loading">
<div class="ui small centered dividing header">
Review Data
@@ -445,7 +475,7 @@
</div>
<!-- calendar -->
<div class="calendar">
<div class="calendar" v-if="false">
<div v-for="day in calendar.weekdays" class="day">
{{ day }}
@@ -476,7 +506,7 @@
</div>
</div>
<div class="ui segment">
<div class="ui segment" v-if="false">
<a class="ui clickable" v-on:click="toggleFolded('key')">
<i class="tiny circular blue clickable plus icon"></i>
Calendar Explanation
@@ -558,31 +588,40 @@
<!-- Temp graph -->
<div class="ui basic segment" >
<div class="ui dividing header">
Chart data for the last {{ tempChartDays }} days
Chart data for the last {{ tempChartDays }} entries
</div>
<div class="ui tiny compact fluid buttons">
<div :class="{'green':(tempChartDays == 1000000)}" v-on:click="tempChartDays = 1000000; graphCurrentData()" class="ui button">ALL</div>
<div :class="{'green':(tempChartDays == 90)}" v-on:click="tempChartDays = 90; graphCurrentData()" class="ui button">90</div>
<div :class="{'green':(tempChartDays == 60)}" v-on:click="tempChartDays = 60; graphCurrentData()" class="ui button">60</div>
<div :class="{'green':(tempChartDays == 30)}" v-on:click="tempChartDays = 30; graphCurrentData()" class="ui button">30</div>
<div :class="{'green':(tempChartDays == 7)}" v-on:click="tempChartDays = 7; graphCurrentData()" class="ui button">7</div>
<div :class="{'green':(tempChartDays == 1000000)}" v-on:click="tempChartDays = 1000000; " class="ui button">ALL</div>
<div :class="{'green':(tempChartDays == 90)}" v-on:click="tempChartDays = 90; " class="ui button">90</div>
<div :class="{'green':(tempChartDays == 60)}" v-on:click="tempChartDays = 60; " class="ui button">60</div>
<div :class="{'green':(tempChartDays == 30)}" v-on:click="tempChartDays = 30; " class="ui button">30</div>
<div :class="{'green':(tempChartDays == 15)}" v-on:click="tempChartDays = 15; " class="ui button">15</div>
<div :class="{'green':(tempChartDays == 7)}" v-on:click="tempChartDays = 7; " class="ui button">7</div>
</div>
<!-- <div id="graphdiv" style="width: 100%; min-height: 320px;"></div> -->
<div class="ui divider"></div>
<div class="ui divider"></div>
<div v-for="(graph, index) in graphs" class="an-graph">
<div class="ui small dividing header">
{{ graph?.title }}
</div>
<div :id="`graphdiv${index}`" style="width: 100%; min-height: 320px;"></div>
<br>
<MetricGraphs
:key="'graph-updates-'+graphUpdates"
:tempChartDays="tempChartDays"
:fields="fields"
:userFields="userFields"
:graphs="graphs"
:cycleData="cycleData"
:calendar="calendar"
@saveGraphs="saveGraphs"
@toggleEditGraphs="toggleEditGraphs"
:editGraphs="editGraphs"
/>
<div class="ui very padded basic segment">
</div>
</div>
<!-- notes -->
<div class="ui basic segment">
<div class="ui basic segment" v-if="false">
<div class="ui clickable" v-on:click="toggleFolded('notes')">
<i class="tiny circular blue clickable plus icon"></i>
Additional Notes
@@ -780,21 +819,21 @@
</div>
<div v-for="(entry, key) in fieldDefinition" class="row" v-if="!['id'].includes(key)">
<div v-if="fieldDefinitionOptions[key]" class="sixteen wide column">
<div v-if="fieldDefinitionOptionsDef[key]" class="sixteen wide column">
{{ mapFormTerm(key) }}
<div v-if="fieldDefinitionOptions[key].type == 'text'" class="ui fluid input">
<div v-if="fieldDefinitionOptionsDef[key].type == 'text'" class="ui fluid input">
<input type="text" :value="editFieldObject[key]" v-on:keyup="e => setNewFieldOption(e, key)">
</div>
<div v-if="fieldDefinitionOptions[key].type == 'commatextoptions' && editFieldObject?.type == 'custom'" class="ui fluid input">
<div v-if="fieldDefinitionOptionsDef[key].type == 'commatextoptions' && editFieldObject?.type == 'custom'" class="ui fluid input">
<input type="text" :value="editFieldObject[key]" v-on:keyup="e => setNewFieldOption(e, key)">
</div>
<div v-if="fieldDefinitionOptions[key].type == 'option'">
<div v-for="option in fieldDefinitionOptions[key].options"
<div v-if="fieldDefinitionOptionsDef[key].type == 'option'">
<div v-for="option in fieldDefinitionOptionsDef[key].options"
v-on:click="setNewFieldOption(null, key, option)"
:class="{'green':editFieldObject[key] == option}"
class="ui button">
@@ -802,8 +841,8 @@
</div>
</div>
<div v-if="fieldDefinitionOptions[key].type == 'icons'">
<div v-for="option in fieldDefinitionOptions[key].options"
<div v-if="fieldDefinitionOptionsDef[key].type == 'icons'">
<div v-for="option in fieldDefinitionOptionsDef[key].options"
v-on:click="setNewFieldOption(null, key, option)"
:class="{'green':editFieldObject[key] == option}"
class="ui icon button">
@@ -812,8 +851,8 @@
</div>
<!-- :class="{'green':}" -->
<div v-if="fieldDefinitionOptions[key].type == 'color'">
<div v-for="option in fieldDefinitionOptions[key].options"
<div v-if="fieldDefinitionOptionsDef[key].type == 'color'">
<div v-for="option in fieldDefinitionOptionsDef[key].options"
v-on:click="setNewFieldOption(null, key, option)"
:class="`ui ${option} icon button`">
<i v-if="editFieldObject[key] == option" class="white check icon"></i>
@@ -865,10 +904,6 @@
import axios from 'axios'
import draggable from 'vuedraggable'
import { Chart } from 'chart.js/auto'
var BASAL_TEMP = 'BT'
export default {
name: 'MetricTracking',
components: {
@@ -876,6 +911,7 @@
'delete-button': require('@/components/NoteDeleteButtonComponent.vue').default,
'modal': require('@/components/ModalComponent.vue').default,
draggable,
'MetricGraphs':require('@/components/Metrictracking/MetricGraphsComponent.vue').default,
},
data () {
return {
@@ -886,15 +922,8 @@
showNotes: false,
fields:[], // Array of field IDs
userFields:{}, // Objects of field definitions
graphs:[
// types [null, 'lastDone', 'pillCalendar']
{type:'pillCalendar', title:'Pill Cal', fieldIds:['JX3QD','K3KII']},
{type:'lastDone', title:'Last Done', fieldIds:['JX3QD','K3KII']},
{title:'Basal Temp', fieldIds:['BT']},
{title:'Basal Temp - Cervical Fluid', fieldIds:['BT','CM']},
{title:'Test Graph', fieldIds:['40DA6','MI9B9']},
],
fieldTypes:{
graphs:[],
fieldTypesDef:{
'float':'Precise Number',
'shortRange':'Options 1-5',
'longRange':'Options 1-10',
@@ -922,7 +951,7 @@
fieldDefinition:{
'id':'','type':'','label':'','icon':'','color':'','width':'','customOptions':''
},
fieldDefinitionOptions:{
fieldDefinitionOptionsDef:{
label:{'type':'text'},
customOptions:{'type':'commatextoptions'},
icon:{'type':'icons', 'options':['heart','smile','cat','crow','dog','dove','dragon','feather','feather alternate','fish','frog','hippo','horse','horse head','kiwi bird','otter','paw','spider','video','headphones','motorcycle','truck','monster truck','campground','cloud sun','drumstick bite','football ball','fruit-apple','hiking','mountain','tractor','tree','wind','wine bottle','coffee','flask','glass cheers','glass martini','beer','toilet paper','gift','globe','hand holding heart','comment','graduation cap','hat cowboy','hat wizard','mitten','user tie','laptop code','microchip','shield alternate','mouse','plug','power off','satellite','hammer','wrench','bell','eye','marker','paperclip','atom','award','theater masks','music','grin alternate','grin tongue squint outline','laugh wink','fire','fire alternate','poop','sun','money bill alternate','piggy bank','heart outline','heartbeat','running','walking','bacon','bone','bread slice','candy cane','carrot','cheese','cloud meatball','cookie','egg','hamburger','hotdog','ice cream','lemon','lemon outline','pepper hot','pizza slice','seedling','stroopwafel','leaf','book dead','broom','cloud moon','ghost','mask','skull crossbones','certificate','check','check circle','joint','cannabis','bong','gem','futbol','brain','dna','hand spock','hand spock outline','meteor','moon','moon outline','robot','rocket','satellite dish','space shuttle','user astronaut','fingerprint','thumbs up','thumbs down']},
@@ -931,15 +960,18 @@
},
cycleData: {},
totalEntries: 0,
openDay: {},
openDay: {}, // current day values, updates into cycleData when saved
loadingDayTimeout: null,
loadingDay: false,
saveDataDebounce:null,
loading: true,
saving: 0, // 0 blank, 1 modified, 2 saving, 3 saved
calendar: {
dateObject: null,
dateCode: null,
monthName: '',
dayName:'',
daysAgo:0,
month: '',
year: '',
days: [],
@@ -952,6 +984,8 @@
editFieldId:'',
editFieldObject:{},
appDataImport:'',
graphUpdates:0,
editGraphs: false,
}
},
beforeCreate() {
@@ -972,11 +1006,6 @@
location.reload();
})
// Include JS libraries
let graphsScript = document.createElement('script')
graphsScript.setAttribute('src', '//cdnjs.cloudflare.com/ajax/libs/dygraph/2.1.0/dygraph.min.js')
document.head.appendChild(graphsScript)
// setup date to today
this.setupCalendar()
@@ -1035,6 +1064,19 @@
}
},
methods: {
saveGraphs(newGraphData){
this.graphs = newGraphData
this.saveCycleData()
// re-render graph data on update
this.$nextTick(() => {
this.graphUpdates++
})
},
toggleEditGraphs(){
this.editGraphs = !this.editGraphs
},
getFieldColor(field){
let color = null
@@ -1134,14 +1176,14 @@
validateCustomFieldsForm(){
const checks = [] //check
const checkFields = Object.keys(this.fieldDefinitionOptions)
const checkFields = Object.keys(this.fieldDefinitionOptionsDef)
checkFields.forEach(row => {
// console.log(this.editFieldObject[row])
// don't worry about optional fields
if(this.fieldDefinitionOptions[row].optional){
if(this.fieldDefinitionOptionsDef[row].optional){
checks.push(true)
return
}
@@ -1323,98 +1365,6 @@
this.fields.push(fieldId)
this.saveCycleData()
},
graphCurrentData(){
const graphOptions = {
interactionModel: {},
// pointClickCallback: function(e, pt){
// console.log(e)
// console.log(pt)
// console.log(this.getValue(pt.idx, 0))
// }
}
// Excel date format YYYYMMDD
const convertToExcelDate = (dateCode) => {
return dateCode
.split('.')
.reverse()
.map(item => String(item).padStart(2,0))
.join('')
}
const sortableDate = (dateCode) => {
return parseInt(
dateCode
.split('.')
.map(i => String(i).padStart(2, '0'))
.reverse()
.join('')
)
}
// Generate set of keys for graph length
let dataKeys = Object.keys(this.cycleData)
dataKeys.sort((a,b) => {
a = sortableDate(a)
b = sortableDate(b)
return b - a
})
dataKeys = dataKeys.splice(0, this.tempChartDays)
// build CSV data for each graph
this.graphs.forEach((graph,index) => {
// CSV or path to a CSV file.
let dataString = ""
// Lookup graph field titles
let graphLabels = ['Date']
graph.fieldIds.forEach(fieldId => {
const graphLabel = this.userFields[fieldId]?.label
graphLabels.push(graphLabel)
})
dataString += graphLabels.join(',') + '\n'
// build each row, for each day
for (var i = 0; i < dataKeys.length; i++) {
let nextFragment = []
// push date code to first column
nextFragment.push(convertToExcelDate(dataKeys[i]))
graph.fieldIds.forEach(fieldId => {
const currentEntry = this.cycleData[dataKeys[i]]
let currentValue = currentEntry[fieldId]
if(fieldId == 'BT'){
// parse temp to fixed length float 00.00
currentValue = parseFloat(currentValue).toFixed(2)
}
if(fieldId == 'CM'){
currentValue = parseFloat('97.'+currentValue)
}
nextFragment.push(currentValue)
})
dataString += nextFragment.join(',') + "\n"
}
let graphDiv = document.getElementById("graphdiv"+index)
const g = new Dygraph(graphDiv, dataString ,graphOptions)
})
return
},
saveField(fieldId, value, optionalTimeout){
// Dont save value if it hasn't changed
@@ -1472,8 +1422,6 @@
this.cycleData[this.calendar.dateCode] = cleanDayData
}
this.graphCurrentData()
this.saveCycleData()
},
@@ -1499,6 +1447,7 @@
axios.post('/api/metric-tracking/get')
.then(({ data }) => {
this.loading = false
this.setApplicationStateJson(data)
})
@@ -1532,27 +1481,49 @@
this.cycleData = json?.cycleData || this.cycleData
this.fields = [...new Set(json?.fields)] || this.fields
this.userFields = json?.userFields || this.userFields
// this.graphs = json?.graphs || this.graphs
// console.log(this.fields)
this.graphs = json?.graphs || this.graphs
this.$nextTick(() => {
this.getApplicationStateJson()
this.totalEntries = Object.keys(this.cycleData).length
this.setupFields()
this.openDayData(this.calendar.dateCode)
this.graphCurrentData()
this.generateTonsOfRandomData()
})
},
getApplicationStateJson(){
// convert date code into sortable int
const sortableDate = (code) => {
code = code
.split('.')
.reverse()
.map(i => String(i).padStart(2, '0'))
.join('')
return parseInt(code)
}
// Sort cycle data, newest first
let sortedData = Object.keys(this.cycleData)
.sort((a,b) => {
return sortableDate(b) - sortableDate(a)
})
// setup new object with sorted data
let sortedCycleData = sortedData.reduce((result, key) => {
result[key] = this.cycleData[key]
return result
},{})
return JSON.stringify({
fields: this.fields,
cycleData: this.cycleData,
cycleData: sortedCycleData,
userFields: this.userFields,
// graphs: this.graphs,
graphs: this.graphs,
})
},
saveCycleData(){
@@ -1624,6 +1595,7 @@
},
setupCalendar(date){
// visualize each day change
this.working = true
setTimeout(() => {
this.working = false
@@ -1640,6 +1612,13 @@
this.calendar.dateCode = this.generateDateCode(date)
// calculate days ago since current date
const now = new Date()
const diffSeconds = Math.floor((now - date) / 1000) // subtract unix timestamps, convert MS to S
const dayInterval = diffSeconds / 86400 // seconds in a day
this.calendar.daysAgo = Math.floor(dayInterval)
// ------------
// setup calendar display
@@ -1656,6 +1635,7 @@
const currentYear = date.getFullYear();
const currentMonth = date.getMonth() + 1;
this.calendar.monthName = date.toLocaleString("en-US", { month: "long" });
this.calendar.dayName = date.toLocaleString("en-US", { weekday: "long" });
this.calendar.year = currentYear
const daysInCurrentMonth = getDaysInMonth(currentYear, currentMonth);
@@ -1704,7 +1684,6 @@
workingDate.setDate(workingDate.getDate()-1)
}
this.graphCurrentData(5000)
},
}
}