diff --git a/packages/pxweb2/public/config/config.js b/packages/pxweb2/public/config/config.js index 87ec89910..c88e878c4 100644 --- a/packages/pxweb2/public/config/config.js +++ b/packages/pxweb2/public/config/config.js @@ -38,4 +38,7 @@ globalThis.PxWeb2Config = { sv: '', // Set to your Swedish homepage URL en: '', // Set to your English homepage URL }, + features: { + chartEnabled: true, + }, }; diff --git a/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerView.spec.tsx b/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerView.spec.tsx index 16d79dd68..26f858a8c 100644 --- a/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerView.spec.tsx +++ b/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerView.spec.tsx @@ -1,49 +1,114 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import '@testing-library/jest-dom/vitest'; import { MemoryRouter } from 'react-router'; +import userEvent from '@testing-library/user-event'; import { DrawerView } from './DrawerView'; +import { renderWithProviders } from '../../../util/testing-utils'; -const mockGetConfig = vi.fn(); +const { + defaultMockConfig, + mockGetConfig, + mockPivotToTable, + mockPivotToChart, + mockIsMobile, +} = vi.hoisted(() => { + const defaultMockConfig = { + baseApplicationPath: '/', + features: { + chartEnabled: true, + }, + language: { + defaultLanguage: 'en', + fallbackLanguage: 'en', + supportedLanguages: [{ shorthand: 'en' }], + positionInPath: 'after', + }, + }; + + return { + defaultMockConfig, + mockGetConfig: vi.fn(() => defaultMockConfig), + mockPivotToTable: vi.fn(), + mockPivotToChart: vi.fn(), + mockIsMobile: vi.fn(() => false), + }; +}); vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), + initReactI18next: { + type: '3rdParty', + init: () => undefined, + }, })); vi.mock('../../../util/config/getConfig', () => ({ - getConfig: () => mockGetConfig(), + getConfig: mockGetConfig, +})); + +vi.mock('../../../context/useTableData', () => ({ + default: () => ({ + pivotToTable: mockPivotToTable, + pivotToChart: mockPivotToChart, + }), +})); + +vi.mock('../../../context/useApp', () => ({ + default: () => ({ + isMobile: mockIsMobile(), + }), })); interface MockActionItemProps { label?: string; + ariaLabel?: string; + onClick?: () => void; + toggleState?: boolean; } -vi.mock('@pxweb2/pxweb2-ui', () => ({ - ContentBox: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), - ActionItem: ({ label }: MockActionItemProps) => ( - - ), - LocalAlert: ({ children }: { children: React.ReactNode }) => ( -
{children}
- ), -})); +vi.mock('@pxweb2/pxweb2-ui', async (importOriginal) => { + const actual = await importOriginal(); + + return { + ...actual, + ContentBox: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + ActionItem: ({ + label, + ariaLabel, + onClick, + toggleState, + }: MockActionItemProps) => ( + + ), + LocalAlert: ({ children }: { children: React.ReactNode }) => ( +
{children}
+ ), + }; +}); beforeEach(() => { - mockGetConfig.mockReturnValue({ - features: { - chartEnabled: true, - }, - }); + vi.clearAllMocks(); + mockGetConfig.mockReset(); + mockGetConfig.mockReturnValue(defaultMockConfig); + mockIsMobile.mockReturnValue(false); }); describe('DrawerView', () => { it('renders successfully', () => { - render( + renderWithProviders( , @@ -56,12 +121,13 @@ describe('DrawerView', () => { it('renders info alert when chart view is disabled', () => { mockGetConfig.mockReturnValue({ + ...defaultMockConfig, features: { chartEnabled: false, }, }); - render( + renderWithProviders( , @@ -76,4 +142,78 @@ describe('DrawerView', () => { it('has correct display name', () => { expect(DrawerView.displayName).toBe('DrawerView'); }); + + it('clicking table action pivots to desktop table when not mobile', async () => { + renderWithProviders( + + + , + ); + + const user = userEvent.setup(); + await user.click( + screen.getByRole('button', { + name: 'presentation_page.side_menu.view.table.title', + }), + ); + + expect(mockPivotToTable).toHaveBeenCalledTimes(1); + expect(mockPivotToTable).toHaveBeenCalledWith(false); + }); + + it('clicking table action pivots to mobile table when mobile', async () => { + mockIsMobile.mockReturnValue(true); + + renderWithProviders( + + + , + ); + + const user = userEvent.setup(); + await user.click( + screen.getByRole('button', { + name: 'presentation_page.side_menu.view.table.title', + }), + ); + + expect(mockPivotToTable).toHaveBeenCalledTimes(1); + expect(mockPivotToTable).toHaveBeenCalledWith(true); + }); + + it('clicking linechart action pivots to chart', async () => { + renderWithProviders( + + + , + ); + + const user = userEvent.setup(); + await user.click( + screen.getByRole('button', { + name: 'presentation_page.side_menu.view.linechart.title', + }), + ); + + expect(mockPivotToChart).toHaveBeenCalledTimes(1); + expect(mockPivotToTable).not.toHaveBeenCalled(); + }); + + it('sets correct toggleState from search param view=linechart', () => { + renderWithProviders( + + + , + ); + + const tableButton = screen.getByRole('button', { + name: 'presentation_page.side_menu.view.table.title', + }); + const chartButton = screen.getByRole('button', { + name: 'presentation_page.side_menu.view.linechart.title', + }); + + expect(tableButton).toHaveAttribute('data-toggle-state', 'off'); + expect(chartButton).toHaveAttribute('data-toggle-state', 'on'); + }); }); diff --git a/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerView.tsx b/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerView.tsx index c8aa9b436..e35c00a55 100644 --- a/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerView.tsx +++ b/packages/pxweb2/src/app/components/NavigationDrawer/Drawers/DrawerView.tsx @@ -1,20 +1,26 @@ import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router'; -import { ActionItem, ContentBox, LocalAlert } from '@pxweb2/pxweb2-ui'; +import useTableData from '../../../context/useTableData'; +import useApp from '../../../context/useApp'; import { getConfig } from '../../../util/config/getConfig'; import { ViewMode, getSearchParamsWithViewMode, getViewMode, } from '../../../pages/TableViewer/Utils/tableViewerHelper'; +import { ActionItem, ContentBox, LocalAlert } from '@pxweb2/pxweb2-ui'; import classes from './DrawerView.module.scss'; + export function DrawerView() { const { t } = useTranslation(); const [searchParams, setSearchParams] = useSearchParams(); + const { pivotToChart, pivotToTable } = useTableData(); + const { isMobile } = useApp(); const config = getConfig(); const chartEnabled = config.features?.chartEnabled === true; const selectedViewMode = getViewMode(searchParams, chartEnabled); + function setViewMode(viewMode: ViewMode) { setSearchParams(getSearchParamsWithViewMode(searchParams, viewMode)); } @@ -29,7 +35,10 @@ export function DrawerView() { size="large" label={t('presentation_page.side_menu.view.table.title')} ariaLabel={t('presentation_page.side_menu.view.table.title')} - onClick={() => setViewMode('table')} + onClick={() => { + pivotToTable(isMobile); + setViewMode('table'); + }} toggleState={selectedViewMode === 'table'} /> @@ -39,7 +48,10 @@ export function DrawerView() { size="large" label={t('presentation_page.side_menu.view.linechart.title')} ariaLabel={t('presentation_page.side_menu.view.linechart.title')} - onClick={() => setViewMode('linechart')} + onClick={() => { + pivotToChart(); + setViewMode('linechart'); + }} toggleState={selectedViewMode === 'linechart'} /> diff --git a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx index 895d7c0d9..a9fcc1893 100644 --- a/packages/pxweb2/src/app/components/Presentation/Presentation.tsx +++ b/packages/pxweb2/src/app/components/Presentation/Presentation.tsx @@ -25,7 +25,10 @@ import useTableData from '../../context/useTableData'; import useVariables from '../../context/useVariables'; import { useDebounce } from '@uidotdev/usehooks'; import { getConfig } from '../../util/config/getConfig'; -import { getViewMode } from '../../pages/TableViewer/Utils/tableViewerHelper'; +import { + getViewMode, + getDataViewMode, +} from '../../pages/TableViewer/Utils/tableViewerHelper'; type propsType = { readonly selectedTabId: string; @@ -139,11 +142,17 @@ export function Presentation({ } }; }); + + // Get the right view mode when switching between mobile/desktop screen sizes. useEffect(() => { - if (isMobile) { - tableData.pivotToMobile(); - } else { - tableData.pivotToDesktop(); + if (viewMode === 'table') { + if (isMobile) { + tableData.pivotToMobile(); + } else { + tableData.pivotToDesktop(); + } + } else if (viewMode === 'linechart') { + tableData.pivotToChart(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMobile]); @@ -162,7 +171,8 @@ export function Presentation({ if (initialRun && !hasSelectedValues) { if (getSavedQueryId()?.length > 0) { - tableData.fetchSavedQuery(getSavedQueryId(), i18n, isMobile); + const dataViewMode = getDataViewMode(viewMode, isMobile); + tableData.fetchSavedQuery(getSavedQueryId(), i18n, dataViewMode); } else { fetchTableDataIfAllowed(); } @@ -203,7 +213,8 @@ export function Presentation({ function fetchTableDataIfAllowed() { if (variables.isMatrixSizeAllowed) { - tableData.fetchTableData(tableId, i18n, isMobile); + const dataViewMode = getDataViewMode(viewMode, isMobile); + tableData.fetchTableData(tableId, i18n, dataViewMode); } else { // fade table and give warning setIsFadingTable(true); diff --git a/packages/pxweb2/src/app/context/DataViewModeType.ts b/packages/pxweb2/src/app/context/DataViewModeType.ts new file mode 100644 index 000000000..bcfe64bd0 --- /dev/null +++ b/packages/pxweb2/src/app/context/DataViewModeType.ts @@ -0,0 +1,5 @@ +export enum DataViewModeType { + MobileTable = 'MobileTable', + DesktopTable = 'DesktopTable', + Chart = 'Chart', +} diff --git a/packages/pxweb2/src/app/context/TableDataProvider.spec.tsx b/packages/pxweb2/src/app/context/TableDataProvider.spec.tsx index a504ab8ca..e1bbb3866 100644 --- a/packages/pxweb2/src/app/context/TableDataProvider.spec.tsx +++ b/packages/pxweb2/src/app/context/TableDataProvider.spec.tsx @@ -1,89 +1,246 @@ import React from 'react'; -import { render } from '@testing-library/react'; -import { TableDataProvider, TableDataContext } from './TableDataProvider'; -import { VariablesProvider } from './VariablesProvider'; -import { vi } from 'vitest'; +import { render, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi, beforeEach } from 'vitest'; import { i18n } from 'i18next'; -describe('TableDataProvider', () => { - it('should call fetchTableData and update context data', async () => { - const mockI18nInstance = { +import { TableDataContext, TableDataProvider } from './TableDataProvider'; +import { DataViewModeType } from './DataViewModeType'; +import { SavedQueriesService } from '@pxweb2/pxweb2-api-client'; +import { mapJsonStat2Response } from '../../mappers/JsonStat2ResponseMapper'; +import { + addFormattingToPxTable, + initStubAndHeadingChart, + initStubAndHeadingDesktop, + initStubAndHeadingMobile, +} from './TableDataProviderUtils'; +import { PxTable, VartypeEnum } from '@pxweb2/pxweb2-ui'; + +vi.mock('./useVariables', () => ({ + default: () => ({ + hasLoadedInitialSelection: false, + getUniqueIds: () => [], + getSelectedCodelistById: () => undefined, + getSelectedValuesByIdSorted: () => [], + pxTableMetadata: undefined, + }), +})); + +vi.mock('../../mappers/JsonStat2ResponseMapper', () => ({ + mapJsonStat2Response: vi.fn(), +})); + +vi.mock('./TableDataProviderUtils', async (importOriginal) => { + const actual = + await importOriginal(); + + return { + ...actual, + addFormattingToPxTable: vi.fn(async () => true), + initStubAndHeadingDesktop: vi.fn(), + initStubAndHeadingMobile: vi.fn(), + initStubAndHeadingChart: vi.fn(), + }; +}); + +vi.mock('@pxweb2/pxweb2-api-client', async (importOriginal) => { + const actual = + await importOriginal(); + + return { + ...actual, + SavedQueriesService: { + ...actual.SavedQueriesService, + runSaveQuery: vi.fn(), + }, + }; +}); + +type ProbeProps = { + mode: DataViewModeType; + onDataChange: (data: PxTable | undefined) => void; +}; + +function ProviderProbe({ mode, onDataChange }: ProbeProps) { + const context = React.useContext(TableDataContext); + const hasRequestedRef = React.useRef(false); + + React.useEffect(() => { + if (context?.data !== undefined) { + onDataChange(context.data); + } + }, [context?.data, onDataChange]); + + React.useEffect(() => { + if (context && !hasRequestedRef.current) { + hasRequestedRef.current = true; + context.fetchSavedQuery( + 'saved-query-id', + { + language: 'en', + } as i18n, + mode, + ); + } + }, [context, mode]); + + return null; +} + +function createPxTable(): PxTable { + const desktopStub = { + id: 'desktopStub', + label: 'Desktop stub', + mandatory: false, + type: VartypeEnum.REGULAR_VARIABLE, + values: [{ code: '1', label: 'One' }], + }; + + const desktopHeading = { + id: 'desktopHeading', + label: 'Desktop heading', + mandatory: false, + type: VartypeEnum.REGULAR_VARIABLE, + values: [{ code: '1', label: 'One' }], + }; + + const mobileStub = { + id: 'mobileStub', + label: 'Mobile stub', + mandatory: false, + type: VartypeEnum.GEOGRAPHICAL_VARIABLE, + values: [{ code: '1', label: 'One' }], + }; + + const chartStub = { + id: 'chartStub', + label: 'Chart stub', + mandatory: false, + type: VartypeEnum.TIME_VARIABLE, + values: [{ code: '1', label: 'One' }], + }; + + const chartHeading = { + id: 'chartHeading', + label: 'Chart heading', + mandatory: false, + type: VartypeEnum.CONTENTS_VARIABLE, + values: [{ code: '1', label: 'One' }], + }; + + return { + metadata: { + id: 'table-id', language: 'en', - t: vi.fn(), - } as unknown as i18n; - - const mockFetchTableData = vi.fn(); - const TestComponent = () => { - const context = React.useContext(TableDataContext); - React.useEffect(() => { - if (context) { - context.fetchTableData('testTableId', mockI18nInstance, false); - } - }, [context]); - return null; - }; - - const consoleErrorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => undefined); + label: 'Table', + updated: new Date(2023, 0, 1), + source: '', + infofile: '', + decimals: 0, + officialStatistics: false, + aggregationAllowed: true, + contents: '', + descriptionDefault: false, + matrix: '', + subjectCode: '', + subjectArea: '', + contacts: [], + definitions: {}, + notes: [], + availableLanguages: [], + variables: [ + desktopStub, + desktopHeading, + mobileStub, + chartStub, + chartHeading, + ], + }, + data: { + cube: {}, + variableOrder: [], + isLoaded: true, + }, + stub: [desktopStub], + heading: [desktopHeading], + }; +} + +describe('TableDataProvider initializeStubAndHeading callbacks', () => { + beforeEach(() => { + vi.clearAllMocks(); + + vi.mocked(SavedQueriesService.runSaveQuery).mockResolvedValue('mock-data'); + + vi.mocked(initStubAndHeadingDesktop).mockReturnValue({ + stubOrderDesktop: ['desktopStub'], + headingOrderDesktop: ['desktopHeading'], + }); + + vi.mocked(initStubAndHeadingMobile).mockReturnValue({ + stubOrderMobile: ['mobileStub', 'desktopStub'], + headingOrderMobile: [], + }); + + vi.mocked(initStubAndHeadingChart).mockReturnValue({ + stubOrderChart: ['chartStub'], + headingOrderChart: ['chartHeading'], + }); + }); + + async function fetchDataForMode(mode: DataViewModeType): Promise { + const mappedTable = createPxTable(); + vi.mocked(mapJsonStat2Response).mockReturnValue(mappedTable); + + let latestData: PxTable | undefined; render( - - + { + latestData = data; }} - > - - - , + /> + , ); - expect(mockFetchTableData).toHaveBeenCalledWith( - 'testTableId', - expect.any(Object), - false, + await waitFor(() => { + expect(latestData).toBeDefined(); + }); + + expect(SavedQueriesService.runSaveQuery).toHaveBeenCalledWith( + 'saved-query-id', + 'en', + 'json-stat2', ); - consoleErrorSpy.mockRestore(); + expect(mapJsonStat2Response).toHaveBeenCalledTimes(1); + expect(addFormattingToPxTable).toHaveBeenCalledWith(mappedTable); + expect(initStubAndHeadingDesktop).toHaveBeenCalledWith(mappedTable); + expect(initStubAndHeadingMobile).toHaveBeenCalledWith(mappedTable); + expect(initStubAndHeadingChart).toHaveBeenCalledWith(mappedTable); + + return latestData as PxTable; + } + + it('uses desktop stub/heading order when mode is DesktopTable', async () => { + const data = await fetchDataForMode(DataViewModeType.DesktopTable); + + expect(data.stub.map((v) => v.id)).toEqual(['desktopStub']); + expect(data.heading.map((v) => v.id)).toEqual(['desktopHeading']); }); - it('should throw an error when triggered', () => { - const TestComponent = () => { - const context = React.useContext(TableDataContext); - React.useEffect(() => { - if (context) { - // Simulate the error being thrown - throw new Error('Simulated error'); - } - }, [context]); - return null; - }; - - const consoleErrorSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => { - // Suppress console error during test - return undefined; - }); - - expect(() => { - render( - - - - - , - ); - }).toThrow('Simulated error'); + it('uses mobile stub/heading order when mode is MobileTable', async () => { + const data = await fetchDataForMode(DataViewModeType.MobileTable); + + expect(data.stub.map((v) => v.id)).toEqual(['mobileStub', 'desktopStub']); + expect(data.heading.map((v) => v.id)).toEqual([]); + }); + + it('uses chart stub/heading order when mode is Chart', async () => { + const data = await fetchDataForMode(DataViewModeType.Chart); - consoleErrorSpy.mockRestore(); + expect(data.stub.map((v) => v.id)).toEqual(['chartStub']); + expect(data.heading.map((v) => v.id)).toEqual(['chartHeading']); }); }); diff --git a/packages/pxweb2/src/app/context/TableDataProvider.tsx b/packages/pxweb2/src/app/context/TableDataProvider.tsx index 16e7d4139..9bee98217 100644 --- a/packages/pxweb2/src/app/context/TableDataProvider.tsx +++ b/packages/pxweb2/src/app/context/TableDataProvider.tsx @@ -26,18 +26,33 @@ import { pivotTableCW, TableTitlePartsType, getTableTitleParts, + initStubAndHeadingDesktop, + initStubAndHeadingMobile, + initStubAndHeadingChart, + getChartStubVariableId, } from './TableDataProviderUtils'; import { problemMessage } from '../util/problemMessage'; import { PivotType } from './PivotType'; +import { DataViewModeType } from './DataViewModeType'; // Define types for the context state and provider props export interface TableDataContextType { isInitialized: boolean; data: PxTable | undefined; - fetchTableData: (tableId: string, i18n: i18n, isMobile: boolean) => void; - fetchSavedQuery: (queryId: string, i18n: i18n, isMobile: boolean) => void; + fetchTableData: ( + tableId: string, + i18n: i18n, + dataViewMode: DataViewModeType, + ) => void; + fetchSavedQuery: ( + queryId: string, + i18n: i18n, + dataViewMode: DataViewModeType, + ) => void; pivotToMobile: () => void; pivotToDesktop: () => void; + pivotToTable: (isMobile: boolean) => void; + pivotToChart: () => void; pivot: (type: PivotType) => void; buildTableTitle: () => TableTitlePartsType; isFadingTable: boolean; @@ -64,6 +79,12 @@ const TableDataContext = createContext({ pivotToDesktop: () => { // No-op: useTableData hook prevents this from being called }, + pivotToTable: () => { + // No-op: useTableData hook prevents this from being called + }, + pivotToChart: () => { + // No-op: useTableData hook prevents this from being called + }, pivot: () => { // No-op: useTableData hook prevents this from being called }, @@ -87,8 +108,10 @@ const TableDataProvider: React.FC = ({ children }) => { undefined, ); - // State for mobile mode. If mobile mode data will be pivoted so that all variables are in the stub. - const [isMobileMode, setIsMobileMode] = useState(false); + // state for data view mode. + const [dataViewMode, setDataViewMode] = useState( + DataViewModeType.DesktopTable, + ); // Variables in the stub (desktop table) const [stubDesktop, setStubDesktop] = useState([]); @@ -100,6 +123,11 @@ const TableDataProvider: React.FC = ({ children }) => { // Variables in the heading (mobile table) const [headingMobile, setHeadingMobile] = useState([]); + // Variables in the stub (chart) + const [stubChart, setStubChart] = useState([]); + // Variables in the heading (chart) + const [headingChart, setHeadingChart] = useState([]); + // When default selection is loaded we need to initialize which codelists are selected for each variable. const [codelistsInitialized, setCodelistsInitialized] = useState(false); @@ -169,54 +197,84 @@ const TableDataProvider: React.FC = ({ children }) => { variables.hasLoadedInitialSelection, ]); + const initializeStubAndHeadingDesktop = React.useCallback( + (pxTable: PxTable) => { + const { stubOrderDesktop, headingOrderDesktop } = + initStubAndHeadingDesktop(pxTable); + + setStubDesktop(stubOrderDesktop); + setHeadingDesktop(headingOrderDesktop); + + return { + stubOrderDesktop, + headingOrderDesktop, + }; + }, + [], + ); + + const initializeStubAndHeadingMobile = React.useCallback( + (pxTable: PxTable) => { + const { stubOrderMobile, headingOrderMobile } = + initStubAndHeadingMobile(pxTable); + + setStubMobile(stubOrderMobile); + setHeadingMobile(headingOrderMobile); + + return { + stubOrderMobile, + headingOrderMobile, + }; + }, + [], + ); + + const initializeStubAndHeadingChart = React.useCallback( + (pxTable: PxTable) => { + const { stubOrderChart, headingOrderChart } = + initStubAndHeadingChart(pxTable); + + setStubChart(stubOrderChart); + setHeadingChart(headingOrderChart); + + return { + stubOrderChart, + headingOrderChart, + }; + }, + [], + ); + /** * Remember order of variables in stub and heading when table setup is changed. * * @param pxTable - PxTable containing the data and metadata for display in table. - * @param isMobile - If the device is mobile or not. + * @param dataViewMode - The current data view mode (e.g., mobile, desktop, chart). + * @param lang - The current language code (e.g., 'en', 'no'). */ const initializeStubAndHeading = React.useCallback( - (pxTable: PxTable, isMobile: boolean, lang: string) => { + (pxTable: PxTable, dataViewMode: DataViewModeType, lang: string) => { if ( - accumulatedData === undefined || + accumulatedData === undefined || //NOSONAR: This is a valid check to determine if the accumulated data is undefined or if the table ID or language has changed. accumulatedData.metadata.id !== pxTable.metadata.id || accumulatedData.metadata.language.toLowerCase() !== lang.toLowerCase() ) { // First time we get data OR we have a new table OR language is changed. + const { stubOrderDesktop, headingOrderDesktop } = + initializeStubAndHeadingDesktop(pxTable); - // -> Set stub and heading order for desktop according to the order in pxTable - const stubOrderDesktop: string[] = pxTable.stub.map( - (variable) => variable.id, - ); - const headingOrderDesktop: string[] = pxTable.heading.map( - (variable) => variable.id, - ); - setStubDesktop(stubOrderDesktop); - setHeadingDesktop(headingOrderDesktop); - - // -> Set stub and heading order for mobile according to the order in pxTable - const tmpStubMobile = structuredClone(pxTable.stub); - const tmpHeadingMobile = structuredClone(pxTable.heading); - - tmpHeadingMobile.forEach((variable) => { - tmpStubMobile.push(variable); - }); - - tmpStubMobile.sort((a, b) => a.values.length - b.values.length); - - const stubOrderMobile: string[] = tmpStubMobile.map( - (variable) => variable.id, - ); - - const headingOrderMobile: string[] = []; + const { stubOrderMobile, headingOrderMobile } = + initializeStubAndHeadingMobile(pxTable); - setStubMobile(stubOrderMobile); - setHeadingMobile(headingOrderMobile); + const { stubOrderChart, headingOrderChart } = + initializeStubAndHeadingChart(pxTable); - if (isMobile) { + if (dataViewMode === DataViewModeType.MobileTable) { pivotTable(pxTable, stubOrderMobile, headingOrderMobile); - } else { + } else if (dataViewMode === DataViewModeType.DesktopTable) { pivotTable(pxTable, stubOrderDesktop, headingOrderDesktop); + } else if (dataViewMode === DataViewModeType.Chart) { + pivotTable(pxTable, stubOrderChart, headingOrderChart); } } else { // The number of variables has changed. @@ -233,17 +291,24 @@ const TableDataProvider: React.FC = ({ children }) => { headingDesktop, stubMobile, headingMobile, + stubChart, + headingChart, ); + setStubDesktop(filtered.stubDesktop); setHeadingDesktop(filtered.headingDesktop); setStubMobile(filtered.stubMobile); setHeadingMobile(filtered.headingMobile); + setStubChart(filtered.stubChart); + setHeadingChart(filtered.headingChart); } - if (isMobile) { + if (dataViewMode === DataViewModeType.MobileTable) { pivotTable(pxTable, stubMobile, headingMobile); - } else { + } else if (dataViewMode === DataViewModeType.DesktopTable) { pivotTable(pxTable, stubDesktop, headingDesktop); + } else if (dataViewMode === DataViewModeType.Chart) { + pivotTable(pxTable, stubChart, headingChart); } // Variable has been added @@ -257,6 +322,7 @@ const TableDataProvider: React.FC = ({ children }) => { if (remainingVariables.length > 0) { const newStubDesktop = structuredClone(stubDesktop); const newStubMobile = structuredClone(stubMobile); + const newHeadingChart = structuredClone(headingChart); remainingVariables.forEach((variable) => { if (!newStubDesktop.includes(variable.id)) { @@ -266,13 +332,28 @@ const TableDataProvider: React.FC = ({ children }) => { if (!newStubMobile.includes(variable.id)) { newStubMobile.push(variable.id); } + if (!newHeadingChart.includes(variable.id)) { + newHeadingChart.push(variable.id); + } }); setStubDesktop(newStubDesktop); setStubMobile(newStubMobile); + setHeadingChart(newHeadingChart); } } }, - [accumulatedData, stubDesktop, headingDesktop, stubMobile, headingMobile], + [ + accumulatedData, + initializeStubAndHeadingDesktop, + initializeStubAndHeadingMobile, + initializeStubAndHeadingChart, + stubDesktop, + headingDesktop, + stubMobile, + headingMobile, + stubChart, + headingChart, + ], ); /** @@ -280,7 +361,7 @@ const TableDataProvider: React.FC = ({ children }) => { * * @param loadSavedQueryId - The unique identifier of the saved query to load. * @param i18n - The i18n object for handling languages. - * @param isMobile - Indicates if the current device is mobile, affecting table initialization. + * @param dataViewMode - The current data view mode (e.g., mobile, desktop, chart). * @returns A promise that resolves when the table data has been fetched, processed, and set. * * This function: @@ -291,7 +372,11 @@ const TableDataProvider: React.FC = ({ children }) => { * - Updates the context state with the new table data and stores a cloned copy as accumulated data. */ const fetchSavedQuery = React.useCallback( - async (loadSavedQueryId: string, i18n: i18n, isMobile: boolean) => { + async ( + loadSavedQueryId: string, + i18n: i18n, + dataViewMode: DataViewModeType, + ) => { const res = await SavedQueriesService.runSaveQuery( loadSavedQueryId, i18n.language, @@ -305,7 +390,7 @@ const TableDataProvider: React.FC = ({ children }) => { // Add formatting to the PxTable datacell values await addFormattingToPxTable(pxTable); - initializeStubAndHeading(pxTable, isMobile, i18n.language); + initializeStubAndHeading(pxTable, dataViewMode, i18n.language); setData(pxTable); // Store as accumulated data @@ -317,6 +402,7 @@ const TableDataProvider: React.FC = ({ children }) => { /* * @param tableId - The id of the table to fetch data for. * @param i18n - The i18n object for handling langauages + * @param dataViewMode - The current data view mode (e.g., mobile, desktop, chart). * @param variablesSelection - User selection of variables and their values. * @param codelistChanged - If the codelist has changed. */ @@ -324,7 +410,7 @@ const TableDataProvider: React.FC = ({ children }) => { async ( tableId: string, i18n: i18n, - isMobile: boolean, + dataViewMode: DataViewModeType, variablesSelection: VariablesSelection, codelistChanged: boolean, ) => { @@ -346,7 +432,7 @@ const TableDataProvider: React.FC = ({ children }) => { variablesSelection, ); - initializeStubAndHeading(pxTable, isMobile, i18n.language); + initializeStubAndHeading(pxTable, dataViewMode, i18n.language); setData(pxTable); // Store as accumulated data @@ -755,18 +841,63 @@ const TableDataProvider: React.FC = ({ children }) => { [stubDesktop, headingDesktop], ); + // Check if the chart needs to be re-initialized. + // If the stubChart is empty or has more than one variable, or if the variable in the stubChart + // does not match the variable in the pxTable, then re-initialize the chart. + // This can happen when the user makes changes in the variable selection, such as adding or + // removing variables and values. + const checkIfChartNeedsReInitialize = React.useCallback( + (pxTable: PxTable): boolean => { + if (stubChart.length === 0 || stubChart.length > 1) { + return true; + } + + if (getChartStubVariableId(pxTable) !== stubChart[0]) { + return true; + } + + return false; + }, + [stubChart], + ); + + /** + * Adjusts the table for chart layout. + * + * @param {PxTable} pxTable - The table to be pivoted. + */ + const pivotForChart = React.useCallback( + (pxTable: PxTable) => { + if (checkIfChartNeedsReInitialize(pxTable)) { + const { stubOrderChart, headingOrderChart } = + initializeStubAndHeadingChart(pxTable); + + pivotTable(pxTable, stubOrderChart, headingOrderChart); + } else { + pivotTable(pxTable, stubChart, headingChart); + } + }, + [ + checkIfChartNeedsReInitialize, + initializeStubAndHeadingChart, + stubChart, + headingChart, + ], + ); + /** * Fetch data. We DO have valid accumulated data in the data cube. * * @param tableId - The id of the table to fetch data for. * @param i18n - The i18n object for handling langauages + * @param dataViewMode - The current data view mode (e.g., mobile, desktop, chart). * @param variablesSelection - User selection of variables and their values. */ const fetchWithValidAccData = React.useCallback( async ( tableId: string, i18n: i18n, - isMobile: boolean, + dataViewMode: DataViewModeType, variablesSelection: VariablesSelection, ) => { // Check if all data and metadata asked for by the user is already loaded from earlier API-calls @@ -779,10 +910,12 @@ const TableDataProvider: React.FC = ({ children }) => { const pxTable = createPxTableFromAccumulatedData(variablesSelection); if (pxTable) { - if (isMobile) { + if (dataViewMode === DataViewModeType.MobileTable) { pivotForMobile(pxTable); - } else { + } else if (dataViewMode === DataViewModeType.DesktopTable) { pivotForDesktop(pxTable); + } else if (dataViewMode === DataViewModeType.Chart) { + pivotForChart(pxTable); } setData(pxTable); return; @@ -828,21 +961,23 @@ const TableDataProvider: React.FC = ({ children }) => { pxTable = pxTableMerged; } - if (isMobile) { + if (dataViewMode === DataViewModeType.MobileTable) { pivotForMobile(pxTable); - } else { + } else if (dataViewMode === DataViewModeType.DesktopTable) { pivotForDesktop(pxTable); + } else if (dataViewMode === DataViewModeType.Chart) { + pivotForChart(pxTable); } setData(pxTable); }, [ - setData, - pivotForMobile, - pivotForDesktop, - createPxTableFromAccumulatedData, isAllDataAlreadyLoaded, mergeWithAccumulatedData, + createPxTableFromAccumulatedData, + pivotForMobile, + pivotForDesktop, + pivotForChart, ], ); @@ -850,6 +985,8 @@ const TableDataProvider: React.FC = ({ children }) => { * Checks if the accumulated data is valid. * @param variablesSelection - User selection of variables and their values. * @param language - Language of the current request. + * @param tableId - The id of the table to fetch data for. + * @param codelistChanged - A boolean indicating whether the codelist has changed. * @returns `true` if the accumulated data is valid, `false` otherwise. */ const isAccumulatedDataValid = React.useCallback( @@ -955,9 +1092,10 @@ const TableDataProvider: React.FC = ({ children }) => { * * @param tableId - The id of the table to fetch data for. * @param i18n - The i18n object for handling langauages + * @param dataViewMode - The current data view mode (e.g., mobile, desktop, chart). */ const fetchTableData = React.useCallback( - async (tableId: string, i18n: i18n, isMobile: boolean) => { + async (tableId: string, i18n: i18n, dataViewMode: DataViewModeType) => { try { const selections: Array = []; @@ -996,7 +1134,7 @@ const TableDataProvider: React.FC = ({ children }) => { await fetchWithValidAccData( tableId, i18n, - isMobile, + dataViewMode, variablesSelection, ); } else { @@ -1004,18 +1142,12 @@ const TableDataProvider: React.FC = ({ children }) => { await fetchWithoutValidAccData( tableId, i18n, - isMobile, + dataViewMode, variablesSelection, codelistChanged, ); } - - if (isMobile && !isMobileMode) { - setIsMobileMode(true); - } - if (!isMobile && isMobileMode) { - setIsMobileMode(false); - } + setDataViewMode(dataViewMode); } catch (error: unknown) { const err = error as Error; @@ -1027,7 +1159,6 @@ const TableDataProvider: React.FC = ({ children }) => { variables, manageSelectedCodelists, isAccumulatedDataValid, - isMobileMode, fetchWithValidAccData, fetchWithoutValidAccData, ], @@ -1086,7 +1217,7 @@ const TableDataProvider: React.FC = ({ children }) => { if (tmpTable !== undefined) { pivotTable(tmpTable, stubMobile, headingMobile); setData(tmpTable); - setIsMobileMode(true); + setDataViewMode(DataViewModeType.MobileTable); } } }, [data, stubMobile, headingMobile]); @@ -1102,11 +1233,45 @@ const TableDataProvider: React.FC = ({ children }) => { if (tmpTable !== undefined) { pivotTable(tmpTable, stubDesktop, headingDesktop); setData(tmpTable); - setIsMobileMode(false); + setDataViewMode(DataViewModeType.DesktopTable); } } }, [data, stubDesktop, headingDesktop]); + /** + * Pivots to table layout. + * This function updates the table structure to fit a table layout by adjusting the stub and heading order. + * If the `isMobile` parameter is true, it pivots to mobile layout; otherwise, it pivots to desktop layout. + * + * @param isMobile - A boolean indicating whether to pivot to mobile layout (true) or desktop layout (false). + */ + const pivotToTable = React.useCallback( + (isMobile: boolean) => { + if (isMobile) { + pivotToMobile(); + } else { + pivotToDesktop(); + } + }, + [pivotToDesktop, pivotToMobile], + ); + + /** + * Pivots the table to chart layout. + * This function updates the table structure to fit a chart layout by adjusting the stub and heading order. + */ + const pivotToChart = React.useCallback(() => { + if (data?.heading !== undefined) { + const tmpTable = structuredClone(data); + + if (tmpTable !== undefined) { + pivotForChart(tmpTable); + setData(tmpTable); + setDataViewMode(DataViewModeType.Chart); + } + } + }, [data, pivotForChart]); + /** * Builds the table title parts based on the current table data and metadata. * @returns An object containing the parts of the table title. @@ -1139,8 +1304,10 @@ const TableDataProvider: React.FC = ({ children }) => { */ const pivot = React.useCallback( (type: PivotType): void => { - // Autopivot not allowed for mobile mode - if (isMobileMode && type === PivotType.Auto) { + if ( + dataViewMode === DataViewModeType.MobileTable && + type === PivotType.Auto + ) { return; } if (data?.heading === undefined || data?.stub === undefined) { @@ -1151,13 +1318,17 @@ const TableDataProvider: React.FC = ({ children }) => { let stub: string[] = []; let heading: string[] = []; - if (isMobileMode) { - stub = structuredClone(stubMobile); - heading = structuredClone(headingMobile); - } else { + if (dataViewMode === DataViewModeType.DesktopTable) { stub = structuredClone(stubDesktop); heading = structuredClone(headingDesktop); + } else if (dataViewMode === DataViewModeType.MobileTable) { + stub = structuredClone(stubMobile); + heading = structuredClone(headingMobile); + } else if (dataViewMode === DataViewModeType.Chart) { + stub = structuredClone(stubChart); + heading = structuredClone(headingChart); } + if (stub.length === 0 && heading.length === 0) { return; } @@ -1175,21 +1346,26 @@ const TableDataProvider: React.FC = ({ children }) => { setData(tmpTable); - if (isMobileMode) { - setStubMobile(stub); - setHeadingMobile(heading); - } else { + if (dataViewMode === DataViewModeType.DesktopTable) { setStubDesktop(stub); setHeadingDesktop(heading); + } else if (dataViewMode === DataViewModeType.MobileTable) { + setStubMobile(stub); + setHeadingMobile(heading); + } else if (dataViewMode === DataViewModeType.Chart) { + setStubChart(stub); + setHeadingChart(heading); } }, [ + dataViewMode, data, - isMobileMode, - stubMobile, - headingMobile, stubDesktop, headingDesktop, + stubMobile, + headingMobile, + stubChart, + headingChart, ], ); @@ -1242,6 +1418,8 @@ const TableDataProvider: React.FC = ({ children }) => { fetchSavedQuery, pivotToMobile, pivotToDesktop, + pivotToTable, + pivotToChart, pivot, buildTableTitle, isInitialized, @@ -1254,6 +1432,8 @@ const TableDataProvider: React.FC = ({ children }) => { fetchSavedQuery, pivotToMobile, pivotToDesktop, + pivotToTable, + pivotToChart, pivot, buildTableTitle, isInitialized, diff --git a/packages/pxweb2/src/app/context/TableDataProviderUtils.spec.tsx b/packages/pxweb2/src/app/context/TableDataProviderUtils.spec.tsx index 3e856b358..e640965d2 100644 --- a/packages/pxweb2/src/app/context/TableDataProviderUtils.spec.tsx +++ b/packages/pxweb2/src/app/context/TableDataProviderUtils.spec.tsx @@ -3,6 +3,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { getFormattedValue, addFormattingToPxTable, + initStubAndHeadingDesktop, + initStubAndHeadingMobile, + initStubAndHeadingChart, filterStubAndHeadingArrays, autoPivotTable, getTableTitleParts, @@ -65,6 +68,7 @@ describe('TableDataProviderUtils', () => { subjectCode: 'Test Subject Code', notes: [], ...((overrides.metadata as object) || {}), + availableLanguages: [], }, data: { variableOrder: [], @@ -258,6 +262,7 @@ describe('TableDataProviderUtils', () => { subjectArea: 'Test Subject Area', subjectCode: 'Test Subject Code', notes: [], + availableLanguages: [], }, data: { variableOrder: ['dim1', 'dim2'], @@ -342,6 +347,7 @@ describe('TableDataProviderUtils', () => { subjectArea: 'Test Subject Area', subjectCode: 'Test Subject Code', notes: [], + availableLanguages: [], }, data: { variableOrder: ['contents'], @@ -383,6 +389,7 @@ describe('TableDataProviderUtils', () => { subjectArea: 'Test Subject Area', subjectCode: 'Test Subject Code', notes: [], + availableLanguages: [], }, data: { variableOrder: [], @@ -444,6 +451,8 @@ describe('TableDataProviderUtils', () => { const headingDesktop = ['d', 'e', 'f']; const stubMobile = ['a', 'e', 'g']; const headingMobile = ['c', 'h', 'i']; + const stubChart = ['a', 'd', 'e']; + const headingChart = ['b', 'c', 'f']; const result = filterStubAndHeadingArrays( variableIds, @@ -451,12 +460,16 @@ describe('TableDataProviderUtils', () => { headingDesktop, stubMobile, headingMobile, + stubChart, + headingChart, ); expect(result.stubDesktop).toEqual(['a', 'c']); expect(result.headingDesktop).toEqual(['e']); expect(result.stubMobile).toEqual(['a', 'e']); expect(result.headingMobile).toEqual(['c']); + expect(result.stubChart).toEqual(['a', 'e']); + expect(result.headingChart).toEqual(['c']); }); it('returns empty arrays if no variableIds match', () => { @@ -465,6 +478,8 @@ describe('TableDataProviderUtils', () => { const headingDesktop = ['c', 'd']; const stubMobile = ['e', 'f']; const headingMobile = ['g', 'h']; + const stubChart = ['i', 'j']; + const headingChart = ['k', 'l']; const result = filterStubAndHeadingArrays( variableIds, @@ -472,12 +487,16 @@ describe('TableDataProviderUtils', () => { headingDesktop, stubMobile, headingMobile, + stubChart, + headingChart, ); expect(result.stubDesktop).toEqual([]); expect(result.headingDesktop).toEqual([]); expect(result.stubMobile).toEqual([]); expect(result.headingMobile).toEqual([]); + expect(result.stubChart).toEqual([]); + expect(result.headingChart).toEqual([]); }); it('returns original arrays if all variableIds match', () => { @@ -486,6 +505,8 @@ describe('TableDataProviderUtils', () => { const headingDesktop = ['a', 'b', 'c']; const stubMobile = ['a', 'b', 'c']; const headingMobile = ['a', 'b', 'c']; + const stubChart = ['a', 'b', 'c']; + const headingChart = ['a', 'b', 'c']; const result = filterStubAndHeadingArrays( variableIds, @@ -493,12 +514,108 @@ describe('TableDataProviderUtils', () => { headingDesktop, stubMobile, headingMobile, + stubChart, + headingChart, ); expect(result.stubDesktop).toEqual(['a', 'b', 'c']); expect(result.headingDesktop).toEqual(['a', 'b', 'c']); expect(result.stubMobile).toEqual(['a', 'b', 'c']); expect(result.headingMobile).toEqual(['a', 'b', 'c']); + expect(result.stubChart).toEqual(['a', 'b', 'c']); + expect(result.headingChart).toEqual(['a', 'b', 'c']); + }); + }); + + describe('initStubAndHeading helpers', () => { + it('initStubAndHeadingDesktop returns stub and heading ids in current order', () => { + const region = createVariable( + 'region', + VartypeEnum.GEOGRAPHICAL_VARIABLE, + 3, + ); + const time = createVariable('time', VartypeEnum.TIME_VARIABLE, 2); + const sex = createVariable('sex', VartypeEnum.REGULAR_VARIABLE, 2); + + const pxTable = createBasePxTable({ + stub: [region, sex], + heading: [time], + }); + + const result = initStubAndHeadingDesktop(pxTable); + + expect(result.stubOrderDesktop).toEqual(['region', 'sex']); + expect(result.headingOrderDesktop).toEqual(['time']); + }); + + it('initStubAndHeadingMobile combines stub and heading then sorts by value length', () => { + const time = createVariable('time', VartypeEnum.TIME_VARIABLE, 2); + const sex = createVariable('sex', VartypeEnum.REGULAR_VARIABLE, 3); + const region = createVariable( + 'region', + VartypeEnum.GEOGRAPHICAL_VARIABLE, + 4, + ); + + const pxTable = createBasePxTable({ + stub: [region], + heading: [sex, time], + }); + + const originalStub = pxTable.stub.map((v) => v.id); + const originalHeading = pxTable.heading.map((v) => v.id); + + const result = initStubAndHeadingMobile(pxTable); + + expect(result.stubOrderMobile).toEqual(['time', 'sex', 'region']); + expect(result.headingOrderMobile).toEqual([]); + + // Utility should not mutate pxTable.stub/pxTable.heading + expect(pxTable.stub.map((v) => v.id)).toEqual(originalStub); + expect(pxTable.heading.map((v) => v.id)).toEqual(originalHeading); + }); + + it('initStubAndHeadingChart places time variable in stub and others in heading order', () => { + const contents = createVariable( + 'contents', + VartypeEnum.CONTENTS_VARIABLE, + 2, + ); + const time = createVariable('time', VartypeEnum.TIME_VARIABLE, 3); + const region = createVariable( + 'region', + VartypeEnum.GEOGRAPHICAL_VARIABLE, + 4, + ); + + const pxTable = createBasePxTable(); + pxTable.metadata.variables = [contents, time, region]; + + const result = initStubAndHeadingChart(pxTable); + + expect(result.stubOrderChart).toEqual(['time']); + expect(result.headingOrderChart).toEqual(['contents', 'region']); + }); + + it('initStubAndHeadingChart uses variable with most values in stub when no time variable exists', () => { + const contents = createVariable( + 'contents', + VartypeEnum.CONTENTS_VARIABLE, + 2, + ); + const region = createVariable( + 'region', + VartypeEnum.GEOGRAPHICAL_VARIABLE, + 4, + ); + + const pxTable = createBasePxTable(); + pxTable.metadata.variables = [contents, region]; + + const result = initStubAndHeadingChart(pxTable); + + expect(result.stubOrderChart).toEqual(['region']); + expect(result.headingOrderChart).toEqual(['contents']); }); }); @@ -585,6 +702,9 @@ describe('TableDataProviderUtils', () => { variableFilterExclusionList: { en: ['statisticalvariable', 'year', 'quarter', 'month', 'week'], }, + features: { + chartEnabled: false, + }, }; beforeEach(() => { diff --git a/packages/pxweb2/src/app/context/TableDataProviderUtils.ts b/packages/pxweb2/src/app/context/TableDataProviderUtils.ts index 8554bfabe..cc011d7a7 100644 --- a/packages/pxweb2/src/app/context/TableDataProviderUtils.ts +++ b/packages/pxweb2/src/app/context/TableDataProviderUtils.ts @@ -208,9 +208,98 @@ export async function addFormattingToPxTable( return true; } +export function initStubAndHeadingDesktop(pxTable: PxTable) { + // -> Set stub and heading order for desktop according to the order in pxTable + const stubOrderDesktop: string[] = pxTable.stub.map( + (variable) => variable.id, + ); + const headingOrderDesktop: string[] = pxTable.heading.map( + (variable) => variable.id, + ); + + return { + stubOrderDesktop, + headingOrderDesktop, + }; +} + +export function initStubAndHeadingMobile(pxTable: PxTable) { + // -> Set stub and heading order for mobile according to the order in pxTable + const tmpStubMobile = structuredClone(pxTable.stub); + const tmpHeadingMobile = structuredClone(pxTable.heading); + + tmpHeadingMobile.forEach((variable) => { + tmpStubMobile.push(variable); + }); + + tmpStubMobile.sort((a, b) => a.values.length - b.values.length); + + const stubOrderMobile: string[] = tmpStubMobile.map( + (variable) => variable.id, + ); + + const headingOrderMobile: string[] = []; + + return { + stubOrderMobile, + headingOrderMobile, + }; +} + +export function initStubAndHeadingChart(pxTable: PxTable) { + // -> Set stub and heading order for chart according to the order in pxTable + const stubOrderChart: string[] = []; + const headingOrderChart: string[] = []; + const stubVariableId: string = getChartStubVariableId(pxTable); + + stubOrderChart.push(stubVariableId); + + for (const variable of pxTable.metadata.variables) { + if (variable.id !== stubVariableId) { + headingOrderChart.push(variable.id); + } + } + + return { + stubOrderChart, + headingOrderChart, + }; +} + +// Returns the variable to be used for the x-axis in charts, prioritizing time variable if it has more than one value, otherwise the variable with the most values, or the first variable if none of the above. +export function getChartStubVariableId(pxTable: PxTable): string { + const timeVariable = pxTable.metadata.variables.find( + (variable) => variable.type === VartypeEnum.TIME_VARIABLE, + ); + + if (timeVariable && timeVariable.values.length > 1) { + return timeVariable.id; + } + + const variableWithMostValues = + pxTable.metadata.variables.reduce( + (maxVariable, currentVariable) => + maxVariable === null || + currentVariable.values.length > maxVariable.values.length + ? currentVariable + : maxVariable, + null, + ); + + if (variableWithMostValues) { + return variableWithMostValues.id; + } + + if (pxTable.metadata.variables.length > 0) { + return pxTable.metadata.variables[0].id; + } + + return ''; +} + /** * Filters stub and heading arrays to only include variable IDs present in variableIds. - * Returns new arrays for stubDesktop, headingDesktop, stubMobile, headingMobile. + * Returns new arrays for stubDesktop, headingDesktop, stubMobile, headingMobile, stubChart, headingChart. */ export function filterStubAndHeadingArrays( variableIds: string[], @@ -218,12 +307,16 @@ export function filterStubAndHeadingArrays( headingDesktop: string[], stubMobile: string[], headingMobile: string[], + stubChart: string[], + headingChart: string[], ) { return { stubDesktop: stubDesktop.filter((id) => variableIds.includes(id)), headingDesktop: headingDesktop.filter((id) => variableIds.includes(id)), stubMobile: stubMobile.filter((id) => variableIds.includes(id)), headingMobile: headingMobile.filter((id) => variableIds.includes(id)), + stubChart: stubChart.filter((id) => variableIds.includes(id)), + headingChart: headingChart.filter((id) => variableIds.includes(id)), }; } diff --git a/packages/pxweb2/src/app/pages/TableViewer/Utils/tableViewerHelper.spec.ts b/packages/pxweb2/src/app/pages/TableViewer/Utils/tableViewerHelper.spec.ts index 170b6e1c7..1d381e6c0 100644 --- a/packages/pxweb2/src/app/pages/TableViewer/Utils/tableViewerHelper.spec.ts +++ b/packages/pxweb2/src/app/pages/TableViewer/Utils/tableViewerHelper.spec.ts @@ -1,4 +1,9 @@ -import { getSearchParamsWithViewMode, getViewMode } from './tableViewerHelper'; +import { DataViewModeType } from '../../../context/DataViewModeType'; +import { + getDataViewMode, + getSearchParamsWithViewMode, + getViewMode, +} from './tableViewerHelper'; describe('tableViewerHelper', () => { describe('getViewMode', () => { @@ -50,4 +55,21 @@ describe('tableViewerHelper', () => { expect(searchParams.get('view')).toBe('table'); }); }); + + describe('getDataViewMode', () => { + it('returns Chart when viewMode is linechart regardless of device type', () => { + expect(getDataViewMode('linechart', true)).toBe(DataViewModeType.Chart); + expect(getDataViewMode('linechart', false)).toBe(DataViewModeType.Chart); + }); + + it('returns MobileTable when viewMode is table and device is mobile', () => { + expect(getDataViewMode('table', true)).toBe(DataViewModeType.MobileTable); + }); + + it('returns DesktopTable when viewMode is table and device is not mobile', () => { + expect(getDataViewMode('table', false)).toBe( + DataViewModeType.DesktopTable, + ); + }); + }); }); diff --git a/packages/pxweb2/src/app/pages/TableViewer/Utils/tableViewerHelper.ts b/packages/pxweb2/src/app/pages/TableViewer/Utils/tableViewerHelper.ts index 2bde77aea..6dde9aedc 100644 --- a/packages/pxweb2/src/app/pages/TableViewer/Utils/tableViewerHelper.ts +++ b/packages/pxweb2/src/app/pages/TableViewer/Utils/tableViewerHelper.ts @@ -1,3 +1,5 @@ +import { DataViewModeType } from '../../../context/DataViewModeType'; + export type ViewMode = 'table' | 'linechart'; export function getViewMode( @@ -19,3 +21,16 @@ export function getSearchParamsWithViewMode( nextSearchParams.set('view', viewMode); return nextSearchParams; } + +export function getDataViewMode( + viewMode: ViewMode, + isMobile: boolean, +): DataViewModeType { + if (viewMode === 'linechart') { + return DataViewModeType.Chart; + } + + return isMobile + ? DataViewModeType.MobileTable + : DataViewModeType.DesktopTable; +}