• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

atomic14 / web-serial-plotter / 19186043244

08 Nov 2025 01:44AM UTC coverage: 59.981% (-1.2%) from 61.184%
19186043244

Pull #23

github

web-flow
Merge bf67d01a4 into ae73642b7
Pull Request #23: Feature: Multi-channel WAV export

421 of 538 branches covered (78.25%)

Branch coverage included in aggregate %.

16 of 101 new or added lines in 3 files covered. (15.84%)

1 existing line in 1 file now uncovered.

2052 of 3585 relevant lines covered (57.24%)

33.24 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

50.0
/src/components/PlotToolsOverlay.tsx
1
import { forwardRef, useState, useRef, useEffect } from 'react'
1✔
2
import Button from './ui/Button'
1✔
3
import Tooltip from './ui/Tooltip'
1✔
4
import { PlayIcon, PauseIcon, MagnifyingGlassPlusIcon, MagnifyingGlassMinusIcon, CameraIcon, ArrowDownTrayIcon, CogIcon } from '@heroicons/react/24/outline'
1✔
5
import type { ChartExportOptions } from '../utils/chartExport'
6

7
interface Props {
8
  frozen: boolean
9
  onToggleFrozen: () => void
10
  onZoomIn: () => void
11
  onZoomOut: () => void
12
  onSavePng: () => Promise<void> | void
13
  onExportCsv: (options: ChartExportOptions) => void
14
  onShowSettings: () => void
15
  hasData: boolean
16
}
17

