myio-js-library 0.1.158 → 0.1.159
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +334 -154
- package/dist/index.js +334 -154
- package/dist/myio-js-library.umd.js +334 -154
- package/dist/myio-js-library.umd.min.js +1 -1
- package/package.json +1 -1
|
@@ -14429,7 +14429,7 @@ ${rangeText}`;
|
|
|
14429
14429
|
maxLevel: "Maximum Level",
|
|
14430
14430
|
dateRange: "Date Range",
|
|
14431
14431
|
deviceInfo: "Device Information",
|
|
14432
|
-
levelChart: "Level
|
|
14432
|
+
levelChart: "Water Level History (m.c.a)",
|
|
14433
14433
|
percentUnit: "%",
|
|
14434
14434
|
status: {
|
|
14435
14435
|
critical: "Critical",
|
|
@@ -14460,6 +14460,20 @@ ${rangeText}`;
|
|
|
14460
14460
|
return { status: "full", color: "#3498db", label: this.i18n.status.full };
|
|
14461
14461
|
}
|
|
14462
14462
|
}
|
|
14463
|
+
/**
|
|
14464
|
+
* Get tank image URL based on level percentage (same logic as device card)
|
|
14465
|
+
*/
|
|
14466
|
+
getTankImageUrl(percentage) {
|
|
14467
|
+
if (percentage >= 70) {
|
|
14468
|
+
return "https://dashboard.myio-bas.com/api/images/public/3t6WVhMQJFsrKA8bSZmrngDsNPkZV7fq";
|
|
14469
|
+
} else if (percentage >= 40) {
|
|
14470
|
+
return "https://dashboard.myio-bas.com/api/images/public/4UBbShfXCVWR9wcw6IzVMNran4x1EW5n";
|
|
14471
|
+
} else if (percentage >= 20) {
|
|
14472
|
+
return "https://dashboard.myio-bas.com/api/images/public/aB9nX28F54fBBQs1Ht8jKUdYAMcq9QSm";
|
|
14473
|
+
} else {
|
|
14474
|
+
return "https://dashboard.myio-bas.com/api/images/public/qLdwhV4qw295poSCa7HinpnmXoN7dAPO";
|
|
14475
|
+
}
|
|
14476
|
+
}
|
|
14463
14477
|
/**
|
|
14464
14478
|
* Format timestamp to readable date
|
|
14465
14479
|
*/
|
|
@@ -14472,13 +14486,18 @@ ${rangeText}`;
|
|
|
14472
14486
|
}
|
|
14473
14487
|
return dateStr;
|
|
14474
14488
|
}
|
|
14489
|
+
/**
|
|
14490
|
+
* Format timestamp to ISO date string for input
|
|
14491
|
+
*/
|
|
14492
|
+
formatDateForInput(ts) {
|
|
14493
|
+
const date = new Date(ts);
|
|
14494
|
+
return date.toISOString().split("T")[0];
|
|
14495
|
+
}
|
|
14475
14496
|
/**
|
|
14476
14497
|
* Render the modal HTML
|
|
14477
14498
|
*/
|
|
14478
14499
|
render() {
|
|
14479
|
-
const {
|
|
14480
|
-
const currentLevel = data.summary.currentLevel ?? context.device.currentLevel ?? 0;
|
|
14481
|
-
this.getLevelStatus(currentLevel);
|
|
14500
|
+
const { params } = this.config;
|
|
14482
14501
|
this.overlay = document.createElement("div");
|
|
14483
14502
|
this.overlay.className = "myio-water-tank-modal-overlay";
|
|
14484
14503
|
this.overlay.style.cssText = `
|
|
@@ -14499,9 +14518,9 @@ ${rangeText}`;
|
|
|
14499
14518
|
this.modal.className = "myio-water-tank-modal";
|
|
14500
14519
|
this.modal.style.cssText = `
|
|
14501
14520
|
background: white;
|
|
14502
|
-
border-radius:
|
|
14521
|
+
border-radius: 12px;
|
|
14503
14522
|
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
|
|
14504
|
-
width: ${params.ui?.width ||
|
|
14523
|
+
width: ${params.ui?.width || 700}px;
|
|
14505
14524
|
max-width: 95vw;
|
|
14506
14525
|
max-height: 90vh;
|
|
14507
14526
|
display: flex;
|
|
@@ -14567,97 +14586,150 @@ ${rangeText}`;
|
|
|
14567
14586
|
overflow-y: auto;
|
|
14568
14587
|
flex: 1;
|
|
14569
14588
|
">
|
|
14570
|
-
${this.
|
|
14589
|
+
${this.renderDateRangePicker()}
|
|
14590
|
+
${this.renderTankVisualization()}
|
|
14571
14591
|
${this.renderChart()}
|
|
14572
|
-
${this.renderDeviceInfo()}
|
|
14573
14592
|
</div>
|
|
14574
14593
|
${this.renderFooter()}
|
|
14575
14594
|
`;
|
|
14576
14595
|
}
|
|
14577
14596
|
/**
|
|
14578
|
-
* Render
|
|
14597
|
+
* Render date range picker
|
|
14579
14598
|
*/
|
|
14580
|
-
|
|
14581
|
-
const {
|
|
14582
|
-
const
|
|
14583
|
-
const
|
|
14599
|
+
renderDateRangePicker() {
|
|
14600
|
+
const { params } = this.config;
|
|
14601
|
+
const startDate = this.formatDateForInput(params.startTs);
|
|
14602
|
+
const endDate = this.formatDateForInput(params.endTs);
|
|
14584
14603
|
return `
|
|
14585
14604
|
<div style="
|
|
14586
|
-
|
|
14587
|
-
|
|
14605
|
+
background: #f8f9fa;
|
|
14606
|
+
border: 1px solid #e0e0e0;
|
|
14607
|
+
border-radius: 8px;
|
|
14608
|
+
padding: 16px;
|
|
14609
|
+
margin-bottom: 20px;
|
|
14610
|
+
display: flex;
|
|
14611
|
+
align-items: center;
|
|
14588
14612
|
gap: 16px;
|
|
14589
|
-
|
|
14613
|
+
flex-wrap: wrap;
|
|
14590
14614
|
">
|
|
14591
|
-
|
|
14592
|
-
|
|
14593
|
-
|
|
14594
|
-
|
|
14595
|
-
|
|
14596
|
-
|
|
14597
|
-
|
|
14598
|
-
|
|
14599
|
-
|
|
14600
|
-
|
|
14601
|
-
|
|
14602
|
-
|
|
14603
|
-
|
|
14604
|
-
|
|
14605
|
-
|
|
14606
|
-
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
14615
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
14616
|
+
<label style="font-size: 14px; font-weight: 500; color: #2c3e50;">From:</label>
|
|
14617
|
+
<input type="date" id="myio-water-tank-start-date" value="${startDate}" style="
|
|
14618
|
+
padding: 8px 12px;
|
|
14619
|
+
border: 1px solid #ddd;
|
|
14620
|
+
border-radius: 6px;
|
|
14621
|
+
font-size: 14px;
|
|
14622
|
+
color: #2c3e50;
|
|
14623
|
+
cursor: pointer;
|
|
14624
|
+
"/>
|
|
14625
|
+
</div>
|
|
14626
|
+
<div style="display: flex; align-items: center; gap: 8px;">
|
|
14627
|
+
<label style="font-size: 14px; font-weight: 500; color: #2c3e50;">To:</label>
|
|
14628
|
+
<input type="date" id="myio-water-tank-end-date" value="${endDate}" style="
|
|
14629
|
+
padding: 8px 12px;
|
|
14630
|
+
border: 1px solid #ddd;
|
|
14631
|
+
border-radius: 6px;
|
|
14632
|
+
font-size: 14px;
|
|
14633
|
+
color: #2c3e50;
|
|
14634
|
+
cursor: pointer;
|
|
14635
|
+
"/>
|
|
14636
|
+
</div>
|
|
14637
|
+
<button id="myio-water-tank-apply-dates" style="
|
|
14638
|
+
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
|
14639
|
+
color: white;
|
|
14640
|
+
border: none;
|
|
14641
|
+
padding: 8px 20px;
|
|
14642
|
+
border-radius: 6px;
|
|
14643
|
+
font-size: 14px;
|
|
14644
|
+
font-weight: 500;
|
|
14645
|
+
cursor: pointer;
|
|
14646
|
+
transition: all 0.2s ease;
|
|
14647
|
+
">
|
|
14648
|
+
Apply
|
|
14649
|
+
</button>
|
|
14612
14650
|
</div>
|
|
14613
14651
|
`;
|
|
14614
14652
|
}
|
|
14615
14653
|
/**
|
|
14616
|
-
* Render
|
|
14654
|
+
* Render tank visualization with percentage
|
|
14617
14655
|
*/
|
|
14618
|
-
|
|
14656
|
+
renderTankVisualization() {
|
|
14657
|
+
const { data, context } = this.config;
|
|
14658
|
+
let percentage = 0;
|
|
14659
|
+
const percentagePoints = data.telemetry.filter((p) => p.key === "water_percentage");
|
|
14660
|
+
if (percentagePoints.length > 0) {
|
|
14661
|
+
const latestPercentage = percentagePoints[percentagePoints.length - 1].value;
|
|
14662
|
+
percentage = latestPercentage <= 1 ? latestPercentage * 100 : latestPercentage;
|
|
14663
|
+
} else if (context.device.currentLevel !== void 0) {
|
|
14664
|
+
const level = context.device.currentLevel;
|
|
14665
|
+
percentage = level <= 1 ? level * 100 : level;
|
|
14666
|
+
}
|
|
14667
|
+
const levelStatus = this.getLevelStatus(percentage);
|
|
14668
|
+
const tankImageUrl = this.getTankImageUrl(percentage);
|
|
14619
14669
|
return `
|
|
14620
14670
|
<div style="
|
|
14621
|
-
|
|
14622
|
-
|
|
14623
|
-
|
|
14624
|
-
|
|
14625
|
-
|
|
14671
|
+
display: flex;
|
|
14672
|
+
align-items: center;
|
|
14673
|
+
justify-content: center;
|
|
14674
|
+
gap: 32px;
|
|
14675
|
+
padding: 24px;
|
|
14676
|
+
background: linear-gradient(135deg, ${levelStatus.color}10 0%, ${levelStatus.color}05 100%);
|
|
14677
|
+
border: 1px solid ${levelStatus.color}30;
|
|
14678
|
+
border-radius: 12px;
|
|
14679
|
+
margin-bottom: 24px;
|
|
14626
14680
|
">
|
|
14627
|
-
|
|
14681
|
+
<div style="
|
|
14682
|
+
display: flex;
|
|
14683
|
+
flex-direction: column;
|
|
14684
|
+
align-items: center;
|
|
14685
|
+
gap: 12px;
|
|
14686
|
+
">
|
|
14687
|
+
<img src="${tankImageUrl}" alt="Water Tank" style="
|
|
14688
|
+
width: 120px;
|
|
14689
|
+
height: auto;
|
|
14690
|
+
filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1));
|
|
14691
|
+
"/>
|
|
14628
14692
|
<div style="
|
|
14629
|
-
|
|
14630
|
-
top: 12px;
|
|
14631
|
-
right: 12px;
|
|
14632
|
-
background: ${color};
|
|
14693
|
+
background: ${levelStatus.color};
|
|
14633
14694
|
color: white;
|
|
14634
|
-
padding: 4px
|
|
14635
|
-
border-radius:
|
|
14636
|
-
font-size:
|
|
14695
|
+
padding: 4px 12px;
|
|
14696
|
+
border-radius: 20px;
|
|
14697
|
+
font-size: 12px;
|
|
14637
14698
|
font-weight: 600;
|
|
14638
14699
|
text-transform: uppercase;
|
|
14639
|
-
">${
|
|
14640
|
-
|
|
14641
|
-
|
|
14642
|
-
font-size: 12px;
|
|
14643
|
-
color: #7f8c8d;
|
|
14644
|
-
margin-bottom: 8px;
|
|
14645
|
-
font-weight: 500;
|
|
14646
|
-
">${label}</div>
|
|
14700
|
+
">${levelStatus.label}</div>
|
|
14701
|
+
</div>
|
|
14702
|
+
|
|
14647
14703
|
<div style="
|
|
14648
|
-
|
|
14649
|
-
|
|
14650
|
-
|
|
14651
|
-
|
|
14704
|
+
display: flex;
|
|
14705
|
+
flex-direction: column;
|
|
14706
|
+
align-items: center;
|
|
14707
|
+
gap: 8px;
|
|
14708
|
+
">
|
|
14709
|
+
<div style="
|
|
14710
|
+
font-size: 48px;
|
|
14711
|
+
font-weight: 700;
|
|
14712
|
+
color: ${levelStatus.color};
|
|
14713
|
+
line-height: 1;
|
|
14714
|
+
">${percentage.toFixed(1)}%</div>
|
|
14715
|
+
<div style="
|
|
14716
|
+
font-size: 14px;
|
|
14717
|
+
color: #7f8c8d;
|
|
14718
|
+
font-weight: 500;
|
|
14719
|
+
">${this.i18n.currentLevel}</div>
|
|
14720
|
+
</div>
|
|
14652
14721
|
</div>
|
|
14653
14722
|
`;
|
|
14654
14723
|
}
|
|
14655
14724
|
/**
|
|
14656
|
-
* Render chart section
|
|
14725
|
+
* Render chart section - shows water_level (m.c.a) over time
|
|
14657
14726
|
*/
|
|
14658
14727
|
renderChart() {
|
|
14659
14728
|
const { data } = this.config;
|
|
14660
|
-
|
|
14729
|
+
const waterLevelPoints = data.telemetry.filter(
|
|
14730
|
+
(p) => p.key === "water_level" || p.key === "waterLevel" || p.key === "nivel" || p.key === "level"
|
|
14731
|
+
);
|
|
14732
|
+
if (waterLevelPoints.length === 0) {
|
|
14661
14733
|
return `
|
|
14662
14734
|
<div style="
|
|
14663
14735
|
background: #f8f9fa;
|
|
@@ -14665,20 +14737,23 @@ ${rangeText}`;
|
|
|
14665
14737
|
border-radius: 8px;
|
|
14666
14738
|
padding: 48px;
|
|
14667
14739
|
text-align: center;
|
|
14668
|
-
margin-bottom: 24px;
|
|
14669
14740
|
">
|
|
14670
14741
|
<div style="font-size: 48px; margin-bottom: 16px; opacity: 0.3;">\u{1F4CA}</div>
|
|
14671
14742
|
<div style="color: #7f8c8d; font-size: 16px;">${this.i18n.noData}</div>
|
|
14743
|
+
<div style="color: #bdc3c7; font-size: 13px; margin-top: 8px;">
|
|
14744
|
+
No water_level (m.c.a) data available for this period
|
|
14745
|
+
</div>
|
|
14672
14746
|
</div>
|
|
14673
14747
|
`;
|
|
14674
14748
|
}
|
|
14749
|
+
const firstTs = waterLevelPoints[0]?.ts;
|
|
14750
|
+
const lastTs = waterLevelPoints[waterLevelPoints.length - 1]?.ts;
|
|
14675
14751
|
return `
|
|
14676
14752
|
<div style="
|
|
14677
14753
|
background: white;
|
|
14678
14754
|
border: 1px solid #e0e0e0;
|
|
14679
14755
|
border-radius: 8px;
|
|
14680
14756
|
padding: 20px;
|
|
14681
|
-
margin-bottom: 24px;
|
|
14682
14757
|
">
|
|
14683
14758
|
<h3 style="
|
|
14684
14759
|
margin: 0 0 16px 0;
|
|
@@ -14686,56 +14761,18 @@ ${rangeText}`;
|
|
|
14686
14761
|
font-weight: 600;
|
|
14687
14762
|
color: #2c3e50;
|
|
14688
14763
|
">${this.i18n.levelChart}</h3>
|
|
14689
|
-
<canvas id="myio-water-tank-chart" style="width: 100%; height:
|
|
14690
|
-
|
|
14691
|
-
|
|
14692
|
-
|
|
14693
|
-
|
|
14694
|
-
|
|
14695
|
-
|
|
14696
|
-
|
|
14697
|
-
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
/**
|
|
14702
|
-
* Render device info section
|
|
14703
|
-
*/
|
|
14704
|
-
renderDeviceInfo() {
|
|
14705
|
-
const { context, data } = this.config;
|
|
14706
|
-
return `
|
|
14707
|
-
<div style="
|
|
14708
|
-
background: #f8f9fa;
|
|
14709
|
-
border: 1px solid #e0e0e0;
|
|
14710
|
-
border-radius: 8px;
|
|
14711
|
-
padding: 20px;
|
|
14712
|
-
">
|
|
14713
|
-
<h3 style="
|
|
14714
|
-
margin: 0 0 16px 0;
|
|
14715
|
-
font-size: 16px;
|
|
14716
|
-
font-weight: 600;
|
|
14717
|
-
color: #2c3e50;
|
|
14718
|
-
">${this.i18n.deviceInfo}</h3>
|
|
14719
|
-
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 12px;">
|
|
14720
|
-
${this.renderInfoRow("Device ID", context.device.id)}
|
|
14721
|
-
${this.renderInfoRow("Label", context.device.label)}
|
|
14722
|
-
${context.device.type ? this.renderInfoRow("Type", context.device.type) : ""}
|
|
14723
|
-
${context.metadata.slaveId ? this.renderInfoRow("Slave ID", String(context.metadata.slaveId)) : ""}
|
|
14724
|
-
${context.metadata.centralId ? this.renderInfoRow("Central ID", context.metadata.centralId) : ""}
|
|
14725
|
-
${this.renderInfoRow("Total Readings", String(data.summary.totalReadings))}
|
|
14726
|
-
${this.renderInfoRow("Data Keys", data.metadata.keys.join(", "))}
|
|
14727
|
-
</div>
|
|
14728
|
-
</div>
|
|
14729
|
-
`;
|
|
14730
|
-
}
|
|
14731
|
-
/**
|
|
14732
|
-
* Render an info row
|
|
14733
|
-
*/
|
|
14734
|
-
renderInfoRow(label, value) {
|
|
14735
|
-
return `
|
|
14736
|
-
<div style="display: flex; justify-content: space-between; align-items: center;">
|
|
14737
|
-
<span style="color: #7f8c8d; font-size: 13px;">${label}:</span>
|
|
14738
|
-
<span style="color: #2c3e50; font-size: 13px; font-weight: 500; margin-left: 8px;">${value}</span>
|
|
14764
|
+
<canvas id="myio-water-tank-chart" style="width: 100%; height: 280px;"></canvas>
|
|
14765
|
+
${firstTs && lastTs ? `
|
|
14766
|
+
<div style="
|
|
14767
|
+
margin-top: 12px;
|
|
14768
|
+
font-size: 12px;
|
|
14769
|
+
color: #7f8c8d;
|
|
14770
|
+
text-align: center;
|
|
14771
|
+
">
|
|
14772
|
+
${this.formatDate(firstTs, false)} \u2014 ${this.formatDate(lastTs, false)}
|
|
14773
|
+
(${waterLevelPoints.length} readings)
|
|
14774
|
+
</div>
|
|
14775
|
+
` : ""}
|
|
14739
14776
|
</div>
|
|
14740
14777
|
`;
|
|
14741
14778
|
}
|
|
@@ -14784,6 +14821,10 @@ ${rangeText}`;
|
|
|
14784
14821
|
if (exportBtn) {
|
|
14785
14822
|
exportBtn.addEventListener("click", () => this.config.onExport());
|
|
14786
14823
|
}
|
|
14824
|
+
const applyDatesBtn = this.modal.querySelector("#myio-water-tank-apply-dates");
|
|
14825
|
+
if (applyDatesBtn) {
|
|
14826
|
+
applyDatesBtn.addEventListener("click", () => this.handleDateRangeChange());
|
|
14827
|
+
}
|
|
14787
14828
|
this.overlay.addEventListener("click", (e) => {
|
|
14788
14829
|
if (e.target === this.overlay) {
|
|
14789
14830
|
this.config.onClose();
|
|
@@ -14797,59 +14838,122 @@ ${rangeText}`;
|
|
|
14797
14838
|
this.renderCanvasChart();
|
|
14798
14839
|
});
|
|
14799
14840
|
}
|
|
14841
|
+
/**
|
|
14842
|
+
* Handle date range change
|
|
14843
|
+
*/
|
|
14844
|
+
handleDateRangeChange() {
|
|
14845
|
+
if (!this.modal) return;
|
|
14846
|
+
const startInput = this.modal.querySelector("#myio-water-tank-start-date");
|
|
14847
|
+
const endInput = this.modal.querySelector("#myio-water-tank-end-date");
|
|
14848
|
+
if (startInput && endInput) {
|
|
14849
|
+
const startTs = new Date(startInput.value).setHours(0, 0, 0, 0);
|
|
14850
|
+
const endTs = new Date(endInput.value).setHours(23, 59, 59, 999);
|
|
14851
|
+
if (startTs >= endTs) {
|
|
14852
|
+
alert("Start date must be before end date");
|
|
14853
|
+
return;
|
|
14854
|
+
}
|
|
14855
|
+
console.log("[WaterTankModalView] Date range changed:", {
|
|
14856
|
+
startTs,
|
|
14857
|
+
endTs,
|
|
14858
|
+
startDate: new Date(startTs).toISOString(),
|
|
14859
|
+
endDate: new Date(endTs).toISOString()
|
|
14860
|
+
});
|
|
14861
|
+
if (this.config.onDateRangeChange) {
|
|
14862
|
+
this.config.onDateRangeChange(startTs, endTs);
|
|
14863
|
+
}
|
|
14864
|
+
}
|
|
14865
|
+
}
|
|
14800
14866
|
handleEscapeKey(e) {
|
|
14801
14867
|
if (e.key === "Escape") {
|
|
14802
14868
|
this.config.onClose();
|
|
14803
14869
|
}
|
|
14804
14870
|
}
|
|
14805
14871
|
/**
|
|
14806
|
-
* Render chart using Canvas API
|
|
14872
|
+
* Render chart using Canvas API - shows water_level (m.c.a) over time
|
|
14807
14873
|
*/
|
|
14808
14874
|
renderCanvasChart() {
|
|
14809
14875
|
const canvas = document.getElementById("myio-water-tank-chart");
|
|
14810
14876
|
if (!canvas) return;
|
|
14811
14877
|
const { data } = this.config;
|
|
14812
|
-
|
|
14878
|
+
const points = data.telemetry.filter(
|
|
14879
|
+
(p) => p.key === "water_level" || p.key === "waterLevel" || p.key === "nivel" || p.key === "level"
|
|
14880
|
+
);
|
|
14881
|
+
if (points.length < 2) return;
|
|
14813
14882
|
const ctx = canvas.getContext("2d");
|
|
14814
14883
|
if (!ctx) return;
|
|
14815
14884
|
const rect = canvas.getBoundingClientRect();
|
|
14816
14885
|
canvas.width = rect.width * window.devicePixelRatio;
|
|
14817
|
-
canvas.height =
|
|
14886
|
+
canvas.height = 280 * window.devicePixelRatio;
|
|
14818
14887
|
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
|
|
14819
14888
|
const width = rect.width;
|
|
14820
|
-
const height =
|
|
14821
|
-
const padding = 40;
|
|
14889
|
+
const height = 280;
|
|
14890
|
+
const padding = { top: 20, right: 20, bottom: 40, left: 60 };
|
|
14822
14891
|
ctx.clearRect(0, 0, width, height);
|
|
14823
|
-
|
|
14892
|
+
const values = points.map((p) => p.value);
|
|
14893
|
+
const minValue = Math.min(...values);
|
|
14894
|
+
const maxValue = Math.max(...values);
|
|
14895
|
+
const valueRange = maxValue - minValue || 1;
|
|
14896
|
+
const valuePadding = valueRange * 0.1;
|
|
14897
|
+
const chartMinY = minValue - valuePadding;
|
|
14898
|
+
const chartMaxY = maxValue + valuePadding;
|
|
14899
|
+
const chartRangeY = chartMaxY - chartMinY;
|
|
14900
|
+
ctx.fillStyle = "#fafafa";
|
|
14901
|
+
ctx.fillRect(padding.left, padding.top, width - padding.left - padding.right, height - padding.top - padding.bottom);
|
|
14902
|
+
ctx.strokeStyle = "#e8e8e8";
|
|
14824
14903
|
ctx.lineWidth = 1;
|
|
14825
|
-
ctx.
|
|
14826
|
-
ctx.
|
|
14827
|
-
ctx.
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
for (let i = 0; i <= 5; i++) {
|
|
14833
|
-
const y = padding + (height - 2 * padding) * (i / 5);
|
|
14904
|
+
ctx.fillStyle = "#666";
|
|
14905
|
+
ctx.font = "11px Arial";
|
|
14906
|
+
ctx.textAlign = "right";
|
|
14907
|
+
const ySteps = 5;
|
|
14908
|
+
for (let i = 0; i <= ySteps; i++) {
|
|
14909
|
+
const y = padding.top + (height - padding.top - padding.bottom) * (i / ySteps);
|
|
14910
|
+
const value = chartMaxY - chartRangeY * i / ySteps;
|
|
14834
14911
|
ctx.beginPath();
|
|
14835
|
-
ctx.moveTo(padding, y);
|
|
14836
|
-
ctx.lineTo(width - padding, y);
|
|
14912
|
+
ctx.moveTo(padding.left, y);
|
|
14913
|
+
ctx.lineTo(width - padding.right, y);
|
|
14837
14914
|
ctx.stroke();
|
|
14838
|
-
ctx.
|
|
14839
|
-
ctx.font = "12px Arial";
|
|
14840
|
-
ctx.textAlign = "right";
|
|
14841
|
-
ctx.fillText(`${100 - i * 20}%`, padding - 10, y + 4);
|
|
14915
|
+
ctx.fillText(`${value.toFixed(2)}`, padding.left - 8, y + 4);
|
|
14842
14916
|
}
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
|
|
14917
|
+
ctx.save();
|
|
14918
|
+
ctx.translate(15, height / 2);
|
|
14919
|
+
ctx.rotate(-Math.PI / 2);
|
|
14920
|
+
ctx.textAlign = "center";
|
|
14921
|
+
ctx.fillStyle = "#666";
|
|
14922
|
+
ctx.font = "12px Arial";
|
|
14923
|
+
ctx.fillText("m.c.a", 0, 0);
|
|
14924
|
+
ctx.restore();
|
|
14925
|
+
ctx.strokeStyle = "#ccc";
|
|
14926
|
+
ctx.lineWidth = 1;
|
|
14927
|
+
ctx.beginPath();
|
|
14928
|
+
ctx.moveTo(padding.left, padding.top);
|
|
14929
|
+
ctx.lineTo(padding.left, height - padding.bottom);
|
|
14930
|
+
ctx.lineTo(width - padding.right, height - padding.bottom);
|
|
14931
|
+
ctx.stroke();
|
|
14932
|
+
const chartWidth = width - padding.left - padding.right;
|
|
14933
|
+
const chartHeight = height - padding.top - padding.bottom;
|
|
14934
|
+
const xScale = chartWidth / (points.length - 1);
|
|
14935
|
+
ctx.beginPath();
|
|
14936
|
+
ctx.moveTo(padding.left, height - padding.bottom);
|
|
14937
|
+
points.forEach((point, index) => {
|
|
14938
|
+
const x = padding.left + index * xScale;
|
|
14939
|
+
const y = padding.top + chartHeight - (point.value - chartMinY) / chartRangeY * chartHeight;
|
|
14940
|
+
ctx.lineTo(x, y);
|
|
14941
|
+
});
|
|
14942
|
+
ctx.lineTo(padding.left + (points.length - 1) * xScale, height - padding.bottom);
|
|
14943
|
+
ctx.closePath();
|
|
14944
|
+
const gradient = ctx.createLinearGradient(0, padding.top, 0, height - padding.bottom);
|
|
14945
|
+
gradient.addColorStop(0, "rgba(52, 152, 219, 0.3)");
|
|
14946
|
+
gradient.addColorStop(1, "rgba(52, 152, 219, 0.05)");
|
|
14947
|
+
ctx.fillStyle = gradient;
|
|
14948
|
+
ctx.fill();
|
|
14847
14949
|
ctx.strokeStyle = "#3498db";
|
|
14848
14950
|
ctx.lineWidth = 2;
|
|
14951
|
+
ctx.lineJoin = "round";
|
|
14952
|
+
ctx.lineCap = "round";
|
|
14849
14953
|
ctx.beginPath();
|
|
14850
14954
|
points.forEach((point, index) => {
|
|
14851
|
-
const x = padding + index * xScale;
|
|
14852
|
-
const y =
|
|
14955
|
+
const x = padding.left + index * xScale;
|
|
14956
|
+
const y = padding.top + chartHeight - (point.value - chartMinY) / chartRangeY * chartHeight;
|
|
14853
14957
|
if (index === 0) {
|
|
14854
14958
|
ctx.moveTo(x, y);
|
|
14855
14959
|
} else {
|
|
@@ -14857,14 +14961,56 @@ ${rangeText}`;
|
|
|
14857
14961
|
}
|
|
14858
14962
|
});
|
|
14859
14963
|
ctx.stroke();
|
|
14860
|
-
|
|
14861
|
-
|
|
14862
|
-
|
|
14863
|
-
|
|
14864
|
-
|
|
14865
|
-
|
|
14866
|
-
|
|
14867
|
-
|
|
14964
|
+
if (points.length <= 50) {
|
|
14965
|
+
ctx.fillStyle = "#3498db";
|
|
14966
|
+
points.forEach((point, index) => {
|
|
14967
|
+
const x = padding.left + index * xScale;
|
|
14968
|
+
const y = padding.top + chartHeight - (point.value - chartMinY) / chartRangeY * chartHeight;
|
|
14969
|
+
ctx.beginPath();
|
|
14970
|
+
ctx.arc(x, y, 3, 0, 2 * Math.PI);
|
|
14971
|
+
ctx.fill();
|
|
14972
|
+
});
|
|
14973
|
+
}
|
|
14974
|
+
ctx.fillStyle = "#888";
|
|
14975
|
+
ctx.font = "10px Arial";
|
|
14976
|
+
ctx.textAlign = "center";
|
|
14977
|
+
const xLabelCount = Math.min(6, points.length);
|
|
14978
|
+
const xLabelStep = Math.floor(points.length / xLabelCount);
|
|
14979
|
+
for (let i = 0; i < points.length; i += xLabelStep) {
|
|
14980
|
+
const x = padding.left + i * xScale;
|
|
14981
|
+
const date = new Date(points[i].ts);
|
|
14982
|
+
const label = `${date.getDate()}/${date.getMonth() + 1}`;
|
|
14983
|
+
ctx.fillText(label, x, height - padding.bottom + 16);
|
|
14984
|
+
}
|
|
14985
|
+
if (points.length > 1) {
|
|
14986
|
+
const lastX = padding.left + (points.length - 1) * xScale;
|
|
14987
|
+
const lastDate = new Date(points[points.length - 1].ts);
|
|
14988
|
+
const lastLabel = `${lastDate.getDate()}/${lastDate.getMonth() + 1}`;
|
|
14989
|
+
ctx.fillText(lastLabel, lastX, height - padding.bottom + 16);
|
|
14990
|
+
}
|
|
14991
|
+
}
|
|
14992
|
+
/**
|
|
14993
|
+
* Update data and re-render chart
|
|
14994
|
+
*/
|
|
14995
|
+
updateData(data) {
|
|
14996
|
+
this.config.data = data;
|
|
14997
|
+
if (this.modal) {
|
|
14998
|
+
const bodyEl = this.modal.querySelector(".myio-water-tank-modal-body");
|
|
14999
|
+
if (bodyEl) {
|
|
15000
|
+
bodyEl.innerHTML = `
|
|
15001
|
+
${this.renderDateRangePicker()}
|
|
15002
|
+
${this.renderTankVisualization()}
|
|
15003
|
+
${this.renderChart()}
|
|
15004
|
+
`;
|
|
15005
|
+
const applyDatesBtn = this.modal.querySelector("#myio-water-tank-apply-dates");
|
|
15006
|
+
if (applyDatesBtn) {
|
|
15007
|
+
applyDatesBtn.addEventListener("click", () => this.handleDateRangeChange());
|
|
15008
|
+
}
|
|
15009
|
+
requestAnimationFrame(() => {
|
|
15010
|
+
this.renderCanvasChart();
|
|
15011
|
+
});
|
|
15012
|
+
}
|
|
15013
|
+
}
|
|
14868
15014
|
}
|
|
14869
15015
|
/**
|
|
14870
15016
|
* Show the modal with animation
|
|
@@ -15129,8 +15275,9 @@ ${rangeText}`;
|
|
|
15129
15275
|
data: this.data,
|
|
15130
15276
|
onExport: () => this.handleExport(),
|
|
15131
15277
|
onError: (error) => this.handleError(error),
|
|
15132
|
-
onClose: () => this.close()
|
|
15278
|
+
onClose: () => this.close(),
|
|
15133
15279
|
// Call close() to destroy view and trigger user callback
|
|
15280
|
+
onDateRangeChange: (startTs, endTs) => this.handleDateRangeChange(startTs, endTs)
|
|
15134
15281
|
});
|
|
15135
15282
|
this.view.render();
|
|
15136
15283
|
this.view.show();
|
|
@@ -15162,6 +15309,39 @@ ${rangeText}`;
|
|
|
15162
15309
|
}
|
|
15163
15310
|
this.handleClose();
|
|
15164
15311
|
}
|
|
15312
|
+
/**
|
|
15313
|
+
* Handle date range change from view
|
|
15314
|
+
*/
|
|
15315
|
+
async handleDateRangeChange(startTs, endTs) {
|
|
15316
|
+
console.log("[WaterTankModal] Date range changed:", {
|
|
15317
|
+
startTs,
|
|
15318
|
+
endTs,
|
|
15319
|
+
startDate: new Date(startTs).toISOString(),
|
|
15320
|
+
endDate: new Date(endTs).toISOString()
|
|
15321
|
+
});
|
|
15322
|
+
this.options.startTs = startTs;
|
|
15323
|
+
this.options.endTs = endTs;
|
|
15324
|
+
this.context.timeRange.startTs = startTs;
|
|
15325
|
+
this.context.timeRange.endTs = endTs;
|
|
15326
|
+
try {
|
|
15327
|
+
console.log("[WaterTankModal] Fetching new data for date range...");
|
|
15328
|
+
this.data = await this.fetchTelemetryData();
|
|
15329
|
+
if (this.view) {
|
|
15330
|
+
this.view.updateData(this.data);
|
|
15331
|
+
}
|
|
15332
|
+
if (this.options.onDataLoaded) {
|
|
15333
|
+
try {
|
|
15334
|
+
this.options.onDataLoaded(this.data);
|
|
15335
|
+
} catch (callbackError) {
|
|
15336
|
+
console.warn("[WaterTankModal] onDataLoaded callback error:", callbackError);
|
|
15337
|
+
}
|
|
15338
|
+
}
|
|
15339
|
+
console.log("[WaterTankModal] Data refreshed successfully");
|
|
15340
|
+
} catch (error) {
|
|
15341
|
+
console.error("[WaterTankModal] Failed to fetch data for new date range:", error);
|
|
15342
|
+
this.handleError(error);
|
|
15343
|
+
}
|
|
15344
|
+
}
|
|
15165
15345
|
/**
|
|
15166
15346
|
* Handle export functionality
|
|
15167
15347
|
*/
|