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;
+}