From f90d94e1bb23c20e72b7c333df81b3957b8b28df Mon Sep 17 00:00:00 2001 From: Sakari Malkki Date: Thu, 4 Sep 2025 10:09:48 +0300 Subject: [PATCH 1/5] =?UTF-8?q?=EF=BB=BFSupport=20for=20key=20figure=20vis?= =?UTF-8?q?ualizationtype?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/tables/htmlTable.ts | 77 ++++++++++++++++--- src/core/tables/index.ts | 2 +- src/core/types/queryVisualizationResponse.ts | 5 +- src/react/components/chart/chart.tsx | 34 +++++++- src/react/components/chart/keyFigureView.tsx | 34 ++++++++ .../tablestories/keyfigure.stories.tsx | 29 +++++++ 6 files changed, 168 insertions(+), 13 deletions(-) create mode 100644 src/react/components/chart/keyFigureView.tsx create mode 100644 src/stories/tablestories/keyfigure.stories.tsx diff --git a/src/core/tables/htmlTable.ts b/src/core/tables/htmlTable.ts index d8c4b2c..e193a7f 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,64 @@ export function renderHtmlTable(view: View, locale: string, showTitles: boolean, } } +export function renderHtmlKeyFigure(view: View, locale: string, containerId: 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 = 'keyFigure-container'; + + // Add title if available + const title = document.createElement('div'); + 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 = 'keyFigure-value'; + + // Format the value or show missing data message + if (dataCell.value === null) { + valueContainer.textContent = formatMissingData(dataCell.missingCode, locale, true); + } else { + let formattedValue = formatNumericValue(dataCell.value, dataCell.precision, locale, true); + + // If show units is enabled, append the unit to the value + if (view.visualizationSettings.showUnit && view.units.length > 0) { + const unitName = getFormattedUnits(view.units, locale); + formattedValue += ` ${unitName}`; + } + + valueContainer.textContent = formattedValue; + } + + keyFigureContainer.append(valueContainer); + + // Show preliminary indicator if needed + if (dataCell.preliminary) { + const preliminaryIndicator = document.createElement('div'); + preliminaryIndicator.className = 'keyFigure-preliminary'; + preliminaryIndicator.textContent = Translations.preliminaryData[locale]; + keyFigureContainer.append(preliminaryIndicator); + } + + 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 +195,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 +217,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/chart.tsx b/src/react/components/chart/chart.tsx index fcae599..2b68215 100644 --- a/src/react/components/chart/chart.tsx +++ b/src/react/components/chart/chart.tsx @@ -1,7 +1,6 @@ import React, { useMemo } from "react"; import styled from "styled-components"; import { convertPxGraphDataToChartOptions, EVisualizationType, IQueryVisualizationResponse, defaultTheme } from "../../../core"; - import Highcharts from 'highcharts'; // Named import HighchartsReact was added to get pxvisualiser to work in pxgraf-creator. // Could be something to be resolved with microbundle (non-)configuration too? @@ -15,10 +14,12 @@ import { extractSelectableVariableValues } from "../../../core/conversion/helper import { convertPxGrafResponseToView } from "../../../core/conversion/viewUtils"; import { formatLocale } from "../../../core/chartOptions/Utility/formatters"; import { TableView } from "./tableView"; +import { KeyFigureView } from "./keyFigureView"; import { GlobalStyle } from "../globalStyle"; import { View } from "../../../core/types/view"; import { ErrorInfo } from "./ErrorInfo"; import { ErrorBoundary } from "../ErrorBoundary/ErrorBoundary"; +import { EVariableType } from "../../../core/types/queryVisualizationResponse"; const initializeHighcharts = (locale: string) => { if (typeof Highcharts === 'object') { @@ -147,8 +148,37 @@ const ReactChart: React.FC = ({ }, [chartRef.current]); try { + // Key figure + if (view && pxGraphData.visualizationSettings.visualizationType === EVisualizationType.KeyFigure) { + const getFirstValueByType = (type: EVariableType) => + pxGraphData.metaData.find(meta => meta.type === type)?.values[0]; + + const timeVariableValue: string | undefined = getFirstValueByType(EVariableType.Time)?.name?.[validLocale]; + const lastUpdated: string | undefined = getFirstValueByType(EVariableType.Content)?.contentComponent?.lastUpdated; + if (!timeVariableValue || !lastUpdated) { + throw new Error('Time variable or last updated info missing for key figure visualization'); + } + + return ( + + { + showContextMenu && + + + + } + + + ); + } + // Chart - if (view && pxGraphData.visualizationSettings.visualizationType !== EVisualizationType.Table) { + if (view && pxGraphData.visualizationSettings.visualizationType !== EVisualizationType.Table && pxGraphData.visualizationSettings.visualizationType !== EVisualizationType.KeyFigure) { const highChartOptions = convertPxGraphDataToChartOptions(validLocale, view, { accessibilityMode: accessibilityMode, showTitle: showTitles ?? true }); return ( diff --git a/src/react/components/chart/keyFigureView.tsx b/src/react/components/chart/keyFigureView.tsx new file mode 100644 index 0000000..eeff6bc --- /dev/null +++ b/src/react/components/chart/keyFigureView.tsx @@ -0,0 +1,34 @@ +import React, { useMemo } from "react"; +import { View } from "../../../core/types/view"; +import { v4 as uuidv4 } from 'uuid'; +import { renderHtmlKeyFigure } from "../../../core/tables/htmlTable"; + +export interface IKeyFigureViewProps { + view: View; + locale: string; + timeVariableValue: string; + lastUpdated: string; +} + +export const KeyFigureView: React.FC = ({ view, locale, timeVariableValue, lastUpdated }) => { + const uuid = useMemo(() => uuidv4(), [view, locale]); + + React.useEffect(() => { + document.getElementById(uuid)?.replaceChildren(); + renderHtmlKeyFigure(view, locale, uuid); + // Add time variable, subheader, and last updated info + const container = document.getElementById(uuid); + + const timeElem = document.createElement('div'); + timeElem.className = 'keyFigure-time'; + timeElem.textContent = timeVariableValue; + container!.append(timeElem); + + const lastUpdatedElem = document.createElement('div'); + lastUpdatedElem.className = 'keyFigure-lastupdated'; + lastUpdatedElem.textContent = lastUpdated; + container!.append(lastUpdatedElem); + }, [view, locale, timeVariableValue, lastUpdated]); + + return
; +} \ No newline at end of file diff --git a/src/stories/tablestories/keyfigure.stories.tsx b/src/stories/tablestories/keyfigure.stories.tsx new file mode 100644 index 0000000..fa52d9d --- /dev/null +++ b/src/stories/tablestories/keyfigure.stories.tsx @@ -0,0 +1,29 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { Chart } from '../../react'; +import { TABLE_WITH_ONE_CELL } from '../fixtures/table'; +import { EVisualizationType } from '../../core/types/queryVisualizationResponse'; + +const keyFigureArgs = { + ...TABLE_WITH_ONE_CELL, + pxGraphData: { + ...TABLE_WITH_ONE_CELL.pxGraphData, + visualizationSettings: { + ...TABLE_WITH_ONE_CELL.pxGraphData.visualizationSettings, + visualizationType: EVisualizationType.KeyFigure, + showUnit: true, + linkUrl: 'https://www.stat.fi', + linkText: { fi: 'Asunnot', en: 'Dwellings', sv: 'Bostäder' } + } + } +}; + +export default { + title: 'Tables/KeyFigure', + component: Chart, + parameters: { }, +} satisfies Meta; + +export const BasicKeyFigure = { + name: 'Basic Key Figure', + args: keyFigureArgs, +} satisfies StoryObj; From 1a804da4e150aae6fac3c2fc3fa54b18ca28bd43 Mon Sep 17 00:00:00 2001 From: Sakari Malkki Date: Thu, 4 Sep 2025 13:33:51 +0300 Subject: [PATCH 2/5] Exclude **/testFixtures/** from sonar --- sonar-project.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From f0bcda0ab73545f67790546b2d0f47baa60b4a97 Mon Sep 17 00:00:00 2001 From: Sakari Malkki Date: Fri, 5 Sep 2025 11:45:55 +0300 Subject: [PATCH 3/5] =?UTF-8?q?=EF=BB=BFSupport=20for=20customized=20styli?= =?UTF-8?q?ng=20of=20key=20figures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/styles/chart.css | 62 +++++++ .../__snapshots__/htmlTable.test.ts.snap | 72 ++++---- src/core/tables/htmlTable.test.ts | 58 +++--- src/core/tables/htmlTable.ts | 45 +++-- .../chart/__snapshots__/chart.test.tsx.snap | 165 +++++------------- .../__snapshots__/keyFigureView.test.tsx.snap | 39 +++++ src/react/components/chart/chart.tsx | 9 +- src/react/components/chart/keyFigureView.tsx | 31 ++-- .../components/globalStyle/globalStyle.ts | 62 +++++++ .../tablestories/keyfigure.stories.tsx | 79 ++++++++- 10 files changed, 407 insertions(+), 215 deletions(-) create mode 100644 src/react/components/chart/__snapshots__/keyFigureView.test.tsx.snap 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 e193a7f..f23ec38 100644 --- a/src/core/tables/htmlTable.ts +++ b/src/core/tables/htmlTable.ts @@ -60,49 +60,56 @@ export function renderHtmlTable(view: View, locale: string, showTitles: boolean, } } -export function renderHtmlKeyFigure(view: View, locale: string, containerId: string): void { +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 = 'keyFigure-container'; - + keyFigureContainer.className = className ? `keyFigure-container ${className}` : 'keyFigure-container'; + // Add title if available const title = document.createElement('div'); - title.className = 'keyFigure-title'; + 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 = 'keyFigure-value'; + 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 { - let formattedValue = formatNumericValue(dataCell.value, dataCell.precision, locale, true); - - // If show units is enabled, append the unit to the value + 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); - formattedValue += ` ${unitName}`; + const unitSpan = document.createElement('span'); + unitSpan.className = 'keyFigure-unit'; + unitSpan.textContent = ` ${unitName}`; + valueContainer.append(unitSpan); } - - valueContainer.textContent = formattedValue; } keyFigureContainer.append(valueContainer); - - // Show preliminary indicator if needed - if (dataCell.preliminary) { - const preliminaryIndicator = document.createElement('div'); - preliminaryIndicator.className = 'keyFigure-preliminary'; - preliminaryIndicator.textContent = Translations.preliminaryData[locale]; - keyFigureContainer.append(preliminaryIndicator); - } + + 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); diff --git a/src/react/components/chart/__snapshots__/chart.test.tsx.snap b/src/react/components/chart/__snapshots__/chart.test.tsx.snap index 29a6905..4cf2048 100644 --- a/src/react/components/chart/__snapshots__/chart.test.tsx.snap +++ b/src/react/components/chart/__snapshots__/chart.test.tsx.snap @@ -115,16 +115,25 @@ exports[`Rendering test renders chart data correctly 1`] = ` -

- Lähde: PxVisualizer-fi -

`; -exports[`Rendering test renders chart data correctly with hidden title 1`] = ` +exports[`Rendering test renders error component on broken data 1`] = ` + +
+

+ Kuviota ei voitu muodostaa +

+
+
+`; + +exports[`Rendering test renders key figure data correctly 1`] = `
-
-
-
- - - - - - - - - - - - - - - - - - - -
- - 2015Q1 - - 2015Q2 -
- Vapaarahoitteinen - - 11 096 - - 11 625 -
- ARA - - 4 845 - - 5 174 -
-

- Lähde: PxVisualizer-fi -

+ Avainluku +
+
+ + 13 021 + + + lukumäärä + +
+
+ 2022Q4 +
+
+ 2023-01-19T06:00:00Z +
`; -exports[`Rendering test renders error component on broken data 1`] = ` - -
-

- Kuviota ei voitu muodostaa -

-
-
-`; - exports[`Rendering test renders table data correctly 1`] = `
@@ -989,11 +945,6 @@ exports[`Rendering test renders table data correctly when given footnote 1`] = ` -
- Tiedot 2022Q1-2022Q4 muuttujina Tiedot, Alue, Huoneluku, Rahoitusmuoto -
Test footnote

-

- Lähde: PxVisualizer-fi -

@@ -1675,11 +1623,6 @@ exports[`Rendering test renders table data correctly when sources are on 1`] = ` -
- Tiedot 2022Q1-2022Q4 muuttujina Tiedot, Alue, Huoneluku, Rahoitusmuoto -
-

- Lähde: PxVisualizer-fi -

`; -exports[`Rendering test renders table data correctly when units and footnote are on 1`] = ` +exports[`Rendering test renders table data correctly when units are on 1`] = `
-
- Tiedot 2022Q1-2022Q4 muuttujina Tiedot, Alue, Huoneluku, Rahoitusmuoto -
Yksikkö: Lukumäärä: lukumäärä, Neliövuokra (eur/m2): eur / m2

-

- Test footnote -

-

- Lähde: PxVisualizer-fi -

diff --git a/src/react/components/chart/__snapshots__/keyFigureView.test.tsx.snap b/src/react/components/chart/__snapshots__/keyFigureView.test.tsx.snap new file mode 100644 index 0000000..ea364f8 --- /dev/null +++ b/src/react/components/chart/__snapshots__/keyFigureView.test.tsx.snap @@ -0,0 +1,39 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`KeyFigure render tests Should render correctly 1`] = ` + +
+
+
+ Lukumäärä, Vantaa, Yksiöt, Vapaarahoitteinen 2022Q4 +
+
+ + 2 548 + +
+
+ 2022 Q4 +
+
+ 2023-10-01T12:00:00Z +
+
+
+
+`; diff --git a/src/react/components/chart/chart.tsx b/src/react/components/chart/chart.tsx index 2b68215..14d3871 100644 --- a/src/react/components/chart/chart.tsx +++ b/src/react/components/chart/chart.tsx @@ -73,6 +73,7 @@ export interface IChartProps { showTableUnits?: boolean; showTableSources?: boolean; footnote?: string; + className?: string; } const ReactChart: React.FC = ({ @@ -85,7 +86,8 @@ const ReactChart: React.FC = ({ menuIconInheritColor = false, showTitles, showTableUnits, - showTableSources}) => { + showTableSources, + className}) => { const validLocale = formatLocale(locale); initializeHighcharts(validLocale); @@ -172,6 +174,7 @@ const ReactChart: React.FC = ({ locale={validLocale} timeVariableValue={timeVariableValue} lastUpdated={lastUpdated} + className={className} /> ); @@ -188,7 +191,7 @@ const ReactChart: React.FC = ({ } - + = ({ options={highChartOptions} /> - + diff --git a/src/react/components/chart/keyFigureView.tsx b/src/react/components/chart/keyFigureView.tsx index eeff6bc..f72305e 100644 --- a/src/react/components/chart/keyFigureView.tsx +++ b/src/react/components/chart/keyFigureView.tsx @@ -8,27 +8,26 @@ export interface IKeyFigureViewProps { locale: string; timeVariableValue: string; lastUpdated: string; + className?: string; } -export const KeyFigureView: React.FC = ({ view, locale, timeVariableValue, lastUpdated }) => { +export const KeyFigureView: React.FC = ({ + view, + locale, + timeVariableValue, + lastUpdated, + className +}) => { const uuid = useMemo(() => uuidv4(), [view, locale]); + const combinedClassName = className + ? `keyFigureChart ${className}` + : 'keyFigureChart'; + React.useEffect(() => { document.getElementById(uuid)?.replaceChildren(); - renderHtmlKeyFigure(view, locale, uuid); - // Add time variable, subheader, and last updated info - const container = document.getElementById(uuid); - - const timeElem = document.createElement('div'); - timeElem.className = 'keyFigure-time'; - timeElem.textContent = timeVariableValue; - container!.append(timeElem); - - const lastUpdatedElem = document.createElement('div'); - lastUpdatedElem.className = 'keyFigure-lastupdated'; - lastUpdatedElem.textContent = lastUpdated; - container!.append(lastUpdatedElem); - }, [view, locale, timeVariableValue, lastUpdated]); + renderHtmlKeyFigure(view, locale, uuid, timeVariableValue, lastUpdated, className); + }, [view, locale, timeVariableValue, lastUpdated, className]); - return
; + return
; } \ No newline at end of file diff --git a/src/react/components/globalStyle/globalStyle.ts b/src/react/components/globalStyle/globalStyle.ts index e7f1da0..b394059 100644 --- a/src/react/components/globalStyle/globalStyle.ts +++ b/src/react/components/globalStyle/globalStyle.ts @@ -74,4 +74,66 @@ export const GlobalStyle = createGlobalStyle` margin-bottom: 2rem; text-align: left; } + + .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/stories/tablestories/keyfigure.stories.tsx b/src/stories/tablestories/keyfigure.stories.tsx index fa52d9d..9c55f80 100644 --- a/src/stories/tablestories/keyfigure.stories.tsx +++ b/src/stories/tablestories/keyfigure.stories.tsx @@ -2,6 +2,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { Chart } from '../../react'; import { TABLE_WITH_ONE_CELL } from '../fixtures/table'; import { EVisualizationType } from '../../core/types/queryVisualizationResponse'; +import React from 'react'; const keyFigureArgs = { ...TABLE_WITH_ONE_CELL, @@ -10,9 +11,7 @@ const keyFigureArgs = { visualizationSettings: { ...TABLE_WITH_ONE_CELL.pxGraphData.visualizationSettings, visualizationType: EVisualizationType.KeyFigure, - showUnit: true, - linkUrl: 'https://www.stat.fi', - linkText: { fi: 'Asunnot', en: 'Dwellings', sv: 'Bostäder' } + showUnit: true } } }; @@ -27,3 +26,77 @@ export const BasicKeyFigure = { name: 'Basic Key Figure', args: keyFigureArgs, } satisfies StoryObj; + +export const CustomStyledKeyFigure = { + name: 'Custom Styled Key Figure', + args: { + ...keyFigureArgs, + className: 'custom-styling' + }, + decorators: [ + (Story) => ( + <> + + + + ) + ] +} satisfies StoryObj; From 22ca73ef4217b32655c175509de73c221b9193a3 Mon Sep 17 00:00:00 2001 From: Sakari Malkki Date: Thu, 25 Sep 2025 15:01:48 +0300 Subject: [PATCH 4/5] Solve merge conflicts --- .../chart/__snapshots__/chart.test.tsx.snap | 165 +++++++++++++----- .../components/chart/keyFigureView.test.tsx | 19 ++ 2 files changed, 137 insertions(+), 47 deletions(-) create mode 100644 src/react/components/chart/keyFigureView.test.tsx diff --git a/src/react/components/chart/__snapshots__/chart.test.tsx.snap b/src/react/components/chart/__snapshots__/chart.test.tsx.snap index 4cf2048..29a6905 100644 --- a/src/react/components/chart/__snapshots__/chart.test.tsx.snap +++ b/src/react/components/chart/__snapshots__/chart.test.tsx.snap @@ -115,25 +115,16 @@ exports[`Rendering test renders chart data correctly 1`] = `
+

+ Lähde: PxVisualizer-fi +

`; -exports[`Rendering test renders error component on broken data 1`] = ` - -
-

- Kuviota ei voitu muodostaa -

-
-
-`; - -exports[`Rendering test renders key figure data correctly 1`] = ` +exports[`Rendering test renders chart data correctly with hidden title 1`] = `
+
+
+
-
- Avainluku -
-
- - 13 021 - - - lukumäärä - -
-
- 2022Q4 -
-
- 2023-01-19T06:00:00Z -
+ + + + + 2015Q1 + + + 2015Q2 + + + + + + + Vapaarahoitteinen + + + 11 096 + + + 11 625 + + + + + ARA + + + 4 845 + + + 5 174 + + + + +

+ Lähde: PxVisualizer-fi +

`; +exports[`Rendering test renders error component on broken data 1`] = ` + +
+

+ Kuviota ei voitu muodostaa +

+
+
+`; + exports[`Rendering test renders table data correctly 1`] = `
@@ -945,6 +989,11 @@ exports[`Rendering test renders table data correctly when given footnote 1`] = ` +
+ Tiedot 2022Q1-2022Q4 muuttujina Tiedot, Alue, Huoneluku, Rahoitusmuoto +
Test footnote

+

+ Lähde: PxVisualizer-fi +

@@ -1623,6 +1675,11 @@ exports[`Rendering test renders table data correctly when sources are on 1`] = ` +
+ Tiedot 2022Q1-2022Q4 muuttujina Tiedot, Alue, Huoneluku, Rahoitusmuoto +
+

+ Lähde: PxVisualizer-fi +

`; -exports[`Rendering test renders table data correctly when units are on 1`] = ` +exports[`Rendering test renders table data correctly when units and footnote are on 1`] = `
+
+ Tiedot 2022Q1-2022Q4 muuttujina Tiedot, Alue, Huoneluku, Rahoitusmuoto +
Yksikkö: Lukumäärä: lukumäärä, Neliövuokra (eur/m2): eur / m2

+

+ Test footnote +

+

+ Lähde: PxVisualizer-fi +

diff --git a/src/react/components/chart/keyFigureView.test.tsx b/src/react/components/chart/keyFigureView.test.tsx new file mode 100644 index 0000000..e54a013 --- /dev/null +++ b/src/react/components/chart/keyFigureView.test.tsx @@ -0,0 +1,19 @@ +import { render } from "@testing-library/react"; +import { convertPxGrafResponseToView } from "../../../core/conversion/viewUtils"; +import React from "react"; +import { TABLE_WITH_ONE_CELL } from "../../../stories/fixtures/table"; +import { KeyFigureView } from "./keyFigureView"; + +jest.mock('uuid', () => ({ + ...jest.requireActual('uuid'), + v4: () => 'foobar' +})); + +describe('KeyFigure render tests', () => { + it('Should render correctly', () => { + const timeVarValName: string = "2022 Q4"; + const lastUpdated: string = "2023-10-01T12:00:00Z"; + const { asFragment } = render(); + expect(asFragment()).toMatchSnapshot(); + }); +}); \ No newline at end of file From 9920810ce38b69fcb0fdf2254497c24fac6ba7d3 Mon Sep 17 00:00:00 2001 From: Sakari Malkki Date: Thu, 25 Sep 2025 15:13:02 +0300 Subject: [PATCH 5/5] More test coverage --- .../chart/__snapshots__/chart.test.tsx.snap | 94 ++++++++++++++++++- src/react/components/chart/chart.test.tsx | 10 ++ .../tablestories/keyfigure.stories.tsx | 2 +- 3 files changed, 101 insertions(+), 5 deletions(-) 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`] = `