fix: improve application robustness and UI consistency

- Set default value for plugins in AppRouter to prevent potential errors.
- Add conditional rendering for ProfilerLatestValue to avoid rendering issues when props are undefined.
- Make profilerLatestValueProps optional in ProfilerStateWrapper interface for better flexibility.
- Refactor ColumnProfileTable to remove unused imports and optimize rendering logic.
- Replace Button with Typography for better styling in ColumnProfileTable.
- Update SingleColumnProfile to use Stack for layout consistency and include ColumnSummary when available.
- Enhance CardinalityDistributionChart and DataDistributionHistogram with new styling and layout using MUI components.
- Introduce DataPill styled component for consistent data display.
- Update color constants for improved visual consistency across charts.
- Modify data insight tooltip to conditionally display date in header for better clarity.
This commit is contained in:
Shailesh Parmar 2025-09-25 21:29:29 +05:30
parent f062908e56
commit 7ef9508f0a
12 changed files with 473 additions and 397 deletions

View File

@ -43,7 +43,7 @@ const AppRouter = () => {
isApplicationLoading,
isAuthenticating,
} = useApplicationStore();
const { plugins } = useApplicationsProvider();
const { plugins = [] } = useApplicationsProvider();
useEffect(() => {
const { pathname } = location;

View File

@ -57,7 +57,9 @@ const ProfilerStateWrapper = ({
boxShadow: 'none',
}}>
<Stack spacing={4}>
<ProfilerLatestValue {...profilerLatestValueProps} />
{profilerLatestValueProps && (
<ProfilerLatestValue {...profilerLatestValueProps} />
)}
<Box flexGrow={1}>{children}</Box>
</Stack>
</Card>

View File

@ -16,6 +16,6 @@ export interface ProfilerStateWrapperProps {
isLoading: boolean;
children: React.ReactNode;
title: string;
profilerLatestValueProps: ProfilerLatestValueProps;
profilerLatestValueProps?: ProfilerLatestValueProps;
dataTestId?: string;
}

View File

@ -12,19 +12,8 @@
*/
import { Grid, Stack, Typography, useTheme } from '@mui/material';
import { Button, Col, Row } from 'antd';
import { ColumnsType } from 'antd/lib/table';
import classNames from 'classnames';
import {
filter,
find,
groupBy,
isEmpty,
isUndefined,
map,
round,
toLower,
} from 'lodash';
import { isEmpty, round } from 'lodash';
import Qs from 'qs';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -38,10 +27,6 @@ import {
Table as TableType,
} from '../../../../../generated/entity/data/table';
import { Operation } from '../../../../../generated/entity/policies/policy';
import {
TestCase,
TestCaseStatus,
} from '../../../../../generated/tests/testCase';
import { usePaging } from '../../../../../hooks/paging/usePaging';
import useCustomLocation from '../../../../../hooks/useCustomLocation/useCustomLocation';
import { useFqn } from '../../../../../hooks/useFqn';
@ -49,26 +34,21 @@ import {
getTableColumnsByFQN,
searchTableColumnsByFQN,
} from '../../../../../rest/tableAPI';
import { getListTestCaseBySearch } from '../../../../../rest/testAPI';
import {
formatNumberWithComma,
getTableFQNFromColumnFQN,
} from '../../../../../utils/CommonUtils';
import { getEntityName } from '../../../../../utils/EntityUtils';
import {
generateEntityLink,
getTableExpandableConfig,
pruneEmptyChildren,
} from '../../../../../utils/TableUtils';
import ErrorPlaceHolder from '../../../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import FilterTablePlaceHolder from '../../../../common/ErrorWithPlaceholder/FilterTablePlaceHolder';
import { PagingHandlerParams } from '../../../../common/NextPrevious/NextPrevious.interface';
import { SummaryCard } from '../../../../common/SummaryCard/SummaryCard.component';
import { SummaryCardProps } from '../../../../common/SummaryCard/SummaryCard.interface';
import SummaryCardV1 from '../../../../common/SummaryCard/SummaryCardV1';
import Table from '../../../../common/Table/Table';
import { TableProfilerTab } from '../../ProfilerDashboard/profilerDashboard.interface';
import ColumnSummary from '../ColumnSummary';
import NoProfilerBanner from '../NoProfilerBanner/NoProfilerBanner.component';
import SingleColumnProfile from '../SingleColumnProfile';
import { ModifiedColumn } from '../TableProfiler.interface';
@ -95,8 +75,6 @@ const ColumnProfileTable = () => {
const isLoading = isTestsLoading || isProfilerDataLoading;
const [searchText, setSearchText] = useState<string>('');
const [data, setData] = useState<ModifiedColumn[]>([]);
const [isTestCaseLoading, setIsTestCaseLoading] = useState(false);
const [columnTestCases, setColumnTestCases] = useState<TestCase[]>([]);
const [isColumnsLoading, setIsColumnsLoading] = useState(false);
const {
currentPage,
@ -138,14 +116,20 @@ const ColumnProfileTable = () => {
fixed: 'left',
render: (_, record) => {
return (
<Button
<Typography
className="break-word p-0"
type="link"
sx={{
color: theme.palette.primary.main,
fontSize: theme.typography.pxToRem(14),
fontWeight: theme.typography.fontWeightMedium,
cursor: 'pointer',
'&:hover': { textDecoration: 'underline' },
}}
onClick={() =>
updateActiveColumnFqn(record.fullyQualifiedName || '')
}>
{getEntityName(record)}
</Button>
</Typography>
);
},
sorter: (col1, col2) => col1.name.localeCompare(col2.name),
@ -157,7 +141,11 @@ const ColumnProfileTable = () => {
width: 150,
render: (dataTypeDisplay: string) => {
return (
<Typography className="break-word">
<Typography
className="break-word"
sx={{
fontSize: theme.typography.pxToRem(14),
}}>
{dataTypeDisplay || 'N/A'}
</Typography>
);
@ -286,51 +274,11 @@ const ColumnProfileTable = () => {
];
}, [testCaseSummary]);
const selectedColumn = useMemo(() => {
return find(
data,
(column: Column) => column.fullyQualifiedName === activeColumnFqn
);
}, [data, activeColumnFqn]);
const selectedColumnTestsObj = useMemo(() => {
const temp = filter(
columnTestCases,
(test: TestCase) => !isUndefined(test.testCaseResult)
);
const statusDict = {
[TestCaseStatus.Success]: [],
[TestCaseStatus.Aborted]: [],
[TestCaseStatus.Failed]: [],
...groupBy(temp, 'testCaseResult.testCaseStatus'),
};
return { statusDict, totalTests: temp.length };
}, [columnTestCases]);
const handleSearchAction = (searchText: string) => {
setSearchText(searchText);
handlePageChange(1);
};
const fetchColumnTestCase = async (activeColumnFqn: string) => {
setIsTestCaseLoading(true);
try {
const { data } = await getListTestCaseBySearch({
fields: TabSpecificField.TEST_CASE_RESULT,
entityLink: generateEntityLink(activeColumnFqn),
limit: PAGE_SIZE_LARGE,
});
setColumnTestCases(data);
} catch {
setColumnTestCases([]);
} finally {
setIsTestCaseLoading(false);
}
};
const fetchTableColumnWithProfiler = useCallback(
async (page: number, searchText: string) => {
if (!tableFqn) {
@ -382,14 +330,6 @@ const ColumnProfileTable = () => {
}
}, [tableFqn, currentPage, searchText, pageSize]);
useEffect(() => {
if (activeColumnFqn) {
fetchColumnTestCase(activeColumnFqn);
} else {
setColumnTestCases([]);
}
}, [activeColumnFqn]);
const pagingProps = useMemo(() => {
return {
currentPage: currentPage,
@ -424,77 +364,43 @@ const ColumnProfileTable = () => {
return (
<Stack data-testid="column-profile-table-container" spacing="30px">
{!isLoading && !isProfilingEnabled && <NoProfilerBanner />}
<Col span={24}>
<Grid container spacing={5}>
{overallSummary?.map((summary) => (
<Grid key={summary.title} size="grow">
<SummaryCardV1
extra={summary.extra}
icon={summary.icon}
isLoading={isLoading}
title={summary.title}
value={summary.value}
/>
</Grid>
))}
</Grid>
<Row gutter={[16, 16]}>
{!isUndefined(selectedColumn) && (
<Col span={10}>
<ColumnSummary column={selectedColumn} />
</Col>
)}
<Col span={selectedColumn ? 14 : 24}>
<Row
wrap
className={classNames(
activeColumnFqn ? 'justify-start' : 'justify-between'
)}
gutter={[16, 16]}>
{!isEmpty(activeColumnFqn) &&
map(selectedColumnTestsObj.statusDict, (data, key) => (
<Col key={key}>
<SummaryCard
showProgressBar
isLoading={isTestCaseLoading}
title={key}
total={selectedColumnTestsObj.totalTests}
type={toLower(key) as SummaryCardProps['type']}
value={data.length}
/>
</Col>
))}
</Row>
</Col>
</Row>
</Col>
<Grid container spacing={5}>
{overallSummary?.map((summary) => (
<Grid key={summary.title} size="grow">
<SummaryCardV1
extra={summary.extra}
icon={summary.icon}
isLoading={isLoading}
title={summary.title}
value={summary.value}
/>
</Grid>
))}
</Grid>
{isEmpty(activeColumnFqn) ? (
<Col span={24}>
<Table
columns={tableColumn}
customPaginationProps={pagingProps}
dataSource={data}
expandable={getTableExpandableConfig<Column>()}
loading={isColumnsLoading || isLoading}
locale={{
emptyText: <FilterTablePlaceHolder />,
}}
pagination={false}
rowKey="name"
scroll={{ x: true }}
searchProps={searchProps}
size="small"
/>
</Col>
<Table
columns={tableColumn}
customPaginationProps={pagingProps}
dataSource={data}
expandable={getTableExpandableConfig<Column>()}
loading={isColumnsLoading || isLoading}
locale={{
emptyText: <FilterTablePlaceHolder />,
}}
pagination={false}
rowKey="name"
scroll={{ x: true }}
searchProps={searchProps}
size="small"
/>
) : (
<Col span={24}>
<SingleColumnProfile
activeColumnFqn={activeColumnFqn}
dateRangeObject={dateRangeObject}
tableDetails={tableDetailsWithColumns}
/>
</Col>
<SingleColumnProfile
activeColumnFqn={activeColumnFqn}
dateRangeObject={dateRangeObject}
tableDetails={tableDetailsWithColumns}
/>
)}
</Stack>
);

View File

@ -10,9 +10,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Card, Col, Row, Typography } from 'antd';
import { Stack } from '@mui/material';
import { AxiosError } from 'axios';
import { first, isString, last, pick } from 'lodash';
import { find, first, isString, last, pick } from 'lodash';
import { DateRangeObject } from 'Models';
import { FC, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -20,7 +20,10 @@ import {
DEFAULT_RANGE_DATA,
INITIAL_COLUMN_METRICS_VALUE,
} from '../../../../constants/profiler.constant';
import { ColumnProfile } from '../../../../generated/entity/data/container';
import {
Column,
ColumnProfile,
} from '../../../../generated/entity/data/container';
import { Table } from '../../../../generated/entity/data/table';
import { getColumnProfilerList } from '../../../../rest/tableAPI';
import { Transi18next } from '../../../../utils/CommonUtils';
@ -35,6 +38,8 @@ import { showErrorToast } from '../../../../utils/ToastUtils';
import CardinalityDistributionChart from '../../../Visualisations/Chart/CardinalityDistributionChart.component';
import DataDistributionHistogram from '../../../Visualisations/Chart/DataDistributionHistogram.component';
import ProfilerDetailsCard from '../ProfilerDetailsCard/ProfilerDetailsCard';
import ProfilerStateWrapper from '../ProfilerStateWrapper/ProfilerStateWrapper.component';
import ColumnSummary from './ColumnSummary';
import CustomMetricGraphs from './CustomMetricGraphs/CustomMetricGraphs.component';
import { useTableProfiler } from './TableProfilerProvider';
@ -63,6 +68,13 @@ const SingleColumnProfile: FC<SingleColumnProfileProps> = ({
[]
);
const selectedColumn = useMemo(() => {
return find(
tableDetails?.columns ?? [],
(column: Column) => column.fullyQualifiedName === activeColumnFqn
);
}, [tableDetails, activeColumnFqn]);
const customMetrics = useMemo(
() =>
getColumnCustomMetric(
@ -151,115 +163,85 @@ const SingleColumnProfile: FC<SingleColumnProfileProps> = ({
}, [activeColumnFqn, dateRangeObject]);
return (
<Row
<Stack
className="m-b-lg"
data-testid="profiler-tab-container"
gutter={[16, 16]}>
<Col span={24}>
<ProfilerDetailsCard
chartCollection={columnMetric.countMetrics}
isLoading={isLoading}
name="count"
noDataPlaceholderText={noProfilerMessage}
title={t('label.data-count-plural')}
/>
</Col>
<Col span={24}>
<ProfilerDetailsCard
chartCollection={columnMetric.proportionMetrics}
isLoading={isLoading}
name="proportion"
noDataPlaceholderText={noProfilerMessage}
tickFormatter="%"
title={t('label.data-proportion-plural')}
/>
</Col>
<Col span={24}>
<ProfilerDetailsCard
chartCollection={columnMetric.mathMetrics}
isLoading={isLoading}
name="math"
noDataPlaceholderText={noProfilerMessage}
showYAxisCategory={isMinMaxStringData}
// only min/max category can be string
title={t('label.data-range')}
/>
</Col>
<Col span={24}>
<ProfilerDetailsCard
chartCollection={columnMetric.sumMetrics}
isLoading={isLoading}
name="sum"
noDataPlaceholderText={noProfilerMessage}
title={t('label.data-aggregate')}
/>
</Col>
<Col span={24}>
<ProfilerDetailsCard
chartCollection={columnMetric.quartileMetrics}
isLoading={isLoading}
name="quartile"
noDataPlaceholderText={noProfilerMessage}
title={t('label.data-quartile-plural')}
/>
</Col>
spacing="30px">
{selectedColumn && <ColumnSummary column={selectedColumn} />}
<ProfilerDetailsCard
chartCollection={columnMetric.countMetrics}
isLoading={isLoading}
name="count"
noDataPlaceholderText={noProfilerMessage}
title={t('label.data-count-plural')}
/>
<ProfilerDetailsCard
chartCollection={columnMetric.proportionMetrics}
isLoading={isLoading}
name="proportion"
noDataPlaceholderText={noProfilerMessage}
tickFormatter="%"
title={t('label.data-proportion-plural')}
/>
<ProfilerDetailsCard
chartCollection={columnMetric.mathMetrics}
isLoading={isLoading}
name="math"
noDataPlaceholderText={noProfilerMessage}
showYAxisCategory={isMinMaxStringData}
// only min/max category can be string
title={t('label.data-range')}
/>
<ProfilerDetailsCard
chartCollection={columnMetric.sumMetrics}
chartType="area"
isLoading={isLoading}
name="sum"
noDataPlaceholderText={noProfilerMessage}
title={t('label.data-aggregate')}
/>
<ProfilerDetailsCard
chartCollection={columnMetric.quartileMetrics}
isLoading={isLoading}
name="quartile"
noDataPlaceholderText={noProfilerMessage}
title={t('label.data-quartile-plural')}
/>
{firstDay?.histogram || currentDay?.histogram ? (
<Col span={24}>
<Card
className="shadow-none global-border-radius"
data-testid="histogram-metrics"
loading={isLoading}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Typography.Title
data-testid="data-distribution-title"
level={5}>
{t('label.data-distribution')}
</Typography.Title>
</Col>
<Col span={24}>
<DataDistributionHistogram
data={{ firstDayData: firstDay, currentDayData: currentDay }}
noDataPlaceholderText={noProfilerMessage}
/>
</Col>
</Row>
</Card>
</Col>
<ProfilerStateWrapper
dataTestId="histogram-metrics"
isLoading={isLoading}
title={t('label.data-distribution')}>
<DataDistributionHistogram
data={{
firstDayData: firstDay,
currentDayData: currentDay,
}}
noDataPlaceholderText={noProfilerMessage}
/>
</ProfilerStateWrapper>
) : null}
{firstDay?.cardinalityDistribution ||
currentDay?.cardinalityDistribution ? (
<Col span={24}>
<Card
className="shadow-none global-border-radius"
data-testid="cardinality-distribution-metrics"
loading={isLoading}>
<Row gutter={[16, 16]}>
<Col span={24}>
<Typography.Title
data-testid="cardinality-distribution-title"
level={5}>
{t('label.cardinality')}
</Typography.Title>
</Col>
<Col span={24}>
<CardinalityDistributionChart
data={{ firstDayData: firstDay, currentDayData: currentDay }}
noDataPlaceholderText={noProfilerMessage}
/>
</Col>
</Row>
</Card>
</Col>
<ProfilerStateWrapper
dataTestId="cardinality-distribution-metrics"
isLoading={isLoading}
title={t('label.cardinality')}>
<CardinalityDistributionChart
data={{
firstDayData: firstDay,
currentDayData: currentDay,
}}
noDataPlaceholderText={noProfilerMessage}
/>
</ProfilerStateWrapper>
) : null}
<Col span={24}>
<CustomMetricGraphs
customMetrics={customMetrics}
customMetricsGraphData={columnCustomMetrics}
isLoading={isLoading || isProfilerDataLoading}
/>
</Col>
</Row>
<CustomMetricGraphs
customMetrics={customMetrics}
customMetricsGraphData={columnCustomMetrics}
isLoading={isLoading || isProfilerDataLoading}
/>
</Stack>
);
};

View File

@ -11,8 +11,8 @@
* limitations under the License.
*/
import { Card, Col, Row, Tag } from 'antd';
import { isUndefined, map } from 'lodash';
import { Box, Card, Divider, Typography, useTheme } from '@mui/material';
import { isUndefined } from 'lodash';
import { useTranslation } from 'react-i18next';
import {
Bar,
@ -30,6 +30,7 @@ import { GRAPH_BACKGROUND_COLOR } from '../../../constants/constants';
import { ColumnProfile } from '../../../generated/entity/data/table';
import { axisTickFormatter, tooltipFormatter } from '../../../utils/ChartUtils';
import { customFormatDateTime } from '../../../utils/date-time/DateTimeUtils';
import { DataPill } from '../../common/DataPill/DataPill.styled';
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
export interface CardinalityDistributionChartProps {
@ -44,6 +45,7 @@ const CardinalityDistributionChart = ({
data,
noDataPlaceholderText,
}: CardinalityDistributionChartProps) => {
const theme = useTheme();
const { t } = useTranslation();
const showSingleGraph =
isUndefined(data.firstDayData?.cardinalityDistribution) ||
@ -54,11 +56,16 @@ const CardinalityDistributionChart = ({
isUndefined(data.currentDayData?.cardinalityDistribution)
) {
return (
<Row align="middle" className="h-full w-full" justify="center">
<Col>
<ErrorPlaceHolder placeholderText={noDataPlaceholderText} />
</Col>
</Row>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
width: '100%',
}}>
<ErrorPlaceHolder placeholderText={noDataPlaceholderText} />
</Box>
);
}
@ -70,16 +77,60 @@ const CardinalityDistributionChart = ({
const data = payload[0].payload;
return (
<Card>
<p className="font-semibold text-sm mb-1">{`${t('label.category')}: ${
data.name
}`}</p>
<p className="text-sm mb-1">{`${t('label.count')}: ${tooltipFormatter(
data.count
)}`}</p>
<p className="text-sm">{`${t('label.percentage')}: ${
data.percentage
}%`}</p>
<Card
sx={{
p: '10px',
bgcolor: theme.palette.allShades.white,
}}>
<Typography
sx={{
color: theme.palette.allShades.gray[900],
fontWeight: theme.typography.fontWeightMedium,
fontSize: theme.typography.pxToRem(12),
}}>
{data.name}
</Typography>
<Divider
sx={{
my: 2,
borderStyle: 'dashed',
borderColor: theme.palette.allShades.gray[300],
}}
/>
<Box className="d-flex items-center justify-between gap-6 p-b-xss text-sm">
<Typography
sx={(theme) => ({
color: theme.palette.allShades.gray[700],
fontSize: theme.typography.pxToRem(11),
})}>
{t('label.count')}
</Typography>
<Typography
sx={(theme) => ({
color: theme.palette.allShades.gray[900],
fontWeight: theme.typography.fontWeightMedium,
fontSize: theme.typography.pxToRem(11),
})}>
{tooltipFormatter(data.count)}
</Typography>
</Box>
<Box className="d-flex items-center justify-between gap-6 p-b-xss text-sm">
<Typography
sx={(theme) => ({
color: theme.palette.allShades.gray[700],
fontSize: theme.typography.pxToRem(11),
})}>
{t('label.percentage')}
</Typography>
<Typography
sx={(theme) => ({
color: theme.palette.allShades.gray[900],
fontWeight: theme.typography.fontWeightMedium,
fontSize: theme.typography.pxToRem(11),
})}>
{`${data.percentage}%`}
</Typography>
</Box>
</Card>
);
}
@ -87,9 +138,19 @@ const CardinalityDistributionChart = ({
return null;
};
const dataEntries = Object.entries(data).filter(
([, columnProfile]) => !isUndefined(columnProfile?.cardinalityDistribution)
);
return (
<Row className="w-full" data-testid="chart-container">
{map(data, (columnProfile, key) => {
<Box
data-testid="chart-container"
sx={{
display: 'flex',
width: '100%',
gap: 0,
}}>
{dataEntries.map(([key, columnProfile], index) => {
if (
isUndefined(columnProfile) ||
isUndefined(columnProfile?.cardinalityDistribution)
@ -108,62 +169,96 @@ const CardinalityDistributionChart = ({
const graphDate = customFormatDateTime(
columnProfile?.timestamp || 0,
'MMM dd'
'MMM dd, yyyy'
);
return (
<Col key={key} span={showSingleGraph ? 24 : 12}>
<Row gutter={[8, 8]}>
<Col
data-testid="date"
offset={showSingleGraph ? 1 : 2}
span={24}>
{graphDate}
</Col>
<Col offset={showSingleGraph ? 1 : 2} span={24}>
<Tag data-testid="cardinality-tag">{`${t('label.total-entity', {
<Box
key={key}
sx={{
flex: showSingleGraph ? '1 1 100%' : '1 1 50%',
minWidth: 0,
display: 'flex',
flexDirection: 'column',
px: showSingleGraph ? 4 : 6,
py: 2,
borderRight:
!showSingleGraph && index === 0
? `1px solid ${theme.palette.grey[200]}`
: 'none',
}}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 5,
}}>
<DataPill data-testid="date">{graphDate}</DataPill>
<DataPill data-testid="cardinality-tag">
{`${t('label.total-entity', {
entity: t('label.category-plural'),
})}: ${cardinalityData.categories?.length || 0}`}</Tag>
</Col>
<Col span={24}>
<ResponsiveContainer
debounce={200}
id={`${key}-cardinality`}
minHeight={300}>
<BarChart
className="w-full"
data={graphData}
layout="vertical"
margin={{ left: 16 }}>
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} />
<XAxis
padding={{ left: 16, right: 16 }}
tick={{ fontSize: 12 }}
tickFormatter={(props) => axisTickFormatter(props, '%')}
type="number"
/>
<YAxis
allowDataOverflow
dataKey="name"
padding={{ top: 16, bottom: 16 }}
tick={{ fontSize: 12 }}
tickFormatter={(value: string) =>
value?.length > 15 ? `${value.slice(0, 15)}...` : value
}
type="category"
width={120}
/>
<Legend />
<Tooltip content={renderTooltip} />
<Bar dataKey="percentage" fill={CHART_BLUE_1} />
</BarChart>
</ResponsiveContainer>
</Col>
</Row>
</Col>
})}: ${cardinalityData.categories?.length || 0}`}
</DataPill>
</Box>
<Box sx={{ flex: 1, minHeight: 350 }}>
<ResponsiveContainer
debounce={200}
id={`${key}-cardinality`}
minHeight={300}>
<BarChart
className="w-full"
data={graphData}
layout="vertical"
margin={{ left: 16 }}>
<CartesianGrid
stroke={GRAPH_BACKGROUND_COLOR}
strokeDasharray="3 3"
vertical={false}
/>
<XAxis
axisLine={{
stroke: theme.palette.grey[200],
}}
padding={{ left: 16, right: 16 }}
tick={{ fontSize: 12 }}
tickFormatter={(props) => axisTickFormatter(props, '%')}
tickLine={false}
type="number"
/>
<YAxis
allowDataOverflow
axisLine={false}
dataKey="name"
padding={{ top: 16, bottom: 16 }}
tick={{ fontSize: 12 }}
tickFormatter={(value: string) =>
value?.length > 15 ? `${value.slice(0, 15)}...` : value
}
tickLine={false}
type="category"
width={120}
/>
<Legend />
<Tooltip
content={renderTooltip}
cursor={{
stroke: theme.palette.grey[200],
strokeDasharray: '3 3',
}}
/>
<Bar
dataKey="percentage"
fill={CHART_BLUE_1}
radius={[0, 8, 8, 0]}
/>
</BarChart>
</ResponsiveContainer>
</Box>
</Box>
);
})}
</Row>
</Box>
);
};

View File

@ -11,8 +11,8 @@
* limitations under the License.
*/
import { Col, Row, Tag } from 'antd';
import { isUndefined, map } from 'lodash';
import { Box, useTheme } from '@mui/material';
import { isUndefined } from 'lodash';
import { useTranslation } from 'react-i18next';
import {
Bar,
@ -29,7 +29,9 @@ import { GRAPH_BACKGROUND_COLOR } from '../../../constants/constants';
import { DEFAULT_HISTOGRAM_DATA } from '../../../constants/profiler.constant';
import { HistogramClass } from '../../../generated/entity/data/table';
import { axisTickFormatter, tooltipFormatter } from '../../../utils/ChartUtils';
import { CustomDQTooltip } from '../../../utils/DataQuality/DataQualityUtils';
import { customFormatDateTime } from '../../../utils/date-time/DateTimeUtils';
import { DataPill } from '../../common/DataPill/DataPill.styled';
import ErrorPlaceHolder from '../../common/ErrorWithPlaceholder/ErrorPlaceHolder';
import { DataDistributionHistogramProps } from './Chart.interface';
@ -37,7 +39,9 @@ const DataDistributionHistogram = ({
data,
noDataPlaceholderText,
}: DataDistributionHistogramProps) => {
const theme = useTheme();
const { t } = useTranslation();
const showSingleGraph =
isUndefined(data.firstDayData?.histogram) ||
isUndefined(data.currentDayData?.histogram);
@ -47,21 +51,32 @@ const DataDistributionHistogram = ({
isUndefined(data.currentDayData?.histogram)
) {
return (
<Row align="middle" className="h-full w-full" justify="center">
<Col>
<ErrorPlaceHolder placeholderText={noDataPlaceholderText} />
</Col>
</Row>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
height: '100%',
width: '100%',
}}>
<ErrorPlaceHolder placeholderText={noDataPlaceholderText} />
</Box>
);
}
return (
<Row className="w-full" data-testid="chart-container">
{map(data, (columnProfile, key) => {
if (isUndefined(columnProfile?.histogram)) {
return;
}
const dataEntries = Object.entries(data).filter(
([, columnProfile]) => !isUndefined(columnProfile?.histogram)
);
return (
<Box
data-testid="chart-container"
sx={{
display: 'flex',
width: '100%',
gap: 0,
}}>
{dataEntries.map(([key, columnProfile], index) => {
const histogramData =
(columnProfile?.histogram as HistogramClass) ||
DEFAULT_HISTOGRAM_DATA;
@ -73,59 +88,105 @@ const DataDistributionHistogram = ({
const graphDate = customFormatDateTime(
columnProfile?.timestamp || 0,
'MMM dd'
'MMM dd, yyyy'
);
const skewColorTheme = columnProfile?.nonParametricSkew
? columnProfile?.nonParametricSkew > 0
? theme.palette.allShades.success
: theme.palette.allShades.error
: theme.palette.allShades.info;
return (
<Col key={key} span={showSingleGraph ? 24 : 12}>
<Row gutter={[8, 8]}>
<Col
data-testid="date"
offset={showSingleGraph ? 1 : 2}
span={24}>
{graphDate}
</Col>
<Col offset={showSingleGraph ? 1 : 2} span={24}>
<Tag data-testid="skew-tag">{`${t('label.skew')}: ${
<Box
key={key}
sx={{
flex: showSingleGraph ? '1 1 100%' : '1 1 50%',
minWidth: 0,
display: 'flex',
flexDirection: 'column',
px: showSingleGraph ? 4 : 3,
py: 2,
borderRight:
!showSingleGraph && index === 0
? `1px solid ${theme.palette.grey[200]}`
: 'none',
}}>
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
mb: 5,
}}>
<DataPill>{graphDate}</DataPill>
<DataPill
sx={{
backgroundColor: skewColorTheme[100],
color: skewColorTheme[900],
}}>
{`${t('label.skew')}: ${
columnProfile?.nonParametricSkew || '--'
}`}</Tag>
</Col>
<Col span={24}>
<ResponsiveContainer
debounce={200}
id={`${key}-histogram`}
minHeight={300}>
<BarChart
className="w-full"
data={graphData}
margin={{ left: 16 }}>
<CartesianGrid stroke={GRAPH_BACKGROUND_COLOR} />
<XAxis
dataKey="name"
padding={{ left: 16, right: 16 }}
tick={{ fontSize: 12 }}
/>
<YAxis
allowDataOverflow
padding={{ top: 16, bottom: 16 }}
tick={{ fontSize: 12 }}
tickFormatter={(props) => axisTickFormatter(props)}
/>
<Legend />
<Tooltip
formatter={(value: number | string) =>
tooltipFormatter(value)
}
/>
<Bar dataKey="frequency" fill={CHART_BLUE_1} />
</BarChart>
</ResponsiveContainer>
</Col>
</Row>
</Col>
}`}
</DataPill>
</Box>
<Box sx={{ flex: 1, minHeight: 350 }}>
<ResponsiveContainer
debounce={200}
height="100%"
id={`${key}-histogram`}
width="100%">
<BarChart
data={graphData}
margin={{ top: 10, right: 10, bottom: 10, left: 10 }}>
<CartesianGrid
stroke={GRAPH_BACKGROUND_COLOR}
strokeDasharray="3 3"
vertical={false}
/>
<XAxis
axisLine={{
stroke: theme.palette.grey[200],
}}
dataKey="name"
padding={{ left: 16, right: 16 }}
tick={{ fontSize: 12 }}
tickLine={false}
/>
<YAxis
allowDataOverflow
axisLine={false}
padding={{ top: 16, bottom: 16 }}
tick={{ fontSize: 12 }}
tickFormatter={(props) => axisTickFormatter(props)}
tickLine={false}
/>
<Legend />
<Tooltip
content={
<CustomDQTooltip
displayDateInHeader={false}
timeStampKey="name"
valueFormatter={(value) => tooltipFormatter(value)}
/>
}
cursor={{
stroke: theme.palette.grey[200],
strokeDasharray: '3 3',
}}
/>
<Bar
dataKey="frequency"
fill={CHART_BLUE_1}
radius={[8, 8, 0, 0]}
/>
</BarChart>
</ResponsiveContainer>
</Box>
</Box>
);
})}
</Row>
</Box>
);
};

View File

@ -0,0 +1,24 @@
/*
* Copyright 2023 Collate.
* Licensed 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 { Box, styled } from '@mui/material';
export const DataPill = styled(Box)(({ theme }) => ({
backgroundColor: theme.palette.grey[100],
color: theme.palette.grey[900],
borderRadius: '6px',
padding: '6px 12px',
fontSize: theme.typography.pxToRem(14),
fontWeight: theme.typography.fontWeightBold,
display: 'inline-block',
}));

View File

@ -34,7 +34,7 @@ export const BLUE_2 = '#3ca2f4';
export const BLUE_500 = '#2E90FA';
export const BLUE_800 = '#1849A9';
export const BLUE_50 = '#EFF8FF';
export const CHART_BLUE_1 = '#1890FF';
export const CHART_BLUE_1 = '#4689FF';
export const RIPTIDE = '#76E9C6';
export const MY_SIN = '#FEB019';
export const SAN_MARINO = '#416BB3';

View File

@ -123,28 +123,28 @@ export const INITIAL_COUNT_METRIC_VALUE = {
entity: t('label.distinct'),
}),
dataKey: 'distinctCount',
color: '#1890FF',
color: '#467DDC',
},
{
title: t('label.entity-count', {
entity: t('label.null'),
}),
dataKey: 'nullCount',
color: '#7147E8',
color: '#3488B5',
},
{
title: t('label.entity-count', {
entity: t('label.unique'),
}),
dataKey: 'uniqueCount',
color: '#008376',
color: '#685997',
},
{
title: t('label.entity-count', {
entity: t('label.value-plural'),
}),
dataKey: 'valuesCount',
color: '#B02AAC',
color: '#464A52',
},
],
data: [],
@ -157,21 +157,21 @@ export const INITIAL_PROPORTION_METRIC_VALUE = {
entity: t('label.distinct'),
}),
dataKey: 'distinctProportion',
color: '#1890FF',
color: '#6B97E3',
},
{
title: t('label.entity-proportion', {
entity: t('label.null'),
}),
dataKey: 'nullProportion',
color: '#7147E8',
color: '#867AAC',
},
{
title: t('label.entity-proportion', {
entity: t('label.unique'),
}),
dataKey: 'uniqueProportion',
color: '#008376',
color: '#6B6E75',
},
],
data: [],
@ -182,17 +182,17 @@ export const INITIAL_MATH_METRIC_VALUE = {
{
title: t('label.max'),
dataKey: 'max',
color: '#1890FF',
color: '#6B97E3',
},
{
title: t('label.mean'),
dataKey: 'mean',
color: '#7147E8',
color: '#6B6E75',
},
{
title: t('label.min'),
dataKey: 'min',
color: '#008376',
color: '#867AAC',
},
],
data: [],
@ -203,7 +203,8 @@ export const INITIAL_SUM_METRIC_VALUE = {
{
title: t('label.sum'),
dataKey: 'sum',
color: '#1890FF',
color: BLUE_500,
fill: BLUE_50,
},
],
data: [],
@ -213,22 +214,22 @@ export const INITIAL_QUARTILE_METRIC_VALUE = {
{
title: t('label.first-quartile'),
dataKey: 'firstQuartile',
color: '#1890FF',
color: '#467DDC',
},
{
title: t('label.median'),
dataKey: 'median',
color: '#7147E8',
color: '#3488B5',
},
{
title: t('label.inter-quartile-range'),
dataKey: 'interQuartileRange',
color: '#008376',
color: '#685997',
},
{
title: t('label.third-quartile'),
dataKey: 'thirdQuartile',
color: '#B02AAC',
color: '#464A52',
},
],
data: [],

View File

@ -41,6 +41,7 @@ export interface ChartFilter {
export interface DataInsightChartTooltipProps extends TooltipProps<any, any> {
cardStyles?: React.CSSProperties;
customValueKey?: string;
displayDateInHeader?: boolean;
dateTimeFormatter?: (date?: number, format?: string) => string;
isPercentage?: boolean;
isTier?: boolean;

View File

@ -369,11 +369,15 @@ export const CustomDQTooltip = (props: DataInsightChartTooltipProps) => {
timeStampKey = 'timestampValue',
transformLabel = true,
valueFormatter,
displayDateInHeader = true,
} = props;
if (active && payload && payload.length) {
// we need to check if the xAxis is a date or not.
const timestamp = dateTimeFormatter(payload[0].payload[timeStampKey] || 0);
const timestamp = displayDateInHeader
? dateTimeFormatter(payload[0].payload[timeStampKey] || 0)
: payload[0].payload[timeStampKey];
const payloadValue = uniqBy(payload, 'dataKey');
return (