diff --git a/sonar-project.properties b/sonar-project.properties index 39c72f6..529ef1d 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -5,5 +5,5 @@ sonar.testExecutionReportPaths=reports/test-reporter.xml sonar.sources=src sonar.tests=src sonar.test.inclusions=**/*.test.js,**/*.test.ts,**/*.test.tsx -sonar.exclusions=**/*.test.js,**/*.test.ts,**/*.test.tsx,**/*.test.tsx.snap,src/stories/**,**/fixtures/** -sonar.cpd.exclusions=**/fixtures/** \ No newline at end of file +sonar.exclusions=**/*.test.js,**/*.test.ts,**/*.test.tsx,**/*.test.tsx.snap,src/stories/**,**/fixtures/**,**/testFixtures/** +sonar.cpd.exclusions=**/fixtures/**,**/testFixtures/** \ No newline at end of file diff --git a/src/core/styles/chart.css b/src/core/styles/chart.css index 42e3679..2503c68 100644 --- a/src/core/styles/chart.css +++ b/src/core/styles/chart.css @@ -50,4 +50,66 @@ margin-bottom: 2rem; margin-top: 6px; width: 90%; +} + +.keyFigureChart { + font-family: "Barlow Semi Condensed", Verdana, sans-serif; +} + +.keyFigure-container { + border: 1px solid #adadad; + padding: 1rem; + border-radius: 1rem; + box-shadow: 0 2px 4px rgba(0,0,0,0.05); + background-color: white; +} + +.keyFigure-title { + font-weight: 600; + letter-spacing: 0; + line-height: 1.4; + color: #1a3061; +} + +.keyFigure-value-main { + font-weight: 600; + font-size: 2rem; + margin-inline-end: 0.25rem; + color: #1a3061; + line-height: 1.2; +} + +.keyFigure-unit { + font-weight: 400; + font-size: 1.2rem; + margin-top: 1.5rem; + margin-bottom: 0rem; + color: #1a3061; +} + +.keyFigure-time { + margin: 0; + line-height: 1.6; + font-size: 1rem; + font-weight: 400; + max-width: 100%; + color: #1a3061; +} + +.keyFigure-lastupdated { + margin: 0; + line-height: 1.6; + font-size: 0.9rem; + font-weight: 400; + max-width: 100%; + color: #1a3061; +} + +.keyFigure-preliminary { + margin: 0; + line-height: 1.6; + font-size: 1rem; + font-weight: 400; + max-width: 100%; + color: #1a3061; } \ No newline at end of file diff --git a/src/core/tables/__snapshots__/htmlTable.test.ts.snap b/src/core/tables/__snapshots__/htmlTable.test.ts.snap index 9562609..b7c05c7 100644 --- a/src/core/tables/__snapshots__/htmlTable.test.ts.snap +++ b/src/core/tables/__snapshots__/htmlTable.test.ts.snap @@ -1,5 +1,45 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Html table render tests should match snapshot: Key figure 1`] = ` +" +  +  + Lukumäärä, Vantaa, Yksiöt, Vapaarahoitteinen 2022Q4 +  +  +  + 2 548 +  +  +  lukumäärä +  +  +  + 2022Q4 +  +  + 2023-03-15 +  +  +" +`; + exports[`Html table render tests should match snapshot: Table with column variables only 1`] = ` " " `; - -exports[`Html table render tests should match snapshot: Table with source and footnote 1`] = ` -" -  -  - Lukumäärä, Vantaa, Yksiöt, Vapaarahoitteinen 2022Q4 -  -  -  -  - 2 548 -  -  -  -  - 

 - Yksikkö: lukumäärä - 

 - 

 - Test footnote - 

 - 

 - Lähde: PxVisualizer-fi - 

 -" -`; diff --git a/src/core/tables/htmlTable.test.ts b/src/core/tables/htmlTable.test.ts index 68ab874..022cf33 100644 --- a/src/core/tables/htmlTable.test.ts +++ b/src/core/tables/htmlTable.test.ts @@ -3,8 +3,9 @@ import { TABLE_WITH_ONE_CELL, TABLE_WITH_ONLY_ROW_VARIABLES, TABLE_WITH_ROW_AND_ import { SELECTABLE_TABLE_WITH_MISSING_DATA, TABLE_WITH_ONLY_COLUMN_VARIABLES } from "../conversion/fixtures/tableChart"; import { extractSelectableVariableValues } from "../conversion/helpers"; import { convertPxGrafResponseToView } from "../conversion/viewUtils"; -import { renderHtmlTable } from "./htmlTable"; +import { renderHtmlKeyFigure, renderHtmlTable } from "./htmlTable"; import { SELECTABLE_TABLE_WITH_INVALID_MISSING_DATA } from "./fixtures/pxGrafResponses"; +import { EVisualizationType } from "../types"; describe('Html table render tests', () => { it('should match snapshot: Table with column variables only', () => { @@ -168,29 +169,6 @@ describe('Html table render tests', () => { document.body.removeChild(div); }); - it('should match snapshot: Table with source and footnote', () => { - const mockVarSelections = extractSelectableVariableValues( - TABLE_WITH_ONE_CELL.pxGraphData.selectableVariableCodes, - TABLE_WITH_ONE_CELL.pxGraphData.metaData, - TABLE_WITH_ONE_CELL.pxGraphData.visualizationSettings.defaultSelectableVariableCodes, - TABLE_WITH_ONE_CELL.selectedVariableCodes); - const mockView = convertPxGrafResponseToView(TABLE_WITH_ONE_CELL.pxGraphData, mockVarSelections); - const locale = 'fi'; - - const testId = 'test-6895638450983059889'; - - const div = document.createElement('div'); - div.id = testId; - document.body.appendChild(div); - - renderHtmlTable(mockView, locale, true, true, true, testId, 'Test footnote'); - - const renderedOutput = prettyDOM(div); - expect(renderedOutput).toMatchSnapshot(); - - document.body.removeChild(div); - }); - it('should match snapshot: Table with missing data and selectable values', () => { const mockVarSelections = extractSelectableVariableValues( SELECTABLE_TABLE_WITH_MISSING_DATA.selectableVariableCodes, @@ -246,4 +224,36 @@ describe('Html table render tests', () => { spy.mockRestore(); }); + + it('should match snapshot: Key figure', () => { + const mockVarSelections = extractSelectableVariableValues( + TABLE_WITH_ONE_CELL.pxGraphData.selectableVariableCodes, + TABLE_WITH_ONE_CELL.pxGraphData.metaData, + TABLE_WITH_ONE_CELL.pxGraphData.visualizationSettings.defaultSelectableVariableCodes, + TABLE_WITH_ONE_CELL.selectedVariableCodes); + const mockView = convertPxGrafResponseToView(TABLE_WITH_ONE_CELL.pxGraphData, mockVarSelections); + mockView.visualizationSettings = { + ...mockView.visualizationSettings, + visualizationType: EVisualizationType.KeyFigure, + showUnit: true + } + + const locale = 'fi'; + + const testId = 'test-49871891798765432'; + + const div = document.createElement('div'); + div.id = testId; + document.body.appendChild(div); + + const timeVariableValue = "2022Q4"; + const lastUpdated = "2023-03-15"; + + renderHtmlKeyFigure(mockView, locale, testId, timeVariableValue, lastUpdated); + + const renderedOutput = prettyDOM(div); + expect(renderedOutput).toMatchSnapshot(); + + document.body.removeChild(div); + }); }); \ No newline at end of file diff --git a/src/core/tables/htmlTable.ts b/src/core/tables/htmlTable.ts index d8c4b2c..f23ec38 100644 --- a/src/core/tables/htmlTable.ts +++ b/src/core/tables/htmlTable.ts @@ -1,7 +1,7 @@ -import { getFormattedUnits } from "../chartOptions/Utility/formatters"; -import { Translations } from "../conversion/translations"; -import { TMultiLanguageString } from "../types/queryVisualizationResponse"; -import { IDataSeries, View } from "../types/view"; +import { getFormattedUnits } from "../../core/chartOptions/Utility/formatters"; +import { Translations } from "../../core/conversion/translations"; +import { TMultiLanguageString } from "../../core/types/queryVisualizationResponse"; +import { IDataSeries, View } from "../../core/types/view"; import { formatMissingData, formatNumericValue } from "./tableUtils"; export function renderHtmlTable(view: View, locale: string, showTitles: boolean, showUnits: boolean, showSources: boolean, containerId: string, footnote?: string): void { @@ -60,9 +60,71 @@ export function renderHtmlTable(view: View, locale: string, showTitles: boolean, } } +export function renderHtmlKeyFigure(view: View, locale: string, containerId: string, timeVariableValue: string, lastUpdated: string, className?: string): void { + const container = document.getElementById(containerId); + if (!container) throw new Error("No container with matching id found in the DOM tree"); + + try { + // Create the key figure display container + const keyFigureContainer = document.createElement('div'); + keyFigureContainer.className = className ? `keyFigure-container ${className}` : 'keyFigure-container'; + + // Add title if available + const title = document.createElement('div'); + title.className = className ? `keyFigure-title ${className}` : 'keyFigure-title'; + title.textContent = view.header[locale]; + keyFigureContainer.append(title); + + const dataCell = view.series[0].series[0]; + const valueContainer = document.createElement('div'); + valueContainer.className = className ? `keyFigure-value ${className}` : 'keyFigure-value'; + + // Format the value or show missing data message + if (dataCell.value === null) { + valueContainer.textContent = formatMissingData(dataCell.missingCode, locale, true); + } else { + const valueSpan = document.createElement('span'); + valueSpan.className = 'keyFigure-value-main'; + valueSpan.textContent = formatNumericValue(dataCell.value, dataCell.precision, locale, true); + valueContainer.append(valueSpan); + // If show units is enabled, append the unit to the value in a span + if (view.visualizationSettings.showUnit && view.units.length > 0) { + const unitName = getFormattedUnits(view.units, locale); + const unitSpan = document.createElement('span'); + unitSpan.className = 'keyFigure-unit'; + unitSpan.textContent = ` ${unitName}`; + valueContainer.append(unitSpan); + } + } + + keyFigureContainer.append(valueContainer); + + const timeElem = document.createElement('div'); + timeElem.className = className ? `keyFigure-time ${className}` : 'keyFigure-time'; + timeElem.textContent = dataCell.preliminary + ? `${timeVariableValue} ${Translations.preliminaryData[locale]}` + : timeVariableValue; + keyFigureContainer.append(timeElem); + + const lastUpdatedElem = document.createElement('div'); + lastUpdatedElem.className = className ? `keyFigure-lastupdated ${className}` : 'keyFigure-lastupdated'; + lastUpdatedElem.textContent = lastUpdated; + keyFigureContainer.append(lastUpdatedElem); + + container.append(keyFigureContainer); + + } catch (error) { + console.error(error); + container.replaceChildren(); + const errorMessage = document.createElement('h1'); + errorMessage.append(Translations.graphCreationError[locale]); + container.append(errorMessage); + } +} + export function generateTable(view: View, locale: string): HTMLTableElement { - const colHeaderRows = view.columnNameGroups[0].length ?? 0; - const rowHeaderCols = view.series[0].rowNameGroup.length ?? 0; + const colHeaderRows = view.columnNameGroups[0]?.length ?? 0; + const rowHeaderCols = view.series[0]?.rowNameGroup.length ?? 0; const table: HTMLTableElement = Object.assign(document.createElement('table'), { tabIndex: 0 }); @@ -140,17 +202,19 @@ function buildDataRows(series: IDataSeries[], locale: string): HTMLTableRowEleme } const compare = (a: TMultiLanguageString, b: TMultiLanguageString) => - Object.keys(a).every(lang => a[lang] == b[lang]); + Object.keys(a).every(lang => a[lang] === b[lang]); const calculateRowSpans = (series: IDataSeries[]): number[] => { if (!series[0].rowNameGroup || series[0].rowNameGroup.length === 0) return []; const rowSpans: number[] = Array(series[0].rowNameGroup.length).fill(1); for (let col = 0; col < series[0].rowNameGroup.length; col++) { + let span = 1; for (let row = 0; row < series.length - 1; row++) { - if (compare(series[row].rowNameGroup[col], series[row+1].rowNameGroup[col])) rowSpans[col]++; + if (compare(series[row].rowNameGroup[col], series[row+1].rowNameGroup[col])) span++; else break; } + rowSpans[col] = span; } return rowSpans; } @@ -160,10 +224,12 @@ const calculateColSpans = (columnNameGroups: TMultiLanguageString[][]): number[] const colSpans: number[] = Array(columnNameGroups[0].length).fill(1); for (let row = 0; row < columnNameGroups[0].length; row++) { + let span = 1; for (let col = 0; col < columnNameGroups.length - 1; col++) { - if (compare(columnNameGroups[col][row], columnNameGroups[col + 1][row])) colSpans[row]++; + if (compare(columnNameGroups[col][row], columnNameGroups[col + 1][row])) span++; else break; } + colSpans[row] = span; } return colSpans; } \ No newline at end of file diff --git a/src/core/tables/index.ts b/src/core/tables/index.ts index 4f41c7b..54cacec 100644 --- a/src/core/tables/index.ts +++ b/src/core/tables/index.ts @@ -1,4 +1,4 @@ -export { renderHtmlTable } from "./htmlTable"; +export { renderHtmlTable, renderHtmlKeyFigure } from "./htmlTable"; export { generateCsv } from "./csvTable"; export { viewToDownloadCSVOption } from "./csvTable"; diff --git a/src/core/types/queryVisualizationResponse.ts b/src/core/types/queryVisualizationResponse.ts index 7cc52b8..91965fa 100644 --- a/src/core/types/queryVisualizationResponse.ts +++ b/src/core/types/queryVisualizationResponse.ts @@ -30,6 +30,7 @@ export type TVisualizationType = | 'LineChart' | 'ScatterPlot' | 'Table' +| 'KeyFigure' export enum EVisualizationType { @@ -45,7 +46,8 @@ export enum EVisualizationType { PieChart = 'PieChart', LineChart = 'LineChart', ScatterPlot = 'ScatterPlot', - Table = 'Table' + Table = 'Table', + KeyFigure = 'KeyFigure' } export type TVariableType = @@ -128,4 +130,5 @@ export interface IVisualizationSettings { markerSize?: number; cutYAxis?: boolean; showDataPoints?: boolean; + showUnit?: boolean; } diff --git a/src/react/components/chart/__snapshots__/chart.test.tsx.snap b/src/react/components/chart/__snapshots__/chart.test.tsx.snap index 29a6905..328ff7b 100644 --- a/src/react/components/chart/__snapshots__/chart.test.tsx.snap +++ b/src/react/components/chart/__snapshots__/chart.test.tsx.snap @@ -255,6 +255,92 @@ exports[`Rendering test renders error component on broken data 1`] = ` `; +exports[`Rendering test renders keyfigure data correctly 1`] = ` + +
+
+
+ +
+
+
+
+
+
+ Lukumäärä, Vantaa, Yksiöt, Vapaarahoitteinen 2022Q4 +
+
+ + 2 548 + + + lukumäärä + +
+
+ 2022Q4 +
+
+ 2023-01-19T06:00:00Z +
+
+
+
+ +`; + exports[`Rendering test renders table data correctly 1`] = `