diff --git a/cypress/elements/chart.js b/cypress/elements/chart.js index e2f2bdb966..f3db15ca20 100644 --- a/cypress/elements/chart.js +++ b/cypress/elements/chart.js @@ -10,8 +10,6 @@ import { } from '@dhis2/analytics' const visualizationContainerEl = 'visualization-container' -const visualizationTitleEl = 'visualization-title' -const visualizationSubtitleEl = 'visualization-subtitle' const chartContainerEl = '.highcharts-container' const highchartsLegendEl = '.highcharts-legend' const highchartsTitleEl = '.highcharts-title' @@ -24,11 +22,7 @@ const AOTitleDirtyEl = 'titlebar-dirty' const timeout = { timeout: 40000, } -const nonHighchartsTypes = [ - VIS_TYPE_OUTLIER_TABLE, - VIS_TYPE_PIVOT_TABLE, - VIS_TYPE_SINGLE_VALUE, -] +const nonHighchartsTypes = [VIS_TYPE_OUTLIER_TABLE, VIS_TYPE_PIVOT_TABLE] export const expectVisualizationToBeVisible = (visType = VIS_TYPE_COLUMN) => nonHighchartsTypes.includes(visType) @@ -64,13 +58,11 @@ export const expectChartToContainDimensionItem = (visType, itemName) => { case VIS_TYPE_GAUGE: case VIS_TYPE_YEAR_OVER_YEAR_COLUMN: case VIS_TYPE_YEAR_OVER_YEAR_LINE: + case VIS_TYPE_SINGLE_VALUE: cy.get(highchartsTitleEl) .should('be.visible') .and('contain', itemName) break - case VIS_TYPE_SINGLE_VALUE: - cy.getBySel(visualizationTitleEl).should('contain', itemName) - break case VIS_TYPE_PIVOT_TABLE: cy.getBySel('visualization-column-header') .contains(itemName) @@ -119,10 +111,7 @@ export const expectChartItemsToHaveLength = (length) => cy.get(highchartsChartItemEl).children().should('have.length', length) export const expectSVTitleToHaveColor = (color) => - cy.getBySel(visualizationTitleEl).invoke('attr', 'fill').should('eq', color) + cy.get('text.highcharts-title').should('have.css', 'color', color) export const expectSVSubtitleToHaveColor = (color) => - cy - .getBySel(visualizationSubtitleEl) - .invoke('attr', 'fill') - .should('eq', color) + cy.get('text.highcharts-subtitle').should('have.css', 'color', color) diff --git a/cypress/elements/optionsModal/index.js b/cypress/elements/optionsModal/index.js index b1a74c55fa..037a33ed55 100644 --- a/cypress/elements/optionsModal/index.js +++ b/cypress/elements/optionsModal/index.js @@ -81,7 +81,6 @@ export { expectLegendDisplayStyleToBeText, expectLegendDisplayStyleToBeFill, expectSingleValueToHaveTextColor, - expectSingleValueToNotHaveBackgroundColor, expectSingleValueToHaveBackgroundColor, toggleLegendKeyOption, expectLegendKeyOptionToBeEnabled, diff --git a/cypress/elements/optionsModal/legend.js b/cypress/elements/optionsModal/legend.js index 4036add064..94a2ee7883 100644 --- a/cypress/elements/optionsModal/legend.js +++ b/cypress/elements/optionsModal/legend.js @@ -5,7 +5,6 @@ const legendKeyContainerEl = 'legend-key-container' const legendKeyItemEl = 'legend-key-item' const singleValueTextEl = 'visualization-primary-value' const singleValueIconEl = 'visualization-icon' -const singleValueOutputEl = 'visualization-container' const legendDisplayStrategyByDataItemEl = 'legend-display-strategy-BY_DATA_ITEM' const legendDisplayStrategyFixedEl = 'legend-display-strategy-FIXED' const legendDisplayStyleOptionTextEl = 'legend-display-style-option-TEXT' @@ -76,16 +75,10 @@ export const expectFixedLegendSetToBe = (legendSetName) => cy.getBySel(fixedLegendSetSelectEl).should('contain', legendSetName) export const expectSingleValueToHaveTextColor = (color) => - cy.getBySel(singleValueTextEl).invoke('attr', 'fill').should('eq', color) - -export const expectSingleValueToNotHaveBackgroundColor = () => - cy.getBySel(singleValueOutputEl).should('not.have.attr', 'style') + cy.getBySel(singleValueTextEl).should('have.css', 'color', color) export const expectSingleValueToHaveBackgroundColor = (color) => - cy - .getBySel(singleValueOutputEl) - .invoke('attr', 'style') - .should('contain', `background-color: ${color}`) + cy.get('rect.highcharts-background').should('have.attr', 'fill', color) export const expectSingleValueToHaveIconColor = (color) => cy diff --git a/cypress/integration/options/legend.cy.js b/cypress/integration/options/legend.cy.js index 42015e3aaf..d94f4ad055 100644 --- a/cypress/integration/options/legend.cy.js +++ b/cypress/integration/options/legend.cy.js @@ -60,7 +60,6 @@ import { setItemToType, clickOptionsModalHideButton, expectSingleValueToHaveBackgroundColor, - expectSingleValueToNotHaveBackgroundColor, changeDisplayStyleToFill, changeColor, OPTIONS_TAB_STYLE, @@ -156,17 +155,19 @@ describe('Options - Legend', () => { it('applies different styles of legend to a Single Value chart', () => { const TEST_ITEM = TEST_ITEMS[0] - const EXPECTED_STANDARD_TEXT_COLOR = '#212934' - const EXPECTED_CONTRAST_TEXT_COLOR = '#ffffff' + const EXPECTED_STANDARD_TEXT_COLOR = 'rgb(33, 41, 52)' + const EXPECTED_CONTRAST_TEXT_COLOR = 'rgb(255, 255, 255)' const EXPECTED_BACKGROUND_COLOR_1 = '#FFFFB2' - const EXPECTED_TEXT_COLOR_1 = '#FFFFB2' + const EXPECTED_TEXT_COLOR_1 = 'rgb(255, 255, 178)' const EXPECTED_BACKGROUND_COLOR_2 = '#B3402B' - const EXPECTED_TEXT_COLOR_2 = '#B3402B' + const EXPECTED_TEXT_COLOR_2 = 'rgb(179, 64, 43)' const EXPECTED_CUSTOM_TITLE_COLOR = '#ff7700' + const EXPECTED_CUSTOM_TITLE_COLOR_RGB = 'rgb(255, 119, 0)' const EXPECTED_CUSTOM_SUBTITLE_COLOR = '#ffaa00' + const EXPECTED_CUSTOM_SUBTITLE_COLOR_RGB = 'rgb(255, 170, 0)' const TEST_LEGEND_SET_WITH_CONTRAST = 'Age 15y interval' - const EXPECTED_STANDARD_TITLE_COLOR = '#212934' - const EXPECTED_STANDARD_SUBTITLE_COLOR = '#4a5768' + const EXPECTED_STANDARD_TITLE_COLOR = 'rgb(33, 41, 52)' + const EXPECTED_STANDARD_SUBTITLE_COLOR = 'rgb(74, 87, 104)' cy.log('navigates to the start page and adds data items') goToStartPage() @@ -176,7 +177,7 @@ describe('Options - Legend', () => { clickDimensionModalUpdateButton() expectVisualizationToBeVisible(VIS_TYPE_SINGLE_VALUE) expectSingleValueToHaveTextColor(EXPECTED_STANDARD_TEXT_COLOR) - expectSingleValueToNotHaveBackgroundColor() + expectSingleValueToHaveBackgroundColor('transparent') cy.log('enables legend') openOptionsModal(OPTIONS_TAB_LEGEND) @@ -206,7 +207,7 @@ describe('Options - Legend', () => { // Legend on text, no contrast, no custom title colors cy.log('verifies text color legend is applied') expectSingleValueToHaveTextColor(EXPECTED_TEXT_COLOR_1) - expectSingleValueToNotHaveBackgroundColor() + expectSingleValueToHaveBackgroundColor('transparent') expectSVTitleToHaveColor(EXPECTED_STANDARD_TITLE_COLOR) expectSVSubtitleToHaveColor(EXPECTED_STANDARD_SUBTITLE_COLOR) @@ -225,11 +226,11 @@ describe('Options - Legend', () => { // Legend on text, with contrast (N/, no custom title colors cy.log('verifies text color legend is applied') expectSingleValueToHaveTextColor(EXPECTED_TEXT_COLOR_2) - expectSingleValueToNotHaveBackgroundColor() + expectSingleValueToHaveBackgroundColor('transparent') expectSVTitleToHaveColor(EXPECTED_STANDARD_TITLE_COLOR) expectSVSubtitleToHaveColor(EXPECTED_STANDARD_SUBTITLE_COLOR) - cy.log('changees legend display style to background color') + cy.log('changes legend display style to background color') openOptionsModal(OPTIONS_TAB_LEGEND) expectLegendDisplayStrategyToBeFixed() expectLegendDisplayStyleToBeText() @@ -259,8 +260,8 @@ describe('Options - Legend', () => { ) expectSingleValueToHaveTextColor(EXPECTED_CONTRAST_TEXT_COLOR) expectSingleValueToHaveBackgroundColor(EXPECTED_BACKGROUND_COLOR_2) - expectSVTitleToHaveColor(EXPECTED_CUSTOM_TITLE_COLOR) - expectSVSubtitleToHaveColor(EXPECTED_CUSTOM_SUBTITLE_COLOR) + expectSVTitleToHaveColor(EXPECTED_CUSTOM_TITLE_COLOR_RGB) + expectSVSubtitleToHaveColor(EXPECTED_CUSTOM_SUBTITLE_COLOR_RGB) cy.log('changes legend display style to text color') openOptionsModal(OPTIONS_TAB_LEGEND) @@ -271,12 +272,12 @@ describe('Options - Legend', () => { clickOptionsModalUpdateButton() expectVisualizationToBeVisible(VIS_TYPE_SINGLE_VALUE) - // Legend on text, with contrast, with custom title colo + // Legend on text, with contrast, with custom title colors cy.log('verifies text color legend and custom title colors are applied') expectSingleValueToHaveTextColor(EXPECTED_TEXT_COLOR_2) - expectSingleValueToNotHaveBackgroundColor() - expectSVTitleToHaveColor(EXPECTED_CUSTOM_TITLE_COLOR) - expectSVSubtitleToHaveColor(EXPECTED_CUSTOM_SUBTITLE_COLOR) + expectSingleValueToHaveBackgroundColor('transparent') + expectSVTitleToHaveColor(EXPECTED_CUSTOM_TITLE_COLOR_RGB) + expectSVSubtitleToHaveColor(EXPECTED_CUSTOM_SUBTITLE_COLOR_RGB) cy.log('changes legend display strategy to by data item') openOptionsModal(OPTIONS_TAB_LEGEND) @@ -287,12 +288,12 @@ describe('Options - Legend', () => { clickOptionsModalUpdateButton() expectVisualizationToBeVisible(VIS_TYPE_SINGLE_VALUE) - // Legend on text, no contrast, with custom title colo + // Legend on text, no contrast, with custom title colors cy.log('verifies text color legend and custom title colors are applied') expectSingleValueToHaveTextColor(EXPECTED_TEXT_COLOR_1) - expectSingleValueToNotHaveBackgroundColor() - expectSVTitleToHaveColor(EXPECTED_CUSTOM_TITLE_COLOR) - expectSVSubtitleToHaveColor(EXPECTED_CUSTOM_SUBTITLE_COLOR) + expectSingleValueToHaveBackgroundColor('transparent') + expectSVTitleToHaveColor(EXPECTED_CUSTOM_TITLE_COLOR_RGB) + expectSVSubtitleToHaveColor(EXPECTED_CUSTOM_SUBTITLE_COLOR_RGB) cy.log('changes legend display style to background color') openOptionsModal(OPTIONS_TAB_LEGEND) @@ -309,8 +310,8 @@ describe('Options - Legend', () => { ) expectSingleValueToHaveTextColor(EXPECTED_STANDARD_TEXT_COLOR) expectSingleValueToHaveBackgroundColor(EXPECTED_BACKGROUND_COLOR_1) - expectSVTitleToHaveColor(EXPECTED_CUSTOM_TITLE_COLOR) - expectSVSubtitleToHaveColor(EXPECTED_CUSTOM_SUBTITLE_COLOR) + expectSVTitleToHaveColor(EXPECTED_CUSTOM_TITLE_COLOR_RGB) + expectSVSubtitleToHaveColor(EXPECTED_CUSTOM_SUBTITLE_COLOR_RGB) cy.log('verifies legend key is hidden') expectLegendKeyToBeHidden() @@ -605,7 +606,7 @@ describe('Options - Legend', () => { const TEST_ITEM = TEST_ITEMS[0] const EXPECTED_FIXED_COLOR = '#c7e9c0' const valueCellEl = 'visualization-value-cell' - const EXPECTED_SV_STANDARD_TEXT_COLOR = '#212934' + const EXPECTED_SV_STANDARD_TEXT_COLOR = 'rgb(33, 41, 52)' const EXPECTED_PT_STANDARD_TEXT_COLOR = 'color: rgb(33, 41, 52)' cy.log('navigates to the start page and adds data items') diff --git a/package.json b/package.json index 9047241eb0..e350776a34 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "typescript": "^4.8.4" }, "dependencies": { - "@dhis2/analytics": "^26.8.7", + "@dhis2/analytics": "^26.9.0", "@dhis2/app-runtime": "^3.10.4", "@dhis2/app-runtime-adapter-d2": "^1.1.0", "@dhis2/app-service-datastore": "^1.0.0-beta.3", diff --git a/public/fonts/NotoSans-Bold.ttf b/public/fonts/NotoSans-Bold.ttf new file mode 100644 index 0000000000..54ad879b41 Binary files /dev/null and b/public/fonts/NotoSans-Bold.ttf differ diff --git a/public/fonts/NotoSans-BoldItalic.ttf b/public/fonts/NotoSans-BoldItalic.ttf new file mode 100644 index 0000000000..530a82835d Binary files /dev/null and b/public/fonts/NotoSans-BoldItalic.ttf differ diff --git a/public/fonts/NotoSans-Italic.ttf b/public/fonts/NotoSans-Italic.ttf new file mode 100644 index 0000000000..27ff1ed60a Binary files /dev/null and b/public/fonts/NotoSans-Italic.ttf differ diff --git a/public/fonts/NotoSans-Regular.ttf b/public/fonts/NotoSans-Regular.ttf new file mode 100644 index 0000000000..10589e277e Binary files /dev/null and b/public/fonts/NotoSans-Regular.ttf differ diff --git a/public/fonts/NotoSansArabic-Bold.ttf b/public/fonts/NotoSansArabic-Bold.ttf new file mode 100644 index 0000000000..311ecd77c5 Binary files /dev/null and b/public/fonts/NotoSansArabic-Bold.ttf differ diff --git a/public/fonts/NotoSansArabic-Regular.ttf b/public/fonts/NotoSansArabic-Regular.ttf new file mode 100644 index 0000000000..79359c460b Binary files /dev/null and b/public/fonts/NotoSansArabic-Regular.ttf differ diff --git a/public/fonts/NotoSansBengali-Bold.ttf b/public/fonts/NotoSansBengali-Bold.ttf new file mode 100644 index 0000000000..0dd8fcef77 Binary files /dev/null and b/public/fonts/NotoSansBengali-Bold.ttf differ diff --git a/public/fonts/NotoSansBengali-Regular.ttf b/public/fonts/NotoSansBengali-Regular.ttf new file mode 100644 index 0000000000..810c3f4ec9 Binary files /dev/null and b/public/fonts/NotoSansBengali-Regular.ttf differ diff --git a/public/fonts/NotoSansEthiopic-Bold.ttf b/public/fonts/NotoSansEthiopic-Bold.ttf new file mode 100644 index 0000000000..872733b479 Binary files /dev/null and b/public/fonts/NotoSansEthiopic-Bold.ttf differ diff --git a/public/fonts/NotoSansEthiopic-Regular.ttf b/public/fonts/NotoSansEthiopic-Regular.ttf new file mode 100644 index 0000000000..c493b7b450 Binary files /dev/null and b/public/fonts/NotoSansEthiopic-Regular.ttf differ diff --git a/public/fonts/NotoSansHebrew-Bold.ttf b/public/fonts/NotoSansHebrew-Bold.ttf new file mode 100644 index 0000000000..4275d7829d Binary files /dev/null and b/public/fonts/NotoSansHebrew-Bold.ttf differ diff --git a/public/fonts/NotoSansHebrew-Regular.ttf b/public/fonts/NotoSansHebrew-Regular.ttf new file mode 100644 index 0000000000..b44a0db044 Binary files /dev/null and b/public/fonts/NotoSansHebrew-Regular.ttf differ diff --git a/public/fonts/NotoSansJP-Bold.ttf b/public/fonts/NotoSansJP-Bold.ttf new file mode 100644 index 0000000000..384f8ebb89 Binary files /dev/null and b/public/fonts/NotoSansJP-Bold.ttf differ diff --git a/public/fonts/NotoSansJP-Regular.ttf b/public/fonts/NotoSansJP-Regular.ttf new file mode 100644 index 0000000000..1583096a2d Binary files /dev/null and b/public/fonts/NotoSansJP-Regular.ttf differ diff --git a/public/fonts/NotoSansKR-Bold.ttf b/public/fonts/NotoSansKR-Bold.ttf new file mode 100644 index 0000000000..6cf639eb7d Binary files /dev/null and b/public/fonts/NotoSansKR-Bold.ttf differ diff --git a/public/fonts/NotoSansKR-Regular.ttf b/public/fonts/NotoSansKR-Regular.ttf new file mode 100644 index 0000000000..1b14d32473 Binary files /dev/null and b/public/fonts/NotoSansKR-Regular.ttf differ diff --git a/public/fonts/NotoSansKhmer-Bold.ttf b/public/fonts/NotoSansKhmer-Bold.ttf new file mode 100644 index 0000000000..5ce10a6ba7 Binary files /dev/null and b/public/fonts/NotoSansKhmer-Bold.ttf differ diff --git a/public/fonts/NotoSansKhmer-Regular.ttf b/public/fonts/NotoSansKhmer-Regular.ttf new file mode 100644 index 0000000000..21097c423b Binary files /dev/null and b/public/fonts/NotoSansKhmer-Regular.ttf differ diff --git a/public/fonts/NotoSansLao-Bold.ttf b/public/fonts/NotoSansLao-Bold.ttf new file mode 100644 index 0000000000..3849db6b0d Binary files /dev/null and b/public/fonts/NotoSansLao-Bold.ttf differ diff --git a/public/fonts/NotoSansLao-Regular.ttf b/public/fonts/NotoSansLao-Regular.ttf new file mode 100644 index 0000000000..aaf30afa09 Binary files /dev/null and b/public/fonts/NotoSansLao-Regular.ttf differ diff --git a/public/fonts/NotoSansMyanmar-Bold.ttf b/public/fonts/NotoSansMyanmar-Bold.ttf new file mode 100644 index 0000000000..aef27f0530 Binary files /dev/null and b/public/fonts/NotoSansMyanmar-Bold.ttf differ diff --git a/public/fonts/NotoSansMyanmar-Regular.ttf b/public/fonts/NotoSansMyanmar-Regular.ttf new file mode 100644 index 0000000000..f4552f2ee5 Binary files /dev/null and b/public/fonts/NotoSansMyanmar-Regular.ttf differ diff --git a/public/fonts/NotoSansOriya-Bold.ttf b/public/fonts/NotoSansOriya-Bold.ttf new file mode 100644 index 0000000000..c53ac4eebc Binary files /dev/null and b/public/fonts/NotoSansOriya-Bold.ttf differ diff --git a/public/fonts/NotoSansOriya-Regular.ttf b/public/fonts/NotoSansOriya-Regular.ttf new file mode 100644 index 0000000000..19e76e3a6f Binary files /dev/null and b/public/fonts/NotoSansOriya-Regular.ttf differ diff --git a/public/fonts/NotoSansSC-Bold.ttf b/public/fonts/NotoSansSC-Bold.ttf new file mode 100644 index 0000000000..b9010dfffb Binary files /dev/null and b/public/fonts/NotoSansSC-Bold.ttf differ diff --git a/public/fonts/NotoSansSC-Regular.ttf b/public/fonts/NotoSansSC-Regular.ttf new file mode 100644 index 0000000000..4d4cadb980 Binary files /dev/null and b/public/fonts/NotoSansSC-Regular.ttf differ diff --git a/public/fonts/NotoSansSinhala-Bold.ttf b/public/fonts/NotoSansSinhala-Bold.ttf new file mode 100644 index 0000000000..feddab66bf Binary files /dev/null and b/public/fonts/NotoSansSinhala-Bold.ttf differ diff --git a/public/fonts/NotoSansSinhala-Regular.ttf b/public/fonts/NotoSansSinhala-Regular.ttf new file mode 100644 index 0000000000..a8e869e6e6 Binary files /dev/null and b/public/fonts/NotoSansSinhala-Regular.ttf differ diff --git a/public/fonts/NotoSansThai-Bold.ttf b/public/fonts/NotoSansThai-Bold.ttf new file mode 100644 index 0000000000..0d93c1f26b Binary files /dev/null and b/public/fonts/NotoSansThai-Bold.ttf differ diff --git a/public/fonts/NotoSansThai-Regular.ttf b/public/fonts/NotoSansThai-Regular.ttf new file mode 100644 index 0000000000..638e709b13 Binary files /dev/null and b/public/fonts/NotoSansThai-Regular.ttf differ diff --git a/src/AppWrapper.js b/src/AppWrapper.js index df06cbd256..da47583cbf 100644 --- a/src/AppWrapper.js +++ b/src/AppWrapper.js @@ -5,6 +5,7 @@ import React from 'react' import { Provider as ReduxProvider } from 'react-redux' import thunk from 'redux-thunk' import { App } from './components/App.js' +import { ChartProvider } from './components/ChartProvider.js' import UserSettingsProvider, { UserSettingsCtx, } from './components/UserSettingsProvider.js' @@ -33,37 +34,43 @@ const AppWrapper = () => { return ( - - - - {({ userSettings }) => { - return userSettings?.uiLocale ? ( - - {({ d2 }) => { - if (!d2) { - // TODO: Handle errors in d2 initialization - return null - } else { - return ( - - ) - } - }} - - ) : null - }} - - - + + + + + {(userSettings) => { + return userSettings?.uiLocale ? ( + + {({ d2 }) => { + if (!d2) { + // TODO: Handle errors in d2 initialization + return null + } else { + return ( + + ) + } + }} + + ) : null + }} + + + + ) } diff --git a/src/actions/chart.js b/src/actions/chart.js deleted file mode 100644 index cf78f68f02..0000000000 --- a/src/actions/chart.js +++ /dev/null @@ -1,6 +0,0 @@ -import { SET_CHART } from '../reducers/chart.js' - -export const acSetChart = (value) => ({ - type: SET_CHART, - value, -}) diff --git a/src/actions/index.js b/src/actions/index.js index 093282825f..55bc750848 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -28,7 +28,6 @@ import { sGetSettingsDigitGroupSeparator, } from '../reducers/settings.js' import { sGetVisualization } from '../reducers/visualization.js' -import * as fromChart from './chart.js' import * as fromCurrent from './current.js' import * as fromDimensions from './dimensions.js' import * as fromLoader from './loader.js' @@ -49,7 +48,6 @@ export { fromMetadata, fromSettings, fromUser, - fromChart, fromSnackbar, fromLoader, } diff --git a/src/components/ChartProvider.js b/src/components/ChartProvider.js new file mode 100644 index 0000000000..da11dc9700 --- /dev/null +++ b/src/components/ChartProvider.js @@ -0,0 +1,31 @@ +import PropTypes from 'prop-types' +import React, { createContext, useCallback, useContext, useRef } from 'react' + +const throwIfNotInitialized = () => { + throw new Error('ChartContext not yet initialized') +} + +export const ChartContext = createContext({ + getChart: throwIfNotInitialized, + setChart: throwIfNotInitialized, +}) + +export const useChartContext = () => useContext(ChartContext) + +export const ChartProvider = ({ children }) => { + const chartRef = useRef(null) + const getChart = useCallback(() => chartRef.current, []) + const setChart = useCallback((chart = null) => { + chartRef.current = chart + }, []) + + return ( + + {children} + + ) +} + +ChartProvider.propTypes = { + children: PropTypes.node, +} diff --git a/src/components/DownloadMenu/useDownload.js b/src/components/DownloadMenu/useDownload.js index 6776a08c17..3b68429dd7 100644 --- a/src/components/DownloadMenu/useDownload.js +++ b/src/components/DownloadMenu/useDownload.js @@ -1,9 +1,9 @@ import { Analytics, VIS_TYPE_OUTLIER_TABLE } from '@dhis2/analytics' -import { useConfig, useDataEngine, useDataMutation } from '@dhis2/app-runtime' +import { useConfig, useDataEngine } from '@dhis2/app-runtime' import { useCallback } from 'react' import { useSelector } from 'react-redux' import { getAnalyticsRequestForOutlierTable } from '../../api/analytics.js' -import { sGetChart } from '../../reducers/chart.js' +import { getNotoPdfFontForLocale } from '../../modules/getNotoPdfFontForLocale/index.js' import { sGetCurrent } from '../../reducers/current.js' import { sGetSettingsDisplayProperty } from '../../reducers/settings.js' import { @@ -11,27 +11,17 @@ import { sGetUiLayoutColumns, sGetUiLayoutRows, } from '../../reducers/ui.js' +import { useChartContext } from '../ChartProvider.js' +import { useUserSettings } from '../UserSettingsProvider.js' import { DOWNLOAD_TYPE_PLAIN, DOWNLOAD_TYPE_TABLE, FILE_FORMAT_HTML_CSS, FILE_FORMAT_CSV, - FILE_FORMAT_PNG, FILE_FORMAT_XLS, + FILE_FORMAT_PDF, } from './constants.js' -const downloadPngMutation = { - resource: 'svg.png', - type: 'create', - data: ({ formData }) => formData, -} - -const downloadPdfMutation = { - resource: 'svg.pdf', - type: 'create', - data: ({ formData }) => formData, -} - const addCommonParameters = (req, visualization, options) => { req = req .withSkipRounding(visualization.skipRounding) @@ -59,45 +49,48 @@ const useDownload = (relativePeriodDate) => { const displayProperty = useSelector(sGetSettingsDisplayProperty) const visualization = useSelector(sGetCurrent) const visType = useSelector(sGetUiType) - const chart = useSelector(sGetChart) const columns = useSelector(sGetUiLayoutColumns) const rows = useSelector(sGetUiLayoutRows) const { baseUrl } = useConfig() + const { dbLocale } = useUserSettings() const dataEngine = useDataEngine() + const { getChart } = useChartContext() const analyticsEngine = Analytics.getAnalytics(dataEngine) - const openDownloadedFileInBlankTab = useCallback((blob) => { - const url = URL.createObjectURL(blob) - window.open(url, '_blank') - }, []) - - const [getPng] = useDataMutation(downloadPngMutation, { - onComplete: openDownloadedFileInBlankTab, - }) - - const [getPdf] = useDataMutation(downloadPdfMutation, { - onComplete: openDownloadedFileInBlankTab, - }) - const doDownloadImage = useCallback( ({ format }) => { - if (!visualization) { + const chart = getChart() + + if (!visualization || !chart) { return false } - const formData = { + const isPdfExport = format === FILE_FORMAT_PDF + + /* Custom visualization types (i.e. SingleValue) that need some + * specific handling for PDF export can read this when they + * re-render before exporting. */ + chart.update({ + exporting: { + chartOptions: { isPdfExport }, + }, + }) + + chart.exportChartLocal({ + sourceHeight: 768, + sourceWidth: 1024, + scale: 1, + fallbackToExportServer: false, + allowHTML: true, + showExportInProgress: true, filename: visualization.name, - } - - if (chart) { - formData.svg = chart - } - - format === FILE_FORMAT_PNG - ? getPng({ formData }) - : getPdf({ formData }) + type: isPdfExport ? 'application/pdf' : 'image/png', + pdfFont: isPdfExport + ? getNotoPdfFontForLocale(dbLocale) + : undefined, + }) }, - [chart, getPdf, getPng, visualization] + [dbLocale, getChart, visualization] ) const doDownloadData = useCallback( diff --git a/src/components/UserSettingsProvider.js b/src/components/UserSettingsProvider.js index 5575535ebb..d2e12a1cca 100644 --- a/src/components/UserSettingsProvider.js +++ b/src/components/UserSettingsProvider.js @@ -5,14 +5,14 @@ import React, { useContext, useState, useEffect, createContext } from 'react' export const userSettingsQuery = { resource: 'userSettings', params: { - key: ['keyUiLocale', 'keyAnalysisDisplayProperty'], + key: ['keyUiLocale', 'keyDbLocale', 'keyAnalysisDisplayProperty'], }, } export const UserSettingsCtx = createContext({}) const UserSettingsProvider = ({ children }) => { - const [settings, setSettings] = useState([]) + const [settings, setSettings] = useState({}) const engine = useDataEngine() useEffect(() => { @@ -21,8 +21,12 @@ const UserSettingsProvider = ({ children }) => { userSettings: userSettingsQuery, }) - const { keyAnalysisDisplayProperty, keyUiLocale, ...rest } = - userSettings + const { + keyAnalysisDisplayProperty, + keyUiLocale, + keyDbLocale, + ...rest + } = userSettings setSettings({ ...rest, @@ -32,17 +36,14 @@ const UserSettingsProvider = ({ children }) => { ? 'displayName' : 'displayShortName', uiLocale: keyUiLocale, + dbLocale: keyDbLocale, }) } fetchData() }, [engine]) return ( - + {children} ) diff --git a/src/components/Visualization/Visualization.js b/src/components/Visualization/Visualization.js index 1c9f233526..37196d6ada 100644 --- a/src/components/Visualization/Visualization.js +++ b/src/components/Visualization/Visualization.js @@ -3,7 +3,6 @@ import debounce from 'lodash-es/debounce' import PropTypes from 'prop-types' import React, { Component, Fragment } from 'react' import { connect } from 'react-redux' -import { acSetChart } from '../../actions/chart.js' import { tSetCurrentFromUi } from '../../actions/current.js' import { acSetLoadError, acSetPluginLoading } from '../../actions/loader.js' import { acAddMetadata } from '../../actions/metadata.js' @@ -32,6 +31,7 @@ import { sGetLoadError, sGetIsPluginLoading } from '../../reducers/loader.js' import { sGetSettingsDisplayProperty } from '../../reducers/settings.js' import { sGetUiRightSidebarOpen } from '../../reducers/ui.js' import LoadingMask from '../../widgets/LoadingMask.js' +import { ChartContext } from '../ChartProvider.js' import { VisualizationPlugin } from '../VisualizationPlugin/VisualizationPlugin.js' import StartScreen from './StartScreen.js' import styles from './styles/Visualization.style.js' @@ -99,7 +99,9 @@ export class UnconnectedVisualization extends Component { this.props.setLoadError(error) } - onChartGenerated = (svg) => this.props.setChart(svg) + onChartGenerated = (chart) => { + this.context.setChart(chart) + } onDataSorted = (sorting) => { this.props.onLoadingStart() @@ -247,7 +249,6 @@ UnconnectedVisualization.propTypes = { error: PropTypes.object, isLoading: PropTypes.bool, rightSidebarOpen: PropTypes.bool, - setChart: PropTypes.func, setCurrent: PropTypes.func, setLoadError: PropTypes.func, setUiDataSorting: PropTypes.func, @@ -257,6 +258,20 @@ UnconnectedVisualization.propTypes = { onLoadingStart: PropTypes.func, } +UnconnectedVisualization.contextType = ChartContext + +/* Setting these contextTypes is required for Jest/Enzyme + * context mocking to work, but a React DevTools warning + * is thrown in development mode, because contextTypes is + * part of the legacy Context API which is deprecated. + * So we have to set them conditionally. */ +if (process.env.JEST_WORKER_ID !== undefined) { + UnconnectedVisualization.contextTypes = { + getChart: PropTypes.func, + setChart: PropTypes.func, + } +} + const mapStateToProps = (state) => ({ visualization: sGetCurrent(state), rightSidebarOpen: sGetUiRightSidebarOpen(state), @@ -271,7 +286,6 @@ const mapDispatchToProps = (dispatch) => ({ addMetadata: (metadata) => dispatch(acAddMetadata(metadata)), addParentGraphMap: (parentGraphMap) => dispatch(acAddParentGraphMap(parentGraphMap)), - setChart: (chart) => dispatch(acSetChart(chart)), setLoadError: (error) => dispatch(acSetLoadError(error)), setUiItems: (data) => dispatch(acSetUiItems(data)), setUiDataSorting: (sorting) => dispatch(acSetUiDataSorting(sorting)), diff --git a/src/components/Visualization/__tests__/Visualization.spec.js b/src/components/Visualization/__tests__/Visualization.spec.js index 98f17d26ef..d18268091c 100644 --- a/src/components/Visualization/__tests__/Visualization.spec.js +++ b/src/components/Visualization/__tests__/Visualization.spec.js @@ -10,9 +10,14 @@ describe('Visualization', () => { describe('component', () => { let props let shallowVisualization + const setChart = jest.fn() const vis = () => { if (!shallowVisualization) { - shallowVisualization = shallow() + shallowVisualization = shallow(, { + context: { + setChart, + }, + }) } return shallowVisualization } @@ -26,7 +31,6 @@ describe('Visualization', () => { error: null, rightSidebarOpen: false, addMetadata: jest.fn(), - setChart: jest.fn(), clearLoadError: jest.fn(), setLoadError: jest.fn(), onLoadingComplete: jest.fn(), @@ -71,12 +75,12 @@ describe('Visualization', () => { }) it('triggers setChart action when chart has been generated', () => { - const svg = 'coolChart' + const highChartChartInstanceMock = {} - vis().instance().onChartGenerated(svg) + vis().instance().onChartGenerated(highChartChartInstanceMock) - expect(props.setChart).toHaveBeenCalled() - expect(props.setChart).toHaveBeenCalledWith(svg) + expect(setChart).toHaveBeenCalled() + expect(setChart).toHaveBeenCalledWith(highChartChartInstanceMock) }) it('renders visualization with new id when rightSidebarOpen prop changes', () => { diff --git a/src/components/VisualizationPlugin/ChartPlugin.js b/src/components/VisualizationPlugin/ChartPlugin.js index f587b7aa4f..901db54c34 100644 --- a/src/components/VisualizationPlugin/ChartPlugin.js +++ b/src/components/VisualizationPlugin/ChartPlugin.js @@ -1,4 +1,4 @@ -import { isSingleValue, createVisualization } from '@dhis2/analytics' +import { createVisualization } from '@dhis2/analytics' import PropTypes from 'prop-types' import React, { useRef, useCallback, useEffect } from 'react' @@ -31,19 +31,10 @@ const ChartPlugin = ({ }, undefined, undefined, - isSingleValue(visualization.type) ? 'dhis' : 'highcharts' // output format + 'highcharts' // output format ) - if (isSingleValue(visualization.type)) { - onChartGenerated(visualizationConfig.visualization) - } else { - onChartGenerated( - visualizationConfig.visualization.getSVGForExport({ - sourceHeight: 768, - sourceWidth: 1024, - }) - ) - } + onChartGenerated(visualizationConfig.visualization) }, [ canvasRef, diff --git a/src/components/VisualizationPlugin/__tests__/ChartPlugin.spec.js b/src/components/VisualizationPlugin/__tests__/ChartPlugin.spec.js index 4d5aff6482..6de9d7b526 100644 --- a/src/components/VisualizationPlugin/__tests__/ChartPlugin.spec.js +++ b/src/components/VisualizationPlugin/__tests__/ChartPlugin.spec.js @@ -7,33 +7,6 @@ import ChartPlugin from '../ChartPlugin.js' jest.mock('@dhis2/analytics') -const dxMock = { - dimension: 'dx', - items: [ - { - id: 'Uvn6LCg7dVU', - }, - ], -} - -const peMock = { - dimension: 'pe', - items: [ - { - id: 'LAST_12_MONTHS', - }, - ], -} - -const ouMock = { - dimension: 'ou', - items: [ - { - id: 'ImspTQPwCqd', - }, - ], -} - const mockExtraOptions = { dashboard: false, noData: { @@ -41,13 +14,6 @@ const mockExtraOptions = { }, } -const singleValueCurrentMock = { - type: analytics.VIS_TYPE_SINGLE_VALUE, - columns: [dxMock], - rows: [], - filters: [ouMock, peMock], -} - const metaDataMock = { items: { a: { name: 'a dim' }, @@ -72,17 +38,13 @@ class MockAnalyticsResponse { const createVisualizationMock = { visualization: { - getSVGForExport: () => '', + exportChartLocal: jest.fn(), }, config: { getConfig: () => {}, }, } -const isSingleValueMockResponse = (visType) => { - return visType === analytics.VIS_TYPE_SINGLE_VALUE -} - describe('ChartPlugin', () => { // eslint-disable-next-line no-import-assign, import/namespace options.getOptionsForRequest = () => [ @@ -148,39 +110,10 @@ describe('ChartPlugin', () => { setTimeout(() => { expect(props.onChartGenerated).toHaveBeenCalled() expect(props.onChartGenerated).toHaveBeenCalledWith( - createVisualizationMock.visualization.getSVGForExport() + createVisualizationMock.visualization ) done() }) }) - - describe('Single value visualization', () => { - beforeEach(() => { - props.visualization = { - ...singleValueCurrentMock, - } - - // eslint-disable-next-line no-import-assign, import/namespace - analytics.isSingleValue = jest - .fn() - .mockReturnValue( - isSingleValueMockResponse(props.visualization.type) - ) - }) - - it('provides dhis as output format to createChart', (done) => { - canvas() - - setTimeout(() => { - expect(analytics.createVisualization).toHaveBeenCalled() - - expect( - analytics.createVisualization.mock.calls[0][6] - ).toEqual('dhis') - - done() - }) - }) - }) }) }) diff --git a/src/modules/getNotoPdfFontForLocale/index.js b/src/modules/getNotoPdfFontForLocale/index.js new file mode 100644 index 0000000000..ff8581415e --- /dev/null +++ b/src/modules/getNotoPdfFontForLocale/index.js @@ -0,0 +1,50 @@ +import { NOTO_FONT_LOOKUP } from './notoFontLookup.js' + +const fontsDir = `${process.env.PUBLIC_URL}/fonts` + +const findInNotoFontLookup = (callback) => { + for (const [fontName, scriptsAndLanguages] of NOTO_FONT_LOOKUP) { + if (callback(scriptsAndLanguages)) { + return { + normal: `${fontsDir}/${fontName}-Regular.ttf`, + bold: `${fontsDir}/${fontName}-Bold.ttf`, + // Note that these fonts do not actually have italic variants + bolditalic: `${fontsDir}/${fontName}-Regular.ttf`, + italic: `${fontsDir}/${fontName}-Regular.ttf`, + } + } + } +} + +const getScriptAndRegionFromJavaLocaleCode = (javaLocaleCode) => { + const [rawLanguage, region, script] = javaLocaleCode.split('_') + /* This will ensure that 3 character ISO639-2 language codes + * for which a 2 character ISO639-1 code also exists are + * normalized to the 2 character ISO639-1 code */ + const jsLocale = new Intl.Locale(rawLanguage, { script, region }) + + return { + language: jsLocale.language, + script, + } +} + +export const getNotoPdfFontForLocale = (javaLocale = 'en') => { + const { script, language } = + getScriptAndRegionFromJavaLocaleCode(javaLocale) + return ( + /* First scan the entire lookup to find a script match because script + * matches should take precedence over language matches, since a + * language can be written in multiple scripts */ + findInNotoFontLookup(({ scripts }) => scripts.has(script)) ?? + // Then scan for language matches + findInNotoFontLookup(({ languages }) => languages.has(language)) ?? { + /* If no match is found return the Noto base font. + * Note that this does have italic variants */ + normal: `${fontsDir}/NotoSans-Regular.ttf`, + bold: `${fontsDir}/NotoSans-Bold.ttf`, + bolditalic: `${fontsDir}/NotoSans-BoldItalic.ttf`, + italic: `${fontsDir}/NotoSans-Italic.ttf`, + } + ) +} diff --git a/src/modules/getNotoPdfFontForLocale/notoFontLookup.js b/src/modules/getNotoPdfFontForLocale/notoFontLookup.js new file mode 100644 index 0000000000..023b62eb18 --- /dev/null +++ b/src/modules/getNotoPdfFontForLocale/notoFontLookup.js @@ -0,0 +1,252 @@ +/* Full list of languages: + * https://www.loc.gov/standards/iso639-2/php/English_list.php + * Full list of scripts: + * https://unicode.org/iso15924/iso15924-codes.html + * An overview of scripts commonly used with different languages: + * https://www.unicode.org/cldr/charts/44/supplemental/languages_and_scripts.html + * We have identified the following languages/scripts which + * require a custom Noto font bundle: + * - Arabic + * - Bengali/Bangla + * - Ethiopic + * - Hewbrew + * - Japanese + * - Khmer + * - Korean + * - Lao + * - Myanmar + * - Odia + * - Simplified Chinese + * - Sinhala + * - Thai + * The lookup below describes the scripts and languages per + * custom font bundle. It's likely that this lookup is not + * complete and that new font-bundles and script- and + * language-codes will be added in the future. When adding a + * new font bundle here, new .ttf files also need to be added to + * `./public/fonts`. + * Note about the language sets: some language codes correspond to + * multiple scripts, but we can only add a single font bundle when + * convering to PDF. The Latn script is supported without a font + * bundle, so if a language corresponds to Latn plus one additional + * script, we can include the bundle for that script. However if the + * language * corresponds to multiple non-ASCII scripts then choosing + * the correct font bundle based on a language code is technicaly + * impossible. The only way to ensure the correct font bundle for + * PDF generation is by ensuring that the locale string contains a + * script section. In the lookup below these ambiguous languages + * have been disabled, because it is better to serve the base Noto + * font in these cases. */ + +export const NOTO_FONT_LOOKUP = new Map([ + [ + 'NotoSansArabic', + { + scripts: new Set(['Arab']), + languages: new Set([ + 'arq', + 'ar', // Technically maps to both Arab and Syrc but we assume Arab + // 'az', (Arab + Cyrl) + 'bqi', + // 'bft', (Arab + Tibt) + 'bal', + 'bej', + 'brh', + 'ckb', + 'swb', + // 'cop', (Arab + Grek + Copt) + 'dcc', + // 'doi', (Arab + Deva + Takr) + // 'cjm', (Arab + Cham) + 'arz', + 'glk', + 'gju', + 'ha', + 'haz', + 'id', + // 'inh', (Arab + Cyrl) + 'dyo', + 'jrb', + 'gjk', + // 'ks', (Arab + Deva) + // 'kk', (Arab + Cyrl) + 'khw', + // 'ku', (Arab + Cyrl) + // 'ky', (Arab + Cyrl) + 'lki', + 'ms', + 'mzn', + 'ary', + // 'ttt', (Arab + Cyrl) + 'ars', + 'wni', + 'zdj', + 'fia', + 'hno', + 'lrc', + 'kvx', + 'prd', + 'ps', + 'mfa', + 'fa', + // 'pa', (Arab + Guru) + // 'rhg', (Arab + Rohg) + 'skr', + // 'sd' (Arab + Deva + Khoj + Sind) + // 'so',(Arab + Osma) + 'hnd', + 'sdh', + 'luz', + 'sus', + // 'shi', (Arab + Tfng) + // 'tg', (Arab + Cyrl) + // 'tly', (Arab + Cyrl) + 'aeb', + 'tr', + // 'tk', (Arab + Cyrl) + 'ur', + 'ug', + // 'uz', (Arab + Cyrl) + 'kxp', + 'bgn', + // 'cja' (Arab + Cham) + 'lah', + 'wo', + 'gbz', + ]), + }, + ], + [ + 'NotoSansJP', + { + scripts: new Set(['Jpan']), + languages: new Set(['ja']), + }, + ], + [ + 'NotoSansKR', + { + scripts: new Set(['Kore']), + languages: new Set(['ko']), + }, + ], + [ + 'NotoSansSC', + { + scripts: new Set(['Hans']), + languages: new Set([ + 'yue', + 'zh', + 'gan', + 'hak', + 'lzh', + 'nan', + 'wuu', + 'hsn', + 'za', + ]), + }, + ], + [ + 'NotoSansThai', + { + scripts: new Set(['Thai']), + languages: new Set([ + 'lwl', + 'kdt', + 'tts', + 'kxm', + 'nod', + // 'pi' (Deva + Sinh + Thai) + 'sou', + 'th', + 'lcp', + ]), + }, + ], + [ + 'NotoSansHebrew', + { + scripts: new Set(['Hebr']), + languages: new Set([ + 'he', + 'jrb', + 'jpr', + 'lad', + // 'sam', (Hebr + Samr) + 'yi', + ]), + }, + ], + [ + 'NotoSansLao', + { + scripts: new Set(['Laoo']), + languages: new Set(['hnj', 'kjg', 'lo']), + }, + ], + [ + 'NotoSansEthiopic', + { + scripts: new Set(['Ethi']), + languages: new Set(['am', 'byn', 'gez', 'om', 'tig', 'ti', 'wal']), + }, + ], + [ + 'NotoSansMyanmar', + { + scripts: new Set(['Mymr']), + languages: new Set(['my', 'kht', 'mnw', 'shn']), + }, + ], + [ + 'NotoSansBengali', + { + scripts: new Set(['Beng']), + languages: new Set([ + 'as', + 'bn', + 'bpy', + // 'ccp', (Beng + Cakm) + 'grt', + 'kha', + // 'mni', (Beng, Mtei) + 'lus', + // 'unx' (Beng + Deva), + // 'unr' (Beng + Deva), + 'rkt', + // 'sat' (Beng + Deva + Orya + Olck) + // 'syl' (Beng + Sylo) + ]), + }, + ], + [ + 'NotoSansKhmer', + { + scripts: new Set(['Khmr']), + languages: new Set(['km']), + }, + ], + [ + 'NotoSansOriya', + { + scripts: new Set(['Orya']), + languages: new Set([ + // 'kxv', (Orya + Deva + Telu) + 'or', + // 'sat' (Orya + Beng + Deva + Olck) + ]), + }, + ], + [ + 'NotoSansSinhala', + { + scripts: new Set(['Sinh']), + languages: new Set([ + 'si', + // 'pi', (Sihn + Deva + Thai) + // 'sa', (Sihn + Deva + Gran + Shrd + Sidd) + ]), + }, + ], +]) diff --git a/src/reducers/chart.js b/src/reducers/chart.js deleted file mode 100644 index 6be8d9a543..0000000000 --- a/src/reducers/chart.js +++ /dev/null @@ -1,15 +0,0 @@ -export const SET_CHART = 'SET_CHART' - -export const DEFAULT_CHART = null - -export default (state = DEFAULT_CHART, action) => { - switch (action.type) { - case SET_CHART: { - return action.value - } - default: - return state - } -} - -export const sGetChart = (state) => state.chart diff --git a/src/reducers/index.js b/src/reducers/index.js index 5cb9ebd8c9..5ccc99767f 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,5 +1,4 @@ import { combineReducers } from 'redux' -import chart, * as fromChart from './chart.js' import current, * as fromCurrent from './current.js' import dimensions, * as fromDimensions from './dimensions.js' import loader, * as fromLoader from './loader.js' @@ -24,7 +23,6 @@ export default combineReducers({ user, snackbar, loader, - chart, }) // Selectors @@ -40,7 +38,6 @@ export { fromUser, fromSnackbar, fromLoader, - fromChart, } export const sGetSeriesSetupItems = (state) => diff --git a/yarn.lock b/yarn.lock index 42d1ab01e4..1cea121a09 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2050,10 +2050,10 @@ classnames "^2.3.1" prop-types "^15.7.2" -"@dhis2/analytics@^26.8.7": - version "26.8.7" - resolved "https://registry.yarnpkg.com/@dhis2/analytics/-/analytics-26.8.7.tgz#46838366066ddd1f92ab7b5bc78f1d87b4f56b75" - integrity sha512-zPdxVDL8IhCwZF2zDEj+2TPG1kS4MTJUgM2xGjSHnBFfrsn/ddN8D8x2tu+V4hh5F+xYggSkMNeGi2hwLsml+A== +"@dhis2/analytics@^26.9.0": + version "26.9.0" + resolved "https://registry.yarnpkg.com/@dhis2/analytics/-/analytics-26.9.0.tgz#562f0f6cb5107df80292f81b4fb53053da947131" + integrity sha512-BA2NOKW2r+NRyJnHTzM6IlhmoEZbv8JbU0Omcyq+0YiNHZEw9pNq+6HG98S5MnvlM0f0W8GYb/E+97YCDQG6ZQ== dependencies: "@dhis2/multi-calendar-dates" "^1.2.2" "@dnd-kit/core" "^6.0.7"