Skip to content

Commit 31f286f

Browse files
authored
Merge pull request #20 from speedarr/develop
Add ability to flip graph orientation
2 parents 8686a9e + 41b97c3 commit 31f286f

1 file changed

Lines changed: 75 additions & 58 deletions

File tree

frontend/src/components/BandwidthChart.tsx

Lines changed: 75 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from '@/components/ui/select';
2525
import { Alert, AlertDescription } from '@/components/ui/alert';
2626
import { Button } from '@/components/ui/button';
27-
import { Loader2, AlertCircle, Layers, BarChart3 } from 'lucide-react';
27+
import { Loader2, AlertCircle, Layers, BarChart3, ArrowUpDown } from 'lucide-react';
2828

2929
// Gradient ID mapping for each client
3030
const DOWNLOAD_GRADIENT_IDS: Record<string, string> = {
@@ -190,17 +190,19 @@ export const BandwidthChart: React.FC<BandwidthChartProps> = ({
190190
timeRanges,
191191
}) => {
192192
const [rawData, setRawData] = useState<ChartDataPoint[]>([]);
193-
const [data, setData] = useState<ChartDataPoint[]>([]);
194193
const [isInitialLoad, setIsInitialLoad] = useState(true);
195194
const [error, setError] = useState('');
196-
const [scalingRatio, setScalingRatio] = useState(1);
197195
const [visibleSeries, setVisibleSeries] = useState<Record<string, boolean>>(loadVisibleSeries);
198196
const [downloadClients, setDownloadClients] = useState<DownloadClient[]>([]);
199197
const [snmpEnabled, setSnmpEnabled] = useState<boolean>(false);
200198
const [stackChart, setStackChart] = useState<boolean>(() => {
201199
const saved = localStorage.getItem('speedarr_chart_stacked');
202200
return saved !== null ? JSON.parse(saved) : false;
203201
});
202+
const [flipped, setFlipped] = useState<boolean>(() => {
203+
const saved = localStorage.getItem('speedarr_chart_flipped');
204+
return saved !== null ? JSON.parse(saved) : false;
205+
});
204206
const [clientOrder, setClientOrder] = useState<string[]>(() => {
205207
try {
206208
const saved = localStorage.getItem('speedarr_chart_client_order');
@@ -213,6 +215,9 @@ export const BandwidthChart: React.FC<BandwidthChartProps> = ({
213215
useEffect(() => {
214216
localStorage.setItem('speedarr_chart_stacked', JSON.stringify(stackChart));
215217
}, [stackChart]);
218+
useEffect(() => {
219+
localStorage.setItem('speedarr_chart_flipped', JSON.stringify(flipped));
220+
}, [flipped]);
216221
useEffect(() => {
217222
if (clientOrder.length > 0) {
218223
localStorage.setItem('speedarr_chart_client_order', JSON.stringify(clientOrder));
@@ -453,85 +458,85 @@ export const BandwidthChart: React.FC<BandwidthChartProps> = ({
453458
if (aggregatedData.length === 0) return { data: [], ratio: 1 };
454459

455460
// Find max values for scaling - only include visible series
456-
let maxDownload = 0;
457-
let maxNegative = 0;
461+
// When flipped, uploads are positive (on top) and downloads are negated (below zero)
462+
let maxPositive = 0;
463+
let maxToNegate = 0;
458464

459465
aggregatedData.forEach((point) => {
460-
// Only include visible download series in max calculation
466+
// Compute download totals from visible series
461467
let totalDownload = 0;
462468
if (visibleSeries.qbittorrent_download) totalDownload += point.qbittorrent_speed || 0;
463469
if (visibleSeries.sabnzbd_download) totalDownload += point.sabnzbd_speed || 0;
464470
if (visibleSeries.nzbget_download) totalDownload += point.nzbget_speed || 0;
465471
if (visibleSeries.transmission_download) totalDownload += point.transmission_speed || 0;
466472
if (visibleSeries.deluge_download) totalDownload += point.deluge_speed || 0;
467473

468-
// Include SNMP download only if visible
469-
if (visibleSeries.snmp_download) {
470-
const snmpDownload = point.snmp_download_speed || 0;
471-
maxDownload = Math.max(maxDownload, totalDownload, snmpDownload);
472-
} else {
473-
maxDownload = Math.max(maxDownload, totalDownload);
474-
}
474+
const snmpDownloadVal = visibleSeries.snmp_download ? (point.snmp_download_speed || 0) : 0;
475475

476-
// Only include visible upload series in max calculation
476+
// Compute upload totals from visible series
477477
let totalUpload = 0;
478478
if (visibleSeries.plex_streams) totalUpload += point.stream_bandwidth || 0;
479479
if (visibleSeries.qbittorrent_upload) totalUpload += point.qbittorrent_upload_speed || 0;
480480
if (visibleSeries.transmission_upload) totalUpload += point.transmission_upload_speed || 0;
481481
if (visibleSeries.deluge_upload) totalUpload += point.deluge_upload_speed || 0;
482482

483-
// Include SNMP upload only if visible
484-
const snmpUpload = visibleSeries.snmp_upload ? (point.snmp_upload_speed || 0) : 0;
483+
const snmpUploadVal = visibleSeries.snmp_upload ? (point.snmp_upload_speed || 0) : 0;
485484

486485
// Include upload limits only if their respective limit lines are visible
487486
let maxUploadLimit = 0;
488487
if (visibleSeries.qbittorrent_upload_limit_line) maxUploadLimit = Math.max(maxUploadLimit, point.qbittorrent_upload_limit || 0);
489488
if (visibleSeries.transmission_upload_limit_line) maxUploadLimit = Math.max(maxUploadLimit, point.transmission_upload_limit || 0);
490489
if (visibleSeries.deluge_upload_limit_line) maxUploadLimit = Math.max(maxUploadLimit, point.deluge_upload_limit || 0);
491490

492-
maxNegative = Math.max(maxNegative, totalUpload, snmpUpload, maxUploadLimit);
491+
if (flipped) {
492+
// Uploads on top (positive), downloads negated
493+
maxPositive = Math.max(maxPositive, totalUpload, snmpUploadVal, maxUploadLimit);
494+
maxToNegate = Math.max(maxToNegate, totalDownload, snmpDownloadVal);
495+
} else {
496+
// Downloads on top (positive), uploads negated
497+
maxPositive = Math.max(maxPositive, totalDownload, snmpDownloadVal);
498+
maxToNegate = Math.max(maxToNegate, totalUpload, snmpUploadVal, maxUploadLimit);
499+
}
493500
});
494501

495502
// Calculate scaling ratio
496-
const ratio = (maxDownload > 0 && maxNegative > 0) ? maxDownload / maxNegative : 1;
503+
const ratio = (maxPositive > 0 && maxToNegate > 0) ? maxPositive / maxToNegate : 1;
497504

498505
// Transform data and include limits as line data
506+
// When flipped, uploads stay positive and downloads get negated+scaled (and vice versa)
499507
const chartData = aggregatedData.map((point) => ({
500508
...point,
501-
// Download series (positive values, stacked)
502-
qbittorrent_download: point.qbittorrent_speed || 0,
503-
sabnzbd_download: point.sabnzbd_speed || 0,
504-
nzbget_download: point.nzbget_speed || 0,
505-
transmission_download: point.transmission_speed || 0,
506-
deluge_download: point.deluge_speed || 0,
507-
// Upload series (negative values, scaled)
508-
plex_streams: -Math.abs(point.stream_bandwidth || 0) * ratio,
509-
qbittorrent_upload: -Math.abs(point.qbittorrent_upload_speed || 0) * ratio,
510-
transmission_upload: -Math.abs(point.transmission_upload_speed || 0) * ratio,
511-
deluge_upload: -Math.abs(point.deluge_upload_speed || 0) * ratio,
509+
// Download series
510+
qbittorrent_download: flipped ? -Math.abs(point.qbittorrent_speed || 0) * ratio : (point.qbittorrent_speed || 0),
511+
sabnzbd_download: flipped ? -Math.abs(point.sabnzbd_speed || 0) * ratio : (point.sabnzbd_speed || 0),
512+
nzbget_download: flipped ? -Math.abs(point.nzbget_speed || 0) * ratio : (point.nzbget_speed || 0),
513+
transmission_download: flipped ? -Math.abs(point.transmission_speed || 0) * ratio : (point.transmission_speed || 0),
514+
deluge_download: flipped ? -Math.abs(point.deluge_speed || 0) * ratio : (point.deluge_speed || 0),
515+
// Upload series
516+
plex_streams: flipped ? Math.abs(point.stream_bandwidth || 0) : -Math.abs(point.stream_bandwidth || 0) * ratio,
517+
qbittorrent_upload: flipped ? Math.abs(point.qbittorrent_upload_speed || 0) : -Math.abs(point.qbittorrent_upload_speed || 0) * ratio,
518+
transmission_upload: flipped ? Math.abs(point.transmission_upload_speed || 0) : -Math.abs(point.transmission_upload_speed || 0) * ratio,
519+
deluge_upload: flipped ? Math.abs(point.deluge_upload_speed || 0) : -Math.abs(point.deluge_upload_speed || 0) * ratio,
512520
// Download limit lines
513-
qbittorrent_download_limit_line: point.qbittorrent_download_limit || null,
514-
sabnzbd_download_limit_line: point.sabnzbd_download_limit || null,
515-
nzbget_download_limit_line: point.nzbget_download_limit || null,
516-
transmission_download_limit_line: point.transmission_download_limit || null,
517-
deluge_download_limit_line: point.deluge_download_limit || null,
518-
// Upload limit lines (negative and scaled)
519-
qbittorrent_upload_limit_line: point.qbittorrent_upload_limit ? -Math.abs(point.qbittorrent_upload_limit) * ratio : null,
520-
transmission_upload_limit_line: point.transmission_upload_limit ? -Math.abs(point.transmission_upload_limit) * ratio : null,
521-
deluge_upload_limit_line: point.deluge_upload_limit ? -Math.abs(point.deluge_upload_limit) * ratio : null,
522-
// Add SNMP actual bandwidth - upload is negative and scaled like other uploads
523-
snmp_download: point.snmp_download_speed ?? null,
524-
snmp_upload: point.snmp_upload_speed ? -Math.abs(point.snmp_upload_speed) * ratio : null,
521+
qbittorrent_download_limit_line: flipped ? (point.qbittorrent_download_limit ? -Math.abs(point.qbittorrent_download_limit) * ratio : null) : (point.qbittorrent_download_limit || null),
522+
sabnzbd_download_limit_line: flipped ? (point.sabnzbd_download_limit ? -Math.abs(point.sabnzbd_download_limit) * ratio : null) : (point.sabnzbd_download_limit || null),
523+
nzbget_download_limit_line: flipped ? (point.nzbget_download_limit ? -Math.abs(point.nzbget_download_limit) * ratio : null) : (point.nzbget_download_limit || null),
524+
transmission_download_limit_line: flipped ? (point.transmission_download_limit ? -Math.abs(point.transmission_download_limit) * ratio : null) : (point.transmission_download_limit || null),
525+
deluge_download_limit_line: flipped ? (point.deluge_download_limit ? -Math.abs(point.deluge_download_limit) * ratio : null) : (point.deluge_download_limit || null),
526+
// Upload limit lines
527+
qbittorrent_upload_limit_line: flipped ? (point.qbittorrent_upload_limit || null) : (point.qbittorrent_upload_limit ? -Math.abs(point.qbittorrent_upload_limit) * ratio : null),
528+
transmission_upload_limit_line: flipped ? (point.transmission_upload_limit || null) : (point.transmission_upload_limit ? -Math.abs(point.transmission_upload_limit) * ratio : null),
529+
deluge_upload_limit_line: flipped ? (point.deluge_upload_limit || null) : (point.deluge_upload_limit ? -Math.abs(point.deluge_upload_limit) * ratio : null),
530+
// SNMP bandwidth
531+
snmp_download: flipped ? (point.snmp_download_speed ? -Math.abs(point.snmp_download_speed) * ratio : null) : (point.snmp_download_speed ?? null),
532+
snmp_upload: flipped ? (point.snmp_upload_speed ?? null) : (point.snmp_upload_speed ? -Math.abs(point.snmp_upload_speed) * ratio : null),
525533
}));
526534

527535
return { data: chartData, ratio };
528-
}, [aggregatedData, visibleSeries, stackChart]);
536+
}, [aggregatedData, visibleSeries, stackChart, flipped]);
529537

530-
// Update state from memoized values
531-
useEffect(() => {
532-
setData(transformedData.data);
533-
setScalingRatio(transformedData.ratio);
534-
}, [transformedData]);
538+
const data = transformedData.data;
539+
const scalingRatio = transformedData.ratio;
535540

536541
useEffect(() => {
537542
fetchData();
@@ -656,6 +661,18 @@ export const BandwidthChart: React.FC<BandwidthChartProps> = ({
656661
{stackChart ? <Layers className="h-4 w-4" aria-hidden="true" /> : <BarChart3 className="h-4 w-4" aria-hidden="true" />}
657662
{stackChart ? 'Stacked' : 'Overlapping'}
658663
</Button>
664+
<Button
665+
variant="outline"
666+
size="sm"
667+
onClick={() => setFlipped(!flipped)}
668+
className="gap-2"
669+
title={flipped ? 'Uploads on top — click to put downloads on top' : 'Downloads on top — click to put uploads on top'}
670+
aria-label={flipped ? 'Currently showing uploads on top, click to flip' : 'Currently showing downloads on top, click to flip'}
671+
aria-pressed={flipped}
672+
>
673+
<ArrowUpDown className="h-4 w-4" aria-hidden="true" />
674+
{flipped ? 'UL on Top' : 'DL on Top'}
675+
</Button>
659676
</div>
660677
</div>
661678
</CardHeader>
@@ -685,63 +702,63 @@ export const BandwidthChart: React.FC<BandwidthChartProps> = ({
685702
)}
686703
<ResponsiveContainer width="100%" height={700}>
687704
<ComposedChart
705+
key={`chart-${flipped}`}
688706
data={data}
689707
margin={{ top: 10, right: 10, left: 10, bottom: 20 }}
690-
stackOffset="sign"
691708
>
692-
<defs>
709+
<defs key={`chart-defs-${flipped}`}>
693710
{/* Download gradients - only for enabled clients */}
694711
{isClientEnabled('qbittorrent') && (
695-
<linearGradient id="qbDownload" x1="0" y1="0" x2="0" y2="1">
712+
<linearGradient id="qbDownload" x1="0" y1={flipped ? "1" : "0"} x2="0" y2={flipped ? "0" : "1"}>
696713
<stop offset="5%" stopColor={qbitInfo.color} stopOpacity={0.8}/>
697714
<stop offset="95%" stopColor={qbitInfo.color} stopOpacity={0.3}/>
698715
</linearGradient>
699716
)}
700717
{isClientEnabled('sabnzbd') && (
701-
<linearGradient id="sabDownload" x1="0" y1="0" x2="0" y2="1">
718+
<linearGradient id="sabDownload" x1="0" y1={flipped ? "1" : "0"} x2="0" y2={flipped ? "0" : "1"}>
702719
<stop offset="5%" stopColor={sabInfo.color} stopOpacity={0.8}/>
703720
<stop offset="95%" stopColor={sabInfo.color} stopOpacity={0.3}/>
704721
</linearGradient>
705722
)}
706723
{isClientEnabled('nzbget') && (
707-
<linearGradient id="nzbgetDownload" x1="0" y1="0" x2="0" y2="1">
724+
<linearGradient id="nzbgetDownload" x1="0" y1={flipped ? "1" : "0"} x2="0" y2={flipped ? "0" : "1"}>
708725
<stop offset="5%" stopColor={nzbgetInfo.color} stopOpacity={0.8}/>
709726
<stop offset="95%" stopColor={nzbgetInfo.color} stopOpacity={0.3}/>
710727
</linearGradient>
711728
)}
712729
{isClientEnabled('transmission') && (
713-
<linearGradient id="transmissionDownload" x1="0" y1="0" x2="0" y2="1">
730+
<linearGradient id="transmissionDownload" x1="0" y1={flipped ? "1" : "0"} x2="0" y2={flipped ? "0" : "1"}>
714731
<stop offset="5%" stopColor={transmissionInfo.color} stopOpacity={0.8}/>
715732
<stop offset="95%" stopColor={transmissionInfo.color} stopOpacity={0.3}/>
716733
</linearGradient>
717734
)}
718735
{isClientEnabled('deluge') && (
719-
<linearGradient id="delugeDownload" x1="0" y1="0" x2="0" y2="1">
736+
<linearGradient id="delugeDownload" x1="0" y1={flipped ? "1" : "0"} x2="0" y2={flipped ? "0" : "1"}>
720737
<stop offset="5%" stopColor={delugeInfo.color} stopOpacity={0.8}/>
721738
<stop offset="95%" stopColor={delugeInfo.color} stopOpacity={0.3}/>
722739
</linearGradient>
723740
)}
724741
{/* Upload gradients - only for enabled clients that support upload */}
725742
{isClientEnabled('qbittorrent') && clientSupportsUpload('qbittorrent') && (
726-
<linearGradient id="qbUpload" x1="0" y1="1" x2="0" y2="0">
743+
<linearGradient id="qbUpload" x1="0" y1={flipped ? "0" : "1"} x2="0" y2={flipped ? "1" : "0"}>
727744
<stop offset="5%" stopColor={qbitInfo.color} stopOpacity={0.8}/>
728745
<stop offset="95%" stopColor={qbitInfo.color} stopOpacity={0.3}/>
729746
</linearGradient>
730747
)}
731748
{isClientEnabled('transmission') && clientSupportsUpload('transmission') && (
732-
<linearGradient id="transmissionUpload" x1="0" y1="1" x2="0" y2="0">
749+
<linearGradient id="transmissionUpload" x1="0" y1={flipped ? "0" : "1"} x2="0" y2={flipped ? "1" : "0"}>
733750
<stop offset="5%" stopColor={transmissionInfo.color} stopOpacity={0.8}/>
734751
<stop offset="95%" stopColor={transmissionInfo.color} stopOpacity={0.3}/>
735752
</linearGradient>
736753
)}
737754
{isClientEnabled('deluge') && clientSupportsUpload('deluge') && (
738-
<linearGradient id="delugeUpload" x1="0" y1="1" x2="0" y2="0">
755+
<linearGradient id="delugeUpload" x1="0" y1={flipped ? "0" : "1"} x2="0" y2={flipped ? "1" : "0"}>
739756
<stop offset="5%" stopColor={delugeInfo.color} stopOpacity={0.8}/>
740757
<stop offset="95%" stopColor={delugeInfo.color} stopOpacity={0.3}/>
741758
</linearGradient>
742759
)}
743760
{/* Plex streams gradient - always shown */}
744-
<linearGradient id="plexStreams" x1="0" y1="1" x2="0" y2="0">
761+
<linearGradient id="plexStreams" x1="0" y1={flipped ? "0" : "1"} x2="0" y2={flipped ? "1" : "0"}>
745762
<stop offset="5%" stopColor="#ff7300" stopOpacity={0.8}/>
746763
<stop offset="95%" stopColor="#ff7300" stopOpacity={0.3}/>
747764
</linearGradient>

0 commit comments

Comments
 (0)