18
const PlotToolsOverlay = forwardRef<HTMLDivElement, Props>(function PlotToolsOverlay({ frozen, onToggleFrozen, onZoomIn, onZoomOut, onSavePng, onExportCsv, onShowSettings, hasData }, ref) {
1✔
19
  const [showExportMenu, setShowExportMenu] = useState(false)
1✔
20
  const exportMenuRef = useRef<HTMLDivElement>(null)
1✔
21

22
  // Close menu when clicking outside
23
  useEffect(() => {
1✔
24
    const handleClickOutside = (event: MouseEvent) => {
1✔
25
      if (exportMenuRef.current && !exportMenuRef.current.contains(event.target as Node)) {
×
26
        setShowExportMenu(false)
×
27
      }
×
28
    }
×
29

30
    if (showExportMenu) {
1!
31
      document.addEventListener('mousedown', handleClickOutside)
×
32
      return () => document.removeEventListener('mousedown', handleClickOutside)
×
33
    }
×
34
  }, [showExportMenu])
1✔
35
  return (
1✔
36
    <div className="flex items-center gap-2" ref={ref}>
1✔
37
      <Tooltip label={frozen ? 'Resume (play)' : 'Freeze (pause)'}>
1!
38
        <Button id="tour-tool-freeze" size="sm" variant="neutral" aria-label={frozen ? 'Play' : 'Pause'} onClick={onToggleFrozen}>
1!
39
          {frozen ? (
1!
40
            <PlayIcon className="w-5 h-5" />
×
41
          ) : (
42
            <PauseIcon className="w-5 h-5" />
1✔
43
          )}
44
        </Button>
1✔
45
      </Tooltip>
1✔
46
      <Tooltip label="Zoom in">
1✔
47
        <Button id="tour-tool-zoomin" size="sm" variant="neutral" aria-label="Zoom in" onClick={onZoomIn}>
1✔
48
          <MagnifyingGlassPlusIcon className="w-5 h-5" />
1✔
49
        </Button>
1✔
50
      </Tooltip>
1✔
51
      <Tooltip label="Zoom out">
1✔
52
        <Button id="tour-tool-zoomout" size="sm" variant="neutral" aria-label="Zoom out" onClick={onZoomOut}>
1✔
53
          <MagnifyingGlassMinusIcon className="w-5 h-5" />
1✔
54
        </Button>
1✔
55
      </Tooltip>
1✔
56
      
57
      {/* CSV Export Button with Dropdown */}
58
      <div className="relative" ref={exportMenuRef}>
1✔
59
        <Tooltip label="Export">
1✔
60
          <Button 
1✔
61
            id="tour-tool-export"
1✔
62
            size="sm" 
1✔
63
            variant="neutral" 
1✔
64
            aria-label="Export" 
1✔
65
            disabled={!hasData}
1✔
66
            onClick={() => setShowExportMenu(!showExportMenu)}
1✔
67
          >
68
            <ArrowDownTrayIcon className="w-5 h-5" />
1✔
69
          </Button>
1✔
70
        </Tooltip>
1✔
71
        
72
        {showExportMenu && (
1!
73
          <div className="absolute right-0 top-full mt-1 bg-white dark:bg-neutral-800 border border-gray-200 dark:border-neutral-700 rounded-md shadow-lg z-10 min-w-48">
×
74
            <div className="p-2 space-y-1">
×
75
              <button
×
76
                onClick={() => {
×
NEW
77
                  onExportCsv({ scope: 'visible', includeTimestamps: true, timeFormat: 'iso', format: 'csv' })
×
78
                  setShowExportMenu(false)
×
79
                }}
×
80
                className="w-full text-left px-2 py-1 text-sm text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded"
×
81
              >
×
82
                📊 Export Visible Data
83
              </button>
×
84
              <button
×
85
                onClick={() => {
×
NEW
86
                  onExportCsv({ scope: 'all', includeTimestamps: true, timeFormat: 'iso', format: 'csv' })
×
87
                  setShowExportMenu(false)
×
88
                }}
×
89
                className="w-full text-left px-2 py-1 text-sm text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded"
×
90
              >
×
91
                📈 Export All Data
92
              </button>
×
93
              <div className="border-t border-gray-200 dark:border-neutral-700 my-1" />
×
94
              <button
×
95
                onClick={() => {
×
NEW
96
                  onExportCsv({ scope: 'visible', includeTimestamps: true, timeFormat: 'relative', format: 'csv' })
×
97
                  setShowExportMenu(false)
×
98
                }}
×
99
                className="w-full text-left px-2 py-1 text-sm text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded"
×
100
              >
×
101
                ⏱️ Visible (Relative Time)
102
              </button>
×
103
              <button
×
104
                onClick={() => {
×
NEW
105
                  onExportCsv({ scope: 'all', includeTimestamps: true, timeFormat: 'relative', format: 'csv' })
×
106
                  setShowExportMenu(false)
×
107
                }}
×
108
                className="w-full text-left px-2 py-1 text-sm text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded"
×
109
              >
×
110
                ⏱️ All Data (Relative Time)
111
              </button>
×
NEW
112
              <button
×
NEW
113
                onClick={() => {
×
NEW
114
                  onExportCsv({ scope: 'all', format: 'wav'})
×
NEW
115
                  setShowExportMenu(false)
×
NEW
116
                }}
×
NEW
117
                className="w-full text-left px-2 py-1 text-sm text-gray-700 dark:text-neutral-300 hover:bg-gray-100 dark:hover:bg-neutral-700 rounded"
×
NEW
118
              >
×
119
                🔊 Export as multi-channel WAV (⚠️ LOUD)
NEW
120
              </button>
×
121
            </div>
×
122
          </div>
×
123
        )}
124
      </div>
1✔
125
      
126
      <Tooltip label="Save PNG">
1✔
127
        <Button id="tour-tool-savepng" size="sm" variant="neutral" aria-label="Save PNG" onClick={onSavePng}>
1✔
128
          <CameraIcon className="w-5 h-5" />
1✔
129
        </Button>
1✔
130
      </Tooltip>
1✔
131

132
      <Tooltip label="Settings">
1✔
133
        <Button id="tour-tool-settings" size="sm" variant="neutral" aria-label="Settings" onClick={onShowSettings}>
1✔
134
          <CogIcon className="w-5 h-5" />
1✔
135
        </Button>
1✔
136
      </Tooltip>
1✔
137
    </div>
1✔
138
  )
139
})
1✔
140

141
export default PlotToolsOverlay
1✔
142

143

STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc