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`] = `
+"[36m
[39m
+ [36m
[39m
+ [36m
[39m
+ [0mLukumäärä, Vantaa, Yksiöt, Vapaarahoitteinen 2022Q4[0m
+ [36m
[39m
+ [36m
[39m
+ [36m[39m
+ [0m2 548[0m
+ [36m[39m
+ [36m[39m
+ [0m lukumäärä[0m
+ [36m[39m
+ [36m
[39m
+ [36m
[39m
+ [0m2022Q4[0m
+ [36m
[39m
+ [36m
[39m
+ [0m2023-03-15[0m
+ [36m
[39m
+ [36m
[39m
+[36m
[39m"
+`;
+
exports[`Html table render tests should match snapshot: Table with column variables only 1`] = `
"[36m[39m
[36m
[39m"
`;
-
-exports[`Html table render tests should match snapshot: Table with source and footnote 1`] = `
-"[36m[39m
- [36m
[39m
- [36m[39m
- [0mLukumäärä, Vantaa, Yksiöt, Vapaarahoitteinen 2022Q4[0m
- [36m[39m
- [36m[39m
- [36m[39m
- [36m| [39m
- [0m2 548[0m
- [36m | [39m
- [36m
[39m
- [36m[39m
- [36m
[39m
- [36m
[39m
- [0mYksikkö: lukumäärä[0m
- [36m
[39m
- [36m
[39m
- [0mTest footnote[0m
- [36m
[39m
- [36m
[39m
- [0mLähde: PxVisualizer-fi[0m
- [36m
[39m
-[36m
[39m"
-`;
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`] = `
+
+
+
+`;
+
exports[`Rendering test renders table data correctly 1`] = `