feat(frontend): stripe the reclaimable-cache memory segment

At warning level the used segment turns threshold-yellow, which sat
next to the amber cache segment as one indistinguishable yellow mass.
Give the cache segment a vertical-stripe SVG pattern so the used/cache
split stays readable at every severity color. Stripes are vertical
because the bar's preserveAspectRatio=none scaling would shear a
diagonal texture.
This commit is contained in:
rcourtman
2026-07-10 14:09:17 +01:00
parent 3073a50614
commit 0ebdbbe07c
3 changed files with 33 additions and 1 deletions
@@ -1,4 +1,4 @@
import { Show, For } from 'solid-js';
import { Show, For, createUniqueId } from 'solid-js';
import { AnimatedNumber } from '@/components/shared/AnimatedNumber';
import { TooltipPortal } from '@/components/shared/TooltipPortal';
import { formatPercent } from '@/utils/format';
@@ -8,6 +8,7 @@ import { useStackedMemoryBarState } from './useStackedMemoryBarState';
export function StackedMemoryBar(props: StackedMemoryBarProps) {
const state = useStackedMemoryBarState(props);
const presentation = state.presentation;
const stripePatternId = createUniqueId();
const swapBarWidth = () => String(Math.max(0, Math.min(presentation().swapBarPercent, 100)));
const segmentEdge = (leftPercent: number, widthPercent: number) =>
String(Math.max(0, Math.min(leftPercent + widthPercent, 100)));
@@ -28,6 +29,19 @@ export function StackedMemoryBar(props: StackedMemoryBarProps) {
viewBox="0 0 100 100"
preserveAspectRatio="none"
>
{/* Vertical stripes only: the non-uniform viewBox scaling would
shear any diagonal texture. Keeps the reclaimable-cache segment
distinguishable from a warning-yellow used segment. */}
<defs>
<pattern
id={stripePatternId}
patternUnits="userSpaceOnUse"
width="2.5"
height="100"
>
<rect x="0" y="0" width="1.25" height="100" fill="rgba(217, 119, 6, 0.5)" />
</pattern>
</defs>
<For each={presentation().segments}>
{(segment, idx) => (
<>
@@ -41,6 +55,18 @@ export function StackedMemoryBar(props: StackedMemoryBarProps) {
rx="3"
fill={segment.color}
/>
<Show when={segment.striped}>
<rect
data-stacked-memory-stripes="true"
class="metric-fill-geometry"
x={String(Math.max(0, Math.min(segment.leftPercent, 100)))}
y="0"
width={String(Math.max(0, segment.widthPercent))}
height="100"
rx="3"
fill={`url(#${stripePatternId})`}
/>
</Show>
<Show when={idx() < presentation().segments.length - 1}>
<line
class="metric-fill-divider"
@@ -17,6 +17,9 @@ describe('buildStackedMemoryBarPresentation', () => {
]);
expect(presentation.segments[1].leftPercent).toBeCloseTo(25);
expect(presentation.segments[1].widthPercent).toBeCloseTo(37.5);
// Cache is textured so it never blends into a warning-yellow used segment.
expect(presentation.segments[1].striped).toBe(true);
expect(presentation.segments[0].striped).toBeUndefined();
const rows = Object.fromEntries(presentation.tooltipRows.map((row) => [row.label, row.value]));
expect(rows['Used']).toBe('4.00 GB');
@@ -29,6 +29,8 @@ export interface StackedMemorySegment {
label: string;
leftPercent: number;
widthPercent: number;
/** Diagonal-stripe texture so reclaimable cache stays distinguishable from a warning-yellow used segment. */
striped?: boolean;
}
export interface StackedMemoryTooltipRow {
@@ -129,6 +131,7 @@ function getSegments(
label: 'Reclaimable',
leftPercent: usedPercent,
widthPercent: cachePercent,
striped: true,
});
}