diff --git a/docs/d3-charts.md b/docs/d3-charts.md index 6cd08302f..d2de923fc 100644 --- a/docs/d3-charts.md +++ b/docs/d3-charts.md @@ -141,3 +141,11 @@ Label loops that size a background rect to its text (`.ll-bg`/`.pl-bg`, point-la Every `setAttribute`, inline-style write, `textContent` assignment, and node move produces a `MutationObserver` record, **even when the value is unchanged**. PostHog session replay (rrweb) serializes each record — a moved node is re-serialized with its whole subtree — and the agentic-point coach mark rescans on them. A throttled Chrome profile of a legend toggle attributed ~40% of the main-thread work to rrweb processing these records; most came from blind re-syncs of hundreds of point labels, decoration rings, gradient stops, and `.raise()` calls that were already in order. Hot re-sync paths (data, metric, and display phases; zoom frames; point-label placement) therefore use the write-if-changed helpers in `src/lib/d3-chart/chart-update.ts`: `setAttrIfChanged`, `setStyleIfChanged`, `setTextIfChanged`, and `raiseInOrder` (sequential `.raise()` semantics, skipped when each parent's tail is already in that order). They compare against the live DOM, not a cache, so a label the placement pass moved is still restored correctly. Reads of attributes and inline styles never force layout. Initial enter-path construction can keep plain `.attr()` chains — new nodes produce one add record regardless. Tests for these paths assert a repeated pass emits zero mutation records. + +## Huawei series identity + +Huawei uses a shared amber/gold identity, separate from NVIDIA green, AMD red, Google blue and OpenAI purple. `HUAWEI_CHART_PALETTE` (including the `HUAWEI_CHART_COLORS` tokens) in `packages/constants/src/gpu-keys.ts` defines a darker amber for light canvases and a brighter gold for dark canvases. Both base colors exceed 4.5:1 contrast against the tested white and `#111214` reference backgrounds. + +`getVendor` recognizes canonical Ascend keys, framework-suffixed keys and Huawei/Ascend display names. The shared normal, date and high-contrast generators retain this identity; multiple Huawei chips/configurations use related amber shades. High-contrast palettes reserve the amber band from other vendors when Huawei is present. OperatorX delegates to the shared generator instead of maintaining a local crimson override. ubenchX uses the same generator for Huawei marks, legend swatches and tooltips while retaining its existing GPU palette. Theme changes update display decoration without changing measurement coordinates. + +These colors encode vendor identity, not a measurement. Sequential heatmap scales encode values and are independent of categorical vendor colors; official logo artwork is also separate. This is presentation-only: no API values, filters, hardware specifications or normalization references change. diff --git a/packages/app/cypress/e2e/operatorx.cy.ts b/packages/app/cypress/e2e/operatorx.cy.ts index 1c6165dc0..2ea93b246 100644 --- a/packages/app/cypress/e2e/operatorx.cy.ts +++ b/packages/app/cypress/e2e/operatorx.cy.ts @@ -218,3 +218,76 @@ describe('OperatorX progressive rendering', { testIsolation: true }, () => { .should('exist'); }); }); + +const operatorPoints = () => + cy + .get('[data-testid="operatorx-viz-roofline"] circle.point') + .filter( + (_, element) => + (element as unknown as { __data__: { hw: string } }).__data__.hw === 'ascend950pr', + ); + +const bandwidthPoints = () => + cy + .get('#ubenchx-mbu circle.point') + .filter( + (_, element) => + (element as unknown as { __data__: { gpuKey: string } }).__data__.gpuKey === 'Huawei 950PR', + ); + +it('uses shared Huawei colors in OperatorX and ubenchX across a live theme switch', () => { + const ascend = normalizeBundle(run('201', 'ascend950pr-external', [64, 128])); + const nvidia = normalizeBundle(run('202', 'h200-dgxc', [64, 128])); + for (const dataset of [ascend, nvidia]) { + for (const row of dataset.results) { + const m = Number(row.args.m); + row.args = { ...row.args, n: 4096, k: 4096 }; + row.shape = `${m} × 4096 × 4096`; + row.latencyUs = 100; + row.tflops = (2 * m * 4096 * 4096) / (100 * 1e6); + } + } + const view = comparisonView( + buildComparison('gemm', [ + { runner: 'ascend950pr-external', dataset: ascend }, + { runner: 'h200-dgxc', dataset: nvidia }, + ]), + null, + ); + cy.intercept('GET', '/api/v1/operatorx/compare?op=gemm*', view).as('colorComparison'); + const visitOptions = { + onBeforeLoad(win: Cypress.AUTWindow) { + win.localStorage.setItem('theme', 'light'); + win.localStorage.setItem('inferencex-star-modal-dismissed', String(Date.now())); + }, + }; + cy.visit('/operatorx', visitOptions); + cy.wait('@colorComparison'); + cy.get('[data-testid="operatorx-viz-roofline"]').scrollIntoView(); + operatorPoints().first().should('have.css', 'fill', 'rgb(148, 106, 0)'); + cy.contains('[data-testid="operatorx-coverage"] li', '950PR') + .find('.rounded-full') + .first() + .should('have.css', 'background-color', 'rgb(148, 106, 0)'); + cy.get('[data-testid="operatorx-viz-roofline"]').screenshot('huawei-operatorx-light'); + cy.get('[data-testid="theme-toggle"]').click(); + cy.get('html').should('have.class', 'dark'); + operatorPoints().first().should('have.css', 'fill', 'rgb(235, 192, 74)'); + cy.contains('[data-testid="operatorx-coverage"] li', '950PR') + .find('.rounded-full') + .first() + .should('have.css', 'background-color', 'rgb(235, 192, 74)'); + cy.get('[data-testid="operatorx-viz-roofline"]').screenshot('huawei-operatorx-dark'); + + cy.visit('/ubenchx/mem-bw', visitOptions); + bandwidthPoints().first().should('have.css', 'fill', 'rgb(148, 106, 0)'); + cy.get('[data-testid="theme-toggle"]').click(); + bandwidthPoints().first().should('have.css', 'fill', 'rgb(235, 192, 74)'); + bandwidthPoints().eq(20).trigger('mouseenter', { force: true }); + cy.get('[data-chart-tooltip="ubenchx-mbu"]') + .should('contain', 'Huawei 950PR') + .find('span') + .first() + .should('have.css', 'background-color', 'rgb(235, 192, 74)'); + cy.get('#ubenchx-mbu').screenshot('huawei-ubenchx-dark'); +}); diff --git a/packages/app/src/components/operatorx/compare/hardware.test.ts b/packages/app/src/components/operatorx/compare/hardware.test.ts index caf44031d..b9292e43c 100644 --- a/packages/app/src/components/operatorx/compare/hardware.test.ts +++ b/packages/app/src/components/operatorx/compare/hardware.test.ts @@ -107,13 +107,15 @@ describe('Ascend OperatorX reference', () => { }); it.each(['light', 'dark'] as const)( - 'uses a separate crimson without recoloring AMD in %s mode', + 'uses the shared Huawei palette without recoloring AMD in %s mode', (theme) => { const otherKeys = ['mi300x', 'mi325x', 'mi355x', 'h100', 'unknown']; const colors = operatorXColors([...otherKeys, 'ascend950pr'], theme); for (const key of otherKeys) expect(colors[key]).toBe(generateVendorColors(otherKeys, theme)[key]); - expect(colors.ascend950pr).toMatch(/ 355\)$/u); + expect(colors.ascend950pr).toBe( + generateVendorColors(['Huawei 950PR'], theme)['Huawei 950PR'], + ); expect(colors.ascend950pr).not.toBe(colors.mi355x); expect(operatorXColors(['ascend950pr'], theme).ascend950pr).toBe(colors.ascend950pr); expect(operatorXColors([], theme)).toEqual({}); diff --git a/packages/app/src/components/operatorx/compare/hardware.ts b/packages/app/src/components/operatorx/compare/hardware.ts index cdd56efac..846396999 100644 --- a/packages/app/src/components/operatorx/compare/hardware.ts +++ b/packages/app/src/components/operatorx/compare/hardware.ts @@ -10,16 +10,8 @@ export function hardwareLabel(key: string): string { return OPERATORX_HARDWARE_REFERENCES[key]?.label ?? HW_REGISTRY[key]?.label ?? key.toUpperCase(); } -/** Cool crimson for Ascend, separate from AMD's warm red/orange hue range (12–42). */ export function operatorXColors(keys: string[], theme: 'light' | 'dark'): Record { - const colors = generateVendorColors( - keys.filter((key) => key !== 'ascend950pr'), - theme, - ); - if (keys.includes('ascend950pr')) { - colors.ascend950pr = theme === 'dark' ? 'oklch(0.72 0.20 355)' : 'oklch(0.48 0.19 355)'; - } - return colors; + return generateVendorColors(keys, theme); } export function sortHardware(keys: string[]): string[] { diff --git a/packages/app/src/components/ubenchx/UbenchxContent.tsx b/packages/app/src/components/ubenchx/UbenchxContent.tsx index 2d373e1bd..d818caa60 100644 --- a/packages/app/src/components/ubenchx/UbenchxContent.tsx +++ b/packages/app/src/components/ubenchx/UbenchxContent.tsx @@ -2,6 +2,9 @@ import * as d3 from 'd3'; import { useMemo, useState } from 'react'; +import { useTheme } from 'next-themes'; +import { isDarkTheme } from '@/lib/themes/themes'; +import { generateVendorColors, getVendor } from '@/lib/charts/dynamic-colors'; import { Select, @@ -32,7 +35,6 @@ const GPU_COLORS: Record = { MI355X: 'oklch(0.62 0.20 18)', MI325X: 'oklch(0.55 0.20 28)', MI300X: 'oklch(0.48 0.20 38)', - 'Huawei 950PR': '#C7000B', }; const STRINGS = { @@ -154,8 +156,13 @@ function paddedDomain(values: number[]): [number, number] { const tooltipLine = (label: string, value: string) => `
${label}: ${value}
`; -function formatTooltip(point: ChartPoint, isPinned: boolean, t: (typeof STRINGS)[Locale]): string { - const color = GPU_COLORS[point.gpuKey] ?? '#888'; +function formatTooltip( + point: ChartPoint, + isPinned: boolean, + t: (typeof STRINGS)[Locale], + colors: Record, +): string { + const color = colors[point.gpuKey] ?? '#888'; return `
${isPinned ? `
${t.dismiss}
` : ''} @@ -171,14 +178,20 @@ function formatTooltip(point: ChartPoint, isPinned: boolean, t: (typeof STRINGS) `; } -function GpuLegend({ gpuKeys }: { gpuKeys: readonly string[] }) { +function GpuLegend({ + gpuKeys, + colors, +}: { + gpuKeys: readonly string[]; + colors: Record; +}) { return (
{gpuKeys.map((key) => ( {key} @@ -194,6 +207,8 @@ function UbenchxChart({ yLabel, yScaleType, locale, + colors, + theme, }: { chartId: string; allPoints: ChartPoint[]; @@ -201,6 +216,8 @@ function UbenchxChart({ yLabel: string; yScaleType: 'log' | 'linear'; locale: Locale; + colors: Record; + theme: 'light' | 'dark'; }) { const t = STRINGS[locale]; @@ -240,6 +257,7 @@ function UbenchxChart({ return ( chartId={chartId} + displayIdentity={theme} data={allPoints} height={440} margin={{ top: 24, right: 20, bottom: 62, left: 78 }} @@ -272,7 +290,7 @@ function UbenchxChart({ key: `ubenchx-line-${chartId}`, lines, config: { - getColor: (key) => GPU_COLORS[key] ?? '#888', + getColor: (key) => colors[key] ?? '#888', strokeWidth: 2.25, curve: d3.curveMonotoneX, }, @@ -286,7 +304,7 @@ function UbenchxChart({ getCy: () => 0, getX: (point) => point.x, getY: (point) => point.y, - getColor: (point) => GPU_COLORS[point.gpuKey] ?? '#888', + getColor: (point) => colors[point.gpuKey] ?? '#888', getRadius: () => 3.5, stroke: 'var(--background)', strokeWidth: 1, @@ -304,13 +322,24 @@ function UbenchxChart({ getRulerX: (point, scale) => (scale as d3.ScaleLogarithmic)(point.x), getRulerY: (point, scale) => scale(point.y), attachToLayer: 1, - content: (point, isPinned) => formatTooltip(point, isPinned, STRINGS[locale]), + content: (point, isPinned) => formatTooltip(point, isPinned, STRINGS[locale], colors), }} /> ); } export function UbenchxContent() { + const theme = isDarkTheme(useTheme().resolvedTheme) ? 'dark' : 'light'; + const colors = useMemo( + () => ({ + ...GPU_COLORS, + ...generateVendorColors( + UBENCHX_GPU_KEYS.filter((key) => getVendor(key) === 'huawei'), + theme, + ), + }), + [theme], + ); const locale = useLocale(); const t = STRINGS[locale]; @@ -377,7 +406,7 @@ export function UbenchxContent() {
- +

{t[metric.title]}

@@ -390,6 +419,8 @@ export function UbenchxContent() { yLabel={t[metric.label]} yScaleType={metric.scale} locale={locale} + colors={colors} + theme={theme} />
diff --git a/packages/app/src/lib/charts/chart-utils.test.ts b/packages/app/src/lib/charts/chart-utils.test.ts index f70bc4e23..326bee25a 100644 --- a/packages/app/src/lib/charts/chart-utils.test.ts +++ b/packages/app/src/lib/charts/chart-utils.test.ts @@ -1,3 +1,5 @@ +import { hsl } from 'd3'; +import { generateVendorColors } from './dynamic-colors'; import { describe, it, expect, vi } from 'vitest'; import iwanthue from 'iwanthue'; @@ -1579,3 +1581,31 @@ describe('metricChartTitle', () => { expect(metricChartTitle(bare, 'y_tpPerGpu', 'en')).toBe('Token Throughput per Chip'); }); }); + +it.each(['light', 'dark'] as const)( + 'keeps Huawei high-contrast colors consistent and amber (%s)', + (theme) => { + const one = generateHighContrastColors(['ascend950pr', 'mi355x', 'h100'], theme); + expect(one.ascend950pr).toBe(generateVendorColors(['ascend950pr'], theme).ascend950pr); + const many = generateHighContrastColors( + ['ascend950pr_bf16', 'ascend950pr_fp8', 'ascend910b3'], + theme, + ); + expect(new Set(Object.values(many)).size).toBe(3); + for (const color of Object.values(many)) { + const [r, g, b] = parseRgb(color); + expect(r).toBeGreaterThan(b); + expect(g).toBeGreaterThan(b); + } + }, +); + +it('reserves amber from other high-contrast series when Huawei is present, including cached palettes', () => { + const other = [...Array.from({ length: 8 }, (_, i) => `nvidia_series${i}`), 'amd_series']; + generateHighContrastColors(other, 'dark'); + const colors = generateHighContrastColors([...other, 'ascend950pr'], 'dark'); + for (const key of other) { + const hue = hsl(colors[key]).h; + expect(hue < 35 || hue > 65, `${key}: ${colors[key]}`).toBe(true); + } +}); diff --git a/packages/app/src/lib/charts/chart-utils.ts b/packages/app/src/lib/charts/chart-utils.ts index de9e3021f..dcbe7a97a 100644 --- a/packages/app/src/lib/charts/chart-utils.ts +++ b/packages/app/src/lib/charts/chart-utils.ts @@ -1,4 +1,9 @@ -import { GOOGLE_BLUE, resolveFrameworkAlias } from '@semianalysisai/inferencex-constants'; +import { + GOOGLE_BLUE, + HUAWEI_CHART_COLORS, + HUAWEI_CHART_PALETTE, + resolveFrameworkAlias, +} from '@semianalysisai/inferencex-constants'; import iwanthue from 'iwanthue'; import { chartPaletteTheme, isDarkTheme } from '@/lib/themes/themes'; @@ -23,6 +28,7 @@ const BANNED_HUE_TEST: Record boolean) | null> = { amd: (hue) => hue >= 120 && hue <= 195, openai: (hue) => hue < 290 || hue > 350, google: (hue) => hue < 255 || hue > 300, + huawei: null, unknown: null, }; @@ -34,6 +40,7 @@ const PREFERRED_ZONE: Record< amd: { hmin: 20, hmax: 50, cmin: 70, lmin: 50 }, openai: { hmin: 300, hmax: 340 }, google: { hmin: 265, hmax: 295 }, + huawei: HUAWEI_CHART_PALETTE.lab, unknown: null, }; @@ -66,9 +73,14 @@ export const generateHighContrastColors = ( } const multiVendor = groups.size > 1; + const reserveHuawei = groups.has('huawei'); for (const [vendor, vendorKeys] of groups) { const count = vendorKeys.length; + if (vendor === 'huawei' && count === 1) { + colors[vendorKeys[0]] = HUAWEI_CHART_COLORS[isDarkTheme(theme) ? 'dark' : 'light']; + continue; + } if (vendor === 'google' && count === 1) { colors[vendorKeys[0]] = GOOGLE_BLUE; continue; @@ -76,11 +88,13 @@ export const generateHighContrastColors = ( const isBanned = BANNED_HUE_TEST[vendor] ?? null; const preferred = PREFERRED_ZONE[vendor] ?? null; - const usePreferred = multiVendor && preferred && count <= PREFERRED_MAX; + const usePreferred = + preferred && ((multiVendor && count <= PREFERRED_MAX) || vendor === 'huawei'); const useBan = multiVendor && !usePreferred && isBanned && count <= BAN_MAX; + const excludeAmber = reserveHuawei && vendor !== 'huawei'; const mode = usePreferred ? 'pref' : useBan ? 'ban' : 'open'; - const cacheKey = `${vendor}|${theme}|${count}|${mode}|${lmin}|${lmax}`; + const cacheKey = `${vendor}|${theme}|${count}|${mode}|${lmin}|${lmax}|${excludeAmber}`; let palette = PALETTE_CACHE.get(cacheKey); if (!palette) { @@ -95,14 +109,20 @@ export const generateHighContrastColors = ( lmax, } : { hmin: 0, hmax: 360, cmin: 30, cmax: 100, lmin, lmax }, - ...(useBan && - isBanned && { - colorFilter: (_rgb: [number, number, number], lab: [number, number, number]) => { - if (lab[0] < lmin || lab[0] > lmax) return false; - const hue = ((Math.atan2(lab[2], lab[1]) * 180) / Math.PI + 360) % 360; - return !isBanned(hue); - }, - }), + ...((useBan || excludeAmber) && { + colorFilter: (_rgb: [number, number, number], lab: [number, number, number]) => { + if (lab[0] < lmin || lab[0] > lmax) return false; + const hue = ((Math.atan2(lab[2], lab[1]) * 180) / Math.PI + 360) % 360; + return ( + !(useBan && isBanned?.(hue)) && + !( + excludeAmber && + hue >= HUAWEI_CHART_PALETTE.reservedLab.hmin && + hue <= HUAWEI_CHART_PALETTE.reservedLab.hmax + ) + ); + }, + }), seed: `${vendor}-${theme}`, clustering: 'force-vector', quality: 50, diff --git a/packages/app/src/lib/charts/dynamic-colors.test.ts b/packages/app/src/lib/charts/dynamic-colors.test.ts index c2bff0e50..d4e338539 100644 --- a/packages/app/src/lib/charts/dynamic-colors.test.ts +++ b/packages/app/src/lib/charts/dynamic-colors.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { hsl } from 'd3'; +import { hsl, rgb } from 'd3'; import { VENDOR_OKLCH_ZONES, VENDOR_HSL_ZONES } from '@semianalysisai/inferencex-constants'; @@ -160,3 +160,59 @@ describe('TPUv7 vendor colors', () => { expect(VENDOR_OKLCH_ZONES.openai.start).toBeGreaterThan(VENDOR_OKLCH_ZONES.google.end); }); }); + +function luminance(css: string): number { + const c = rgb(css); + const values = [c.r, c.g, c.b] + .map((v) => v / 255) + .map((v) => (v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4)); + return values[0] * 0.2126 + values[1] * 0.7152 + values[2] * 0.0722; +} + +function contrastAgainst(color: string, background: string): number { + const a = luminance(color), + b = luminance(background); + return (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05); +} + +describe('Huawei shared identity colors', () => { + it('recognizes canonical, display, SKU and framework keys without catching unrelated words', () => { + for (const key of [ + 'ascend950pr', + 'Huawei 950PR', + 'Ascend950PR_9579', + 'ascend950pr_torch_npu_linear', + 'huawei_950pr', + 'ascend910b3', + ]) + expect(getVendor(key)).toBe('huawei'); + expect(getVendor('ascendant')).toBe('unknown'); + expect(getVendor('other_huawei')).toBe('unknown'); + }); + it.each(['light', 'dark'] as const)( + 'uses the same readable amber for aliases and preserves other vendor colors (%s)', + (theme) => { + const other = ['mi300x', 'mi355x', 'h100', 'tpuv7', 'jalapeno', 'mystery']; + const before = generateVendorColors(other, theme), + after = generateVendorColors([...other, 'ascend950pr'], theme); + for (const key of other) expect(after[key]).toBe(before[key]); + const color = after.ascend950pr; + expect(generateVendorColors(['Huawei 950PR'], theme)['Huawei 950PR']).toBe(color); + expect(hsl(color).h).toBeGreaterThan(35); + expect(hsl(color).h).toBeLessThan(55); + expect( + contrastAgainst(color, theme === 'light' ? '#ffffff' : '#111214'), + ).toBeGreaterThanOrEqual(4.5); + expect(generateGpuDateColors(['ascend950pr'], 1, theme)['0_ascend950pr']).toBe(color); + const dates = generateGpuDateColors(['ascend950pr'], 3, theme); + expect(hueOf(dates['0_ascend950pr'])).toBeCloseTo(hueOf(dates['2_ascend950pr']), 6); + expect(dates['0_ascend950pr']).not.toBe(dates['2_ascend950pr']); + }, + ); + it('keeps multiple Huawei chips in their own amber family', () => { + const colors = generateVendorColors(['ascend950pr', 'ascend910b3'], 'dark'); + expect(hueOf(colors.ascend950pr)).toBeGreaterThanOrEqual(80); + expect(hueOf(colors.ascend950pr)).toBeLessThanOrEqual(100); + expect(colors.ascend950pr).not.toBe(colors.ascend910b3); + }); +}); diff --git a/packages/app/src/lib/charts/dynamic-colors.ts b/packages/app/src/lib/charts/dynamic-colors.ts index 87c16f606..965c1440d 100644 --- a/packages/app/src/lib/charts/dynamic-colors.ts +++ b/packages/app/src/lib/charts/dynamic-colors.ts @@ -1,10 +1,17 @@ -import { GOOGLE_BLUE, GPU_VENDORS, VENDOR_OKLCH_ZONES } from '@semianalysisai/inferencex-constants'; +import { + GOOGLE_BLUE, + HUAWEI_CHART_COLORS, + GPU_VENDORS, + VENDOR_OKLCH_ZONES, +} from '@semianalysisai/inferencex-constants'; import { getModelSortIndex } from '@/lib/catalog/constants'; -export type Vendor = 'nvidia' | 'amd' | 'openai' | 'google' | 'unknown'; +export type Vendor = 'nvidia' | 'amd' | 'openai' | 'google' | 'huawei' | 'unknown'; export function getVendor(hwKey: string): Vendor { - const base = hwKey.split('_')[0]; + const normalized = hwKey.trim().toLowerCase(); + if (/^(?:huawei|ascend)(?=\d|[\s_-]|$)/u.test(normalized)) return 'huawei'; + const base = normalized.split('_')[0]; if (base === 'nvidia' || base === 'amd' || base === 'openai' || base === 'google') return base; const vendor = GPU_VENDORS[base]; if (vendor === 'NVIDIA') return 'nvidia'; @@ -48,6 +55,10 @@ export function generateVendorColors( const zone = VENDOR_OKLCH_ZONES[vendor]; const chroma = zone.chroma[theme]; const count = keys.length; + if (vendor === 'huawei' && count === 1) { + result[keys[0]] = HUAWEI_CHART_COLORS[theme]; + continue; + } if (vendor === 'google' && count === 1) { result[keys[0]] = GOOGLE_BLUE; continue; @@ -93,11 +104,19 @@ export function generateGpuDateColors( const zone = VENDOR_OKLCH_ZONES[vendor]; const chroma = zone.chroma[theme]; const gpuCount = keys.length; - const googleBase = vendor === 'google' && gpuCount === 1 ? hexToOklch(GOOGLE_BLUE) : null; + const fixedColor = + gpuCount === 1 + ? vendor === 'google' + ? GOOGLE_BLUE + : vendor === 'huawei' + ? HUAWEI_CHART_COLORS[theme] + : null + : null; + const fixedBase = fixedColor ? hexToOklch(fixedColor) : null; for (let gi = 0; gi < gpuCount; gi++) { const hue = - googleBase?.[2] ?? + fixedBase?.[2] ?? (gpuCount <= 1 ? (zone.start + zone.end) / 2 : zone.start + ((gi + 0.5) / gpuCount) * (zone.end - zone.start)); @@ -107,9 +126,9 @@ export function generateGpuDateColors( dateCount <= 1 ? (lMin + lMax) / 2 : lMax - (di / (dateCount - 1)) * (lMax - lMin); const compositeKey = `${di}_${keys[gi]}`; result[compositeKey] = - googleBase && dateCount === 1 - ? GOOGLE_BLUE - : `oklch(${lightness.toFixed(3)} ${googleBase?.[1] ?? chroma} ${hue.toFixed(1)})`; + fixedBase && dateCount === 1 + ? fixedColor! + : `oklch(${lightness.toFixed(3)} ${fixedBase?.[1] ?? chroma} ${hue.toFixed(1)})`; } } } diff --git a/packages/constants/src/gpu-keys.ts b/packages/constants/src/gpu-keys.ts index 232650fbb..6bda20f78 100644 --- a/packages/constants/src/gpu-keys.ts +++ b/packages/constants/src/gpu-keys.ts @@ -161,10 +161,21 @@ export const GPU_VENDORS: Record = Object.fromEntries( export const GOOGLE_BLUE = '#4285F4'; +export const HUAWEI_CHART_PALETTE = { + colors: { light: '#946A00', dark: '#EBC04A' }, + oklch: { start: 80, end: 100, chroma: { light: 0.14, dark: 0.16 } }, + // iwanthue uses CIE Lab hue, not OKLCH. + lab: { hmin: 75, hmax: 100 }, + reservedLab: { hmin: 60, hmax: 115 }, +} as const; + +export const HUAWEI_CHART_COLORS = HUAWEI_CHART_PALETTE.colors; + export const VENDOR_OKLCH_ZONES: Record< string, { start: number; end: number; chroma: { light: number; dark: number } } > = { + huawei: HUAWEI_CHART_PALETTE.oklch, amd: { start: 12, end: 42, chroma: { light: 0.18, dark: 0.22 } }, nvidia: { start: 120, end: 170, chroma: { light: 0.15, dark: 0.15 } }, openai: { start: 290, end: 330, chroma: { light: 0.16, dark: 0.18 } }, diff --git a/packages/skills/skills/inferencex-api/integrity.json b/packages/skills/skills/inferencex-api/integrity.json index bf69dab0f..889778a9e 100644 --- a/packages/skills/skills/inferencex-api/integrity.json +++ b/packages/skills/skills/inferencex-api/integrity.json @@ -9,7 +9,7 @@ "references/cli-contract.md": "4f070877eeefffad2860c58f79f68432fbe27aa21fc578a1c696f0309a3d7d7b", "references/cli.md": "11e1183f7ed70d669019e8435ac16a6a9f873de027507b8850a9943f6659bfd9", "references/collectivex.md": "f79cf8e2779a9289ab69f79f4a4b338e0b9a5f76a3750a3e969f33a744e726a1", - "references/dashboard-views.md": "24c1f4d8bebf776a2ddfda588baa81f584c770c0d71ab8011bfa1ec19b69082e", + "references/dashboard-views.md": "5b39a62b5fa97d80cd529bcb0f3da58bc7e902117456d26f43cf9f8ea541b70a", "references/offline-exports.md": "95aa565dcd4c9e592159baa560a9a58217bc61e395de1c6daf0576795c7f4ec9", "references/pareto.md": "1b4d2d163f982e3f2d97addce310789eae9c1e5badd82dd7501708c9e0385027", "references/powerx.md": "5af5527d54a7e6191a5769080c122638b333b1ceb59e19fdee7df662a5cd3c8f", diff --git a/packages/skills/skills/inferencex-api/references/dashboard-views.md b/packages/skills/skills/inferencex-api/references/dashboard-views.md index 904d52d0f..10e33f122 100644 --- a/packages/skills/skills/inferencex-api/references/dashboard-views.md +++ b/packages/skills/skills/inferencex-api/references/dashboard-views.md @@ -641,3 +641,5 @@ switch/topology fields remain null (unspecified), not their NICs. The and CSV. MI355X's scale-up technology is fourth-generation Infinity Fabric. Chinese ownership-tier wording denotes owning hardware at hyperscaler purchase volume, not a rental quote; the wording does not change the underlying TCO rates. + +Vendor-series colors are presentation-only. Huawei/Ascend series use a shared, theme-aware amber identity across the chart generators; historical dates and high-contrast configurations may use related shades. Colors do not change API measurements, hardware identities, filters or normalization references. Sequential heatmap scales encode values rather than vendor identity.