This is an automated email from the ASF dual-hosted git repository.

pawarprasad123 pushed a commit to branch atlas-2.6
in repository https://gitbox.apache.org/repos/asf/atlas.git


The following commit(s) were added to refs/heads/atlas-2.6 by this push:
     new 218084d3a ATLAS-5329: Atlas React UI: Long classification names 
overflow and break the layout in the Classification Distribution (#681) ( 
cherry-icked from the commit 59081c4eaa9bf1b6c56daab79f5bd2241a7ea24b)
218084d3a is described below

commit 218084d3afd597c24abfddaed0d958de072cb001
Author: Brijesh Bhalala <[email protected]>
AuthorDate: Tue Jul 7 11:53:33 2026 +0530

    ATLAS-5329: Atlas React UI: Long classification names overflow and break 
the layout in the Classification Distribution (#681)
    ( cherry-icked from the commit 59081c4eaa9bf1b6c56daab79f5bd2241a7ea24b)
---
 .../ClassificationDistributionCard.tsx             |  23 +++--
 .../ClassificationDistributionCard.test.tsx        | 113 +++++++++++++++++++++
 .../__tests__/dashboardChartPalette.test.ts        | 112 ++++++++++++++++++++
 .../DashboardOverview/dashboardChartPalette.ts     |  38 ++++++-
 4 files changed, 275 insertions(+), 11 deletions(-)

diff --git 
a/dashboard/src/views/DashboardOverview/ClassificationDistributionCard.tsx 
b/dashboard/src/views/DashboardOverview/ClassificationDistributionCard.tsx
index 6da4704c7..41b2741be 100644
--- a/dashboard/src/views/DashboardOverview/ClassificationDistributionCard.tsx
+++ b/dashboard/src/views/DashboardOverview/ClassificationDistributionCard.tsx
@@ -38,6 +38,9 @@ import { navigateToSearch, navigateToClassificationSearch } 
from "@utils/dashboa
 import {
        CHART_BAR_ACTIVE_BLUE,
        CLASSIFICATION_DISTRIBUTION_CHART_MARGIN,
+       getClassificationYAxisWidth,
+       isClassificationYAxisLabelTruncated,
+       truncateClassificationYAxisLabel,
 } from "./dashboardChartPalette";
 
 const BAR_COLOR = CHART_BAR_ACTIVE_BLUE;
@@ -51,6 +54,10 @@ const ClassificationDistributionCard = memo(({ tag, 
isLoading }: ClassificationD
        const navigate = useNavigate();
        const data = getClassificationDistribution(tag, 5);
        const associationTotal = useMemo(() => 
getTagEntityAssociationTotal(tag), [tag]);
+       const yAxisWidth = useMemo(
+               () => getClassificationYAxisWidth(data.map((item) => 
item.name)),
+               [data],
+       );
 
        const handleBarClick = useCallback(
                (entry: { name: string }) => {
@@ -155,18 +162,14 @@ const ClassificationDistributionCard = memo(({ tag, 
isLoading }: ClassificationD
                                                        <YAxis
                                                                type="category"
                                                                dataKey="name"
-                                                               width={52}
-                                                               label={{
-                                                                       value: 
"Classification",
-                                                                       angle: 
-90,
-                                                                       
position: "left",
-                                                                       offset: 
2,
-                                                                       style: 
{ fontSize: 10, fill: "#6c757d", textAnchor: "middle" },
-                                                               }}
+                                                               
width={yAxisWidth}
+                                                               tickMargin={4}
                                                                tick={(props: 
Record<string, unknown>) => {
                                                                        const { 
x = 0, y = 0, payload } = props;
                                                                        const p 
= payload as { value?: string; name?: string } | undefined;
                                                                        const 
value = p?.value ?? p?.name ?? (typeof payload === "string" ? payload : "");
+                                                                       const 
displayLabel = truncateClassificationYAxisLabel(value);
+                                                                       const 
isTruncated = isClassificationYAxisLabelTruncated(value);
                                                                        return (
                                                                                
<g
                                                                                
        transform={`translate(${x},${y})`}
@@ -174,6 +177,7 @@ const ClassificationDistributionCard = memo(({ tag, 
isLoading }: ClassificationD
                                                                                
        style={{ cursor: value ? "pointer" : "default" }}
                                                                                
        role={value ? "button" : undefined}
                                                                                
        tabIndex={value ? 0 : undefined}
+                                                                               
        aria-label={value || undefined}
                                                                                
        onKeyDown={
                                                                                
                value
                                                                                
                        ? (e: React.KeyboardEvent<SVGGElement>) => {
@@ -185,8 +189,9 @@ const ClassificationDistributionCard = memo(({ tag, 
isLoading }: ClassificationD
                                                                                
                        : undefined
                                                                                
        }
                                                                                
>
+                                                                               
        {isTruncated ? <title>{value}</title> : null}
                                                                                
        <text x={0} y={0} dy={4} textAnchor="end" fill="#333" fontSize={12}>
-                                                                               
                {value}
+                                                                               
                {displayLabel}
                                                                                
        </text>
                                                                                
</g>
                                                                        );
diff --git 
a/dashboard/src/views/DashboardOverview/__tests__/ClassificationDistributionCard.test.tsx
 
b/dashboard/src/views/DashboardOverview/__tests__/ClassificationDistributionCard.test.tsx
new file mode 100644
index 000000000..8e423c160
--- /dev/null
+++ 
b/dashboard/src/views/DashboardOverview/__tests__/ClassificationDistributionCard.test.tsx
@@ -0,0 +1,113 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import ClassificationDistributionCard from '../ClassificationDistributionCard';
+import {
+       CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH,
+       CLASSIFICATION_Y_AXIS_LABEL_SUFFIX,
+} from '../dashboardChartPalette';
+
+jest.mock('@utils/Helper', () => ({
+       numberFormatWithComma: (n: number | string) => String(n),
+}));
+
+const mockNavigateToSearch = jest.fn();
+const mockNavigateToClassificationSearch = jest.fn();
+jest.mock('@utils/dashboardSearchUtils', () => ({
+       navigateToSearch: (...args: unknown[]) => mockNavigateToSearch(...args),
+       navigateToClassificationSearch: (...args: unknown[]) =>
+               mockNavigateToClassificationSearch(...args),
+}));
+
+const shortName = 'PII';
+const longName = 'a'.repeat(CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH + 10);
+const truncatedLongName = 
`${'a'.repeat(CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH)}${CLASSIFICATION_Y_AXIS_LABEL_SUFFIX}`;
+
+jest.mock('@utils/metricsUtils', () => ({
+       getClassificationDistribution: jest.fn(() => [
+               { name: shortName, count: 12 },
+               { name: longName, count: 8 },
+       ]),
+       getTagEntityAssociationTotal: jest.fn(() => 20),
+}));
+
+jest.mock('recharts', () => ({
+       ResponsiveContainer: ({ children }: { children?: React.ReactNode }) => (
+               <div data-testid="rc">{children}</div>
+       ),
+       BarChart: ({ children }: { children?: React.ReactNode }) => (
+               <div data-testid="bar-chart">{children}</div>
+       ),
+       CartesianGrid: () => <div data-testid="grid" />,
+       XAxis: () => <div data-testid="x-axis" />,
+       YAxis: ({ tick }: { tick?: React.ComponentType<Record<string, unknown>> 
}) => {
+               const Tick = tick;
+               if (!Tick) return null;
+               return (
+                       <div data-testid="y-axis">
+                               <Tick x={10} y={20} payload={{ value: shortName 
}} />
+                               <Tick x={10} y={40} payload={{ value: longName 
}} />
+                       </div>
+               );
+       },
+       Tooltip: () => <div data-testid="tooltip-mock" />,
+       Bar: ({ children }: { children?: React.ReactNode }) => (
+               <div data-testid="bar">{children}</div>
+       ),
+       Cell: () => <div data-testid="cell" />,
+       LabelList: () => <div data-testid="label-list" />,
+}));
+
+describe('ClassificationDistributionCard', () => {
+       beforeEach(() => {
+               jest.clearAllMocks();
+       });
+
+       it('renders short Y-axis labels without truncation or title tooltip', 
() => {
+               render(
+                       <MemoryRouter>
+                               <ClassificationDistributionCard tag={{}} />
+                       </MemoryRouter>,
+               );
+
+               expect(screen.getByText(shortName)).toBeInTheDocument();
+               const shortLabelGroup = 
screen.getByText(shortName).closest('g');
+               expect(shortLabelGroup?.querySelector('title')).toBeNull();
+       });
+
+       it('truncates long Y-axis labels and exposes full name in SVG title 
tooltip', () => {
+               render(
+                       <MemoryRouter>
+                               <ClassificationDistributionCard tag={{}} />
+                       </MemoryRouter>,
+               );
+
+               expect(screen.getByText(truncatedLongName)).toBeInTheDocument();
+
+               const truncatedLabelGroup = 
screen.getByText(truncatedLongName).closest('g');
+               const titleNode = truncatedLabelGroup?.querySelector('title');
+               expect(titleNode).not.toBeNull();
+               expect(titleNode?.textContent).toBe(longName);
+
+               const visibleTextNodes = 
truncatedLabelGroup?.querySelectorAll('text');
+               expect(visibleTextNodes?.length).toBe(1);
+               
expect(visibleTextNodes?.[0]?.textContent).toBe(truncatedLongName);
+       });
+});
diff --git 
a/dashboard/src/views/DashboardOverview/__tests__/dashboardChartPalette.test.ts 
b/dashboard/src/views/DashboardOverview/__tests__/dashboardChartPalette.test.ts
new file mode 100644
index 000000000..11e22f99a
--- /dev/null
+++ 
b/dashboard/src/views/DashboardOverview/__tests__/dashboardChartPalette.test.ts
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+       CLASSIFICATION_Y_AXIS_CHAR_WIDTH,
+       CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH,
+       CLASSIFICATION_Y_AXIS_LABEL_SUFFIX,
+       CLASSIFICATION_Y_AXIS_MAX_WIDTH,
+       CLASSIFICATION_Y_AXIS_MIN_WIDTH,
+       getChartYAxisWidth,
+       getClassificationYAxisWidth,
+       isChartYAxisLabelTruncated,
+       isClassificationYAxisLabelTruncated,
+       truncateChartYAxisLabel,
+       truncateClassificationYAxisLabel,
+} from '../dashboardChartPalette';
+
+describe('dashboardChartPalette', () => {
+       describe('truncateClassificationYAxisLabel', () => {
+               it('returns the label unchanged when within max length', () => {
+                       
expect(truncateClassificationYAxisLabel('PII')).toBe('PII');
+                       
expect(truncateClassificationYAxisLabel('a'.repeat(30))).toBe(
+                               'a'.repeat(30),
+                       );
+               });
+
+               it('truncates to 30 characters and appends ellipsis', () => {
+                       const fullName = 'a'.repeat(45);
+                       expect(truncateClassificationYAxisLabel(fullName)).toBe(
+                               
`${'a'.repeat(CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH)}${CLASSIFICATION_Y_AXIS_LABEL_SUFFIX}`,
+                       );
+               });
+       });
+
+       describe('isClassificationYAxisLabelTruncated', () => {
+               it('returns false for labels at or below max length', () => {
+                       
expect(isClassificationYAxisLabelTruncated('PII')).toBe(false);
+                       
expect(isClassificationYAxisLabelTruncated('a'.repeat(30))).toBe(false);
+               });
+
+               it('returns true for labels longer than max length', () => {
+                       
expect(isClassificationYAxisLabelTruncated('a'.repeat(31))).toBe(true);
+               });
+       });
+
+       describe('getClassificationYAxisWidth', () => {
+               it('returns minimum width when labels are empty', () => {
+                       expect(getClassificationYAxisWidth([])).toBe(
+                               CLASSIFICATION_Y_AXIS_MIN_WIDTH,
+                       );
+               });
+
+               it('returns minimum width for short classification names', () 
=> {
+                       expect(getClassificationYAxisWidth(['PII', 
'HIPAA'])).toBe(
+                               CLASSIFICATION_Y_AXIS_MIN_WIDTH,
+                       );
+               });
+
+               it('scales width from truncated display length', () => {
+                       const longLabel = 'a'.repeat(25);
+                       expect(getClassificationYAxisWidth(['PII', 
longLabel])).toBe(
+                               25 * CLASSIFICATION_Y_AXIS_CHAR_WIDTH,
+                       );
+               });
+
+               it('uses truncated length cap for very long classification 
names', () => {
+                       const veryLongLabel = 'a'.repeat(80);
+                       const truncatedLength =
+                               CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH +
+                               CLASSIFICATION_Y_AXIS_LABEL_SUFFIX.length;
+                       
expect(getClassificationYAxisWidth([veryLongLabel])).toBe(
+                               truncatedLength * 
CLASSIFICATION_Y_AXIS_CHAR_WIDTH,
+                       );
+               });
+
+               it('does not exceed maximum width', () => {
+                       const labels = Array.from({ length: 10 }, () => 
'x'.repeat(80));
+                       
expect(getClassificationYAxisWidth(labels)).toBeLessThanOrEqual(
+                               CLASSIFICATION_Y_AXIS_MAX_WIDTH,
+                       );
+               });
+       });
+
+       describe('shared chart Y-axis aliases', () => {
+               it('exposes the same helpers for service type and 
classification charts', () => {
+                       const label = 'a'.repeat(45);
+                       expect(truncateChartYAxisLabel(label)).toBe(
+                               truncateClassificationYAxisLabel(label),
+                       );
+                       expect(isChartYAxisLabelTruncated(label)).toBe(
+                               isClassificationYAxisLabelTruncated(label),
+                       );
+                       expect(getChartYAxisWidth([label])).toBe(
+                               getClassificationYAxisWidth([label]),
+                       );
+               });
+       });
+});
diff --git a/dashboard/src/views/DashboardOverview/dashboardChartPalette.ts 
b/dashboard/src/views/DashboardOverview/dashboardChartPalette.ts
index fb012a445..ec6c03ea7 100644
--- a/dashboard/src/views/DashboardOverview/dashboardChartPalette.ts
+++ b/dashboard/src/views/DashboardOverview/dashboardChartPalette.ts
@@ -33,10 +33,44 @@ export const HORIZONTAL_BAR_CHART_MARGIN = {
        bottom: 48,
 } as const;
 
-/** Tighter layout: short classification names — minimize dead space left of Y 
ticks */
+/** Classification bar chart: Y-axis width reserves label space; keep left 
margin minimal */
 export const CLASSIFICATION_DISTRIBUTION_CHART_MARGIN = {
        top: 8,
        right: 72,
-       left: 12,
+       left: 8,
        bottom: 48,
 } as const;
+
+export const CLASSIFICATION_Y_AXIS_MIN_WIDTH = 160;
+export const CLASSIFICATION_Y_AXIS_MAX_WIDTH = 360;
+export const CLASSIFICATION_Y_AXIS_CHAR_WIDTH = 8;
+export const CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH = 30;
+export const CLASSIFICATION_Y_AXIS_LABEL_SUFFIX = '...';
+
+/** Truncate Y-axis classification labels for display (full name shown via SVG 
title on hover). */
+export const truncateClassificationYAxisLabel = (label: string): string => {
+       if (label.length <= CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH) {
+               return label;
+       }
+       return `${label.slice(0, 
CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH)}${CLASSIFICATION_Y_AXIS_LABEL_SUFFIX}`;
+};
+
+export const isClassificationYAxisLabelTruncated = (label: string): boolean =>
+       label.length > CLASSIFICATION_Y_AXIS_LABEL_MAX_LENGTH;
+
+/** Estimate Y-axis width from truncated label length (12px font, end-anchored 
ticks). */
+export const getClassificationYAxisWidth = (labels: string[]): number => {
+       if (!labels.length) return CLASSIFICATION_Y_AXIS_MIN_WIDTH;
+       const longest = Math.max(
+               ...labels.map((label) => 
truncateClassificationYAxisLabel(label).length),
+       );
+       return Math.min(
+               CLASSIFICATION_Y_AXIS_MAX_WIDTH,
+               Math.max(CLASSIFICATION_Y_AXIS_MIN_WIDTH, longest * 
CLASSIFICATION_Y_AXIS_CHAR_WIDTH),
+       );
+};
+
+/** Shared by classification and service type distribution chart Y-axis ticks 
*/
+export const truncateChartYAxisLabel = truncateClassificationYAxisLabel;
+export const isChartYAxisLabelTruncated = isClassificationYAxisLabelTruncated;
+export const getChartYAxisWidth = getClassificationYAxisWidth;

Reply via email to