mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-17 02:36:17 +01:00
add a total line to the tooltip of charts with multiple values #1280
Co-authored-by: Titouan V <titouan.verhille@gmail.com>
This commit is contained in:
@@ -30,6 +30,7 @@ export default function AreaChartDefault({
|
|||||||
domain,
|
domain,
|
||||||
legend,
|
legend,
|
||||||
itemSorter,
|
itemSorter,
|
||||||
|
showTotal = false,
|
||||||
reverseStackOrder = false,
|
reverseStackOrder = false,
|
||||||
hideYAxis = false,
|
hideYAxis = false,
|
||||||
}: // logRender = false,
|
}: // logRender = false,
|
||||||
@@ -42,6 +43,7 @@ export default function AreaChartDefault({
|
|||||||
dataPoints?: DataPoint[]
|
dataPoints?: DataPoint[]
|
||||||
domain?: [number, number]
|
domain?: [number, number]
|
||||||
legend?: boolean
|
legend?: boolean
|
||||||
|
showTotal?: boolean
|
||||||
itemSorter?: (a: any, b: any) => number
|
itemSorter?: (a: any, b: any) => number
|
||||||
reverseStackOrder?: boolean
|
reverseStackOrder?: boolean
|
||||||
hideYAxis?: boolean
|
hideYAxis?: boolean
|
||||||
@@ -65,9 +67,15 @@ export default function AreaChartDefault({
|
|||||||
"ps-4": hideYAxis,
|
"ps-4": hideYAxis,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<AreaChart reverseStackOrder={reverseStackOrder} accessibilityLayer data={chartData.systemStats} margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}>
|
<AreaChart
|
||||||
|
reverseStackOrder={reverseStackOrder}
|
||||||
|
accessibilityLayer
|
||||||
|
data={chartData.systemStats}
|
||||||
|
margin={hideYAxis ? { ...chartMargin, left: 5 } : chartMargin}
|
||||||
|
>
|
||||||
<CartesianGrid vertical={false} />
|
<CartesianGrid vertical={false} />
|
||||||
{!hideYAxis && <YAxis
|
{!hideYAxis && (
|
||||||
|
<YAxis
|
||||||
direction="ltr"
|
direction="ltr"
|
||||||
orientation={chartData.orientation}
|
orientation={chartData.orientation}
|
||||||
className="tracking-tighter"
|
className="tracking-tighter"
|
||||||
@@ -76,7 +84,8 @@ export default function AreaChartDefault({
|
|||||||
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
|
tickFormatter={(value, index) => updateYAxisWidth(tickFormatter(value, index))}
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
{xAxis(chartData)}
|
{xAxis(chartData)}
|
||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
animationEasing="ease-out"
|
animationEasing="ease-out"
|
||||||
@@ -87,6 +96,7 @@ export default function AreaChartDefault({
|
|||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
contentFormatter={contentFormatter}
|
contentFormatter={contentFormatter}
|
||||||
|
showTotal={showTotal}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -114,5 +124,5 @@ export default function AreaChartDefault({
|
|||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled])
|
}, [chartData.systemStats.at(-1), yAxisWidth, maxToggled, showTotal])
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export default memo(function ContainerChart({
|
|||||||
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
labelFormatter={(_, data) => formatShortDate(data[0].payload.created)}
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
itemSorter={(a, b) => b.value - a.value}
|
itemSorter={(a, b) => b.value - a.value}
|
||||||
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} />}
|
content={<ChartTooltipContent filter={filter} contentFormatter={toolTipFormatter} showTotal={true} />}
|
||||||
/>
|
/>
|
||||||
{Object.keys(chartConfig).map((key) => {
|
{Object.keys(chartConfig).map((key) => {
|
||||||
const filtered = filteredKeys.has(key)
|
const filtered = filteredKeys.has(key)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export default memo(function MemChart({ chartData, showMax }: { chartData: Chart
|
|||||||
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
|
const { value: convertedValue, unit } = formatBytes(value * 1024, false, Unit.Bytes, true)
|
||||||
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
return decimalString(convertedValue, convertedValue >= 100 ? 1 : 2) + " " + unit
|
||||||
}}
|
}}
|
||||||
|
showTotal={true}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -699,6 +699,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)
|
const { value: convertedValue, unit } = formatBytes(value, true, userSettings.unitDisk, false)
|
||||||
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
|
return `${decimalString(convertedValue, convertedValue >= 100 ? 1 : 2)} ${unit}`
|
||||||
}}
|
}}
|
||||||
|
showTotal={true}
|
||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
@@ -752,6 +753,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
|
|||||||
const { value, unit } = formatBytes(data.value, true, userSettings.unitNet, false)
|
const { value, unit } = formatBytes(data.value, true, userSettings.unitNet, false)
|
||||||
return `${decimalString(value, value >= 100 ? 1 : 2)} ${unit}`
|
return `${decimalString(value, value >= 100 ? 1 : 2)} ${unit}`
|
||||||
}}
|
}}
|
||||||
|
showTotal={true}
|
||||||
/>
|
/>
|
||||||
</ChartCard>
|
</ChartCard>
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import type { JSX } from "react"
|
import type { JSX } from "react"
|
||||||
|
import { useLingui } from "@lingui/react/macro"
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import * as RechartsPrimitive from "recharts"
|
import * as RechartsPrimitive from "recharts"
|
||||||
import { chartTimeData, cn } from "@/lib/utils"
|
import { chartTimeData, cn } from "@/lib/utils"
|
||||||
import type { ChartData } from "@/types"
|
import type { ChartData } from "@/types"
|
||||||
|
import { Separator } from "./separator"
|
||||||
|
|
||||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
const THEMES = { light: "", dark: ".dark" } as const
|
const THEMES = { light: "", dark: ".dark" } as const
|
||||||
@@ -100,6 +102,8 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
filter?: string
|
filter?: string
|
||||||
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
contentFormatter?: (item: any, key: string) => React.ReactNode | string
|
||||||
truncate?: boolean
|
truncate?: boolean
|
||||||
|
showTotal?: boolean
|
||||||
|
totalLabel?: React.ReactNode
|
||||||
}
|
}
|
||||||
>(
|
>(
|
||||||
(
|
(
|
||||||
@@ -121,11 +125,16 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
itemSorter,
|
itemSorter,
|
||||||
contentFormatter: content = undefined,
|
contentFormatter: content = undefined,
|
||||||
truncate = false,
|
truncate = false,
|
||||||
|
showTotal = false,
|
||||||
|
totalLabel,
|
||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
// const { config } = useChart()
|
// const { config } = useChart()
|
||||||
const config = {}
|
const config = {}
|
||||||
|
const { t } = useLingui()
|
||||||
|
const totalLabelNode = totalLabel ?? t`Total`
|
||||||
|
const totalName = typeof totalLabelNode === "string" ? totalLabelNode : t`Total`
|
||||||
|
|
||||||
React.useMemo(() => {
|
React.useMemo(() => {
|
||||||
if (filter) {
|
if (filter) {
|
||||||
@@ -141,6 +150,76 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
}
|
}
|
||||||
}, [itemSorter, payload])
|
}, [itemSorter, payload])
|
||||||
|
|
||||||
|
const totalValueDisplay = React.useMemo(() => {
|
||||||
|
if (!showTotal || !payload?.length) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalValue = 0
|
||||||
|
let hasNumericValue = false
|
||||||
|
const aggregatedNestedValues: Record<string, number> = {}
|
||||||
|
|
||||||
|
for (const item of payload) {
|
||||||
|
const numericValue = typeof item.value === "number" ? item.value : Number(item.value)
|
||||||
|
if (Number.isFinite(numericValue)) {
|
||||||
|
totalValue += numericValue
|
||||||
|
hasNumericValue = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content && item?.payload) {
|
||||||
|
const payloadKey = `${nameKey || item.name || item.dataKey || "value"}`
|
||||||
|
const nestedPayload = (item.payload as Record<string, unknown> | undefined)?.[payloadKey]
|
||||||
|
|
||||||
|
if (nestedPayload && typeof nestedPayload === "object") {
|
||||||
|
for (const [nestedKey, nestedValue] of Object.entries(nestedPayload)) {
|
||||||
|
if (typeof nestedValue === "number" && Number.isFinite(nestedValue)) {
|
||||||
|
aggregatedNestedValues[nestedKey] = (aggregatedNestedValues[nestedKey] ?? 0) + nestedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasNumericValue) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalKey = "__total__"
|
||||||
|
const totalItem: any = {
|
||||||
|
value: totalValue,
|
||||||
|
name: totalName,
|
||||||
|
dataKey: totalKey,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
const basePayload =
|
||||||
|
payload[0]?.payload && typeof payload[0].payload === "object"
|
||||||
|
? { ...(payload[0].payload as Record<string, unknown>) }
|
||||||
|
: {}
|
||||||
|
totalItem.payload = {
|
||||||
|
...basePayload,
|
||||||
|
[totalKey]: aggregatedNestedValues,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof formatter === "function") {
|
||||||
|
return formatter(
|
||||||
|
totalValue,
|
||||||
|
totalName,
|
||||||
|
totalItem,
|
||||||
|
payload.length,
|
||||||
|
totalItem.payload ?? payload[0]?.payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
return content(totalItem, totalKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${totalValue.toLocaleString()}${unit ?? ""}`
|
||||||
|
}, [color, content, formatter, nameKey, payload, showTotal, totalName, unit])
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
const tooltipLabel = React.useMemo(() => {
|
||||||
if (hideLabel || !payload?.length) {
|
if (hideLabel || !payload?.length) {
|
||||||
return null
|
return null
|
||||||
@@ -242,6 +321,15 @@ const ChartTooltipContent = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
{totalValueDisplay ? (
|
||||||
|
<>
|
||||||
|
<Separator className="mt-0.5" />
|
||||||
|
<div className="flex items-center justify-between gap-2 -mt-0.75 font-medium">
|
||||||
|
<span className="text-muted-foreground ps-3">{totalLabelNode}</span>
|
||||||
|
<span>{totalValueDisplay}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user