From dee77f52dbaef05cd922b440db9020514fa3696c Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 27 Aug 2024 17:44:29 +0200 Subject: [PATCH 01/82] feat: single value as a highcharts instance WIP --- src/__demo__/SingleValue.stories.js | 639 ++++++++++++++++++ src/visualizations/.eslintrc | 4 +- src/visualizations/config/adapters/index.js | 1 + .../config/generators/highcharts/index.js | 26 +- .../renderSingleValueSvg/constants.js | 38 ++ .../renderSingleValueSvg/generateDVItem.js | 134 ++++ .../renderSingleValueSvg/generateValueSVG.js | 135 ++++ .../getTextAnchorFromTextAlign.js | 17 + .../renderSingleValueSvg/getXFromTextAlign.js | 17 + .../highcharts/renderSingleValueSvg/index.js | 76 +++ .../shouldUseContrastColor.js | 17 + .../renderSingleValueSvg/textSize.js | 52 ++ src/visualizations/config/generators/index.js | 3 +- src/visualizations/store/adapters/index.js | 1 + 14 files changed, 1157 insertions(+), 3 deletions(-) create mode 100644 src/__demo__/SingleValue.stories.js create mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js create mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js create mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js create mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js create mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js create mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js create mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/shouldUseContrastColor.js create mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js new file mode 100644 index 000000000..1720ac28e --- /dev/null +++ b/src/__demo__/SingleValue.stories.js @@ -0,0 +1,639 @@ +import { storiesOf } from '@storybook/react' +import React, { useCallback } from 'react' +import { createVisualization } from '../index.js' +const constainerStyle = { + width: 400, + height: 400, + border: '1px solid magenta', + marginBottom: 14, +} +const data = [ + { + response: { + headers: [ + { + name: 'dx', + column: 'Data', + valueType: 'TEXT', + type: 'java.lang.String', + hidden: false, + meta: true, + }, + { + name: 'value', + column: 'Value', + valueType: 'NUMBER', + type: 'java.lang.Double', + hidden: false, + meta: false, + }, + ], + metaData: { + items: { + 202308: { + uid: '202308', + code: '202308', + name: 'August 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-08-01T00:00:00.000', + endDate: '2023-08-31T00:00:00.000', + }, + 202309: { + uid: '202309', + code: '202309', + name: 'September 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-09-01T00:00:00.000', + endDate: '2023-09-30T00:00:00.000', + }, + 202310: { + uid: '202310', + code: '202310', + name: 'October 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-10-01T00:00:00.000', + endDate: '2023-10-31T00:00:00.000', + }, + 202311: { + uid: '202311', + code: '202311', + name: 'November 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-11-01T00:00:00.000', + endDate: '2023-11-30T00:00:00.000', + }, + 202312: { + uid: '202312', + code: '202312', + name: 'December 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-12-01T00:00:00.000', + endDate: '2023-12-31T00:00:00.000', + }, + 202401: { + uid: '202401', + code: '202401', + name: 'January 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-01-01T00:00:00.000', + endDate: '2024-01-31T00:00:00.000', + }, + 202402: { + uid: '202402', + code: '202402', + name: 'February 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-02-01T00:00:00.000', + endDate: '2024-02-29T00:00:00.000', + }, + 202403: { + uid: '202403', + code: '202403', + name: 'March 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-03-01T00:00:00.000', + endDate: '2024-03-31T00:00:00.000', + }, + 202404: { + uid: '202404', + code: '202404', + name: 'April 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-04-01T00:00:00.000', + endDate: '2024-04-30T00:00:00.000', + }, + 202405: { + uid: '202405', + code: '202405', + name: 'May 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-05-01T00:00:00.000', + endDate: '2024-05-31T00:00:00.000', + }, + 202406: { + uid: '202406', + code: '202406', + name: 'June 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-06-01T00:00:00.000', + endDate: '2024-06-30T00:00:00.000', + }, + 202407: { + uid: '202407', + code: '202407', + name: 'July 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-07-01T00:00:00.000', + endDate: '2024-07-31T00:00:00.000', + }, + ou: { + uid: 'ou', + name: 'Organisation unit', + dimensionType: 'ORGANISATION_UNIT', + }, + O6uvpzGd5pu: { + uid: 'O6uvpzGd5pu', + code: 'OU_264', + name: 'Bo', + dimensionItemType: 'ORGANISATION_UNIT', + valueType: 'TEXT', + totalAggregationType: 'SUM', + }, + LAST_12_MONTHS: { + name: 'Last 12 months', + }, + dx: { + uid: 'dx', + name: 'Data', + dimensionType: 'DATA_X', + }, + pe: { + uid: 'pe', + name: 'Period', + dimensionType: 'PERIOD', + }, + FnYCr2EAzWS: { + uid: 'FnYCr2EAzWS', + code: 'IN_52493', + name: 'BCG Coverage <1y', + legendSet: 'BtxOoQuLyg1', + dimensionItemType: 'INDICATOR', + valueType: 'NUMBER', + totalAggregationType: 'AVERAGE', + indicatorType: { + name: 'Per cent', + displayName: 'Per cent', + factor: 100, + number: false, + }, + }, + }, + dimensions: { + dx: ['FnYCr2EAzWS'], + pe: [ + '202308', + '202309', + '202310', + '202311', + '202312', + '202401', + '202402', + '202403', + '202404', + '202405', + '202406', + '202407', + ], + ou: ['O6uvpzGd5pu'], + co: [], + }, + }, + rowContext: {}, + rows: [['FnYCr2EAzWS', '34.19']], + width: 2, + height: 1, + headerWidth: 2, + }, + headers: [ + { + name: 'dx', + column: 'Data', + valueType: 'TEXT', + type: 'java.lang.String', + hidden: false, + meta: true, + isPrefix: false, + isCollect: false, + index: 0, + }, + { + name: 'value', + column: 'Value', + valueType: 'NUMBER', + type: 'java.lang.Double', + hidden: false, + meta: false, + isPrefix: false, + isCollect: false, + index: 1, + }, + ], + rows: [['FnYCr2EAzWS', '34.19']], + metaData: { + items: { + 202308: { + uid: '202308', + code: '202308', + name: 'August 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-08-01T00:00:00.000', + endDate: '2023-08-31T00:00:00.000', + }, + 202309: { + uid: '202309', + code: '202309', + name: 'September 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-09-01T00:00:00.000', + endDate: '2023-09-30T00:00:00.000', + }, + 202310: { + uid: '202310', + code: '202310', + name: 'October 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-10-01T00:00:00.000', + endDate: '2023-10-31T00:00:00.000', + }, + 202311: { + uid: '202311', + code: '202311', + name: 'November 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-11-01T00:00:00.000', + endDate: '2023-11-30T00:00:00.000', + }, + 202312: { + uid: '202312', + code: '202312', + name: 'December 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-12-01T00:00:00.000', + endDate: '2023-12-31T00:00:00.000', + }, + 202401: { + uid: '202401', + code: '202401', + name: 'January 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-01-01T00:00:00.000', + endDate: '2024-01-31T00:00:00.000', + }, + 202402: { + uid: '202402', + code: '202402', + name: 'February 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-02-01T00:00:00.000', + endDate: '2024-02-29T00:00:00.000', + }, + 202403: { + uid: '202403', + code: '202403', + name: 'March 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-03-01T00:00:00.000', + endDate: '2024-03-31T00:00:00.000', + }, + 202404: { + uid: '202404', + code: '202404', + name: 'April 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-04-01T00:00:00.000', + endDate: '2024-04-30T00:00:00.000', + }, + 202405: { + uid: '202405', + code: '202405', + name: 'May 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-05-01T00:00:00.000', + endDate: '2024-05-31T00:00:00.000', + }, + 202406: { + uid: '202406', + code: '202406', + name: 'June 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-06-01T00:00:00.000', + endDate: '2024-06-30T00:00:00.000', + }, + 202407: { + uid: '202407', + code: '202407', + name: 'July 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-07-01T00:00:00.000', + endDate: '2024-07-31T00:00:00.000', + }, + ou: { + uid: 'ou', + name: 'Organisation unit', + dimensionType: 'ORGANISATION_UNIT', + }, + O6uvpzGd5pu: { + uid: 'O6uvpzGd5pu', + code: 'OU_264', + name: 'Bo', + dimensionItemType: 'ORGANISATION_UNIT', + valueType: 'TEXT', + totalAggregationType: 'SUM', + }, + LAST_12_MONTHS: { + name: 'Last 12 months', + }, + dx: { + uid: 'dx', + name: 'Data', + dimensionType: 'DATA_X', + }, + pe: { + uid: 'pe', + name: 'Period', + dimensionType: 'PERIOD', + }, + FnYCr2EAzWS: { + uid: 'FnYCr2EAzWS', + code: 'IN_52493', + name: 'BCG Coverage <1y', + legendSet: 'BtxOoQuLyg1', + dimensionItemType: 'INDICATOR', + valueType: 'NUMBER', + totalAggregationType: 'AVERAGE', + indicatorType: { + name: 'Per cent', + displayName: 'Per cent', + factor: 100, + number: false, + }, + }, + }, + dimensions: { + dx: ['FnYCr2EAzWS'], + pe: [ + '202308', + '202309', + '202310', + '202311', + '202312', + '202401', + '202402', + '202403', + '202404', + '202405', + '202406', + '202407', + ], + ou: ['O6uvpzGd5pu'], + co: [], + }, + }, + }, +] +const layout = { + name: 'BCG coverage last 12 months - Bo', + created: '2013-10-16T19:50:52.464', + lastUpdated: '2021-07-06T12:53:57.296', + translations: [], + favorites: [], + lastUpdatedBy: { + id: 'xE7jOejl9FI', + code: null, + name: 'John Traore', + displayName: 'John Traore', + username: 'admin', + }, + regressionType: 'NONE', + displayDensity: 'NORMAL', + fontSize: 'NORMAL', + sortOrder: 0, + topLimit: 0, + hideEmptyRows: false, + showHierarchy: false, + completedOnly: false, + skipRounding: false, + dataDimensionItems: [ + { + indicator: { + name: 'BCG Coverage <1y', + dimensionItemType: 'INDICATOR', + displayName: 'BCG Coverage <1y', + access: { + manage: true, + externalize: true, + write: true, + read: true, + update: true, + delete: true, + }, + displayShortName: 'BCG Coverage <1y', + id: 'FnYCr2EAzWS', + }, + dataDimensionItemType: 'INDICATOR', + }, + ], + subscribers: [], + aggregationType: 'DEFAULT', + digitGroupSeparator: 'SPACE', + hideEmptyRowItems: 'NONE', + noSpaceBetweenColumns: false, + cumulativeValues: false, + percentStackedValues: false, + showData: true, + colTotals: false, + rowTotals: false, + rowSubTotals: false, + colSubTotals: false, + hideTitle: false, + hideSubtitle: false, + showDimensionLabels: false, + interpretations: [], + type: 'SINGLE_VALUE', + reportingParams: { + grandParentOrganisationUnit: false, + parentOrganisationUnit: false, + organisationUnit: false, + reportingPeriod: false, + }, + numberType: 'VALUE', + fontStyle: {}, + colorSet: 'DEFAULT', + yearlySeries: [], + regression: false, + hideEmptyColumns: false, + fixColumnHeaders: false, + fixRowHeaders: false, + filters: [ + { + items: [ + { + name: 'Bo', + dimensionItemType: 'ORGANISATION_UNIT', + displayShortName: 'Bo', + displayName: 'Bo', + access: { + manage: true, + externalize: true, + write: true, + read: true, + update: true, + delete: true, + }, + id: 'O6uvpzGd5pu', + }, + ], + dimension: 'ou', + }, + { + items: [ + { + name: 'LAST_12_MONTHS', + dimensionItemType: 'PERIOD', + displayShortName: 'LAST_12_MONTHS', + displayName: 'LAST_12_MONTHS', + access: { + manage: true, + externalize: true, + write: true, + read: true, + update: true, + delete: true, + }, + id: 'LAST_12_MONTHS', + }, + ], + dimension: 'pe', + }, + ], + parentGraphMap: { + O6uvpzGd5pu: 'ImspTQPwCqd', + }, + columns: [ + { + items: [ + { + name: 'BCG Coverage <1y', + dimensionItemType: 'INDICATOR', + displayName: 'BCG Coverage <1y', + access: { + manage: true, + externalize: true, + write: true, + read: true, + update: true, + delete: true, + }, + displayShortName: 'BCG Coverage <1y', + id: 'FnYCr2EAzWS', + }, + ], + dimension: 'dx', + }, + ], + rows: [], + subscribed: false, + displayName: 'BCG coverage last 12 months - Bo', + access: { + manage: true, + externalize: true, + write: true, + read: true, + update: true, + delete: true, + }, + favorite: false, + user: { + id: 'xE7jOejl9FI', + code: null, + name: 'John Traore', + displayName: 'John Traore', + username: 'admin', + }, + href: 'http://localhost:8080/api/41/visualizations/mYMnDl5Z9oD', + id: 'mYMnDl5Z9oD', + legend: { + showKey: false, + }, + sorting: [], + series: [], + icons: [], + seriesKey: { + hidden: false, + }, + axes: [], +} +const extraOptions = { + dashboard: false, + animation: 200, + legendSets: [], +} + +storiesOf('SingleValue', module).add('default', () => { + const onOldContainerMounted = useCallback((el) => { + createVisualization( + data, + layout, + el, + extraOptions, + undefined, + undefined, + 'dhis' + ) + }, []) + const onNewContainerMounted = useCallback((el) => { + createVisualization( + data, + layout, + el, + extraOptions, + undefined, + undefined, + 'singleValue' + ) + }, []) + return ( + <> +
+
+ + ) +}) diff --git a/src/visualizations/.eslintrc b/src/visualizations/.eslintrc index ce5078472..f8259534e 100644 --- a/src/visualizations/.eslintrc +++ b/src/visualizations/.eslintrc @@ -1,5 +1,7 @@ { "rules": { - "max-params": "off" + "max-params": "off", + // TODO: switch back on before merging + "no-unused-vars": "off" } } diff --git a/src/visualizations/config/adapters/index.js b/src/visualizations/config/adapters/index.js index 7b49438ee..e567b54d1 100644 --- a/src/visualizations/config/adapters/index.js +++ b/src/visualizations/config/adapters/index.js @@ -4,4 +4,5 @@ import dhis_highcharts from './dhis_highcharts/index.js' export default { dhis_highcharts, dhis_dhis, + dhis_singleValue: dhis_dhis, } diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index 92a775910..1f29d6f3d 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -5,6 +5,7 @@ import HE from 'highcharts/modules/exporting' import HNDTD from 'highcharts/modules/no-data-to-display' import HPF from 'highcharts/modules/pattern-fill' import HSG from 'highcharts/modules/solid-gauge' +import renderSingleValueSvg from './renderSingleValueSvg/index.js' // apply HM(H) @@ -69,7 +70,7 @@ function drawLegendSymbolWrap() { ) } -export default function (config, el) { +export function highcharts(config, el) { if (config) { config.chart.renderTo = el || config.chart.renderTo @@ -87,3 +88,26 @@ export default function (config, el) { return new H.Chart(config) } } + +export function singleValue(config, el, extraOptions) { + return H.chart(el, { + accessibility: { enabled: false }, + chart: { + backgroundColor: 'transparent', + events: { + load: function () { + renderSingleValueSvg(config, el, extraOptions, this) + }, + }, + }, + credits: { enabled: false }, + // exporting: { + // enabled: false, + // }, + lang: { + noData: null, + }, + noData: {}, + title: null, + }) +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js new file mode 100644 index 000000000..ce7cea956 --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js @@ -0,0 +1,38 @@ +// TODO: remove this, sch thing it should not be needed +export const svgNS = 'http://www.w3.org/2000/svg' +// multiply text width with this factor +// to get very close to actual text width +// nb: dependent on viewbox etc +export const ACTUAL_TEXT_WIDTH_FACTOR = 0.9 + +// multiply value text size with this factor +// to get very close to the actual number height +// as numbers don't go below the baseline like e.g. "j" and "g" +export const ACTUAL_NUMBER_HEIGHT_FACTOR = 0.67 + +// do not allow text width to exceed this threshold +// a threshold >1 does not really make sense but text width vs viewbox is complicated +export const TEXT_WIDTH_CONTAINER_WIDTH_FACTOR = 1.3 + +// do not allow text size to exceed this +export const TEXT_SIZE_CONTAINER_HEIGHT_FACTOR = 0.6 +export const TEXT_SIZE_MAX_THRESHOLD = 400 + +// multiply text size with this factor +// to get an appropriate letter spacing +export const LETTER_SPACING_TEXT_SIZE_FACTOR = (1 / 35) * -1 +export const LETTER_SPACING_MIN_THRESHOLD = -6 +export const LETTER_SPACING_MAX_THRESHOLD = -1 + +// fixed top margin above title/subtitle +export const TOP_MARGIN_FIXED = 16 + +// multiply text size with this factor +// to get an appropriate sub text size +export const SUB_TEXT_SIZE_FACTOR = 0.5 +export const SUB_TEXT_SIZE_MIN_THRESHOLD = 26 +export const SUB_TEXT_SIZE_MAX_THRESHOLD = 40 + +// multiply text size with this factor +// to get an appropriate icon padding +export const ICON_PADDING_FACTOR = 0.3 diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js new file mode 100644 index 000000000..2291d341d --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js @@ -0,0 +1,134 @@ +import { + defaultFontStyle, + FONT_STYLE_OPTION_BOLD, + FONT_STYLE_OPTION_FONT_SIZE, + FONT_STYLE_OPTION_ITALIC, + FONT_STYLE_OPTION_TEXT_ALIGN, + FONT_STYLE_OPTION_TEXT_COLOR, + FONT_STYLE_VISUALIZATION_SUBTITLE, + FONT_STYLE_VISUALIZATION_TITLE, + mergeFontStyleWithDefault, +} from '../../../../../modules/fontStyle.js' +import { TOP_MARGIN_FIXED } from './constants.js' +import { generateValueSVG } from './generateValueSVG.js' +import { getTextAnchorFromTextAlign } from './getTextAnchorFromTextAlign.js' +import { getXFromTextAlign } from './getXFromTextAlign.js' + +export const generateDVItem = ( + config, + { + renderer, + width, + height, + valueColor, + noData, + backgroundColor, + titleColor, + fontStyle, + icon, + } +) => { + backgroundColor = 'red' + if (backgroundColor) { + renderer + .rect(0, 0, width, height) + .attr({ fill: backgroundColor, width: '100%', height: '100%' }) + .add() + } + + // TITLE + const titleFontStyle = mergeFontStyleWithDefault( + fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_TITLE], + FONT_STYLE_VISUALIZATION_TITLE + ) + + const titleYPosition = + TOP_MARGIN_FIXED + + parseInt(titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]) + + 'px' + + const titleFontSize = `${titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px` + + renderer + .text(config.title) + .attr({ + x: getXFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]), + y: titleYPosition, + 'text-anchor': getTextAnchorFromTextAlign( + titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] + ), + 'font-size': titleFontSize, + 'font-weight': titleFontStyle[FONT_STYLE_OPTION_BOLD] + ? FONT_STYLE_OPTION_BOLD + : 'normal', + 'font-style': titleFontStyle[FONT_STYLE_OPTION_ITALIC] + ? FONT_STYLE_OPTION_ITALIC + : 'normal', + 'data-test': 'visualization-title', + fill: + titleColor && + titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === + defaultFontStyle[FONT_STYLE_VISUALIZATION_TITLE][ + FONT_STYLE_OPTION_TEXT_COLOR + ] + ? titleColor + : titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR], + }) + .add() + + // SUBTITLE + const subtitleFontStyle = mergeFontStyleWithDefault( + fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], + FONT_STYLE_VISUALIZATION_SUBTITLE + ) + const subtitleFontSize = `${subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px` + + if (config.subtitle) { + renderer + .text(config.subtitle) + .attr({ + x: getXFromTextAlign( + subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] + ), + y: titleYPosition, + dy: `${subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE] + 10}`, + 'text-anchor': getTextAnchorFromTextAlign( + subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] + ), + 'font-size': subtitleFontSize, + 'font-weight': subtitleFontStyle[FONT_STYLE_OPTION_BOLD] + ? FONT_STYLE_OPTION_BOLD + : 'normal', + 'font-style': subtitleFontStyle[FONT_STYLE_OPTION_ITALIC] + ? FONT_STYLE_OPTION_ITALIC + : 'normal', + fill: + titleColor && + subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === + defaultFontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE][ + FONT_STYLE_OPTION_TEXT_COLOR + ] + ? titleColor + : subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR], + 'data-test': 'visualization-subtitle', + }) + .add() + } + + generateValueSVG({ + renderer, + formattedValue: config.formattedValue, + subText: config.subText, + valueColor, + textColor: titleColor, + noData, + icon, + containerWidth: width, + containerHeight: height, + topMargin: + TOP_MARGIN_FIXED + + ((config.title ? parseInt(titleFontSize) : 0) + + (config.subtitle ? parseInt(subtitleFontSize) : 0)) * + 2.5, + }) +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js new file mode 100644 index 000000000..4d8e0292a --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js @@ -0,0 +1,135 @@ +import { colors } from '@dhis2/ui' +import { + LETTER_SPACING_MAX_THRESHOLD, + LETTER_SPACING_MIN_THRESHOLD, + LETTER_SPACING_TEXT_SIZE_FACTOR, + SUB_TEXT_SIZE_FACTOR, + SUB_TEXT_SIZE_MAX_THRESHOLD, + SUB_TEXT_SIZE_MIN_THRESHOLD, + svgNS, +} from './constants.js' +import { + getIconPadding, + getTextHeightForNumbers, + getTextSize, + getTextWidth, +} from './textSize.js' + +export const generateValueSVG = ({ + renderer, + formattedValue, + subText, + valueColor, + textColor, + icon, + noData, + containerWidth, + containerHeight, + topMargin = 0, +}) => { + console.log('show value') + const showIcon = icon && formattedValue !== noData.text + + const textSize = getTextSize( + formattedValue, + containerWidth, + containerHeight, + showIcon + ) + + const textWidth = getTextWidth(formattedValue, `${textSize}px Roboto`) + + const iconSize = textSize + + const subTextSize = + textSize * SUB_TEXT_SIZE_FACTOR > SUB_TEXT_SIZE_MAX_THRESHOLD + ? SUB_TEXT_SIZE_MAX_THRESHOLD + : textSize * SUB_TEXT_SIZE_FACTOR < SUB_TEXT_SIZE_MIN_THRESHOLD + ? SUB_TEXT_SIZE_MIN_THRESHOLD + : textSize * SUB_TEXT_SIZE_FACTOR + + const svgValue = document.createElementNS(svgNS, 'svg') + svgValue.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`) + svgValue.setAttribute('width', '50%') + svgValue.setAttribute('height', '50%') + svgValue.setAttribute('x', '50%') + svgValue.setAttribute('y', '50%') + svgValue.setAttribute('style', 'overflow: visible') + + let fillColor = colors.grey900 + + if (valueColor) { + fillColor = valueColor + } else if (formattedValue === noData.text) { + fillColor = colors.grey600 + } + + // show icon if configured in maintenance app + if (showIcon) { + // embed icon to allow changing color + // (elements with fill need to use "currentColor" for this to work) + const iconSvgNode = document.createElementNS(svgNS, 'svg') + iconSvgNode.setAttribute('viewBox', '0 0 48 48') + iconSvgNode.setAttribute('width', iconSize) + iconSvgNode.setAttribute('height', iconSize) + iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1) + iconSvgNode.setAttribute( + 'x', + `-${(iconSize + getIconPadding(textSize) + textWidth) / 2}` + ) + iconSvgNode.setAttribute('style', `color: ${fillColor}`) + iconSvgNode.setAttribute('data-test', 'visualization-icon') + + const parser = new DOMParser() + const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml') + + Array.from(svgIconDocument.documentElement.children).forEach((node) => + iconSvgNode.appendChild(node) + ) + + svgValue.appendChild(iconSvgNode) + } + + const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) + + const textNode = document.createElementNS(svgNS, 'text') + textNode.setAttribute('font-size', textSize) + textNode.setAttribute('font-weight', '300') + textNode.setAttribute( + 'letter-spacing', + letterSpacing < LETTER_SPACING_MIN_THRESHOLD + ? LETTER_SPACING_MIN_THRESHOLD + : letterSpacing > LETTER_SPACING_MAX_THRESHOLD + ? LETTER_SPACING_MAX_THRESHOLD + : letterSpacing + ) + textNode.setAttribute('text-anchor', 'middle') + textNode.setAttribute( + 'x', + showIcon ? `${(iconSize + getIconPadding(textSize)) / 2}` : 0 + ) + textNode.setAttribute( + 'y', + topMargin / 2 + getTextHeightForNumbers(textSize) / 2 + ) + textNode.setAttribute('fill', fillColor) + textNode.setAttribute('data-test', 'visualization-primary-value') + + textNode.appendChild(document.createTextNode(formattedValue)) + + svgValue.appendChild(textNode) + + if (subText) { + const subTextNode = document.createElementNS(svgNS, 'text') + subTextNode.setAttribute('text-anchor', 'middle') + subTextNode.setAttribute('font-size', subTextSize) + subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2) + subTextNode.setAttribute('dy', subTextSize * 1.7) + subTextNode.setAttribute('fill', textColor) + subTextNode.appendChild(document.createTextNode(subText)) + + svgValue.appendChild(subTextNode) + } + + return svgValue +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js new file mode 100644 index 000000000..5d66ba074 --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js @@ -0,0 +1,17 @@ +import { + TEXT_ALIGN_LEFT, + TEXT_ALIGN_CENTER, + TEXT_ALIGN_RIGHT, +} from '../../../../../modules/fontStyle.js' + +export const getTextAnchorFromTextAlign = (textAlign) => { + switch (textAlign) { + default: + case TEXT_ALIGN_LEFT: + return 'start' + case TEXT_ALIGN_CENTER: + return 'middle' + case TEXT_ALIGN_RIGHT: + return 'end' + } +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js new file mode 100644 index 000000000..d9383b4e9 --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js @@ -0,0 +1,17 @@ +import { + TEXT_ALIGN_LEFT, + TEXT_ALIGN_CENTER, + TEXT_ALIGN_RIGHT, +} from '../../../../../modules/fontStyle.js' + +export const getXFromTextAlign = (textAlign) => { + switch (textAlign) { + default: + case TEXT_ALIGN_LEFT: + return '1%' + case TEXT_ALIGN_CENTER: + return '50%' + case TEXT_ALIGN_RIGHT: + return '99%' + } +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js new file mode 100644 index 000000000..be1fc9cde --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js @@ -0,0 +1,76 @@ +import { colors } from '@dhis2/ui' +import { + getColorByValueFromLegendSet, + LEGEND_DISPLAY_STYLE_FILL, +} from '../../../../../modules/legends.js' +import { generateDVItem } from './generateDVItem.js' +import { shouldUseContrastColor } from './shouldUseContrastColor.js' + +export default function ( + config, + parentEl, + { dashboard, legendSets, fontStyle, noData, legendOptions, icon }, + chart +) { + const renderer = chart.renderer + const legendSet = legendOptions && legendSets[0] + const legendColor = + legendSet && getColorByValueFromLegendSet(legendSet, config.value) + let valueColor, titleColor, backgroundColor + if (legendColor) { + if (legendOptions.style === LEGEND_DISPLAY_STYLE_FILL) { + backgroundColor = legendColor + valueColor = titleColor = + shouldUseContrastColor(legendColor) && colors.white + } else { + valueColor = legendColor + } + } + + parentEl.style.overflow = 'hidden' + parentEl.style.display = 'flex' + parentEl.style.justifyContent = 'center' + + // We need the inner width so borders etc are excluded + const width = parentEl.clientWidth + const height = parentEl.clientHeight + + const svgContainer = renderer.box + svgContainer.setAttribute('viewBox', `0 0 ${width} ${height}`) + svgContainer.setAttribute('data-test', 'visualization-container') + + chart.setSize(dashboard ? '100%' : width, dashboard ? '100%' : height) + + // if (dashboard) { + // parentEl.style.borderRadius = '3px' + + // return generateDashboardItem(config, { + // svgContainer, + // width, + // height, + // valueColor, + // backgroundColor, + // noData, + // icon, + // ...(legendOptions.style === LEGEND_DISPLAY_STYLE_FILL && + // legendColor && + // shouldUseContrastColor(legendColor) + // ? { titleColor: colors.white } + // : {}), + // }) + // } else { + parentEl.style.height = `100%` + + return generateDVItem(config, { + renderer, + width, + height, + valueColor, + backgroundColor, + titleColor, + noData, + icon, + fontStyle, + }) + // } +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/shouldUseContrastColor.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/shouldUseContrastColor.js new file mode 100644 index 000000000..d01616c9a --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/shouldUseContrastColor.js @@ -0,0 +1,17 @@ +export const shouldUseContrastColor = (inputColor = '') => { + // based on https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color + var color = + inputColor.charAt(0) === '#' ? inputColor.substring(1, 7) : inputColor + var r = parseInt(color.substring(0, 2), 16) // hexToR + var g = parseInt(color.substring(2, 4), 16) // hexToG + var b = parseInt(color.substring(4, 6), 16) // hexToB + var uicolors = [r / 255, g / 255, b / 255] + var c = uicolors.map((col) => { + if (col <= 0.03928) { + return col / 12.92 + } + return Math.pow((col + 0.055) / 1.055, 2.4) + }) + var L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2] + return L <= 0.179 +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js new file mode 100644 index 000000000..a94ad7266 --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js @@ -0,0 +1,52 @@ +// Compute text width before rendering + +import { + ACTUAL_NUMBER_HEIGHT_FACTOR, + ACTUAL_TEXT_WIDTH_FACTOR, + ICON_PADDING_FACTOR, + TEXT_SIZE_CONTAINER_HEIGHT_FACTOR, + TEXT_SIZE_MAX_THRESHOLD, + TEXT_WIDTH_CONTAINER_WIDTH_FACTOR, +} from './constants.js' + +// Not exactly precise but close enough +export const getTextWidth = (text, font) => { + const canvas = document.createElement('canvas') + const context = canvas.getContext('2d') + context.font = font + return Math.round( + context.measureText(text).width * ACTUAL_TEXT_WIDTH_FACTOR + ) +} + +export const getTextHeightForNumbers = (textSize) => + textSize * ACTUAL_NUMBER_HEIGHT_FACTOR + +export const getIconPadding = (textSize) => + Math.round(textSize * ICON_PADDING_FACTOR) + +export const getTextSize = ( + formattedValue, + containerWidth, + containerHeight, + showIcon +) => { + let size = Math.min( + Math.round(containerHeight * TEXT_SIZE_CONTAINER_HEIGHT_FACTOR), + TEXT_SIZE_MAX_THRESHOLD + ) + + const widthThreshold = Math.round( + containerWidth * TEXT_WIDTH_CONTAINER_WIDTH_FACTOR + ) + + const textWidth = + getTextWidth(formattedValue, `${size}px Roboto`) + + (showIcon ? getIconPadding(size) : 0) + + if (textWidth > widthThreshold) { + size = Math.round(size * (widthThreshold / textWidth)) + } + + return size +} diff --git a/src/visualizations/config/generators/index.js b/src/visualizations/config/generators/index.js index bc7a75872..290cac165 100644 --- a/src/visualizations/config/generators/index.js +++ b/src/visualizations/config/generators/index.js @@ -1,7 +1,8 @@ import dhis from './dhis/index.js' -import highcharts from './highcharts/index.js' +import { highcharts, singleValue } from './highcharts/index.js' export default { highcharts, dhis, + singleValue, } diff --git a/src/visualizations/store/adapters/index.js b/src/visualizations/store/adapters/index.js index 7b49438ee..e567b54d1 100644 --- a/src/visualizations/store/adapters/index.js +++ b/src/visualizations/store/adapters/index.js @@ -4,4 +4,5 @@ import dhis_highcharts from './dhis_highcharts/index.js' export default { dhis_highcharts, dhis_dhis, + dhis_singleValue: dhis_dhis, } From f344ece2ae932809da4e5ba83ba6e0eb572724b6 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Mon, 2 Sep 2024 15:36:47 +0200 Subject: [PATCH 02/82] chore: resizing the container --- src/__demo__/SingleValue.stories.js | 63 +++++++++++++++++-- .../config/generators/highcharts/index.js | 17 ++++- .../renderSingleValueSvg/generateValueSVG.js | 16 ++++- 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index 1720ac28e..ac71e1e1b 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -1,7 +1,7 @@ import { storiesOf } from '@storybook/react' -import React, { useCallback } from 'react' +import React, { useCallback, useState, useMemo, useRef, useEffect } from 'react' import { createVisualization } from '../index.js' -const constainerStyle = { +const constainerStyleBase = { width: 400, height: 400, border: '1px solid magenta', @@ -601,13 +601,20 @@ const layout = { }, axes: [], } +// const icon = +// '' + const extraOptions = { dashboard: false, animation: 200, legendSets: [], + // icon, } storiesOf('SingleValue', module).add('default', () => { + const newChartRef = useRef(null) + const [width, setWidth] = useState(constainerStyleBase.width) + const [height, setHeight] = useState(constainerStyleBase.height) const onOldContainerMounted = useCallback((el) => { createVisualization( data, @@ -620,7 +627,7 @@ storiesOf('SingleValue', module).add('default', () => { ) }, []) const onNewContainerMounted = useCallback((el) => { - createVisualization( + const obj = createVisualization( data, layout, el, @@ -629,11 +636,57 @@ storiesOf('SingleValue', module).add('default', () => { undefined, 'singleValue' ) + newChartRef.current = obj.visualization }, []) + const containerStyle = useMemo( + () => ({ + ...constainerStyleBase, + width, + height, + }), + [width, height] + ) + useEffect(() => { + if (newChartRef.current) { + console.log('calling reflow') + newChartRef.current.redraw() + } + }, [containerStyle]) + return ( <> -
-
+
+
+
+
+ + + setWidth(parseInt(event.target.value)) + } + value={width.toString()} + /> +
+
+ + + setHeight(parseInt(event.target.value)) + } + value={height.toString()} + /> +
+
) }) diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index 1f29d6f3d..ba3daf380 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -90,15 +90,28 @@ export function highcharts(config, el) { } export function singleValue(config, el, extraOptions) { + console.log('el', el) + let elClientHeight, elClientWidth return H.chart(el, { accessibility: { enabled: false }, chart: { backgroundColor: 'transparent', events: { - load: function () { - renderSingleValueSvg(config, el, extraOptions, this) + redraw: function () { + if ( + el.clientHeight !== elClientHeight || + el.clientWidth !== elClientWidth + ) { + console.log('resize!!!', el) + elClientHeight = el.clientHeight + elClientWidth = el.clientWidth + renderSingleValueSvg(config, el, extraOptions, this) + } else { + console.log('No action needed') + } }, }, + animation: false, }, credits: { enabled: false }, // exporting: { diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js index 4d8e0292a..99f2247f3 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js @@ -27,7 +27,7 @@ export const generateValueSVG = ({ containerHeight, topMargin = 0, }) => { - console.log('show value') + console.log('show value', renderer) const showIcon = icon && formattedValue !== noData.text const textSize = getTextSize( @@ -56,6 +56,20 @@ export const generateValueSVG = ({ svgValue.setAttribute('y', '50%') svgValue.setAttribute('style', 'overflow: visible') + const box = renderer + .rect(0, 0, containerWidth, containerHeight) + .attr({ + with: '50%', + height: '50%', + x: '50%', + y: '50%', + }) + .css({ + overflow: 'visible', + backgroundColor: 'green', + }) + .add() + let fillColor = colors.grey900 if (valueColor) { From ac966833b31ffc161d722c4f1d76de97fcd7e94b Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 4 Sep 2024 11:46:05 +0200 Subject: [PATCH 03/82] chore: scaling the icon correctly --- src/__demo__/SingleValue.stories.js | 119 ++++++++++---- .../config/generators/dhis/singleValue.js | 1 + .../config/generators/highcharts/index.js | 33 ++-- .../renderSingleValueSvg/generateValueSVG.js | 150 ++++++++---------- .../generateValueSVGOLD.js | 135 ++++++++++++++++ 5 files changed, 306 insertions(+), 132 deletions(-) create mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index ac71e1e1b..0b3bda6fa 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -1,5 +1,5 @@ import { storiesOf } from '@storybook/react' -import React, { useCallback, useState, useMemo, useRef, useEffect } from 'react' +import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react' import { createVisualization } from '../index.js' const constainerStyleBase = { width: 400, @@ -7,6 +7,13 @@ const constainerStyleBase = { border: '1px solid magenta', marginBottom: 14, } +const innerContainerStyle = { + overflow: 'hidden', + display: 'flex', + justifyContent: 'center', + height: '100%', +} + const data = [ { response: { @@ -601,43 +608,23 @@ const layout = { }, axes: [], } -// const icon = -// '' +const icon = + '' const extraOptions = { dashboard: false, animation: 200, legendSets: [], - // icon, + icon, } storiesOf('SingleValue', module).add('default', () => { const newChartRef = useRef(null) + const oldContainerRef = useRef(null) + const newContainerRef = useRef(null) + const [transpose, setTranspose] = useState(false) const [width, setWidth] = useState(constainerStyleBase.width) const [height, setHeight] = useState(constainerStyleBase.height) - const onOldContainerMounted = useCallback((el) => { - createVisualization( - data, - layout, - el, - extraOptions, - undefined, - undefined, - 'dhis' - ) - }, []) - const onNewContainerMounted = useCallback((el) => { - const obj = createVisualization( - data, - layout, - el, - extraOptions, - undefined, - undefined, - 'singleValue' - ) - newChartRef.current = obj.visualization - }, []) const containerStyle = useMemo( () => ({ ...constainerStyleBase, @@ -647,17 +634,47 @@ storiesOf('SingleValue', module).add('default', () => { [width, height] ) useEffect(() => { - if (newChartRef.current) { - console.log('calling reflow') - newChartRef.current.redraw() + if (oldContainerRef.current && newContainerRef.current) { + requestAnimationFrame(() => { + createVisualization( + data, + layout, + oldContainerRef.current, + extraOptions, + undefined, + undefined, + 'dhis' + ) + const newVisualization = createVisualization( + data, + layout, + newContainerRef.current, + extraOptions, + undefined, + undefined, + 'singleValue' + ) + newChartRef.current = newVisualization.visualization + }) } }, [containerStyle]) + const downloadOffline = useCallback(() => { + if (newChartRef.current) { + newChartRef.current.exportChartLocal({ + sourceHeight: 768, + sourceWidth: 1024, + scale: 1, + fallbackToExportServer: false, + filename: 'testOfflineDownload', + showExportInProgress: true, + type: 'image/png', + }) + } + }, []) return ( <> -
-
-
+
{ value={height.toString()} />
+ + +
+
+
+
+
+
+
+
) diff --git a/src/visualizations/config/generators/dhis/singleValue.js b/src/visualizations/config/generators/dhis/singleValue.js index 25ec5bab9..934e14fdb 100644 --- a/src/visualizations/config/generators/dhis/singleValue.js +++ b/src/visualizations/config/generators/dhis/singleValue.js @@ -151,6 +151,7 @@ const generateValueSVG = ({ // embed icon to allow changing color // (elements with fill need to use "currentColor" for this to work) const iconSvgNode = document.createElementNS(svgNS, 'svg') + console.log('old', iconSize) iconSvgNode.setAttribute('viewBox', '0 0 48 48') iconSvgNode.setAttribute('width', iconSize) iconSvgNode.setAttribute('height', iconSize) diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index ba3daf380..5356cc27f 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -3,6 +3,7 @@ import HM from 'highcharts/highcharts-more' import HB from 'highcharts/modules/boost' import HE from 'highcharts/modules/exporting' import HNDTD from 'highcharts/modules/no-data-to-display' +import HOE from 'highcharts/modules/offline-exporting' import HPF from 'highcharts/modules/pattern-fill' import HSG from 'highcharts/modules/solid-gauge' import renderSingleValueSvg from './renderSingleValueSvg/index.js' @@ -12,6 +13,7 @@ HM(H) HSG(H) HNDTD(H) HE(H) +HOE(H) HPF(H) HB(H) @@ -90,33 +92,30 @@ export function highcharts(config, el) { } export function singleValue(config, el, extraOptions) { - console.log('el', el) - let elClientHeight, elClientWidth return H.chart(el, { accessibility: { enabled: false }, chart: { backgroundColor: 'transparent', events: { - redraw: function () { - if ( - el.clientHeight !== elClientHeight || - el.clientWidth !== elClientWidth - ) { - console.log('resize!!!', el) - elClientHeight = el.clientHeight - elClientWidth = el.clientWidth - renderSingleValueSvg(config, el, extraOptions, this) - } else { - console.log('No action needed') - } + load: function () { + renderSingleValueSvg(config, el, extraOptions, this) }, }, animation: false, }, credits: { enabled: false }, - // exporting: { - // enabled: false, - // }, + exporting: { + enabled: true, + error: (options, error) => { + console.log('options', options) + console.log(error) + }, + chartOptions: { + title: { + text: null, + }, + }, + }, lang: { noData: null, }, diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js index 99f2247f3..dd7efd382 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js @@ -1,12 +1,12 @@ import { colors } from '@dhis2/ui' import { + svgNS, LETTER_SPACING_MAX_THRESHOLD, LETTER_SPACING_MIN_THRESHOLD, LETTER_SPACING_TEXT_SIZE_FACTOR, SUB_TEXT_SIZE_FACTOR, SUB_TEXT_SIZE_MAX_THRESHOLD, SUB_TEXT_SIZE_MIN_THRESHOLD, - svgNS, } from './constants.js' import { getIconPadding, @@ -15,6 +15,8 @@ import { getTextWidth, } from './textSize.js' +const parser = new DOMParser() + export const generateValueSVG = ({ renderer, formattedValue, @@ -27,8 +29,13 @@ export const generateValueSVG = ({ containerHeight, topMargin = 0, }) => { - console.log('show value', renderer) const showIcon = icon && formattedValue !== noData.text + const group = renderer + .g('value') + .css({ + transform: 'scale(0.5) translate(100%, 100%)', + }) + .add() const textSize = getTextSize( formattedValue, @@ -48,28 +55,6 @@ export const generateValueSVG = ({ ? SUB_TEXT_SIZE_MIN_THRESHOLD : textSize * SUB_TEXT_SIZE_FACTOR - const svgValue = document.createElementNS(svgNS, 'svg') - svgValue.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`) - svgValue.setAttribute('width', '50%') - svgValue.setAttribute('height', '50%') - svgValue.setAttribute('x', '50%') - svgValue.setAttribute('y', '50%') - svgValue.setAttribute('style', 'overflow: visible') - - const box = renderer - .rect(0, 0, containerWidth, containerHeight) - .attr({ - with: '50%', - height: '50%', - x: '50%', - y: '50%', - }) - .css({ - overflow: 'visible', - backgroundColor: 'green', - }) - .add() - let fillColor = colors.grey900 if (valueColor) { @@ -78,72 +63,73 @@ export const generateValueSVG = ({ fillColor = colors.grey600 } + const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) + + const formattedValueText = renderer + .text(formattedValue) + .attr({ + 'font-size': textSize, + 'font-weight': '300', + 'letter-spacing': + letterSpacing < LETTER_SPACING_MIN_THRESHOLD + ? LETTER_SPACING_MIN_THRESHOLD + : letterSpacing > LETTER_SPACING_MAX_THRESHOLD + ? LETTER_SPACING_MAX_THRESHOLD + : letterSpacing, + 'text-anchor': 'middle', + width: '100%', + x: showIcon ? `${iconSize / 2 + getIconPadding(textSize / 2)}` : 0, + y: topMargin / 2 + getTextHeightForNumbers(textSize) / 2, + fill: fillColor, + 'data-test': 'visualization-primary-value', + }) + .add(group) + // show icon if configured in maintenance app if (showIcon) { - // embed icon to allow changing color - // (elements with fill need to use "currentColor" for this to work) - const iconSvgNode = document.createElementNS(svgNS, 'svg') - iconSvgNode.setAttribute('viewBox', '0 0 48 48') - iconSvgNode.setAttribute('width', iconSize) - iconSvgNode.setAttribute('height', iconSize) - iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1) - iconSvgNode.setAttribute( - 'x', - `-${(iconSize + getIconPadding(textSize) + textWidth) / 2}` - ) - iconSvgNode.setAttribute('style', `color: ${fillColor}`) - iconSvgNode.setAttribute('data-test', 'visualization-icon') - - const parser = new DOMParser() const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml') + const iconElHeight = + svgIconDocument.documentElement.getAttribute('height') + const iconElWidth = + svgIconDocument.documentElement.getAttribute('width') + const x = ((iconSize + getIconPadding(textSize) + textWidth) / 2) * -1 + const y = (iconSize / 2 - topMargin / 2) * -1 + const iconGroup = renderer + .g('icon') + .attr('data-test', 'visualization-icon') + .css({ + color: 'green', + // color: fillColor, + }) + /* Force the group element to have the same dimensions as the original + * SVG image by adding this rect. This ensures the icon has the intended + * whitespace around it and makes scaling and translating easier. */ + renderer.rect(0, 0, iconElWidth, iconElHeight).add(iconGroup) Array.from(svgIconDocument.documentElement.children).forEach((node) => - iconSvgNode.appendChild(node) + iconGroup.element.appendChild(node) ) - - svgValue.appendChild(iconSvgNode) + iconGroup.add() + const formattedValueBox = formattedValueText.getBBox() + const targetHeight = textSize / 2 + const scaleFactor = targetHeight / iconElHeight + + console.log(formattedValueBox) + iconGroup.css({ + transform: `scale(${scaleFactor}) translate(16px, 104px)`, + }) } - const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) - - const textNode = document.createElementNS(svgNS, 'text') - textNode.setAttribute('font-size', textSize) - textNode.setAttribute('font-weight', '300') - textNode.setAttribute( - 'letter-spacing', - letterSpacing < LETTER_SPACING_MIN_THRESHOLD - ? LETTER_SPACING_MIN_THRESHOLD - : letterSpacing > LETTER_SPACING_MAX_THRESHOLD - ? LETTER_SPACING_MAX_THRESHOLD - : letterSpacing - ) - textNode.setAttribute('text-anchor', 'middle') - textNode.setAttribute( - 'x', - showIcon ? `${(iconSize + getIconPadding(textSize)) / 2}` : 0 - ) - textNode.setAttribute( - 'y', - topMargin / 2 + getTextHeightForNumbers(textSize) / 2 - ) - textNode.setAttribute('fill', fillColor) - textNode.setAttribute('data-test', 'visualization-primary-value') - - textNode.appendChild(document.createTextNode(formattedValue)) - - svgValue.appendChild(textNode) - if (subText) { - const subTextNode = document.createElementNS(svgNS, 'text') - subTextNode.setAttribute('text-anchor', 'middle') - subTextNode.setAttribute('font-size', subTextSize) - subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2) - subTextNode.setAttribute('dy', subTextSize * 1.7) - subTextNode.setAttribute('fill', textColor) - subTextNode.appendChild(document.createTextNode(subText)) - - svgValue.appendChild(subTextNode) + renderer + .text(subText) + .attr({ + 'text-anchor': 'middle', + 'font-size': subTextSize, + y: iconSize / 2 + topMargin / 2, + dy: subTextSize * 1.7, + fill: textColor, + }) + .add(group) } - - return svgValue } diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js new file mode 100644 index 000000000..1a36f7eda --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js @@ -0,0 +1,135 @@ +import { colors } from '@dhis2/ui' +import { + svgNS, + LETTER_SPACING_MAX_THRESHOLD, + LETTER_SPACING_MIN_THRESHOLD, + LETTER_SPACING_TEXT_SIZE_FACTOR, + SUB_TEXT_SIZE_FACTOR, + SUB_TEXT_SIZE_MAX_THRESHOLD, + SUB_TEXT_SIZE_MIN_THRESHOLD, +} from './constants.js' +import { + getIconPadding, + getTextHeightForNumbers, + getTextSize, + getTextWidth, +} from './textSize.js' + +export const generateValueSVG = ({ + renderer, + formattedValue, + subText, + valueColor, + textColor, + icon, + noData, + containerWidth, + containerHeight, + topMargin = 0, +}) => { + const showIcon = icon && formattedValue !== noData.text + + const textSize = getTextSize( + formattedValue, + containerWidth, + containerHeight, + showIcon + ) + + const textWidth = getTextWidth(formattedValue, `${textSize}px Roboto`) + + const iconSize = textSize + + const subTextSize = + textSize * SUB_TEXT_SIZE_FACTOR > SUB_TEXT_SIZE_MAX_THRESHOLD + ? SUB_TEXT_SIZE_MAX_THRESHOLD + : textSize * SUB_TEXT_SIZE_FACTOR < SUB_TEXT_SIZE_MIN_THRESHOLD + ? SUB_TEXT_SIZE_MIN_THRESHOLD + : textSize * SUB_TEXT_SIZE_FACTOR + + const svgValue = document.createElementNS(svgNS, 'svg') + svgValue.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`) + svgValue.setAttribute('width', '50%') + svgValue.setAttribute('height', '50%') + svgValue.setAttribute('x', '50%') + svgValue.setAttribute('y', '50%') + svgValue.setAttribute('style', 'overflow: visible') + + let fillColor = colors.grey900 + + if (valueColor) { + fillColor = valueColor + } else if (formattedValue === noData.text) { + fillColor = colors.grey600 + } + + // show icon if configured in maintenance app + if (showIcon) { + // embed icon to allow changing color + // (elements with fill need to use "currentColor" for this to work) + const iconSvgNode = document.createElementNS(svgNS, 'svg') + console.log('old', iconSize) + iconSvgNode.setAttribute('viewBox', '0 0 48 48') + iconSvgNode.setAttribute('width', iconSize) + iconSvgNode.setAttribute('height', iconSize) + iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1) + iconSvgNode.setAttribute( + 'x', + `-${(iconSize + getIconPadding(textSize) + textWidth) / 2}` + ) + iconSvgNode.setAttribute('style', `color: ${fillColor}`) + iconSvgNode.setAttribute('data-test', 'visualization-icon') + + const parser = new DOMParser() + const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml') + + Array.from(svgIconDocument.documentElement.children).forEach((node) => + iconSvgNode.appendChild(node) + ) + + svgValue.appendChild(iconSvgNode) + } + + const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) + + const textNode = document.createElementNS(svgNS, 'text') + textNode.setAttribute('font-size', textSize) + textNode.setAttribute('font-weight', '300') + textNode.setAttribute( + 'letter-spacing', + letterSpacing < LETTER_SPACING_MIN_THRESHOLD + ? LETTER_SPACING_MIN_THRESHOLD + : letterSpacing > LETTER_SPACING_MAX_THRESHOLD + ? LETTER_SPACING_MAX_THRESHOLD + : letterSpacing + ) + textNode.setAttribute('text-anchor', 'middle') + textNode.setAttribute( + 'x', + showIcon ? `${(iconSize + getIconPadding(textSize)) / 2}` : 0 + ) + textNode.setAttribute( + 'y', + topMargin / 2 + getTextHeightForNumbers(textSize) / 2 + ) + textNode.setAttribute('fill', fillColor) + textNode.setAttribute('data-test', 'visualization-primary-value') + + textNode.appendChild(document.createTextNode(formattedValue)) + + svgValue.appendChild(textNode) + + if (subText) { + const subTextNode = document.createElementNS(svgNS, 'text') + subTextNode.setAttribute('text-anchor', 'middle') + subTextNode.setAttribute('font-size', subTextSize) + subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2) + subTextNode.setAttribute('dy', subTextSize * 1.7) + subTextNode.setAttribute('fill', textColor) + subTextNode.appendChild(document.createTextNode(subText)) + + svgValue.appendChild(subTextNode) + } + + renderer.box.appendChild(svgValue) +} From ea4e16cff4771417262383b0f722d268e6aae7af Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 4 Sep 2024 16:32:43 +0200 Subject: [PATCH 04/82] fix: rework old implementation WIP --- src/__demo__/SingleValue.stories.js | 19 +++++++++----- .../renderSingleValueSvg/constants.js | 2 +- .../renderSingleValueSvg/generateValueSVG.js | 26 +++++++++---------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index 0b3bda6fa..f28b775df 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -2,8 +2,8 @@ import { storiesOf } from '@storybook/react' import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react' import { createVisualization } from '../index.js' const constainerStyleBase = { - width: 400, - height: 400, + width: 800, + height: 800, border: '1px solid magenta', marginBottom: 14, } @@ -406,11 +406,16 @@ const data = [ dimensionItemType: 'INDICATOR', valueType: 'NUMBER', totalAggregationType: 'AVERAGE', + // indicatorType: { + // name: 'Per cent', + // displayName: 'Per cent', + // factor: 100, + // number: false, + // }, indicatorType: { - name: 'Per cent', - displayName: 'Per cent', - factor: 100, - number: false, + name: 'Custom', + displayName: 'Custom subtext', + number: true, }, }, }, @@ -720,7 +725,7 @@ storiesOf('SingleValue', module).add('default', () => { ...containerStyle, ...{ opacity: 0.45, - transform: 'translateX(410px)', + transform: `translateX(${width + 10}px)`, zIndex: 10, backgroundColor: 'purple', }, diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js index ce7cea956..06fd79fd0 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js @@ -16,7 +16,7 @@ export const TEXT_WIDTH_CONTAINER_WIDTH_FACTOR = 1.3 // do not allow text size to exceed this export const TEXT_SIZE_CONTAINER_HEIGHT_FACTOR = 0.6 -export const TEXT_SIZE_MAX_THRESHOLD = 400 +export const TEXT_SIZE_MAX_THRESHOLD = 200 // multiply text size with this factor // to get an appropriate letter spacing diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js index dd7efd382..5f365f0b5 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js @@ -33,7 +33,7 @@ export const generateValueSVG = ({ const group = renderer .g('value') .css({ - transform: 'scale(0.5) translate(100%, 100%)', + transform: 'translate(50%, 50%)', }) .add() @@ -92,14 +92,11 @@ export const generateValueSVG = ({ svgIconDocument.documentElement.getAttribute('height') const iconElWidth = svgIconDocument.documentElement.getAttribute('width') - const x = ((iconSize + getIconPadding(textSize) + textWidth) / 2) * -1 - const y = (iconSize / 2 - topMargin / 2) * -1 const iconGroup = renderer .g('icon') .attr('data-test', 'visualization-icon') .css({ - color: 'green', - // color: fillColor, + color: fillColor, }) /* Force the group element to have the same dimensions as the original * SVG image by adding this rect. This ensures the icon has the intended @@ -109,15 +106,18 @@ export const generateValueSVG = ({ Array.from(svgIconDocument.documentElement.children).forEach((node) => iconGroup.element.appendChild(node) ) - iconGroup.add() - const formattedValueBox = formattedValueText.getBBox() - const targetHeight = textSize / 2 - const scaleFactor = targetHeight / iconElHeight + const formattedValueTextBox = formattedValueText.getBBox() + const scaleFactor = textSize / iconElHeight + const textHeight = formattedValueTextBox.height / 2 + const iconHeight = (iconElHeight * scaleFactor) / 2 + const translateY = + (formattedValueTextBox.y + (textHeight - iconHeight)) / scaleFactor - console.log(formattedValueBox) - iconGroup.css({ - transform: `scale(${scaleFactor}) translate(16px, 104px)`, - }) + iconGroup + .css({ + transform: `scale(${scaleFactor}) translate(-98px, ${translateY}px)`, + }) + .add(group) } if (subText) { From 30a1168e4c111f6d259db617c6d3ff97b4c1dcf1 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 10 Sep 2024 17:37:32 +0200 Subject: [PATCH 05/82] chore: align single value store with other highcharts charts --- .../store/adapters/dhis_highcharts/index.js | 19 ++++++++++++++++++- .../adapters/dhis_highcharts/singleValue.js | 9 +++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 src/visualizations/store/adapters/dhis_highcharts/singleValue.js diff --git a/src/visualizations/store/adapters/dhis_highcharts/index.js b/src/visualizations/store/adapters/dhis_highcharts/index.js index 026a430c3..fa8b85272 100644 --- a/src/visualizations/store/adapters/dhis_highcharts/index.js +++ b/src/visualizations/store/adapters/dhis_highcharts/index.js @@ -6,9 +6,11 @@ import { VIS_TYPE_PIE, VIS_TYPE_GAUGE, isTwoCategoryChartType, + VIS_TYPE_SINGLE_VALUE, } from '../../../../modules/visTypes.js' import getGauge from './gauge.js' import getPie from './pie.js' +import getSingleValue from './singleValue.js' import getTwoCategory from './twoCategory.js' import getYearOnYear from './yearOnYear.js' @@ -93,6 +95,8 @@ function getSeriesFunction(type, categoryIds) { } switch (type) { + case VIS_TYPE_SINGLE_VALUE: + return getSingleValue case VIS_TYPE_PIE: return getPie case VIS_TYPE_GAUGE: @@ -108,9 +112,20 @@ function getSeriesFunction(type, categoryIds) { export default function ({ type, data, seriesId, categoryIds, extraOptions }) { categoryIds = categoryIds || [] + // if (type === VIS_TYPE_SINGLE_VALUE) { + // console.log('I want to do things here', data) + // const categoryId = data[0].metaData.dimensions.dx[0] + // return getSingleValue({ + // type, + // data, + // seriesId, + // categoryId, + // }) + // } + const seriesFunction = getSeriesFunction(type, categoryIds) - return data.reduce((acc, res) => { + const returnValue = data.reduce((acc, res) => { const headers = res.headers const metaData = res.metaData const rows = res.rows @@ -142,4 +157,6 @@ export default function ({ type, data, seriesId, categoryIds, extraOptions }) { return acc }, []) + console.log('IS THIS IT THEN?????', returnValue) + return returnValue } diff --git a/src/visualizations/store/adapters/dhis_highcharts/singleValue.js b/src/visualizations/store/adapters/dhis_highcharts/singleValue.js new file mode 100644 index 000000000..7eda97eb0 --- /dev/null +++ b/src/visualizations/store/adapters/dhis_highcharts/singleValue.js @@ -0,0 +1,9 @@ +export default function getSingleValue( + acc, + seriesIds, + categoryIds, + idValueMap +) { + const seriesId = seriesIds[0][0] + acc.push(idValueMap.get(seriesId)) +} From 61f3cf39175456a6f45ed7117570a9e9847e8c94 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 10 Sep 2024 17:40:01 +0200 Subject: [PATCH 06/82] chore: use noAxis for single value type --- .../config/adapters/dhis_highcharts/xAxis/index.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/visualizations/config/adapters/dhis_highcharts/xAxis/index.js b/src/visualizations/config/adapters/dhis_highcharts/xAxis/index.js index c3af4b20b..1439fc201 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/xAxis/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/xAxis/index.js @@ -16,6 +16,7 @@ import { VIS_TYPE_RADAR, VIS_TYPE_SCATTER, isTwoCategoryChartType, + VIS_TYPE_SINGLE_VALUE, } from '../../../../../modules/visTypes.js' import { getAxis } from '../../../../util/axes.js' import getAxisTitle from '../getAxisTitle.js' @@ -82,6 +83,7 @@ export default function (store, layout, extraOptions, series) { switch (layout.type) { case VIS_TYPE_PIE: case VIS_TYPE_GAUGE: + case VIS_TYPE_SINGLE_VALUE: xAxis = noAxis() break case VIS_TYPE_YEAR_OVER_YEAR_LINE: From b110076fcec5a8a958deda7f677645f958ae9133 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 10 Sep 2024 17:42:18 +0200 Subject: [PATCH 07/82] chore: clean up comments and console log --- .../store/adapters/dhis_highcharts/index.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/visualizations/store/adapters/dhis_highcharts/index.js b/src/visualizations/store/adapters/dhis_highcharts/index.js index fa8b85272..22f70cc1d 100644 --- a/src/visualizations/store/adapters/dhis_highcharts/index.js +++ b/src/visualizations/store/adapters/dhis_highcharts/index.js @@ -112,20 +112,9 @@ function getSeriesFunction(type, categoryIds) { export default function ({ type, data, seriesId, categoryIds, extraOptions }) { categoryIds = categoryIds || [] - // if (type === VIS_TYPE_SINGLE_VALUE) { - // console.log('I want to do things here', data) - // const categoryId = data[0].metaData.dimensions.dx[0] - // return getSingleValue({ - // type, - // data, - // seriesId, - // categoryId, - // }) - // } - const seriesFunction = getSeriesFunction(type, categoryIds) - const returnValue = data.reduce((acc, res) => { + return data.reduce((acc, res) => { const headers = res.headers const metaData = res.metaData const rows = res.rows @@ -157,6 +146,4 @@ export default function ({ type, data, seriesId, categoryIds, extraOptions }) { return acc }, []) - console.log('IS THIS IT THEN?????', returnValue) - return returnValue } From 8a1dadc167434948c4cdb4db55c91b0049379de1 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 11 Sep 2024 10:45:37 +0200 Subject: [PATCH 08/82] chore: switch to highcharts adapter in story --- src/__demo__/SingleValue.stories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index f28b775df..f1dc22d56 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -657,7 +657,7 @@ storiesOf('SingleValue', module).add('default', () => { extraOptions, undefined, undefined, - 'singleValue' + 'highcharts' ) newChartRef.current = newVisualization.visualization }) From 9ccc404a68ef2c078ff77880c7e69aca01426bed Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 11 Sep 2024 10:47:54 +0200 Subject: [PATCH 09/82] feat: add generic mechanism for rendering custom SVGs on chart load event --- .../config/adapters/dhis_highcharts/chart.js | 2 ++ .../adapters/dhis_highcharts/custom/index.js | 29 +++++++++++++++++++ .../config/adapters/dhis_highcharts/index.js | 10 +++++++ 3 files changed, 41 insertions(+) create mode 100644 src/visualizations/config/adapters/dhis_highcharts/custom/index.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/chart.js b/src/visualizations/config/adapters/dhis_highcharts/chart.js index e50a52ca9..ddffaf140 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/chart.js +++ b/src/visualizations/config/adapters/dhis_highcharts/chart.js @@ -1,3 +1,4 @@ +import { renderCustomSVG } from './custom/index.js' import getType from './type.js' const DEFAULT_CHART = { @@ -31,6 +32,7 @@ const getEvents = () => ({ }) } }) + renderCustomSVG.call(this) }, }, }) diff --git a/src/visualizations/config/adapters/dhis_highcharts/custom/index.js b/src/visualizations/config/adapters/dhis_highcharts/custom/index.js new file mode 100644 index 000000000..a89d8e42b --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/custom/index.js @@ -0,0 +1,29 @@ +import { VIS_TYPE_SINGLE_VALUE } from '../../../../../modules/visTypes.js' + +export function renderCustomSVG() { + const renderer = this.renderer + const options = this.userOptions.customSVGOptions + + switch (options.visualizationType) { + case VIS_TYPE_SINGLE_VALUE: + console.log('now render SV viz', renderer, options) + break + default: + break + } +} + +export function getCustomSVGOptions({ layout }) { + const baseOptions = { + visualizationType: layout.type, + } + switch (layout.type) { + case VIS_TYPE_SINGLE_VALUE: + return { + ...baseOptions, + test: 1, + } + default: + return null + } +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/index.js b/src/visualizations/config/adapters/dhis_highcharts/index.js index 29ecf41c0..4538cfd22 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/index.js @@ -15,6 +15,7 @@ import { import { defaultMultiAxisTheme1 } from '../../../util/colors/themes.js' import addTrendLines, { isRegressionIneligible } from './addTrendLines.js' import getChart from './chart.js' +import { getCustomSVGOptions } from './custom/index.js' import getScatterData from './getScatterData.js' import getSortedConfig from './getSortedConfig.js' import getTrimmedConfig from './getTrimmedConfig.js' @@ -231,8 +232,17 @@ export default function ({ store, layout, el, extraConfig, extraOptions }) { ) } + /* The config object passed to the Highcharts Chart constructor + * can contain arbitrary properties, which are made accessible + * under the Chart instance's `userOptions` member. This means + * that in event callback functions the custom SVG options are + * accessible as `this.userOptions.customSVGOptions` */ + config.customSVGOptions = getCustomSVGOptions({ layout }) + // force apply extra config Object.assign(config, extraConfig) + console.log('CONFIG', objectClean(config)) + return objectClean(config) } From 5d2ee6e410da1029cdc27c7ba4a19a02ef393f3b Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 11 Sep 2024 10:48:31 +0200 Subject: [PATCH 10/82] chore: produce empty series for single value --- .../config/adapters/dhis_highcharts/series/index.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/visualizations/config/adapters/dhis_highcharts/series/index.js b/src/visualizations/config/adapters/dhis_highcharts/series/index.js index e4d4eae67..1622e4313 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/series/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/series/index.js @@ -9,6 +9,7 @@ import { isYearOverYear, VIS_TYPE_LINE, VIS_TYPE_SCATTER, + VIS_TYPE_SINGLE_VALUE, } from '../../../../../modules/visTypes.js' import { getAxisStringFromId } from '../../../../util/axisId.js' import { @@ -225,6 +226,9 @@ export default function ({ displayStrategy, }) { switch (layout.type) { + case VIS_TYPE_SINGLE_VALUE: + series = null + break case VIS_TYPE_PIE: series = getPie( series, @@ -249,7 +253,7 @@ export default function ({ }) } - series.forEach((seriesObj) => { + series?.forEach((seriesObj) => { // animation seriesObj.animation = { duration: getAnimation( From aca4aadf9414cca6cabc5d6d55f8663fb06ab5da Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 11 Sep 2024 16:57:49 +0200 Subject: [PATCH 11/82] chore: return null instead of an empty object so it is cleaned later --- .../config/adapters/dhis_highcharts/plotOptions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/visualizations/config/adapters/dhis_highcharts/plotOptions.js b/src/visualizations/config/adapters/dhis_highcharts/plotOptions.js index 928019506..e9e775096 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/plotOptions.js +++ b/src/visualizations/config/adapters/dhis_highcharts/plotOptions.js @@ -79,6 +79,6 @@ export default ({ } : {} default: - return {} + return null } } From 568a0f11a8e264835dc59cec8c906071c69310bb Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 11 Sep 2024 16:59:22 +0200 Subject: [PATCH 12/82] fix: do not return a chart type for single value because it does not correspond to a highcharts chart type --- src/visualizations/config/adapters/dhis_highcharts/type.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/visualizations/config/adapters/dhis_highcharts/type.js b/src/visualizations/config/adapters/dhis_highcharts/type.js index bc56c6d98..08cb62a49 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/type.js +++ b/src/visualizations/config/adapters/dhis_highcharts/type.js @@ -12,6 +12,7 @@ import { VIS_TYPE_STACKED_COLUMN, VIS_TYPE_YEAR_OVER_YEAR_COLUMN, VIS_TYPE_SCATTER, + VIS_TYPE_SINGLE_VALUE, } from '../../../../modules/visTypes.js' export default function (type) { @@ -33,6 +34,8 @@ export default function (type) { return { type: 'solidgauge' } case VIS_TYPE_SCATTER: return { type: 'scatter', zoomType: 'xy' } + case VIS_TYPE_SINGLE_VALUE: + return {} case VIS_TYPE_COLUMN: case VIS_TYPE_STACKED_COLUMN: case VIS_TYPE_YEAR_OVER_YEAR_COLUMN: From 772640bd41b98755c52f5ae7e82278ba769cf5fa Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 11 Sep 2024 17:01:32 +0200 Subject: [PATCH 13/82] fix: implement subtitle and title logic for single value chart --- .../config/adapters/dhis_highcharts/chart.js | 5 +- .../subtitle/__tests__/singleValue.spec.js | 64 +++++++++++++++++++ .../dhis_highcharts/subtitle/index.js | 5 ++ .../dhis_highcharts/subtitle/singleValue.js | 17 +++++ .../title/__tests__/singleValue.spec.js | 56 ++++++++++++++++ .../adapters/dhis_highcharts/title/index.js | 5 ++ .../dhis_highcharts/title/singleValue.js | 22 +++++++ 7 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/visualizations/config/adapters/dhis_highcharts/subtitle/__tests__/singleValue.spec.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/chart.js b/src/visualizations/config/adapters/dhis_highcharts/chart.js index ddffaf140..df229c4f3 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/chart.js +++ b/src/visualizations/config/adapters/dhis_highcharts/chart.js @@ -44,6 +44,9 @@ export default function (layout, el, dashboard) { { renderTo: el || layout.el }, DEFAULT_CHART, dashboard ? DASHBOARD_CHART : undefined, - getEvents() + getEvents(), + { + backgroundColor: 'red', + } ) } diff --git a/src/visualizations/config/adapters/dhis_highcharts/subtitle/__tests__/singleValue.spec.js b/src/visualizations/config/adapters/dhis_highcharts/subtitle/__tests__/singleValue.spec.js new file mode 100644 index 000000000..c7baa2ad6 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/subtitle/__tests__/singleValue.spec.js @@ -0,0 +1,64 @@ +import getSingleValueSubtitle from '../singleValue.js' + +jest.mock( + '../../../../../util/getFilterText', + () => () => 'The default filter text' +) + +describe('getSingleValueSubtitle', () => { + it('returns empty subtitle when flag hideSubtitle exists', () => { + expect(getSingleValueSubtitle({ hideSubtitle: true })).toEqual('') + }) + + it('returns the subtitle provided in the layout', () => { + const subtitle = 'The subtitle was already set' + expect(getSingleValueSubtitle({ subtitle })).toEqual(subtitle) + }) + + it('returns an empty string when layout does not have filters', () => { + expect(getSingleValueSubtitle({})).toEqual('') + }) + + it('returns the filter text', () => { + expect(getSingleValueSubtitle({ filters: [] })).toEqual( + 'The default filter text' + ) + }) + + describe('not dashboard', () => { + describe('layout does not include title', () => { + it('returns empty subtitle', () => { + expect( + getSingleValueSubtitle({ filters: undefined }, {}, false) + ).toEqual('') + }) + }) + + /* All these tests have been moved and adjusted from here: + * src/visualizations/config/adapters/dhis_dhis/title/__tests__` + * The test below asserted the default subtitle behaviour, for + * visualization types other than SingleValue. It expected that + * the title was being used as subtitle. It fails now, and I + * believe that this behaviour does not make sense. So instead + * of fixing it, I disabled it. */ + // describe('layout includes title', () => { + // it('returns filter title as subtitle', () => { + // expect( + // getSingleValueSubtitle( + // { filters: undefined, title: 'Chart title' }, + // {}, + // false + // ) + // ).toEqual('The default filter text') + // }) + // }) + }) + + describe('dashboard', () => { + it('returns filter title as subtitle', () => { + expect(getSingleValueSubtitle({ filters: {} }, {}, true)).toEqual( + 'The default filter text' + ) + }) + }) +}) diff --git a/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js b/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js index 9d2dc1bc7..0e7515a94 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js @@ -13,10 +13,12 @@ import { VIS_TYPE_YEAR_OVER_YEAR_COLUMN, isVerticalType, VIS_TYPE_SCATTER, + VIS_TYPE_SINGLE_VALUE, } from '../../../../../modules/visTypes.js' import getFilterText from '../../../../util/getFilterText.js' import { getTextAlignOption } from '../getTextAlignOption.js' import getYearOverYearTitle from '../title/yearOverYear.js' +import getSingleValueSubtitle from './singleValue.js' const DASHBOARD_SUBTITLE = { style: { @@ -59,6 +61,9 @@ export default function (series, layout, metaData, dashboard) { const filterTitle = getFilterText(layout.filters, metaData) switch (layout.type) { + case VIS_TYPE_SINGLE_VALUE: + subtitle.text = getSingleValueSubtitle(layout, metaData) + break case VIS_TYPE_YEAR_OVER_YEAR_LINE: case VIS_TYPE_YEAR_OVER_YEAR_COLUMN: subtitle.text = getYearOverYearTitle( diff --git a/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js new file mode 100644 index 000000000..15b2b6fa8 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js @@ -0,0 +1,17 @@ +import getFilterText from '../../../../util/getFilterText.js' + +export default function getSingleValueSubtitle(layout, metaData) { + if (layout.hideSubtitle) { + return '' + } + + if (typeof layout.subtitle === 'string' && layout.subtitle.length) { + return layout.subtitle + } + + if (layout.filters) { + return getFilterText(layout.filters, metaData) + } + + return '' +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js b/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js new file mode 100644 index 000000000..4f5843c5d --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js @@ -0,0 +1,56 @@ +import { VIS_TYPE_SINGLE_VALUE } from '../../../../../../modules/visTypes.js' +import getSingleValueTitle from '../singleValue.js' + +jest.mock('../../../../../util/getFilterText', () => () => 'The filter text') + +describe('getSingleValueTitle', () => { + it('returns empty title when flag hideTitle exists', () => { + expect(getSingleValueTitle({ hideTitle: true })).toEqual('') + }) + + it('returns the title provided in the layout', () => { + const title = 'The title was already set' + expect(getSingleValueTitle({ title })).toEqual(title) + }) + + it('returns null when layout does not have columns', () => { + expect(getSingleValueTitle({})).toEqual('') + }) + + it('returns the filter text based on column items', () => { + expect( + getSingleValueTitle({ + columns: [ + { + items: [{}], + }, + ], + }) + ).toEqual('The filter text') + }) + + describe('not dashboard', () => { + it('returns filter text as title', () => { + expect( + getSingleValueTitle( + { + columns: [ + { + items: [{}], + }, + ], + filters: [], + }, + {}, + false + ) + ).toEqual('The filter text') + }) + }) + + describe('dashboard', () => { + it('returns empty string', () => { + expect(getSingleValueTitle({ filters: {} }, {}, true)).toEqual('') + }) + }) +}) diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/index.js b/src/visualizations/config/adapters/dhis_highcharts/title/index.js index e4e4f1a4a..40029e42f 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/title/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/title/index.js @@ -14,10 +14,12 @@ import { VIS_TYPE_GAUGE, isVerticalType, VIS_TYPE_SCATTER, + VIS_TYPE_SINGLE_VALUE, } from '../../../../../modules/visTypes.js' import getFilterText from '../../../../util/getFilterText.js' import { getTextAlignOption } from '../getTextAlignOption.js' import getScatterTitle from './scatter.js' +import getSingleValueTitle from './singleValue.js' import getYearOverYearTitle from './yearOverYear.js' const DASHBOARD_TITLE_STYLE = { @@ -61,6 +63,9 @@ export default function (layout, metaData, dashboard) { title.text = customTitle } else { switch (layout.type) { + case VIS_TYPE_SINGLE_VALUE: + title.text = getSingleValueTitle(layout, metaData, dashboard) + break case VIS_TYPE_GAUGE: case VIS_TYPE_YEAR_OVER_YEAR_LINE: case VIS_TYPE_YEAR_OVER_YEAR_COLUMN: diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js b/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js new file mode 100644 index 000000000..0d779eed0 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js @@ -0,0 +1,22 @@ +import getFilterText from '../../../../util/getFilterText.js' + +export default function (layout, metaData, dashboard) { + if (layout.hideTitle) { + return '' + } + + if (typeof layout.title === 'string' && layout.title.length) { + return layout.title + } + + if (layout.columns) { + const firstItem = layout.columns[0].items[0] + + const column = Object.assign({}, layout.columns[0], { + items: [firstItem], + }) + + return getFilterText([column], metaData) + } + return '' +} From b6ebe34f9c652db38238bcb83f67954c4a1e5c92 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 11 Sep 2024 17:02:28 +0200 Subject: [PATCH 14/82] fix: remove yAxis from single value highcharts config --- .../config/adapters/dhis_highcharts/yAxis/index.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/visualizations/config/adapters/dhis_highcharts/yAxis/index.js b/src/visualizations/config/adapters/dhis_highcharts/yAxis/index.js index 1e9aab2a9..d253acdff 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/yAxis/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/yAxis/index.js @@ -11,6 +11,7 @@ import { isStacked, VIS_TYPE_GAUGE, VIS_TYPE_SCATTER, + VIS_TYPE_SINGLE_VALUE, } from '../../../../../modules/visTypes.js' import { getAxis } from '../../../../util/axes.js' import { getAxisStringFromId } from '../../../../util/axisId.js' @@ -148,14 +149,12 @@ function getDefault(layout, series, extraOptions) { } export default function (layout, series, extraOptions) { - let yAxis switch (layout.type) { + case VIS_TYPE_SINGLE_VALUE: + return null case VIS_TYPE_GAUGE: - yAxis = getGauge(layout, series, extraOptions.legendSets[0]) - break + return getGauge(layout, series, extraOptions.legendSets[0]) default: - yAxis = getDefault(layout, series, extraOptions) + return getDefault(layout, series, extraOptions) } - - return yAxis } From d9fb7b854ce5136e7afc93782e02f9e971e4a51a Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 12 Sep 2024 16:36:18 +0200 Subject: [PATCH 15/82] refactor: implement single value config via Highcharts adapter --- .../config/adapters/dhis_highcharts/chart.js | 20 ++-- .../{custom => customSVGOptions}/index.js | 21 +++- .../getSingleValueBackgroundColor.js | 17 +++ .../getSingleValueCustomSVGOptions.js | 47 ++++++++ .../getSingleValueFormattedValue.js | 23 ++++ .../singleValue/getSingleValueLegendColor.js | 8 ++ .../singleValue/getSingleValueTextColor.js | 27 +++++ .../customSVGOptions/singleValue/index.js | 3 + .../singleValue}/shouldUseContrastColor.js | 0 .../config/adapters/dhis_highcharts/index.js | 34 +++--- .../config/adapters/dhis_highcharts/noData.js | 7 +- .../dhis_highcharts/subtitle/index.js | 94 +++++++++------- .../dhis_highcharts/subtitle/singleValue.js | 1 + .../adapters/dhis_highcharts/title/index.js | 101 +++++++++++------- .../dhis_highcharts/title/singleValue.js | 3 +- .../config/generators/dhis/singleValue.js | 1 + .../config/generators/highcharts/index.js | 2 +- .../highcharts/renderSingleValueSvg/index.js | 2 +- 18 files changed, 301 insertions(+), 110 deletions(-) rename src/visualizations/config/adapters/dhis_highcharts/{custom => customSVGOptions}/index.js (54%) create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js rename src/visualizations/config/{generators/highcharts/renderSingleValueSvg => adapters/dhis_highcharts/customSVGOptions/singleValue}/shouldUseContrastColor.js (100%) diff --git a/src/visualizations/config/adapters/dhis_highcharts/chart.js b/src/visualizations/config/adapters/dhis_highcharts/chart.js index df229c4f3..be264f100 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/chart.js +++ b/src/visualizations/config/adapters/dhis_highcharts/chart.js @@ -1,4 +1,6 @@ -import { renderCustomSVG } from './custom/index.js' +import { VIS_TYPE_SINGLE_VALUE } from '../../../../modules/visTypes.js' +import { renderCustomSVG } from './customSVGOptions/index.js' +import { getSingleValueBackgroundColor } from './customSVGOptions/singleValue/index.js' import getType from './type.js' const DEFAULT_CHART = { @@ -37,16 +39,22 @@ const getEvents = () => ({ }, }) -export default function (layout, el, dashboard) { +export default function (layout, el, extraOptions, series) { return Object.assign( {}, getType(layout.type), { renderTo: el || layout.el }, DEFAULT_CHART, - dashboard ? DASHBOARD_CHART : undefined, + extraOptions.dashboard ? DASHBOARD_CHART : undefined, getEvents(), - { - backgroundColor: 'red', - } + layout.type === VIS_TYPE_SINGLE_VALUE + ? { + backgroundColor: getSingleValueBackgroundColor( + extraOptions.legendOptions, + extraOptions.legendSets, + series[0] + ), + } + : undefined ) } diff --git a/src/visualizations/config/adapters/dhis_highcharts/custom/index.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js similarity index 54% rename from src/visualizations/config/adapters/dhis_highcharts/custom/index.js rename to src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js index a89d8e42b..2bfb781b9 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/custom/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js @@ -1,4 +1,5 @@ import { VIS_TYPE_SINGLE_VALUE } from '../../../../../modules/visTypes.js' +import { getSingleValueCustomSVGOptions } from './singleValue/index.js' export function renderCustomSVG() { const renderer = this.renderer @@ -6,14 +7,20 @@ export function renderCustomSVG() { switch (options.visualizationType) { case VIS_TYPE_SINGLE_VALUE: - console.log('now render SV viz', renderer, options) + console.log('now render SV viz', this) break default: break } } -export function getCustomSVGOptions({ layout }) { +export function getCustomSVGOptions({ + extraConfig, + layout, + extraOptions, + metaData, + series, +}) { const baseOptions = { visualizationType: layout.type, } @@ -21,9 +28,15 @@ export function getCustomSVGOptions({ layout }) { case VIS_TYPE_SINGLE_VALUE: return { ...baseOptions, - test: 1, + ...getSingleValueCustomSVGOptions({ + extraConfig, + layout, + extraOptions, + metaData, + series, + }), } default: - return null + break } } diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js new file mode 100644 index 000000000..650c895a5 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js @@ -0,0 +1,17 @@ +import { LEGEND_DISPLAY_STYLE_FILL } from '../../../../../../modules/legends.js' +import { getSingleValueLegendColor } from './getSingleValueLegendColor.js' + +export function getSingleValueBackgroundColor( + legendOptions, + legendSets, + value +) { + const legendColor = getSingleValueLegendColor( + legendOptions, + legendSets, + value + ) + return legendColor && legendOptions.style === LEGEND_DISPLAY_STYLE_FILL + ? legendColor + : 'transparent' +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js new file mode 100644 index 000000000..4e482a333 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js @@ -0,0 +1,47 @@ +import { getSingleValueFormattedValue } from './getSingleValueFormattedValue.js' + +export function getSingleValueCustomSVGOptions({ + extraConfig, + layout, + extraOptions, + metaData, + series, +}) { + const { dashboard, legendSets, fontStyle, noData, legendOptions, icon } = + extraOptions + const value = series[0] + + console.log( + '++++setSingleValueOptions++++', + '\nextraConfig: ', + extraConfig, + '\nlayout: ', + layout, + '\nmetaData: ', + metaData, + '\nseries: ', + series, + '\ndashboard: ', + dashboard, + '\nlegendSets: ', + legendSets, + '\nfontStyle: ', + fontStyle, + '\nnoData: ', + noData, + '\nlegendOptions: ', + legendOptions, + '\nicon: ', + icon, + '\n=============' + ) + const customSVGOptions = { + value, + formattedValue: getSingleValueFormattedValue(value, layout, metaData), + icon, + dashboard, + subText: 'Test', + } + console.log('singleValueCustomSvgOptions', customSVGOptions) + return customSVGOptions +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js new file mode 100644 index 000000000..f0b91dee3 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js @@ -0,0 +1,23 @@ +import { renderValue } from '../../../../../../modules/renderValue.js' +import { VALUE_TYPE_TEXT } from '../../../../../../modules/valueTypes.js' + +export const INDICATOR_FACTOR_100 = 100 + +export function getSingleValueFormattedValue(value, layout, metaData) { + const valueType = metaData.items[metaData.dimensions.dx[0]].valueType + const indicatorType = + metaData.items[metaData.dimensions.dx[0]].indicatorType + + let formattedValue = renderValue(value, valueType || VALUE_TYPE_TEXT, { + digitGroupSeparator: layout.digitGroupSeparator, + skipRounding: layout.skipRounding, + }) + + // only show the percentage symbol for per cent + // for other factors, show the full text under the value + if (indicatorType?.factor === INDICATOR_FACTOR_100) { + formattedValue += '%' + } + + return formattedValue +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js new file mode 100644 index 000000000..9f042fc4d --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js @@ -0,0 +1,8 @@ +import { getColorByValueFromLegendSet } from '../../../../../../modules/legends.js' + +export function getSingleValueLegendColor(legendOptions, legendSets, value) { + const legendSet = legendOptions && legendSets[0] + return legendSet + ? getColorByValueFromLegendSet(legendSet, value) + : undefined +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js new file mode 100644 index 000000000..109c71fb9 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js @@ -0,0 +1,27 @@ +import { colors } from '@dhis2/ui' +import { LEGEND_DISPLAY_STYLE_TEXT } from '../../../../../../modules/legends.js' +import { getSingleValueLegendColor } from './getSingleValueLegendColor.js' +import { shouldUseContrastColor } from './shouldUseContrastColor.js' + +export function getSingleValueTextColor( + baseColor, + value, + legendOptions, + legendSets +) { + const legendColor = getSingleValueLegendColor( + legendOptions, + legendSets, + value + ) + + if (!legendColor) { + return baseColor + } + + if (LEGEND_DISPLAY_STYLE_TEXT) { + return legendColor + } + + return shouldUseContrastColor(legendColor) ? colors.white : baseColor +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js new file mode 100644 index 000000000..892761190 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js @@ -0,0 +1,3 @@ +export { getSingleValueCustomSVGOptions } from './getSingleValueCustomSVGOptions.js' +export { getSingleValueBackgroundColor } from './getSingleValueBackgroundColor.js' +export { getSingleValueTextColor } from './getSingleValueTextColor.js' diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/shouldUseContrastColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js similarity index 100% rename from src/visualizations/config/generators/highcharts/renderSingleValueSvg/shouldUseContrastColor.js rename to src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/index.js b/src/visualizations/config/adapters/dhis_highcharts/index.js index 4538cfd22..f9e97469b 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/index.js @@ -15,7 +15,7 @@ import { import { defaultMultiAxisTheme1 } from '../../../util/colors/themes.js' import addTrendLines, { isRegressionIneligible } from './addTrendLines.js' import getChart from './chart.js' -import { getCustomSVGOptions } from './custom/index.js' +import { getCustomSVGOptions } from './customSVGOptions/index.js' import getScatterData from './getScatterData.js' import getSortedConfig from './getSortedConfig.js' import getTrimmedConfig from './getTrimmedConfig.js' @@ -78,21 +78,17 @@ export default function ({ store, layout, el, extraConfig, extraOptions }) { let config = { // type etc - chart: getChart(_layout, el, _extraOptions.dashboard), + chart: getChart(_layout, el, _extraOptions, series), // title - title: getTitle( - _layout, - store.data[0].metaData, - _extraOptions.dashboard - ), + title: getTitle(_layout, store.data[0].metaData, _extraOptions, series), // subtitle subtitle: getSubtitle( series, _layout, store.data[0].metaData, - _extraOptions.dashboard + _extraOptions ), // x-axis @@ -128,7 +124,7 @@ export default function ({ store, layout, el, extraConfig, extraOptions }) { noData: _extraOptions.noData.text, resetZoom: _extraOptions.resetZoom.text, }, - noData: getNoData(), + noData: getNoData(_layout.type), // credits credits: { @@ -140,6 +136,19 @@ export default function ({ store, layout, el, extraConfig, extraOptions }) { // disable exporting context menu enabled: false, }, + + /* The config object passed to the Highcharts Chart constructor + * can contain arbitrary properties, which are made accessible + * under the Chart instance's `userOptions` member. This means + * that in event callback functions the custom SVG options are + * accessible as `this.userOptions.customSVGOptions` */ + customSVGOptions: getCustomSVGOptions({ + extraConfig, + layout: _layout, + extraOptions: _extraOptions, + metaData: store.data[0].metaData, + series, + }), } // get plot options for scatter @@ -232,13 +241,6 @@ export default function ({ store, layout, el, extraConfig, extraOptions }) { ) } - /* The config object passed to the Highcharts Chart constructor - * can contain arbitrary properties, which are made accessible - * under the Chart instance's `userOptions` member. This means - * that in event callback functions the custom SVG options are - * accessible as `this.userOptions.customSVGOptions` */ - config.customSVGOptions = getCustomSVGOptions({ layout }) - // force apply extra config Object.assign(config, extraConfig) diff --git a/src/visualizations/config/adapters/dhis_highcharts/noData.js b/src/visualizations/config/adapters/dhis_highcharts/noData.js index 8597b5ccd..b2f40d7ff 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/noData.js +++ b/src/visualizations/config/adapters/dhis_highcharts/noData.js @@ -1,8 +1,13 @@ -export default function () { +import { VIS_TYPE_SINGLE_VALUE } from '../../../../modules/visTypes.js' + +export default function (visualizationType) { return { style: { fontSize: '13px', fontWeight: 'normal', + /* Hide no data label for single value visualizations because + * the data is always missing. */ + opacity: visualizationType === VIS_TYPE_SINGLE_VALUE ? 0 : 1, }, } } diff --git a/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js b/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js index 0e7515a94..a317f1a9b 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js @@ -18,7 +18,9 @@ import { import getFilterText from '../../../../util/getFilterText.js' import { getTextAlignOption } from '../getTextAlignOption.js' import getYearOverYearTitle from '../title/yearOverYear.js' -import getSingleValueSubtitle from './singleValue.js' +import getSingleValueSubtitle, { + getSingleValueSubtitleColor, +} from './singleValue.js' const DASHBOARD_SUBTITLE = { style: { @@ -33,23 +35,47 @@ const DASHBOARD_SUBTITLE = { } function getDefault(layout, dashboard, filterTitle) { - return { - text: dashboard || isString(layout.title) ? filterTitle : undefined, - } + return dashboard || isString(layout.title) ? filterTitle : undefined } -export default function (series, layout, metaData, dashboard) { +export default function (series, layout, metaData, extraOptions) { + if (layout.hideSubtitle) { + return null + } + + const { dashboard, legendSets, legendOptions } = extraOptions const fontStyle = mergeFontStyleWithDefault( layout.fontStyle && layout.fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], FONT_STYLE_VISUALIZATION_SUBTITLE ) - let subtitle = { - text: undefined, - } - - if (layout.hideSubtitle) { - return null - } + const subtitle = Object.assign( + { + text: undefined, + }, + dashboard + ? DASHBOARD_SUBTITLE + : { + align: getTextAlignOption( + fontStyle[FONT_STYLE_OPTION_TEXT_ALIGN], + FONT_STYLE_VISUALIZATION_SUBTITLE, + isVerticalType(layout.type) + ), + style: { + // DHIS2-578: dynamically truncate subtitle when it's taking more than 1 line + color: undefined, + fontSize: `${fontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px`, + fontWeight: fontStyle[FONT_STYLE_OPTION_BOLD] + ? FONT_STYLE_OPTION_BOLD + : 'normal', + fontStyle: fontStyle[FONT_STYLE_OPTION_ITALIC] + ? FONT_STYLE_OPTION_ITALIC + : 'normal', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + }, + } + ) // DHIS2-578: allow for optional custom subtitle const customSubtitle = @@ -76,37 +102,23 @@ export default function (series, layout, metaData, dashboard) { subtitle.text = filterTitle break default: - subtitle = getDefault(layout, dashboard, filterTitle) + subtitle.text = getDefault(layout, dashboard, filterTitle) } } + switch (layout.type) { + case VIS_TYPE_SINGLE_VALUE: + subtitle.style.color = getSingleValueSubtitleColor( + fontStyle[FONT_STYLE_OPTION_TEXT_COLOR], + series[0], + legendOptions, + legendSets + ) + break + default: + subtitle.style.color = fontStyle[FONT_STYLE_OPTION_TEXT_COLOR] + break + } + return subtitle - ? Object.assign( - {}, - dashboard - ? DASHBOARD_SUBTITLE - : { - align: getTextAlignOption( - fontStyle[FONT_STYLE_OPTION_TEXT_ALIGN], - FONT_STYLE_VISUALIZATION_SUBTITLE, - isVerticalType(layout.type) - ), - style: { - // DHIS2-578: dynamically truncate subtitle when it's taking more than 1 line - color: fontStyle[FONT_STYLE_OPTION_TEXT_COLOR], - fontSize: `${fontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px`, - fontWeight: fontStyle[FONT_STYLE_OPTION_BOLD] - ? FONT_STYLE_OPTION_BOLD - : 'normal', - fontStyle: fontStyle[FONT_STYLE_OPTION_ITALIC] - ? FONT_STYLE_OPTION_ITALIC - : 'normal', - whiteSpace: 'nowrap', - overflow: 'hidden', - textOverflow: 'ellipsis', - }, - }, - subtitle - ) - : subtitle } diff --git a/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js index 15b2b6fa8..4bf8b394a 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js +++ b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js @@ -1,4 +1,5 @@ import getFilterText from '../../../../util/getFilterText.js' +export { getSingleValueTextColor as getSingleValueSubtitleColor } from '../customSVGOptions/singleValue/index.js' export default function getSingleValueSubtitle(layout, metaData) { if (layout.hideSubtitle) { diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/index.js b/src/visualizations/config/adapters/dhis_highcharts/title/index.js index 40029e42f..75000cd93 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/title/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/title/index.js @@ -19,7 +19,10 @@ import { import getFilterText from '../../../../util/getFilterText.js' import { getTextAlignOption } from '../getTextAlignOption.js' import getScatterTitle from './scatter.js' -import getSingleValueTitle from './singleValue.js' +import { + getSingleValueTitleColor, + getSingleValueTitleText, +} from './singleValue.js' import getYearOverYearTitle from './yearOverYear.js' const DASHBOARD_TITLE_STYLE = { @@ -43,45 +46,22 @@ function getDefault(layout, metaData, dashboard) { return null } -export default function (layout, metaData, dashboard) { - const fontStyle = mergeFontStyleWithDefault( - layout.fontStyle && layout.fontStyle[FONT_STYLE_VISUALIZATION_TITLE], - FONT_STYLE_VISUALIZATION_TITLE - ) - - const title = { - text: undefined, - } - +export default function (layout, metaData, extraOptions, series) { if (layout.hideTitle) { - return title - } - - const customTitle = (layout.title && layout.displayTitle) || layout.title - - if (isString(customTitle) && customTitle.length) { - title.text = customTitle - } else { - switch (layout.type) { - case VIS_TYPE_SINGLE_VALUE: - title.text = getSingleValueTitle(layout, metaData, dashboard) - break - case VIS_TYPE_GAUGE: - case VIS_TYPE_YEAR_OVER_YEAR_LINE: - case VIS_TYPE_YEAR_OVER_YEAR_COLUMN: - title.text = getYearOverYearTitle(layout, metaData, dashboard) - break - case VIS_TYPE_SCATTER: - title.text = getScatterTitle(layout, metaData, dashboard) - break - default: - title.text = getDefault(layout, metaData, dashboard) - break + return { + text: undefined, } } - return Object.assign( - {}, + const { dashboard, legendSets, legendOptions } = extraOptions + const fontStyle = mergeFontStyleWithDefault( + layout.fontStyle && layout.fontStyle[FONT_STYLE_VISUALIZATION_TITLE], + FONT_STYLE_VISUALIZATION_TITLE + ) + const title = Object.assign( + { + text: undefined, + }, dashboard ? DASHBOARD_TITLE_STYLE : { @@ -92,7 +72,7 @@ export default function (layout, metaData, dashboard) { isVerticalType(layout.type) ), style: { - color: fontStyle[FONT_STYLE_OPTION_TEXT_COLOR], + color: undefined, fontSize: `${fontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px`, fontWeight: fontStyle[FONT_STYLE_OPTION_BOLD] ? FONT_STYLE_OPTION_BOLD @@ -104,7 +84,50 @@ export default function (layout, metaData, dashboard) { overflow: 'hidden', textOverflow: 'ellipsis', }, - }, - title + } ) + + const customTitleText = + (layout.title && layout.displayTitle) || layout.title + + if (isString(customTitleText) && customTitleText.length) { + title.text = customTitleText + } else { + switch (layout.type) { + case VIS_TYPE_SINGLE_VALUE: + title.text = getSingleValueTitleText( + layout, + metaData, + dashboard + ) + break + case VIS_TYPE_GAUGE: + case VIS_TYPE_YEAR_OVER_YEAR_LINE: + case VIS_TYPE_YEAR_OVER_YEAR_COLUMN: + title.text = getYearOverYearTitle(layout, metaData, dashboard) + break + case VIS_TYPE_SCATTER: + title.text = getScatterTitle(layout, metaData, dashboard) + break + default: + title.text = getDefault(layout, metaData, dashboard) + break + } + } + + switch (layout.type) { + case VIS_TYPE_SINGLE_VALUE: + title.style.color = getSingleValueTitleColor( + fontStyle[FONT_STYLE_OPTION_TEXT_COLOR], + series[0], + legendOptions, + legendSets + ) + break + default: + title.style.color = fontStyle[FONT_STYLE_OPTION_TEXT_COLOR] + break + } + + return title } diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js b/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js index 0d779eed0..82ae95417 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js +++ b/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js @@ -1,6 +1,7 @@ import getFilterText from '../../../../util/getFilterText.js' +export { getSingleValueTextColor as getSingleValueTitleColor } from '../customSVGOptions/singleValue/index.js' -export default function (layout, metaData, dashboard) { +export function getSingleValueTitleText(layout, metaData) { if (layout.hideTitle) { return '' } diff --git a/src/visualizations/config/generators/dhis/singleValue.js b/src/visualizations/config/generators/dhis/singleValue.js index 934e14fdb..07c57854f 100644 --- a/src/visualizations/config/generators/dhis/singleValue.js +++ b/src/visualizations/config/generators/dhis/singleValue.js @@ -468,6 +468,7 @@ export default function ( parentEl, { dashboard, legendSets, fontStyle, noData, legendOptions, icon } ) { + console.log('CONFIG OLD', config) const legendSet = legendOptions && legendSets[0] const legendColor = legendSet && getColorByValueFromLegendSet(legendSet, config.value) diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index 5356cc27f..07fca2f68 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -78,7 +78,7 @@ export function highcharts(config, el) { // silence warning about accessibility config.accessibility = { enabled: false } - + console.log('Homt ie hier?', config) if (config.lang) { H.setOptions({ lang: config.lang, diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js index be1fc9cde..35c1c4769 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js @@ -3,8 +3,8 @@ import { getColorByValueFromLegendSet, LEGEND_DISPLAY_STYLE_FILL, } from '../../../../../modules/legends.js' +import { shouldUseContrastColor } from '../../../adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js' import { generateDVItem } from './generateDVItem.js' -import { shouldUseContrastColor } from './shouldUseContrastColor.js' export default function ( config, From 31040e6005a2d3ef1050d664a34d378970825b2a Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 12 Sep 2024 16:58:19 +0200 Subject: [PATCH 16/82] fix: set subtext in customSVGOptions --- .../getSingleValueCustomSVGOptions.js | 36 +++---------------- .../singleValue/getSingleValueSubtext.js | 11 ++++++ 2 files changed, 15 insertions(+), 32 deletions(-) create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js index 4e482a333..f6452d893 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js @@ -1,47 +1,19 @@ import { getSingleValueFormattedValue } from './getSingleValueFormattedValue.js' +import { getSingleValueSubtext } from './getSingleValueSubtext.js' export function getSingleValueCustomSVGOptions({ - extraConfig, layout, extraOptions, metaData, series, }) { - const { dashboard, legendSets, fontStyle, noData, legendOptions, icon } = - extraOptions + const { dashboard, icon } = extraOptions const value = series[0] - - console.log( - '++++setSingleValueOptions++++', - '\nextraConfig: ', - extraConfig, - '\nlayout: ', - layout, - '\nmetaData: ', - metaData, - '\nseries: ', - series, - '\ndashboard: ', - dashboard, - '\nlegendSets: ', - legendSets, - '\nfontStyle: ', - fontStyle, - '\nnoData: ', - noData, - '\nlegendOptions: ', - legendOptions, - '\nicon: ', - icon, - '\n=============' - ) - const customSVGOptions = { + return { value, formattedValue: getSingleValueFormattedValue(value, layout, metaData), icon, dashboard, - subText: 'Test', + subText: getSingleValueSubtext(metaData), } - console.log('singleValueCustomSvgOptions', customSVGOptions) - return customSVGOptions } diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js new file mode 100644 index 000000000..b14a3f263 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js @@ -0,0 +1,11 @@ +import { INDICATOR_FACTOR_100 } from './getSingleValueFormattedValue.js' + +export function getSingleValueSubtext(metaData) { + const indicatorType = + metaData.items[metaData.dimensions.dx[0]].indicatorType + + return indicatorType?.displayName && + indicatorType?.factor !== INDICATOR_FACTOR_100 + ? indicatorType?.displayName + : undefined +} From 0ed75e9acaf5b469e12df8d777b0c65fad5cdfd8 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 17 Sep 2024 17:15:54 +0200 Subject: [PATCH 17/82] chore: positioning WIP --- .../dhis_highcharts/customSVGOptions/index.js | 8 +- .../getSingleValueBackgroundColor.js | 2 +- .../getSingleValueCustomSVGOptions.js | 0 .../getSingleValueFormattedValue.js | 4 +- .../{ => config}/getSingleValueLegendColor.js | 2 +- .../{ => config}/getSingleValueSubtext.js | 0 .../{ => config}/getSingleValueTextColor.js | 2 +- .../{ => config}/shouldUseContrastColor.js | 0 .../customSVGOptions/singleValue/index.js | 6 +- .../singleValue/renderer/addIconElement.js | 25 ++++ .../renderer/checkIfFitsWithinContainer.js | 23 +++ .../singleValue/renderer/computeSpacingTop.js | 15 ++ .../singleValue/renderer/getAvailableSpace.js | 10 ++ .../singleValue/renderer/positionElements.js | 132 ++++++++++++++++++ .../renderer/renderSingleValueSVG.js | 66 +++++++++ .../singleValue/renderer/styles.js | 83 +++++++++++ .../dhis_highcharts/subtitle/singleValue.js | 2 +- .../highcharts/renderSingleValueSvg/index.js | 2 +- 18 files changed, 368 insertions(+), 14 deletions(-) rename src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/{ => config}/getSingleValueBackgroundColor.js (82%) rename src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/{ => config}/getSingleValueCustomSVGOptions.js (100%) rename src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/{ => config}/getSingleValueFormattedValue.js (82%) rename src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/{ => config}/getSingleValueLegendColor.js (92%) rename src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/{ => config}/getSingleValueSubtext.js (100%) rename src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/{ => config}/getSingleValueTextColor.js (87%) rename src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/{ => config}/shouldUseContrastColor.js (100%) create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/computeSpacingTop.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/getAvailableSpace.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js index 2bfb781b9..7ac3bdbc7 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js @@ -1,13 +1,13 @@ import { VIS_TYPE_SINGLE_VALUE } from '../../../../../modules/visTypes.js' import { getSingleValueCustomSVGOptions } from './singleValue/index.js' +import { renderSingleValueSVG } from './singleValue/renderer/renderSingleValueSVG.js' export function renderCustomSVG() { - const renderer = this.renderer - const options = this.userOptions.customSVGOptions + const { visualizationType } = this.userOptions.customSVGOptions - switch (options.visualizationType) { + switch (visualizationType) { case VIS_TYPE_SINGLE_VALUE: - console.log('now render SV viz', this) + renderSingleValueSVG.call(this) break default: break diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueBackgroundColor.js similarity index 82% rename from src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js rename to src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueBackgroundColor.js index 650c895a5..8ab54896f 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueBackgroundColor.js @@ -1,4 +1,4 @@ -import { LEGEND_DISPLAY_STYLE_FILL } from '../../../../../../modules/legends.js' +import { LEGEND_DISPLAY_STYLE_FILL } from '../../../../../../../modules/legends.js' import { getSingleValueLegendColor } from './getSingleValueLegendColor.js' export function getSingleValueBackgroundColor( diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueCustomSVGOptions.js similarity index 100% rename from src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js rename to src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueCustomSVGOptions.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueFormattedValue.js similarity index 82% rename from src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js rename to src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueFormattedValue.js index f0b91dee3..01f3aad09 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueFormattedValue.js @@ -1,5 +1,5 @@ -import { renderValue } from '../../../../../../modules/renderValue.js' -import { VALUE_TYPE_TEXT } from '../../../../../../modules/valueTypes.js' +import { renderValue } from '../../../../../../../modules/renderValue.js' +import { VALUE_TYPE_TEXT } from '../../../../../../../modules/valueTypes.js' export const INDICATOR_FACTOR_100 = 100 diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueLegendColor.js similarity index 92% rename from src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js rename to src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueLegendColor.js index 9f042fc4d..3e2067cad 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueLegendColor.js @@ -1,4 +1,4 @@ -import { getColorByValueFromLegendSet } from '../../../../../../modules/legends.js' +import { getColorByValueFromLegendSet } from '../../../../../../../modules/legends.js' export function getSingleValueLegendColor(legendOptions, legendSets, value) { const legendSet = legendOptions && legendSets[0] diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueSubtext.js similarity index 100% rename from src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js rename to src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueSubtext.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueTextColor.js similarity index 87% rename from src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js rename to src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueTextColor.js index 109c71fb9..9dc78c322 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/getSingleValueTextColor.js @@ -1,5 +1,5 @@ import { colors } from '@dhis2/ui' -import { LEGEND_DISPLAY_STYLE_TEXT } from '../../../../../../modules/legends.js' +import { LEGEND_DISPLAY_STYLE_TEXT } from '../../../../../../../modules/legends.js' import { getSingleValueLegendColor } from './getSingleValueLegendColor.js' import { shouldUseContrastColor } from './shouldUseContrastColor.js' diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/shouldUseContrastColor.js similarity index 100% rename from src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js rename to src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/config/shouldUseContrastColor.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js index 892761190..a1923d808 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js @@ -1,3 +1,3 @@ -export { getSingleValueCustomSVGOptions } from './getSingleValueCustomSVGOptions.js' -export { getSingleValueBackgroundColor } from './getSingleValueBackgroundColor.js' -export { getSingleValueTextColor } from './getSingleValueTextColor.js' +export { getSingleValueCustomSVGOptions } from './config/getSingleValueCustomSVGOptions.js' +export { getSingleValueBackgroundColor } from './config/getSingleValueBackgroundColor.js' +export { getSingleValueTextColor } from './config/getSingleValueTextColor.js' diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js new file mode 100644 index 000000000..ee3aa0ff9 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js @@ -0,0 +1,25 @@ +const parser = new DOMParser() + +export function addIconElement(svgString, color) { + const svgIconDocument = parser.parseFromString(svgString, 'image/svg+xml') + const iconElHeight = svgIconDocument.documentElement.getAttribute('height') + const iconElWidth = svgIconDocument.documentElement.getAttribute('width') + const iconGroup = this.renderer + .g('icon') + .attr('data-test', 'visualization-icon') + .css({ + color, + }) + /* Force the group element to have the same dimensions as the original + * SVG image by adding this rect. This ensures the icon has the intended + * whitespace around it and makes scaling and translating easier. */ + this.renderer.rect(0, 0, iconElWidth, iconElHeight).add(iconGroup) + + Array.from(svgIconDocument.documentElement.children).forEach((node) => + iconGroup.element.appendChild(node) + ) + + iconGroup.add() + + return iconGroup +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js new file mode 100644 index 000000000..ada8af973 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js @@ -0,0 +1,23 @@ +export function checkIfFitsWithinContainer( + availableSpace, + valueElement, + subTextElement, + icon, + subText, + spacing +) { + const valueRect = valueElement.getBBox() + const subTextRect = subTextElement.getBBox() + const requiredValueWidth = icon + ? valueRect.width + spacing.iconGap + spacing.iconSize + : valueRect.width + const requiredHeight = subText + ? valueRect.height + spacing.subTextTop + subTextRect.height + : valueRect.height + const fitsHorizontally = + availableSpace.width > requiredValueWidth && + availableSpace.width > subTextRect.width + const fitsVertically = availableSpace.height > requiredHeight + + return fitsHorizontally && fitsVertically +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/computeSpacingTop.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/computeSpacingTop.js new file mode 100644 index 000000000..b506f23d3 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/computeSpacingTop.js @@ -0,0 +1,15 @@ +export function computeSpacingTop(valueSpacingTop) { + if (this.subtitle.textStr) { + /* If a subtitle is present this will be below the title so base + * the value X position on this */ + const subTitleRect = this.subtitle.element.getBBox() + return subTitleRect.y + subTitleRect.height + valueSpacingTop + } else if (this.title.textStr) { + // Otherwise base on title + const titleRect = this.title.element.getBBox() + return titleRect.y + titleRect.height + valueSpacingTop + } else { + // If neither are present only adjust for valueSpacingTop + return this.chartHeight - valueSpacingTop + } +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/getAvailableSpace.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/getAvailableSpace.js new file mode 100644 index 000000000..c9f567f4c --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/getAvailableSpace.js @@ -0,0 +1,10 @@ +import { computeSpacingTop } from './computeSpacingTop.js' +import { MIN_SIDE_WHITESPACE } from './styles.js' + +export function getAvailableSpace(valueSpacingTop) { + return { + height: + this.chartHeight - computeSpacingTop.call(this, valueSpacingTop), + width: this.chartWidth - MIN_SIDE_WHITESPACE * 2, + } +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js new file mode 100644 index 000000000..2c8383946 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js @@ -0,0 +1,132 @@ +import { computeSpacingTop } from './computeSpacingTop.js' + +export function positionElements( + valueElement, + subTextElement, + iconElement, + spacing +) { + console.log( + '++++positionElements++++', + '\nvalueElement: ', + valueElement, + '\nsubTextElement: ', + subTextElement, + '\niconElement: ', + iconElement, + '\nspacing: ', + spacing, + '\n===============' + ) + /* Layout here refers to a virtual rect that wraps + * all indiviual parts of the single value visualization + * (value, subtext and icon) */ + const layoutRect = computeLayoutRect.call( + this, + valueElement, + subTextElement, + iconElement, + spacing + ) + + // DEBUGGING THE RECT + const debugRect = this.renderer + .rect(layoutRect.x, layoutRect.y, layoutRect.width, layoutRect.height) + .attr({ fill: 'orange', opacity: 0.3 }) + .add() + + const myBBox = debugRect.getBBox() + + // const valueBox = valueElement.getBBox() + // const valueTranslateX = iconElement + // ? layoutRect.x + spacing.iconSize + spacing.iconGap + // : layoutRect.x + // valueElement.css({ + // transform: `translate(${valueTranslateX}px, ${layoutRect.y}px)`, + // }) + // valueElement.attr({ + // // TODO: cover the case where subtext is wider than value + // x: iconElement + // ? layoutRect.x + spacing.iconSize + spacing.iconGap + // : layoutRect.x, + // y: layoutRect.y, + // dy: valueBox.height, + // }) + const valueElementBox = valueElement.getBBox() + valueElement.align( + { + align: 'right', + verticalAlign: 'top', + alignByTranslate: false, + x: valueElementBox.width * -1, + y: valueElementBox.height * (2 / 3), + }, + false, + layoutRect + ) + + if (iconElement) { + const { height } = iconElement.getBBox() + const scaleFactor = spacing.iconSize / height + + // This all needs to be done using CSS translate because of the path cooordinates in the SVG icon + iconElement.css({ + transform: `translate(${layoutRect.x}px, ${layoutRect.y}px) scale(${scaleFactor})`, + }) + } + + if (subTextElement) { + const { height: subTextHeight } = subTextElement.getBBox() + subTextElement.attr({ + x: iconElement + ? layoutRect.x + spacing.iconSize + spacing.iconGap + : layoutRect.x, + y: layoutRect.y + layoutRect.height - subTextHeight, + }) + } + + console.log( + '++++positionElements++++', + '\nvalueElement: ', + valueElement, + '\nsubTextElement: ', + subTextElement, + '\niconElement: ', + iconElement, + '\nspacing: ', + spacing, + '\nlayoutRect: ', + layoutRect, + '\n===============' + ) +} + +function computeLayoutRect(valueElement, subTextElement, iconElement, spacing) { + const valueRect = valueElement.getBBox() + const containerCenterY = this.chartHeight / 2 + const containerCenterX = this.chartWidth / 2 + const minY = computeSpacingTop.call(this, spacing.valueTop) + + let width = valueRect.width + let height = valueRect.height + + if (iconElement) { + width += spacing.iconGap + spacing.iconSize + } + + if (subTextElement) { + const subTextRect = subTextElement.getBBox() + console.log( + `What is bigger? valueWidth: ${width} subTexttWidth ${subTextRect.width}` + ) + width = Math.max(width, subTextRect.width) + height += spacing.subTextTop + subTextRect.height + } + + return { + x: containerCenterX - width / 2, + y: Math.max(containerCenterY - height / 2, minY), + width, + height, + } +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js new file mode 100644 index 000000000..d068e488d --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js @@ -0,0 +1,66 @@ +import { addIconElement } from './addIconElement.js' +import { checkIfFitsWithinContainer } from './checkIfFitsWithinContainer.js' +import { getAvailableSpace } from './getAvailableSpace.js' +import { positionElements } from './positionElements.js' +import { DynamicStyles } from './styles.js' + +export function renderSingleValueSVG() { + const color = this.title.styles.color + const { dashboard, formattedValue, icon, subText } = + this.userOptions.customSVGOptions + const dynamicStyles = new DynamicStyles() + const valueElement = this.renderer + .text(formattedValue) + .css({ color, visibility: 'visible' }) + .add() + const subTextElement = subText + ? this.renderer + .text(subText) + .css({ color, visibility: 'visible' }) + .add() + : null + const iconElement = icon ? addIconElement.call(this, icon) : null + + let fitsWithinContainer = false + let styles = {} + + while (!fitsWithinContainer && dynamicStyles.hasNext()) { + styles = dynamicStyles.next() + + valueElement.css(styles.value) + subTextElement?.css(styles.subText) + + fitsWithinContainer = checkIfFitsWithinContainer( + getAvailableSpace.call(this, styles.spacing.valueTop), + valueElement, + subTextElement, + icon, + subText, + styles.spacing + ) + } + + positionElements.call( + this, + valueElement, + subTextElement, + iconElement, + styles.spacing + ) + + console.log( + '+++++Render the SVG++++++', + '\ncolor: ', + color, + '\ndashboard: ', + dashboard, + '\nformattedValue: ', + formattedValue, + '\nicon: ', + icon, + '\nsubText: ', + subText, + '\n=============' + ) + console.log('CHART', this) +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js new file mode 100644 index 000000000..f141c285d --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js @@ -0,0 +1,83 @@ +const baseStyle = { + value: { + fontWeight: 300, + }, + subText: {}, +} + +const valueStyles = [ + { fontSize: 200, letterSpacing: -6 }, + { fontSize: 182, letterSpacing: -5.5 }, + { fontSize: 164, letterSpacing: -5 }, + { fontSize: 146, letterSpacing: -4.5 }, + { fontSize: 128, letterSpacing: -4 }, + { fontSize: 110, letterSpacing: -3.5 }, + { fontSize: 92, letterSpacing: -3 }, + { fontSize: 74, letterSpacing: -2.5 }, + { fontSize: 56, letterSpacing: -2 }, + { fontSize: 38, letterSpacing: -1.5 }, + { fontSize: 20, letterSpacing: -1 }, +] + +const subTextStyles = [ + { fontSize: 100, letterSpacing: -3 }, + { fontSize: 91, letterSpacing: -2.7 }, + { fontSize: 82, letterSpacing: -2.4 }, + { fontSize: 73, letterSpacing: -2.1 }, + { fontSize: 64, letterSpacing: -1.8 }, + { fontSize: 55, letterSpacing: -1.5 }, + { fontSize: 46, letterSpacing: -1.2 }, + { fontSize: 37, letterSpacing: -0.9 }, + { fontSize: 28, letterSpacing: -0.6 }, + { fontSize: 19, letterSpacing: 0.3 }, + { fontSize: 10, letterSpacing: 0 }, +] + +const spacings = [ + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 140 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 127 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 115 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 102 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 90 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 77 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 64 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 52 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 39 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 27 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 14 }, +] + +export const MIN_SIDE_WHITESPACE = 4 + +export class DynamicStyles { + constructor() { + this.currentIndex = 0 + } + getStyle() { + return { + value: { ...baseStyle.value, ...valueStyles[this.currentIndex] }, + subText: { + ...baseStyle.subText, + ...subTextStyles[this.currentIndex], + }, + spacing: spacings[this.currentIndex], + } + } + next() { + if (this.currentIndex === valueStyles.length - 1) { + throw new Error('No next available, already on the smallest style') + } else { + ++this.currentIndex + } + + return this.getStyle() + } + first() { + this.currentIndex = 0 + + return this.getStyle() + } + hasNext() { + return this.currentIndex < valueStyles.length - 1 + } +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js index 4bf8b394a..2ba68032a 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js +++ b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js @@ -2,7 +2,7 @@ import getFilterText from '../../../../util/getFilterText.js' export { getSingleValueTextColor as getSingleValueSubtitleColor } from '../customSVGOptions/singleValue/index.js' export default function getSingleValueSubtitle(layout, metaData) { - if (layout.hideSubtitle) { + if (layout.hideSubtitle || 1 === 0) { return '' } diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js index 35c1c4769..b4cfd8842 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js @@ -3,7 +3,7 @@ import { getColorByValueFromLegendSet, LEGEND_DISPLAY_STYLE_FILL, } from '../../../../../modules/legends.js' -import { shouldUseContrastColor } from '../../../adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js' +import { shouldUseContrastColor } from '../../../adapters/dhis_highcharts/customSVGOptions/singleValue/config/shouldUseContrastColor.js' import { generateDVItem } from './generateDVItem.js' export default function ( From 6676a45d37b65bd96686f3c8ec27fa4998b3b80c Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 18 Sep 2024 12:00:19 +0200 Subject: [PATCH 18/82] chore: ensure Roboto font is preloaded to prevent text size computations going wrong --- .storybook/preview-head.html | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .storybook/preview-head.html diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 000000000..965f8201c --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,6 @@ + + + From 1e5465fe1294658572f15d602ebb68155774074a Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 18 Sep 2024 14:39:56 +0200 Subject: [PATCH 19/82] chore: position all SVG elements using Highcharts SVGElement.align() --- .../singleValue/renderer/addIconElement.js | 5 +- .../renderer/checkIfFitsWithinContainer.js | 8 +- .../singleValue/renderer/computeLayoutRect.js | 43 +++++++ .../singleValue/renderer/constants.js | 4 + .../singleValue/renderer/positionElements.js | 118 ++++-------------- .../renderer/renderSingleValueSVG.js | 27 +--- 6 files changed, 86 insertions(+), 119 deletions(-) create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/computeLayoutRect.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/constants.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js index ee3aa0ff9..6248bdeb4 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js @@ -6,10 +6,11 @@ export function addIconElement(svgString, color) { const iconElWidth = svgIconDocument.documentElement.getAttribute('width') const iconGroup = this.renderer .g('icon') - .attr('data-test', 'visualization-icon') + .attr({ color, 'data-test': 'visualization-icon' }) .css({ - color, + visibility: 'hidden', }) + /* Force the group element to have the same dimensions as the original * SVG image by adding this rect. This ensures the icon has the intended * whitespace around it and makes scaling and translating easier. */ diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js index ada8af973..51bb5c253 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js @@ -1,3 +1,5 @@ +import { ACTUAL_NUMBER_HEIGHT_FACTOR } from './constants.js' + export function checkIfFitsWithinContainer( availableSpace, valueElement, @@ -12,8 +14,10 @@ export function checkIfFitsWithinContainer( ? valueRect.width + spacing.iconGap + spacing.iconSize : valueRect.width const requiredHeight = subText - ? valueRect.height + spacing.subTextTop + subTextRect.height - : valueRect.height + ? valueRect.height * ACTUAL_NUMBER_HEIGHT_FACTOR + + spacing.subTextTop + + subTextRect.height + : valueRect.height * ACTUAL_NUMBER_HEIGHT_FACTOR const fitsHorizontally = availableSpace.width > requiredValueWidth && availableSpace.width > subTextRect.width diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/computeLayoutRect.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/computeLayoutRect.js new file mode 100644 index 000000000..a5d2705c9 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/computeLayoutRect.js @@ -0,0 +1,43 @@ +import { computeSpacingTop } from './computeSpacingTop.js' +import { ACTUAL_NUMBER_HEIGHT_FACTOR } from './constants.js' + +export function computeLayoutRect( + valueElement, + subTextElement, + iconElement, + spacing +) { + const valueRect = valueElement.getBBox() + const containerCenterY = this.chartHeight / 2 + const containerCenterX = this.chartWidth / 2 + const minY = computeSpacingTop.call(this, spacing.valueTop) + + let width = valueRect.width + let height = valueRect.height * ACTUAL_NUMBER_HEIGHT_FACTOR + let sideMarginTop = 0 + let sideMarginBottom = 0 + + if (iconElement) { + width += spacing.iconGap + spacing.iconSize + } + + if (subTextElement) { + const subTextRect = subTextElement.getBBox() + if (subTextRect.width > width) { + sideMarginTop = (subTextRect.width - width) / 2 + width = subTextRect.width + } else { + sideMarginBottom = (width - subTextRect.width) / 2 + } + height += spacing.subTextTop + subTextRect.height + } + + return { + x: containerCenterX - width / 2, + y: Math.max(containerCenterY - height / 2, minY), + width, + height, + sideMarginTop, + sideMarginBottom, + } +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/constants.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/constants.js new file mode 100644 index 000000000..b76e26a44 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/constants.js @@ -0,0 +1,4 @@ +// multiply value text size with this factor +// to get very close to the actual number height +// as numbers don't go below the baseline like e.g. "j" and "g" +export const ACTUAL_NUMBER_HEIGHT_FACTOR = 2 / 3 diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js index 2c8383946..052c86b5b 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js @@ -1,4 +1,5 @@ -import { computeSpacingTop } from './computeSpacingTop.js' +import { computeLayoutRect } from './computeLayoutRect.js' +import { ACTUAL_NUMBER_HEIGHT_FACTOR } from './constants.js' export function positionElements( valueElement, @@ -6,18 +7,7 @@ export function positionElements( iconElement, spacing ) { - console.log( - '++++positionElements++++', - '\nvalueElement: ', - valueElement, - '\nsubTextElement: ', - subTextElement, - '\niconElement: ', - iconElement, - '\nspacing: ', - spacing, - '\n===============' - ) + const valueElementBox = valueElement.getBBox() /* Layout here refers to a virtual rect that wraps * all indiviual parts of the single value visualization * (value, subtext and icon) */ @@ -29,37 +19,13 @@ export function positionElements( spacing ) - // DEBUGGING THE RECT - const debugRect = this.renderer - .rect(layoutRect.x, layoutRect.y, layoutRect.width, layoutRect.height) - .attr({ fill: 'orange', opacity: 0.3 }) - .add() - - const myBBox = debugRect.getBBox() - - // const valueBox = valueElement.getBBox() - // const valueTranslateX = iconElement - // ? layoutRect.x + spacing.iconSize + spacing.iconGap - // : layoutRect.x - // valueElement.css({ - // transform: `translate(${valueTranslateX}px, ${layoutRect.y}px)`, - // }) - // valueElement.attr({ - // // TODO: cover the case where subtext is wider than value - // x: iconElement - // ? layoutRect.x + spacing.iconSize + spacing.iconGap - // : layoutRect.x, - // y: layoutRect.y, - // dy: valueBox.height, - // }) - const valueElementBox = valueElement.getBBox() valueElement.align( { align: 'right', verticalAlign: 'top', alignByTranslate: false, - x: valueElementBox.width * -1, - y: valueElementBox.height * (2 / 3), + x: (valueElementBox.width + layoutRect.sideMarginTop) * -1, + y: valueElementBox.height * ACTUAL_NUMBER_HEIGHT_FACTOR, }, false, layoutRect @@ -67,66 +33,30 @@ export function positionElements( if (iconElement) { const { height } = iconElement.getBBox() - const scaleFactor = spacing.iconSize / height - - // This all needs to be done using CSS translate because of the path cooordinates in the SVG icon + const scale = spacing.iconSize / height + const translateX = layoutRect.x + layoutRect.sideMarginTop + const iconHeight = height * scale + const valueElementHeight = + valueElementBox.height * ACTUAL_NUMBER_HEIGHT_FACTOR + const translateY = layoutRect.y + (valueElementHeight - iconHeight) / 2 + + /* The icon is a with elements that contain coordinates. + * These path-coordinates only scale correctly when using CSS translate */ iconElement.css({ - transform: `translate(${layoutRect.x}px, ${layoutRect.y}px) scale(${scaleFactor})`, - }) - } - - if (subTextElement) { - const { height: subTextHeight } = subTextElement.getBBox() - subTextElement.attr({ - x: iconElement - ? layoutRect.x + spacing.iconSize + spacing.iconGap - : layoutRect.x, - y: layoutRect.y + layoutRect.height - subTextHeight, + transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`, }) } - console.log( - '++++positionElements++++', - '\nvalueElement: ', - valueElement, - '\nsubTextElement: ', - subTextElement, - '\niconElement: ', - iconElement, - '\nspacing: ', - spacing, - '\nlayoutRect: ', - layoutRect, - '\n===============' - ) -} - -function computeLayoutRect(valueElement, subTextElement, iconElement, spacing) { - const valueRect = valueElement.getBBox() - const containerCenterY = this.chartHeight / 2 - const containerCenterX = this.chartWidth / 2 - const minY = computeSpacingTop.call(this, spacing.valueTop) - - let width = valueRect.width - let height = valueRect.height - - if (iconElement) { - width += spacing.iconGap + spacing.iconSize - } - if (subTextElement) { - const subTextRect = subTextElement.getBBox() - console.log( - `What is bigger? valueWidth: ${width} subTexttWidth ${subTextRect.width}` + subTextElement.align( + { + align: 'left', + verticalAlign: 'bottom', + alignByTranslate: false, + x: layoutRect.sideMarginBottom, + }, + false, + layoutRect ) - width = Math.max(width, subTextRect.width) - height += spacing.subTextTop + subTextRect.height - } - - return { - x: containerCenterX - width / 2, - y: Math.max(containerCenterY - height / 2, minY), - width, - height, } } diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js index d068e488d..21f4ea398 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js @@ -11,15 +11,12 @@ export function renderSingleValueSVG() { const dynamicStyles = new DynamicStyles() const valueElement = this.renderer .text(formattedValue) - .css({ color, visibility: 'visible' }) + .css({ color, visibility: 'hidden' }) .add() const subTextElement = subText - ? this.renderer - .text(subText) - .css({ color, visibility: 'visible' }) - .add() + ? this.renderer.text(subText).css({ color, visibility: 'hidden' }).add() : null - const iconElement = icon ? addIconElement.call(this, icon) : null + const iconElement = icon ? addIconElement.call(this, icon, color) : null let fitsWithinContainer = false let styles = {} @@ -48,19 +45,7 @@ export function renderSingleValueSVG() { styles.spacing ) - console.log( - '+++++Render the SVG++++++', - '\ncolor: ', - color, - '\ndashboard: ', - dashboard, - '\nformattedValue: ', - formattedValue, - '\nicon: ', - icon, - '\nsubText: ', - subText, - '\n=============' - ) - console.log('CHART', this) + valueElement.css({ visibility: 'visible' }) + iconElement?.css({ visibility: 'visible' }) + subTextElement?.css({ visibility: 'visible' }) } From fb02ca6b33838ef0ecdf70882abe525934919b31 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 18 Sep 2024 14:51:34 +0200 Subject: [PATCH 20/82] chore: remove files from failed initial re-implementation --- .../config/adapters/dhis_highcharts/index.js | 2 - src/visualizations/config/adapters/index.js | 1 - .../config/generators/dhis/singleValue.js | 2 - .../config/generators/highcharts/index.js | 36 +---- .../renderSingleValueSvg/constants.js | 38 ----- .../renderSingleValueSvg/generateDVItem.js | 134 ----------------- .../renderSingleValueSvg/generateValueSVG.js | 135 ------------------ .../generateValueSVGOLD.js | 135 ------------------ .../getTextAnchorFromTextAlign.js | 17 --- .../renderSingleValueSvg/getXFromTextAlign.js | 17 --- .../highcharts/renderSingleValueSvg/index.js | 76 ---------- .../renderSingleValueSvg/textSize.js | 52 ------- src/visualizations/config/generators/index.js | 3 +- src/visualizations/store/adapters/index.js | 1 - 14 files changed, 2 insertions(+), 647 deletions(-) delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/index.js b/src/visualizations/config/adapters/dhis_highcharts/index.js index f9e97469b..b39a263c2 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/index.js @@ -244,7 +244,5 @@ export default function ({ store, layout, el, extraConfig, extraOptions }) { // force apply extra config Object.assign(config, extraConfig) - console.log('CONFIG', objectClean(config)) - return objectClean(config) } diff --git a/src/visualizations/config/adapters/index.js b/src/visualizations/config/adapters/index.js index e567b54d1..7b49438ee 100644 --- a/src/visualizations/config/adapters/index.js +++ b/src/visualizations/config/adapters/index.js @@ -4,5 +4,4 @@ import dhis_highcharts from './dhis_highcharts/index.js' export default { dhis_highcharts, dhis_dhis, - dhis_singleValue: dhis_dhis, } diff --git a/src/visualizations/config/generators/dhis/singleValue.js b/src/visualizations/config/generators/dhis/singleValue.js index 07c57854f..25ec5bab9 100644 --- a/src/visualizations/config/generators/dhis/singleValue.js +++ b/src/visualizations/config/generators/dhis/singleValue.js @@ -151,7 +151,6 @@ const generateValueSVG = ({ // embed icon to allow changing color // (elements with fill need to use "currentColor" for this to work) const iconSvgNode = document.createElementNS(svgNS, 'svg') - console.log('old', iconSize) iconSvgNode.setAttribute('viewBox', '0 0 48 48') iconSvgNode.setAttribute('width', iconSize) iconSvgNode.setAttribute('height', iconSize) @@ -468,7 +467,6 @@ export default function ( parentEl, { dashboard, legendSets, fontStyle, noData, legendOptions, icon } ) { - console.log('CONFIG OLD', config) const legendSet = legendOptions && legendSets[0] const legendColor = legendSet && getColorByValueFromLegendSet(legendSet, config.value) diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index 07fca2f68..f3222b257 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -72,13 +72,12 @@ function drawLegendSymbolWrap() { ) } -export function highcharts(config, el) { +export default function (config, el) { if (config) { config.chart.renderTo = el || config.chart.renderTo // silence warning about accessibility config.accessibility = { enabled: false } - console.log('Homt ie hier?', config) if (config.lang) { H.setOptions({ lang: config.lang, @@ -90,36 +89,3 @@ export function highcharts(config, el) { return new H.Chart(config) } } - -export function singleValue(config, el, extraOptions) { - return H.chart(el, { - accessibility: { enabled: false }, - chart: { - backgroundColor: 'transparent', - events: { - load: function () { - renderSingleValueSvg(config, el, extraOptions, this) - }, - }, - animation: false, - }, - credits: { enabled: false }, - exporting: { - enabled: true, - error: (options, error) => { - console.log('options', options) - console.log(error) - }, - chartOptions: { - title: { - text: null, - }, - }, - }, - lang: { - noData: null, - }, - noData: {}, - title: null, - }) -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js deleted file mode 100644 index 06fd79fd0..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js +++ /dev/null @@ -1,38 +0,0 @@ -// TODO: remove this, sch thing it should not be needed -export const svgNS = 'http://www.w3.org/2000/svg' -// multiply text width with this factor -// to get very close to actual text width -// nb: dependent on viewbox etc -export const ACTUAL_TEXT_WIDTH_FACTOR = 0.9 - -// multiply value text size with this factor -// to get very close to the actual number height -// as numbers don't go below the baseline like e.g. "j" and "g" -export const ACTUAL_NUMBER_HEIGHT_FACTOR = 0.67 - -// do not allow text width to exceed this threshold -// a threshold >1 does not really make sense but text width vs viewbox is complicated -export const TEXT_WIDTH_CONTAINER_WIDTH_FACTOR = 1.3 - -// do not allow text size to exceed this -export const TEXT_SIZE_CONTAINER_HEIGHT_FACTOR = 0.6 -export const TEXT_SIZE_MAX_THRESHOLD = 200 - -// multiply text size with this factor -// to get an appropriate letter spacing -export const LETTER_SPACING_TEXT_SIZE_FACTOR = (1 / 35) * -1 -export const LETTER_SPACING_MIN_THRESHOLD = -6 -export const LETTER_SPACING_MAX_THRESHOLD = -1 - -// fixed top margin above title/subtitle -export const TOP_MARGIN_FIXED = 16 - -// multiply text size with this factor -// to get an appropriate sub text size -export const SUB_TEXT_SIZE_FACTOR = 0.5 -export const SUB_TEXT_SIZE_MIN_THRESHOLD = 26 -export const SUB_TEXT_SIZE_MAX_THRESHOLD = 40 - -// multiply text size with this factor -// to get an appropriate icon padding -export const ICON_PADDING_FACTOR = 0.3 diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js deleted file mode 100644 index 2291d341d..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js +++ /dev/null @@ -1,134 +0,0 @@ -import { - defaultFontStyle, - FONT_STYLE_OPTION_BOLD, - FONT_STYLE_OPTION_FONT_SIZE, - FONT_STYLE_OPTION_ITALIC, - FONT_STYLE_OPTION_TEXT_ALIGN, - FONT_STYLE_OPTION_TEXT_COLOR, - FONT_STYLE_VISUALIZATION_SUBTITLE, - FONT_STYLE_VISUALIZATION_TITLE, - mergeFontStyleWithDefault, -} from '../../../../../modules/fontStyle.js' -import { TOP_MARGIN_FIXED } from './constants.js' -import { generateValueSVG } from './generateValueSVG.js' -import { getTextAnchorFromTextAlign } from './getTextAnchorFromTextAlign.js' -import { getXFromTextAlign } from './getXFromTextAlign.js' - -export const generateDVItem = ( - config, - { - renderer, - width, - height, - valueColor, - noData, - backgroundColor, - titleColor, - fontStyle, - icon, - } -) => { - backgroundColor = 'red' - if (backgroundColor) { - renderer - .rect(0, 0, width, height) - .attr({ fill: backgroundColor, width: '100%', height: '100%' }) - .add() - } - - // TITLE - const titleFontStyle = mergeFontStyleWithDefault( - fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_TITLE], - FONT_STYLE_VISUALIZATION_TITLE - ) - - const titleYPosition = - TOP_MARGIN_FIXED + - parseInt(titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]) + - 'px' - - const titleFontSize = `${titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px` - - renderer - .text(config.title) - .attr({ - x: getXFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]), - y: titleYPosition, - 'text-anchor': getTextAnchorFromTextAlign( - titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] - ), - 'font-size': titleFontSize, - 'font-weight': titleFontStyle[FONT_STYLE_OPTION_BOLD] - ? FONT_STYLE_OPTION_BOLD - : 'normal', - 'font-style': titleFontStyle[FONT_STYLE_OPTION_ITALIC] - ? FONT_STYLE_OPTION_ITALIC - : 'normal', - 'data-test': 'visualization-title', - fill: - titleColor && - titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === - defaultFontStyle[FONT_STYLE_VISUALIZATION_TITLE][ - FONT_STYLE_OPTION_TEXT_COLOR - ] - ? titleColor - : titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR], - }) - .add() - - // SUBTITLE - const subtitleFontStyle = mergeFontStyleWithDefault( - fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], - FONT_STYLE_VISUALIZATION_SUBTITLE - ) - const subtitleFontSize = `${subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px` - - if (config.subtitle) { - renderer - .text(config.subtitle) - .attr({ - x: getXFromTextAlign( - subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] - ), - y: titleYPosition, - dy: `${subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE] + 10}`, - 'text-anchor': getTextAnchorFromTextAlign( - subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] - ), - 'font-size': subtitleFontSize, - 'font-weight': subtitleFontStyle[FONT_STYLE_OPTION_BOLD] - ? FONT_STYLE_OPTION_BOLD - : 'normal', - 'font-style': subtitleFontStyle[FONT_STYLE_OPTION_ITALIC] - ? FONT_STYLE_OPTION_ITALIC - : 'normal', - fill: - titleColor && - subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === - defaultFontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE][ - FONT_STYLE_OPTION_TEXT_COLOR - ] - ? titleColor - : subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR], - 'data-test': 'visualization-subtitle', - }) - .add() - } - - generateValueSVG({ - renderer, - formattedValue: config.formattedValue, - subText: config.subText, - valueColor, - textColor: titleColor, - noData, - icon, - containerWidth: width, - containerHeight: height, - topMargin: - TOP_MARGIN_FIXED + - ((config.title ? parseInt(titleFontSize) : 0) + - (config.subtitle ? parseInt(subtitleFontSize) : 0)) * - 2.5, - }) -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js deleted file mode 100644 index 5f365f0b5..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js +++ /dev/null @@ -1,135 +0,0 @@ -import { colors } from '@dhis2/ui' -import { - svgNS, - LETTER_SPACING_MAX_THRESHOLD, - LETTER_SPACING_MIN_THRESHOLD, - LETTER_SPACING_TEXT_SIZE_FACTOR, - SUB_TEXT_SIZE_FACTOR, - SUB_TEXT_SIZE_MAX_THRESHOLD, - SUB_TEXT_SIZE_MIN_THRESHOLD, -} from './constants.js' -import { - getIconPadding, - getTextHeightForNumbers, - getTextSize, - getTextWidth, -} from './textSize.js' - -const parser = new DOMParser() - -export const generateValueSVG = ({ - renderer, - formattedValue, - subText, - valueColor, - textColor, - icon, - noData, - containerWidth, - containerHeight, - topMargin = 0, -}) => { - const showIcon = icon && formattedValue !== noData.text - const group = renderer - .g('value') - .css({ - transform: 'translate(50%, 50%)', - }) - .add() - - const textSize = getTextSize( - formattedValue, - containerWidth, - containerHeight, - showIcon - ) - - const textWidth = getTextWidth(formattedValue, `${textSize}px Roboto`) - - const iconSize = textSize - - const subTextSize = - textSize * SUB_TEXT_SIZE_FACTOR > SUB_TEXT_SIZE_MAX_THRESHOLD - ? SUB_TEXT_SIZE_MAX_THRESHOLD - : textSize * SUB_TEXT_SIZE_FACTOR < SUB_TEXT_SIZE_MIN_THRESHOLD - ? SUB_TEXT_SIZE_MIN_THRESHOLD - : textSize * SUB_TEXT_SIZE_FACTOR - - let fillColor = colors.grey900 - - if (valueColor) { - fillColor = valueColor - } else if (formattedValue === noData.text) { - fillColor = colors.grey600 - } - - const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) - - const formattedValueText = renderer - .text(formattedValue) - .attr({ - 'font-size': textSize, - 'font-weight': '300', - 'letter-spacing': - letterSpacing < LETTER_SPACING_MIN_THRESHOLD - ? LETTER_SPACING_MIN_THRESHOLD - : letterSpacing > LETTER_SPACING_MAX_THRESHOLD - ? LETTER_SPACING_MAX_THRESHOLD - : letterSpacing, - 'text-anchor': 'middle', - width: '100%', - x: showIcon ? `${iconSize / 2 + getIconPadding(textSize / 2)}` : 0, - y: topMargin / 2 + getTextHeightForNumbers(textSize) / 2, - fill: fillColor, - 'data-test': 'visualization-primary-value', - }) - .add(group) - - // show icon if configured in maintenance app - if (showIcon) { - const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml') - const iconElHeight = - svgIconDocument.documentElement.getAttribute('height') - const iconElWidth = - svgIconDocument.documentElement.getAttribute('width') - const iconGroup = renderer - .g('icon') - .attr('data-test', 'visualization-icon') - .css({ - color: fillColor, - }) - /* Force the group element to have the same dimensions as the original - * SVG image by adding this rect. This ensures the icon has the intended - * whitespace around it and makes scaling and translating easier. */ - renderer.rect(0, 0, iconElWidth, iconElHeight).add(iconGroup) - - Array.from(svgIconDocument.documentElement.children).forEach((node) => - iconGroup.element.appendChild(node) - ) - const formattedValueTextBox = formattedValueText.getBBox() - const scaleFactor = textSize / iconElHeight - const textHeight = formattedValueTextBox.height / 2 - const iconHeight = (iconElHeight * scaleFactor) / 2 - const translateY = - (formattedValueTextBox.y + (textHeight - iconHeight)) / scaleFactor - - iconGroup - .css({ - transform: `scale(${scaleFactor}) translate(-98px, ${translateY}px)`, - }) - .add(group) - } - - if (subText) { - renderer - .text(subText) - .attr({ - 'text-anchor': 'middle', - 'font-size': subTextSize, - y: iconSize / 2 + topMargin / 2, - dy: subTextSize * 1.7, - fill: textColor, - }) - .add(group) - } -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js deleted file mode 100644 index 1a36f7eda..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js +++ /dev/null @@ -1,135 +0,0 @@ -import { colors } from '@dhis2/ui' -import { - svgNS, - LETTER_SPACING_MAX_THRESHOLD, - LETTER_SPACING_MIN_THRESHOLD, - LETTER_SPACING_TEXT_SIZE_FACTOR, - SUB_TEXT_SIZE_FACTOR, - SUB_TEXT_SIZE_MAX_THRESHOLD, - SUB_TEXT_SIZE_MIN_THRESHOLD, -} from './constants.js' -import { - getIconPadding, - getTextHeightForNumbers, - getTextSize, - getTextWidth, -} from './textSize.js' - -export const generateValueSVG = ({ - renderer, - formattedValue, - subText, - valueColor, - textColor, - icon, - noData, - containerWidth, - containerHeight, - topMargin = 0, -}) => { - const showIcon = icon && formattedValue !== noData.text - - const textSize = getTextSize( - formattedValue, - containerWidth, - containerHeight, - showIcon - ) - - const textWidth = getTextWidth(formattedValue, `${textSize}px Roboto`) - - const iconSize = textSize - - const subTextSize = - textSize * SUB_TEXT_SIZE_FACTOR > SUB_TEXT_SIZE_MAX_THRESHOLD - ? SUB_TEXT_SIZE_MAX_THRESHOLD - : textSize * SUB_TEXT_SIZE_FACTOR < SUB_TEXT_SIZE_MIN_THRESHOLD - ? SUB_TEXT_SIZE_MIN_THRESHOLD - : textSize * SUB_TEXT_SIZE_FACTOR - - const svgValue = document.createElementNS(svgNS, 'svg') - svgValue.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`) - svgValue.setAttribute('width', '50%') - svgValue.setAttribute('height', '50%') - svgValue.setAttribute('x', '50%') - svgValue.setAttribute('y', '50%') - svgValue.setAttribute('style', 'overflow: visible') - - let fillColor = colors.grey900 - - if (valueColor) { - fillColor = valueColor - } else if (formattedValue === noData.text) { - fillColor = colors.grey600 - } - - // show icon if configured in maintenance app - if (showIcon) { - // embed icon to allow changing color - // (elements with fill need to use "currentColor" for this to work) - const iconSvgNode = document.createElementNS(svgNS, 'svg') - console.log('old', iconSize) - iconSvgNode.setAttribute('viewBox', '0 0 48 48') - iconSvgNode.setAttribute('width', iconSize) - iconSvgNode.setAttribute('height', iconSize) - iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1) - iconSvgNode.setAttribute( - 'x', - `-${(iconSize + getIconPadding(textSize) + textWidth) / 2}` - ) - iconSvgNode.setAttribute('style', `color: ${fillColor}`) - iconSvgNode.setAttribute('data-test', 'visualization-icon') - - const parser = new DOMParser() - const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml') - - Array.from(svgIconDocument.documentElement.children).forEach((node) => - iconSvgNode.appendChild(node) - ) - - svgValue.appendChild(iconSvgNode) - } - - const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) - - const textNode = document.createElementNS(svgNS, 'text') - textNode.setAttribute('font-size', textSize) - textNode.setAttribute('font-weight', '300') - textNode.setAttribute( - 'letter-spacing', - letterSpacing < LETTER_SPACING_MIN_THRESHOLD - ? LETTER_SPACING_MIN_THRESHOLD - : letterSpacing > LETTER_SPACING_MAX_THRESHOLD - ? LETTER_SPACING_MAX_THRESHOLD - : letterSpacing - ) - textNode.setAttribute('text-anchor', 'middle') - textNode.setAttribute( - 'x', - showIcon ? `${(iconSize + getIconPadding(textSize)) / 2}` : 0 - ) - textNode.setAttribute( - 'y', - topMargin / 2 + getTextHeightForNumbers(textSize) / 2 - ) - textNode.setAttribute('fill', fillColor) - textNode.setAttribute('data-test', 'visualization-primary-value') - - textNode.appendChild(document.createTextNode(formattedValue)) - - svgValue.appendChild(textNode) - - if (subText) { - const subTextNode = document.createElementNS(svgNS, 'text') - subTextNode.setAttribute('text-anchor', 'middle') - subTextNode.setAttribute('font-size', subTextSize) - subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2) - subTextNode.setAttribute('dy', subTextSize * 1.7) - subTextNode.setAttribute('fill', textColor) - subTextNode.appendChild(document.createTextNode(subText)) - - svgValue.appendChild(subTextNode) - } - - renderer.box.appendChild(svgValue) -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js deleted file mode 100644 index 5d66ba074..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js +++ /dev/null @@ -1,17 +0,0 @@ -import { - TEXT_ALIGN_LEFT, - TEXT_ALIGN_CENTER, - TEXT_ALIGN_RIGHT, -} from '../../../../../modules/fontStyle.js' - -export const getTextAnchorFromTextAlign = (textAlign) => { - switch (textAlign) { - default: - case TEXT_ALIGN_LEFT: - return 'start' - case TEXT_ALIGN_CENTER: - return 'middle' - case TEXT_ALIGN_RIGHT: - return 'end' - } -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js deleted file mode 100644 index d9383b4e9..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js +++ /dev/null @@ -1,17 +0,0 @@ -import { - TEXT_ALIGN_LEFT, - TEXT_ALIGN_CENTER, - TEXT_ALIGN_RIGHT, -} from '../../../../../modules/fontStyle.js' - -export const getXFromTextAlign = (textAlign) => { - switch (textAlign) { - default: - case TEXT_ALIGN_LEFT: - return '1%' - case TEXT_ALIGN_CENTER: - return '50%' - case TEXT_ALIGN_RIGHT: - return '99%' - } -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js deleted file mode 100644 index b4cfd8842..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js +++ /dev/null @@ -1,76 +0,0 @@ -import { colors } from '@dhis2/ui' -import { - getColorByValueFromLegendSet, - LEGEND_DISPLAY_STYLE_FILL, -} from '../../../../../modules/legends.js' -import { shouldUseContrastColor } from '../../../adapters/dhis_highcharts/customSVGOptions/singleValue/config/shouldUseContrastColor.js' -import { generateDVItem } from './generateDVItem.js' - -export default function ( - config, - parentEl, - { dashboard, legendSets, fontStyle, noData, legendOptions, icon }, - chart -) { - const renderer = chart.renderer - const legendSet = legendOptions && legendSets[0] - const legendColor = - legendSet && getColorByValueFromLegendSet(legendSet, config.value) - let valueColor, titleColor, backgroundColor - if (legendColor) { - if (legendOptions.style === LEGEND_DISPLAY_STYLE_FILL) { - backgroundColor = legendColor - valueColor = titleColor = - shouldUseContrastColor(legendColor) && colors.white - } else { - valueColor = legendColor - } - } - - parentEl.style.overflow = 'hidden' - parentEl.style.display = 'flex' - parentEl.style.justifyContent = 'center' - - // We need the inner width so borders etc are excluded - const width = parentEl.clientWidth - const height = parentEl.clientHeight - - const svgContainer = renderer.box - svgContainer.setAttribute('viewBox', `0 0 ${width} ${height}`) - svgContainer.setAttribute('data-test', 'visualization-container') - - chart.setSize(dashboard ? '100%' : width, dashboard ? '100%' : height) - - // if (dashboard) { - // parentEl.style.borderRadius = '3px' - - // return generateDashboardItem(config, { - // svgContainer, - // width, - // height, - // valueColor, - // backgroundColor, - // noData, - // icon, - // ...(legendOptions.style === LEGEND_DISPLAY_STYLE_FILL && - // legendColor && - // shouldUseContrastColor(legendColor) - // ? { titleColor: colors.white } - // : {}), - // }) - // } else { - parentEl.style.height = `100%` - - return generateDVItem(config, { - renderer, - width, - height, - valueColor, - backgroundColor, - titleColor, - noData, - icon, - fontStyle, - }) - // } -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js deleted file mode 100644 index a94ad7266..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js +++ /dev/null @@ -1,52 +0,0 @@ -// Compute text width before rendering - -import { - ACTUAL_NUMBER_HEIGHT_FACTOR, - ACTUAL_TEXT_WIDTH_FACTOR, - ICON_PADDING_FACTOR, - TEXT_SIZE_CONTAINER_HEIGHT_FACTOR, - TEXT_SIZE_MAX_THRESHOLD, - TEXT_WIDTH_CONTAINER_WIDTH_FACTOR, -} from './constants.js' - -// Not exactly precise but close enough -export const getTextWidth = (text, font) => { - const canvas = document.createElement('canvas') - const context = canvas.getContext('2d') - context.font = font - return Math.round( - context.measureText(text).width * ACTUAL_TEXT_WIDTH_FACTOR - ) -} - -export const getTextHeightForNumbers = (textSize) => - textSize * ACTUAL_NUMBER_HEIGHT_FACTOR - -export const getIconPadding = (textSize) => - Math.round(textSize * ICON_PADDING_FACTOR) - -export const getTextSize = ( - formattedValue, - containerWidth, - containerHeight, - showIcon -) => { - let size = Math.min( - Math.round(containerHeight * TEXT_SIZE_CONTAINER_HEIGHT_FACTOR), - TEXT_SIZE_MAX_THRESHOLD - ) - - const widthThreshold = Math.round( - containerWidth * TEXT_WIDTH_CONTAINER_WIDTH_FACTOR - ) - - const textWidth = - getTextWidth(formattedValue, `${size}px Roboto`) + - (showIcon ? getIconPadding(size) : 0) - - if (textWidth > widthThreshold) { - size = Math.round(size * (widthThreshold / textWidth)) - } - - return size -} diff --git a/src/visualizations/config/generators/index.js b/src/visualizations/config/generators/index.js index 290cac165..bc7a75872 100644 --- a/src/visualizations/config/generators/index.js +++ b/src/visualizations/config/generators/index.js @@ -1,8 +1,7 @@ import dhis from './dhis/index.js' -import { highcharts, singleValue } from './highcharts/index.js' +import highcharts from './highcharts/index.js' export default { highcharts, dhis, - singleValue, } diff --git a/src/visualizations/store/adapters/index.js b/src/visualizations/store/adapters/index.js index e567b54d1..7b49438ee 100644 --- a/src/visualizations/store/adapters/index.js +++ b/src/visualizations/store/adapters/index.js @@ -4,5 +4,4 @@ import dhis_highcharts from './dhis_highcharts/index.js' export default { dhis_highcharts, dhis_dhis, - dhis_singleValue: dhis_dhis, } From cfdc62414e60dd541048a158814c96c9ffa79c1b Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 18 Sep 2024 14:56:18 +0200 Subject: [PATCH 21/82] chore: re-enable no-unused-vars ESLint rule and address related issues --- src/visualizations/.eslintrc | 4 +--- .../renderer/renderSingleValueSVG.js | 2 +- .../title/__tests__/singleValue.spec.js | 17 +++++++++-------- .../config/generators/highcharts/index.js | 1 - 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/visualizations/.eslintrc b/src/visualizations/.eslintrc index f8259534e..ce5078472 100644 --- a/src/visualizations/.eslintrc +++ b/src/visualizations/.eslintrc @@ -1,7 +1,5 @@ { "rules": { - "max-params": "off", - // TODO: switch back on before merging - "no-unused-vars": "off" + "max-params": "off" } } diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js index 21f4ea398..715075df2 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js @@ -6,7 +6,7 @@ import { DynamicStyles } from './styles.js' export function renderSingleValueSVG() { const color = this.title.styles.color - const { dashboard, formattedValue, icon, subText } = + const { /* dashboard, */ formattedValue, icon, subText } = this.userOptions.customSVGOptions const dynamicStyles = new DynamicStyles() const valueElement = this.renderer diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js b/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js index 4f5843c5d..bc8022f81 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js +++ b/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js @@ -1,25 +1,24 @@ -import { VIS_TYPE_SINGLE_VALUE } from '../../../../../../modules/visTypes.js' -import getSingleValueTitle from '../singleValue.js' +import { getSingleValueTitleText } from '../singleValue.js' jest.mock('../../../../../util/getFilterText', () => () => 'The filter text') describe('getSingleValueTitle', () => { it('returns empty title when flag hideTitle exists', () => { - expect(getSingleValueTitle({ hideTitle: true })).toEqual('') + expect(getSingleValueTitleText({ hideTitle: true })).toEqual('') }) it('returns the title provided in the layout', () => { const title = 'The title was already set' - expect(getSingleValueTitle({ title })).toEqual(title) + expect(getSingleValueTitleText({ title })).toEqual(title) }) it('returns null when layout does not have columns', () => { - expect(getSingleValueTitle({})).toEqual('') + expect(getSingleValueTitleText({})).toEqual('') }) it('returns the filter text based on column items', () => { expect( - getSingleValueTitle({ + getSingleValueTitleText({ columns: [ { items: [{}], @@ -32,7 +31,7 @@ describe('getSingleValueTitle', () => { describe('not dashboard', () => { it('returns filter text as title', () => { expect( - getSingleValueTitle( + getSingleValueTitleText( { columns: [ { @@ -50,7 +49,9 @@ describe('getSingleValueTitle', () => { describe('dashboard', () => { it('returns empty string', () => { - expect(getSingleValueTitle({ filters: {} }, {}, true)).toEqual('') + expect(getSingleValueTitleText({ filters: {} }, {}, true)).toEqual( + '' + ) }) }) }) diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index f3222b257..731905aaf 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -6,7 +6,6 @@ import HNDTD from 'highcharts/modules/no-data-to-display' import HOE from 'highcharts/modules/offline-exporting' import HPF from 'highcharts/modules/pattern-fill' import HSG from 'highcharts/modules/solid-gauge' -import renderSingleValueSvg from './renderSingleValueSvg/index.js' // apply HM(H) From d72b3c0bec2c8bed960a2723b15e2512f23c1d3c Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 18 Sep 2024 16:22:33 +0200 Subject: [PATCH 22/82] chore: update background to white when exporting and background is transparent --- src/__demo__/SingleValue.stories.js | 30 ++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index f1dc22d56..a66315afb 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -665,15 +665,27 @@ storiesOf('SingleValue', module).add('default', () => { }, [containerStyle]) const downloadOffline = useCallback(() => { if (newChartRef.current) { - newChartRef.current.exportChartLocal({ - sourceHeight: 768, - sourceWidth: 1024, - scale: 1, - fallbackToExportServer: false, - filename: 'testOfflineDownload', - showExportInProgress: true, - type: 'image/png', - }) + const currentBackgroundColor = + newChartRef.current.userOptions.chart.backgroundColor + newChartRef.current.exportChartLocal( + { + sourceHeight: 768, + sourceWidth: 1024, + scale: 1, + fallbackToExportServer: false, + filename: 'testOfflineDownload', + showExportInProgress: true, + type: 'image/png', + }, + { + chart: { + backgroundColor: + currentBackgroundColor === 'transparent' + ? '#ffffff' + : currentBackgroundColor, + }, + } + ) } }, []) From 4bd39c66cc04c17ecdf9be3207e13aac30a07de6 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 19 Sep 2024 09:35:56 +0200 Subject: [PATCH 23/82] chore: give icon the same height as value text --- .../singleValue/renderer/styles.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js index f141c285d..e83dfb149 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js @@ -34,17 +34,17 @@ const subTextStyles = [ ] const spacings = [ - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 140 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 127 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 115 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 102 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 90 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 77 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 64 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 52 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 39 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 27 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 14 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 200 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 182 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 164 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 146 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 128 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 110 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 92 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 74 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 56 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 38 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 20 }, ] export const MIN_SIDE_WHITESPACE = 4 From 1af4cdaabe20ad6e259ca3fe0da7c1db2cf45f07 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 19 Sep 2024 14:43:53 +0200 Subject: [PATCH 24/82] chore: adjust story so all scenarios can be tested --- src/__demo__/SingleValue.stories.js | 520 +++++++++++++++------------- 1 file changed, 287 insertions(+), 233 deletions(-) diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index a66315afb..0236a7eac 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -14,217 +14,8 @@ const innerContainerStyle = { height: '100%', } -const data = [ - { - response: { - headers: [ - { - name: 'dx', - column: 'Data', - valueType: 'TEXT', - type: 'java.lang.String', - hidden: false, - meta: true, - }, - { - name: 'value', - column: 'Value', - valueType: 'NUMBER', - type: 'java.lang.Double', - hidden: false, - meta: false, - }, - ], - metaData: { - items: { - 202308: { - uid: '202308', - code: '202308', - name: 'August 2023', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2023-08-01T00:00:00.000', - endDate: '2023-08-31T00:00:00.000', - }, - 202309: { - uid: '202309', - code: '202309', - name: 'September 2023', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2023-09-01T00:00:00.000', - endDate: '2023-09-30T00:00:00.000', - }, - 202310: { - uid: '202310', - code: '202310', - name: 'October 2023', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2023-10-01T00:00:00.000', - endDate: '2023-10-31T00:00:00.000', - }, - 202311: { - uid: '202311', - code: '202311', - name: 'November 2023', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2023-11-01T00:00:00.000', - endDate: '2023-11-30T00:00:00.000', - }, - 202312: { - uid: '202312', - code: '202312', - name: 'December 2023', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2023-12-01T00:00:00.000', - endDate: '2023-12-31T00:00:00.000', - }, - 202401: { - uid: '202401', - code: '202401', - name: 'January 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-01-01T00:00:00.000', - endDate: '2024-01-31T00:00:00.000', - }, - 202402: { - uid: '202402', - code: '202402', - name: 'February 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-02-01T00:00:00.000', - endDate: '2024-02-29T00:00:00.000', - }, - 202403: { - uid: '202403', - code: '202403', - name: 'March 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-03-01T00:00:00.000', - endDate: '2024-03-31T00:00:00.000', - }, - 202404: { - uid: '202404', - code: '202404', - name: 'April 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-04-01T00:00:00.000', - endDate: '2024-04-30T00:00:00.000', - }, - 202405: { - uid: '202405', - code: '202405', - name: 'May 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-05-01T00:00:00.000', - endDate: '2024-05-31T00:00:00.000', - }, - 202406: { - uid: '202406', - code: '202406', - name: 'June 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-06-01T00:00:00.000', - endDate: '2024-06-30T00:00:00.000', - }, - 202407: { - uid: '202407', - code: '202407', - name: 'July 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-07-01T00:00:00.000', - endDate: '2024-07-31T00:00:00.000', - }, - ou: { - uid: 'ou', - name: 'Organisation unit', - dimensionType: 'ORGANISATION_UNIT', - }, - O6uvpzGd5pu: { - uid: 'O6uvpzGd5pu', - code: 'OU_264', - name: 'Bo', - dimensionItemType: 'ORGANISATION_UNIT', - valueType: 'TEXT', - totalAggregationType: 'SUM', - }, - LAST_12_MONTHS: { - name: 'Last 12 months', - }, - dx: { - uid: 'dx', - name: 'Data', - dimensionType: 'DATA_X', - }, - pe: { - uid: 'pe', - name: 'Period', - dimensionType: 'PERIOD', - }, - FnYCr2EAzWS: { - uid: 'FnYCr2EAzWS', - code: 'IN_52493', - name: 'BCG Coverage <1y', - legendSet: 'BtxOoQuLyg1', - dimensionItemType: 'INDICATOR', - valueType: 'NUMBER', - totalAggregationType: 'AVERAGE', - indicatorType: { - name: 'Per cent', - displayName: 'Per cent', - factor: 100, - number: false, - }, - }, - }, - dimensions: { - dx: ['FnYCr2EAzWS'], - pe: [ - '202308', - '202309', - '202310', - '202311', - '202312', - '202401', - '202402', - '202403', - '202404', - '202405', - '202406', - '202407', - ], - ou: ['O6uvpzGd5pu'], - co: [], - }, - }, - rowContext: {}, - rows: [['FnYCr2EAzWS', '34.19']], - width: 2, - height: 1, - headerWidth: 2, - }, +const baseDataObj = { + response: { headers: [ { name: 'dx', @@ -233,9 +24,6 @@ const data = [ type: 'java.lang.String', hidden: false, meta: true, - isPrefix: false, - isCollect: false, - index: 0, }, { name: 'value', @@ -244,12 +32,8 @@ const data = [ type: 'java.lang.Double', hidden: false, meta: false, - isPrefix: false, - isCollect: false, - index: 1, }, ], - rows: [['FnYCr2EAzWS', '34.19']], metaData: { items: { 202308: { @@ -406,16 +190,11 @@ const data = [ dimensionItemType: 'INDICATOR', valueType: 'NUMBER', totalAggregationType: 'AVERAGE', - // indicatorType: { - // name: 'Per cent', - // displayName: 'Per cent', - // factor: 100, - // number: false, - // }, indicatorType: { - name: 'Custom', - displayName: 'Custom subtext', - number: true, + name: 'Per cent', + displayName: 'Per cent', + factor: 100, + number: false, }, }, }, @@ -439,8 +218,231 @@ const data = [ co: [], }, }, + rowContext: {}, + rows: [['FnYCr2EAzWS', '34.19']], + width: 2, + height: 1, + headerWidth: 2, + }, + headers: [ + { + name: 'dx', + column: 'Data', + valueType: 'TEXT', + type: 'java.lang.String', + hidden: false, + meta: true, + isPrefix: false, + isCollect: false, + index: 0, + }, + { + name: 'value', + column: 'Value', + valueType: 'NUMBER', + type: 'java.lang.Double', + hidden: false, + meta: false, + isPrefix: false, + isCollect: false, + index: 1, + }, + ], + rows: [['FnYCr2EAzWS', '34.19']], + metaData: { + items: { + 202308: { + uid: '202308', + code: '202308', + name: 'August 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-08-01T00:00:00.000', + endDate: '2023-08-31T00:00:00.000', + }, + 202309: { + uid: '202309', + code: '202309', + name: 'September 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-09-01T00:00:00.000', + endDate: '2023-09-30T00:00:00.000', + }, + 202310: { + uid: '202310', + code: '202310', + name: 'October 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-10-01T00:00:00.000', + endDate: '2023-10-31T00:00:00.000', + }, + 202311: { + uid: '202311', + code: '202311', + name: 'November 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-11-01T00:00:00.000', + endDate: '2023-11-30T00:00:00.000', + }, + 202312: { + uid: '202312', + code: '202312', + name: 'December 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-12-01T00:00:00.000', + endDate: '2023-12-31T00:00:00.000', + }, + 202401: { + uid: '202401', + code: '202401', + name: 'January 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-01-01T00:00:00.000', + endDate: '2024-01-31T00:00:00.000', + }, + 202402: { + uid: '202402', + code: '202402', + name: 'February 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-02-01T00:00:00.000', + endDate: '2024-02-29T00:00:00.000', + }, + 202403: { + uid: '202403', + code: '202403', + name: 'March 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-03-01T00:00:00.000', + endDate: '2024-03-31T00:00:00.000', + }, + 202404: { + uid: '202404', + code: '202404', + name: 'April 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-04-01T00:00:00.000', + endDate: '2024-04-30T00:00:00.000', + }, + 202405: { + uid: '202405', + code: '202405', + name: 'May 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-05-01T00:00:00.000', + endDate: '2024-05-31T00:00:00.000', + }, + 202406: { + uid: '202406', + code: '202406', + name: 'June 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-06-01T00:00:00.000', + endDate: '2024-06-30T00:00:00.000', + }, + 202407: { + uid: '202407', + code: '202407', + name: 'July 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-07-01T00:00:00.000', + endDate: '2024-07-31T00:00:00.000', + }, + ou: { + uid: 'ou', + name: 'Organisation unit', + dimensionType: 'ORGANISATION_UNIT', + }, + O6uvpzGd5pu: { + uid: 'O6uvpzGd5pu', + code: 'OU_264', + name: 'Bo', + dimensionItemType: 'ORGANISATION_UNIT', + valueType: 'TEXT', + totalAggregationType: 'SUM', + }, + LAST_12_MONTHS: { + name: 'Last 12 months', + }, + dx: { + uid: 'dx', + name: 'Data', + dimensionType: 'DATA_X', + }, + pe: { + uid: 'pe', + name: 'Period', + dimensionType: 'PERIOD', + }, + FnYCr2EAzWS: { + uid: 'FnYCr2EAzWS', + code: 'IN_52493', + name: 'BCG Coverage <1y', + legendSet: 'BtxOoQuLyg1', + dimensionItemType: 'INDICATOR', + valueType: 'NUMBER', + totalAggregationType: 'AVERAGE', + }, + }, + dimensions: { + dx: ['FnYCr2EAzWS'], + pe: [ + '202308', + '202309', + '202310', + '202311', + '202312', + '202401', + '202402', + '202403', + '202404', + '202405', + '202406', + '202407', + ], + ou: ['O6uvpzGd5pu'], + co: [], + }, }, -] +} +const numberIndicatorType = { + name: 'Plain', + number: true, +} +const subtextIndicatorType = { + name: 'Custom', + displayName: 'Custom subtext', + number: true, +} +const percentIndicatorType = { + name: 'Per cent', + displayName: 'Per cent', + factor: 100, + number: false, +} const layout = { name: 'BCG coverage last 12 months - Bo', created: '2013-10-16T19:50:52.464', @@ -616,18 +618,23 @@ const layout = { const icon = '' -const extraOptions = { - dashboard: false, +const baseExtraOptions = { + dashboard: true, animation: 200, legendSets: [], icon, } +const indicatorTypes = ['plain', 'percent', 'subtext'] + storiesOf('SingleValue', module).add('default', () => { const newChartRef = useRef(null) const oldContainerRef = useRef(null) const newContainerRef = useRef(null) const [transpose, setTranspose] = useState(false) + const [dashboard, setDashboard] = useState(false) + const [showIcon, setShowIcon] = useState(false) + const [indicatorType, setIndicatorType] = useState('plain') const [width, setWidth] = useState(constainerStyleBase.width) const [height, setHeight] = useState(constainerStyleBase.height) const containerStyle = useMemo( @@ -641,8 +648,27 @@ storiesOf('SingleValue', module).add('default', () => { useEffect(() => { if (oldContainerRef.current && newContainerRef.current) { requestAnimationFrame(() => { + const extraOptions = { + ...baseExtraOptions, + dashboard, + icon: showIcon ? icon : undefined, + } + const dataObj = { ...baseDataObj } + + if (indicatorType === 'plain') { + dataObj.metaData.items.FnYCr2EAzWS.indicatorType = + numberIndicatorType + } + if (indicatorType === 'percent') { + dataObj.metaData.items.FnYCr2EAzWS.indicatorType = + percentIndicatorType + } + if (indicatorType === 'subtext') { + dataObj.metaData.items.FnYCr2EAzWS.indicatorType = + subtextIndicatorType + } createVisualization( - data, + [dataObj], layout, oldContainerRef.current, extraOptions, @@ -651,7 +677,7 @@ storiesOf('SingleValue', module).add('default', () => { 'dhis' ) const newVisualization = createVisualization( - data, + [dataObj], layout, newContainerRef.current, extraOptions, @@ -662,7 +688,7 @@ storiesOf('SingleValue', module).add('default', () => { newChartRef.current = newVisualization.visualization }) } - }, [containerStyle]) + }, [containerStyle, dashboard, showIcon, indicatorType]) const downloadOffline = useCallback(() => { if (newChartRef.current) { const currentBackgroundColor = @@ -720,6 +746,34 @@ storiesOf('SingleValue', module).add('default', () => { value={height.toString()} />
+ + + - -
-
-
-
-
-
-
-
-
+
+
) }) diff --git a/src/visualizations/.eslintrc b/src/visualizations/.eslintrc index ce5078472..f8259534e 100644 --- a/src/visualizations/.eslintrc +++ b/src/visualizations/.eslintrc @@ -1,5 +1,7 @@ { "rules": { - "max-params": "off" + "max-params": "off", + // TODO: switch back on before merging + "no-unused-vars": "off" } } diff --git a/src/visualizations/config/adapters/index.js b/src/visualizations/config/adapters/index.js index 7b49438ee..e567b54d1 100644 --- a/src/visualizations/config/adapters/index.js +++ b/src/visualizations/config/adapters/index.js @@ -4,4 +4,5 @@ import dhis_highcharts from './dhis_highcharts/index.js' export default { dhis_highcharts, dhis_dhis, + dhis_singleValue: dhis_dhis, } diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index 731905aaf..b08833342 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -6,6 +6,7 @@ import HNDTD from 'highcharts/modules/no-data-to-display' import HOE from 'highcharts/modules/offline-exporting' import HPF from 'highcharts/modules/pattern-fill' import HSG from 'highcharts/modules/solid-gauge' +import renderSingleValueSvg from './renderSingleValueSvg/index.js' // apply HM(H) @@ -71,7 +72,7 @@ function drawLegendSymbolWrap() { ) } -export default function (config, el) { +export function highcharts(config, el) { if (config) { config.chart.renderTo = el || config.chart.renderTo @@ -88,3 +89,26 @@ export default function (config, el) { return new H.Chart(config) } } + +export function singleValue(config, el, extraOptions) { + return H.chart(el, { + accessibility: { enabled: false }, + chart: { + backgroundColor: 'transparent', + events: { + load: function () { + renderSingleValueSvg(config, el, extraOptions, this) + }, + }, + }, + credits: { enabled: false }, + // exporting: { + // enabled: false, + // }, + lang: { + noData: null, + }, + noData: {}, + title: null, + }) +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js new file mode 100644 index 000000000..ce7cea956 --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js @@ -0,0 +1,38 @@ +// TODO: remove this, sch thing it should not be needed +export const svgNS = 'http://www.w3.org/2000/svg' +// multiply text width with this factor +// to get very close to actual text width +// nb: dependent on viewbox etc +export const ACTUAL_TEXT_WIDTH_FACTOR = 0.9 + +// multiply value text size with this factor +// to get very close to the actual number height +// as numbers don't go below the baseline like e.g. "j" and "g" +export const ACTUAL_NUMBER_HEIGHT_FACTOR = 0.67 + +// do not allow text width to exceed this threshold +// a threshold >1 does not really make sense but text width vs viewbox is complicated +export const TEXT_WIDTH_CONTAINER_WIDTH_FACTOR = 1.3 + +// do not allow text size to exceed this +export const TEXT_SIZE_CONTAINER_HEIGHT_FACTOR = 0.6 +export const TEXT_SIZE_MAX_THRESHOLD = 400 + +// multiply text size with this factor +// to get an appropriate letter spacing +export const LETTER_SPACING_TEXT_SIZE_FACTOR = (1 / 35) * -1 +export const LETTER_SPACING_MIN_THRESHOLD = -6 +export const LETTER_SPACING_MAX_THRESHOLD = -1 + +// fixed top margin above title/subtitle +export const TOP_MARGIN_FIXED = 16 + +// multiply text size with this factor +// to get an appropriate sub text size +export const SUB_TEXT_SIZE_FACTOR = 0.5 +export const SUB_TEXT_SIZE_MIN_THRESHOLD = 26 +export const SUB_TEXT_SIZE_MAX_THRESHOLD = 40 + +// multiply text size with this factor +// to get an appropriate icon padding +export const ICON_PADDING_FACTOR = 0.3 diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js new file mode 100644 index 000000000..2291d341d --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js @@ -0,0 +1,134 @@ +import { + defaultFontStyle, + FONT_STYLE_OPTION_BOLD, + FONT_STYLE_OPTION_FONT_SIZE, + FONT_STYLE_OPTION_ITALIC, + FONT_STYLE_OPTION_TEXT_ALIGN, + FONT_STYLE_OPTION_TEXT_COLOR, + FONT_STYLE_VISUALIZATION_SUBTITLE, + FONT_STYLE_VISUALIZATION_TITLE, + mergeFontStyleWithDefault, +} from '../../../../../modules/fontStyle.js' +import { TOP_MARGIN_FIXED } from './constants.js' +import { generateValueSVG } from './generateValueSVG.js' +import { getTextAnchorFromTextAlign } from './getTextAnchorFromTextAlign.js' +import { getXFromTextAlign } from './getXFromTextAlign.js' + +export const generateDVItem = ( + config, + { + renderer, + width, + height, + valueColor, + noData, + backgroundColor, + titleColor, + fontStyle, + icon, + } +) => { + backgroundColor = 'red' + if (backgroundColor) { + renderer + .rect(0, 0, width, height) + .attr({ fill: backgroundColor, width: '100%', height: '100%' }) + .add() + } + + // TITLE + const titleFontStyle = mergeFontStyleWithDefault( + fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_TITLE], + FONT_STYLE_VISUALIZATION_TITLE + ) + + const titleYPosition = + TOP_MARGIN_FIXED + + parseInt(titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]) + + 'px' + + const titleFontSize = `${titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px` + + renderer + .text(config.title) + .attr({ + x: getXFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]), + y: titleYPosition, + 'text-anchor': getTextAnchorFromTextAlign( + titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] + ), + 'font-size': titleFontSize, + 'font-weight': titleFontStyle[FONT_STYLE_OPTION_BOLD] + ? FONT_STYLE_OPTION_BOLD + : 'normal', + 'font-style': titleFontStyle[FONT_STYLE_OPTION_ITALIC] + ? FONT_STYLE_OPTION_ITALIC + : 'normal', + 'data-test': 'visualization-title', + fill: + titleColor && + titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === + defaultFontStyle[FONT_STYLE_VISUALIZATION_TITLE][ + FONT_STYLE_OPTION_TEXT_COLOR + ] + ? titleColor + : titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR], + }) + .add() + + // SUBTITLE + const subtitleFontStyle = mergeFontStyleWithDefault( + fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], + FONT_STYLE_VISUALIZATION_SUBTITLE + ) + const subtitleFontSize = `${subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px` + + if (config.subtitle) { + renderer + .text(config.subtitle) + .attr({ + x: getXFromTextAlign( + subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] + ), + y: titleYPosition, + dy: `${subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE] + 10}`, + 'text-anchor': getTextAnchorFromTextAlign( + subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] + ), + 'font-size': subtitleFontSize, + 'font-weight': subtitleFontStyle[FONT_STYLE_OPTION_BOLD] + ? FONT_STYLE_OPTION_BOLD + : 'normal', + 'font-style': subtitleFontStyle[FONT_STYLE_OPTION_ITALIC] + ? FONT_STYLE_OPTION_ITALIC + : 'normal', + fill: + titleColor && + subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === + defaultFontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE][ + FONT_STYLE_OPTION_TEXT_COLOR + ] + ? titleColor + : subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR], + 'data-test': 'visualization-subtitle', + }) + .add() + } + + generateValueSVG({ + renderer, + formattedValue: config.formattedValue, + subText: config.subText, + valueColor, + textColor: titleColor, + noData, + icon, + containerWidth: width, + containerHeight: height, + topMargin: + TOP_MARGIN_FIXED + + ((config.title ? parseInt(titleFontSize) : 0) + + (config.subtitle ? parseInt(subtitleFontSize) : 0)) * + 2.5, + }) +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js new file mode 100644 index 000000000..4d8e0292a --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js @@ -0,0 +1,135 @@ +import { colors } from '@dhis2/ui' +import { + LETTER_SPACING_MAX_THRESHOLD, + LETTER_SPACING_MIN_THRESHOLD, + LETTER_SPACING_TEXT_SIZE_FACTOR, + SUB_TEXT_SIZE_FACTOR, + SUB_TEXT_SIZE_MAX_THRESHOLD, + SUB_TEXT_SIZE_MIN_THRESHOLD, + svgNS, +} from './constants.js' +import { + getIconPadding, + getTextHeightForNumbers, + getTextSize, + getTextWidth, +} from './textSize.js' + +export const generateValueSVG = ({ + renderer, + formattedValue, + subText, + valueColor, + textColor, + icon, + noData, + containerWidth, + containerHeight, + topMargin = 0, +}) => { + console.log('show value') + const showIcon = icon && formattedValue !== noData.text + + const textSize = getTextSize( + formattedValue, + containerWidth, + containerHeight, + showIcon + ) + + const textWidth = getTextWidth(formattedValue, `${textSize}px Roboto`) + + const iconSize = textSize + + const subTextSize = + textSize * SUB_TEXT_SIZE_FACTOR > SUB_TEXT_SIZE_MAX_THRESHOLD + ? SUB_TEXT_SIZE_MAX_THRESHOLD + : textSize * SUB_TEXT_SIZE_FACTOR < SUB_TEXT_SIZE_MIN_THRESHOLD + ? SUB_TEXT_SIZE_MIN_THRESHOLD + : textSize * SUB_TEXT_SIZE_FACTOR + + const svgValue = document.createElementNS(svgNS, 'svg') + svgValue.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`) + svgValue.setAttribute('width', '50%') + svgValue.setAttribute('height', '50%') + svgValue.setAttribute('x', '50%') + svgValue.setAttribute('y', '50%') + svgValue.setAttribute('style', 'overflow: visible') + + let fillColor = colors.grey900 + + if (valueColor) { + fillColor = valueColor + } else if (formattedValue === noData.text) { + fillColor = colors.grey600 + } + + // show icon if configured in maintenance app + if (showIcon) { + // embed icon to allow changing color + // (elements with fill need to use "currentColor" for this to work) + const iconSvgNode = document.createElementNS(svgNS, 'svg') + iconSvgNode.setAttribute('viewBox', '0 0 48 48') + iconSvgNode.setAttribute('width', iconSize) + iconSvgNode.setAttribute('height', iconSize) + iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1) + iconSvgNode.setAttribute( + 'x', + `-${(iconSize + getIconPadding(textSize) + textWidth) / 2}` + ) + iconSvgNode.setAttribute('style', `color: ${fillColor}`) + iconSvgNode.setAttribute('data-test', 'visualization-icon') + + const parser = new DOMParser() + const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml') + + Array.from(svgIconDocument.documentElement.children).forEach((node) => + iconSvgNode.appendChild(node) + ) + + svgValue.appendChild(iconSvgNode) + } + + const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) + + const textNode = document.createElementNS(svgNS, 'text') + textNode.setAttribute('font-size', textSize) + textNode.setAttribute('font-weight', '300') + textNode.setAttribute( + 'letter-spacing', + letterSpacing < LETTER_SPACING_MIN_THRESHOLD + ? LETTER_SPACING_MIN_THRESHOLD + : letterSpacing > LETTER_SPACING_MAX_THRESHOLD + ? LETTER_SPACING_MAX_THRESHOLD + : letterSpacing + ) + textNode.setAttribute('text-anchor', 'middle') + textNode.setAttribute( + 'x', + showIcon ? `${(iconSize + getIconPadding(textSize)) / 2}` : 0 + ) + textNode.setAttribute( + 'y', + topMargin / 2 + getTextHeightForNumbers(textSize) / 2 + ) + textNode.setAttribute('fill', fillColor) + textNode.setAttribute('data-test', 'visualization-primary-value') + + textNode.appendChild(document.createTextNode(formattedValue)) + + svgValue.appendChild(textNode) + + if (subText) { + const subTextNode = document.createElementNS(svgNS, 'text') + subTextNode.setAttribute('text-anchor', 'middle') + subTextNode.setAttribute('font-size', subTextSize) + subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2) + subTextNode.setAttribute('dy', subTextSize * 1.7) + subTextNode.setAttribute('fill', textColor) + subTextNode.appendChild(document.createTextNode(subText)) + + svgValue.appendChild(subTextNode) + } + + return svgValue +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js new file mode 100644 index 000000000..5d66ba074 --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js @@ -0,0 +1,17 @@ +import { + TEXT_ALIGN_LEFT, + TEXT_ALIGN_CENTER, + TEXT_ALIGN_RIGHT, +} from '../../../../../modules/fontStyle.js' + +export const getTextAnchorFromTextAlign = (textAlign) => { + switch (textAlign) { + default: + case TEXT_ALIGN_LEFT: + return 'start' + case TEXT_ALIGN_CENTER: + return 'middle' + case TEXT_ALIGN_RIGHT: + return 'end' + } +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js new file mode 100644 index 000000000..d9383b4e9 --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js @@ -0,0 +1,17 @@ +import { + TEXT_ALIGN_LEFT, + TEXT_ALIGN_CENTER, + TEXT_ALIGN_RIGHT, +} from '../../../../../modules/fontStyle.js' + +export const getXFromTextAlign = (textAlign) => { + switch (textAlign) { + default: + case TEXT_ALIGN_LEFT: + return '1%' + case TEXT_ALIGN_CENTER: + return '50%' + case TEXT_ALIGN_RIGHT: + return '99%' + } +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js new file mode 100644 index 000000000..be1fc9cde --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js @@ -0,0 +1,76 @@ +import { colors } from '@dhis2/ui' +import { + getColorByValueFromLegendSet, + LEGEND_DISPLAY_STYLE_FILL, +} from '../../../../../modules/legends.js' +import { generateDVItem } from './generateDVItem.js' +import { shouldUseContrastColor } from './shouldUseContrastColor.js' + +export default function ( + config, + parentEl, + { dashboard, legendSets, fontStyle, noData, legendOptions, icon }, + chart +) { + const renderer = chart.renderer + const legendSet = legendOptions && legendSets[0] + const legendColor = + legendSet && getColorByValueFromLegendSet(legendSet, config.value) + let valueColor, titleColor, backgroundColor + if (legendColor) { + if (legendOptions.style === LEGEND_DISPLAY_STYLE_FILL) { + backgroundColor = legendColor + valueColor = titleColor = + shouldUseContrastColor(legendColor) && colors.white + } else { + valueColor = legendColor + } + } + + parentEl.style.overflow = 'hidden' + parentEl.style.display = 'flex' + parentEl.style.justifyContent = 'center' + + // We need the inner width so borders etc are excluded + const width = parentEl.clientWidth + const height = parentEl.clientHeight + + const svgContainer = renderer.box + svgContainer.setAttribute('viewBox', `0 0 ${width} ${height}`) + svgContainer.setAttribute('data-test', 'visualization-container') + + chart.setSize(dashboard ? '100%' : width, dashboard ? '100%' : height) + + // if (dashboard) { + // parentEl.style.borderRadius = '3px' + + // return generateDashboardItem(config, { + // svgContainer, + // width, + // height, + // valueColor, + // backgroundColor, + // noData, + // icon, + // ...(legendOptions.style === LEGEND_DISPLAY_STYLE_FILL && + // legendColor && + // shouldUseContrastColor(legendColor) + // ? { titleColor: colors.white } + // : {}), + // }) + // } else { + parentEl.style.height = `100%` + + return generateDVItem(config, { + renderer, + width, + height, + valueColor, + backgroundColor, + titleColor, + noData, + icon, + fontStyle, + }) + // } +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/shouldUseContrastColor.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/shouldUseContrastColor.js new file mode 100644 index 000000000..d01616c9a --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/shouldUseContrastColor.js @@ -0,0 +1,17 @@ +export const shouldUseContrastColor = (inputColor = '') => { + // based on https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color + var color = + inputColor.charAt(0) === '#' ? inputColor.substring(1, 7) : inputColor + var r = parseInt(color.substring(0, 2), 16) // hexToR + var g = parseInt(color.substring(2, 4), 16) // hexToG + var b = parseInt(color.substring(4, 6), 16) // hexToB + var uicolors = [r / 255, g / 255, b / 255] + var c = uicolors.map((col) => { + if (col <= 0.03928) { + return col / 12.92 + } + return Math.pow((col + 0.055) / 1.055, 2.4) + }) + var L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2] + return L <= 0.179 +} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js new file mode 100644 index 000000000..a94ad7266 --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js @@ -0,0 +1,52 @@ +// Compute text width before rendering + +import { + ACTUAL_NUMBER_HEIGHT_FACTOR, + ACTUAL_TEXT_WIDTH_FACTOR, + ICON_PADDING_FACTOR, + TEXT_SIZE_CONTAINER_HEIGHT_FACTOR, + TEXT_SIZE_MAX_THRESHOLD, + TEXT_WIDTH_CONTAINER_WIDTH_FACTOR, +} from './constants.js' + +// Not exactly precise but close enough +export const getTextWidth = (text, font) => { + const canvas = document.createElement('canvas') + const context = canvas.getContext('2d') + context.font = font + return Math.round( + context.measureText(text).width * ACTUAL_TEXT_WIDTH_FACTOR + ) +} + +export const getTextHeightForNumbers = (textSize) => + textSize * ACTUAL_NUMBER_HEIGHT_FACTOR + +export const getIconPadding = (textSize) => + Math.round(textSize * ICON_PADDING_FACTOR) + +export const getTextSize = ( + formattedValue, + containerWidth, + containerHeight, + showIcon +) => { + let size = Math.min( + Math.round(containerHeight * TEXT_SIZE_CONTAINER_HEIGHT_FACTOR), + TEXT_SIZE_MAX_THRESHOLD + ) + + const widthThreshold = Math.round( + containerWidth * TEXT_WIDTH_CONTAINER_WIDTH_FACTOR + ) + + const textWidth = + getTextWidth(formattedValue, `${size}px Roboto`) + + (showIcon ? getIconPadding(size) : 0) + + if (textWidth > widthThreshold) { + size = Math.round(size * (widthThreshold / textWidth)) + } + + return size +} diff --git a/src/visualizations/config/generators/index.js b/src/visualizations/config/generators/index.js index bc7a75872..290cac165 100644 --- a/src/visualizations/config/generators/index.js +++ b/src/visualizations/config/generators/index.js @@ -1,7 +1,8 @@ import dhis from './dhis/index.js' -import highcharts from './highcharts/index.js' +import { highcharts, singleValue } from './highcharts/index.js' export default { highcharts, dhis, + singleValue, } diff --git a/src/visualizations/store/adapters/index.js b/src/visualizations/store/adapters/index.js index 7b49438ee..e567b54d1 100644 --- a/src/visualizations/store/adapters/index.js +++ b/src/visualizations/store/adapters/index.js @@ -4,4 +4,5 @@ import dhis_highcharts from './dhis_highcharts/index.js' export default { dhis_highcharts, dhis_dhis, + dhis_singleValue: dhis_dhis, } From 9fe54eda62ecd854d503d146e08c164f1e8eaa46 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Mon, 2 Sep 2024 15:36:47 +0200 Subject: [PATCH 32/82] chore: resizing the container --- src/__demo__/SingleValue.stories.js | 63 +++++++++++++++++-- .../config/generators/highcharts/index.js | 17 ++++- .../renderSingleValueSvg/generateValueSVG.js | 16 ++++- 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index 1720ac28e..ac71e1e1b 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -1,7 +1,7 @@ import { storiesOf } from '@storybook/react' -import React, { useCallback } from 'react' +import React, { useCallback, useState, useMemo, useRef, useEffect } from 'react' import { createVisualization } from '../index.js' -const constainerStyle = { +const constainerStyleBase = { width: 400, height: 400, border: '1px solid magenta', @@ -601,13 +601,20 @@ const layout = { }, axes: [], } +// const icon = +// '' + const extraOptions = { dashboard: false, animation: 200, legendSets: [], + // icon, } storiesOf('SingleValue', module).add('default', () => { + const newChartRef = useRef(null) + const [width, setWidth] = useState(constainerStyleBase.width) + const [height, setHeight] = useState(constainerStyleBase.height) const onOldContainerMounted = useCallback((el) => { createVisualization( data, @@ -620,7 +627,7 @@ storiesOf('SingleValue', module).add('default', () => { ) }, []) const onNewContainerMounted = useCallback((el) => { - createVisualization( + const obj = createVisualization( data, layout, el, @@ -629,11 +636,57 @@ storiesOf('SingleValue', module).add('default', () => { undefined, 'singleValue' ) + newChartRef.current = obj.visualization }, []) + const containerStyle = useMemo( + () => ({ + ...constainerStyleBase, + width, + height, + }), + [width, height] + ) + useEffect(() => { + if (newChartRef.current) { + console.log('calling reflow') + newChartRef.current.redraw() + } + }, [containerStyle]) + return ( <> -
-
+
+
+
+
+ + + setWidth(parseInt(event.target.value)) + } + value={width.toString()} + /> +
+
+ + + setHeight(parseInt(event.target.value)) + } + value={height.toString()} + /> +
+
) }) diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index b08833342..f9c619c67 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -91,15 +91,28 @@ export function highcharts(config, el) { } export function singleValue(config, el, extraOptions) { + console.log('el', el) + let elClientHeight, elClientWidth return H.chart(el, { accessibility: { enabled: false }, chart: { backgroundColor: 'transparent', events: { - load: function () { - renderSingleValueSvg(config, el, extraOptions, this) + redraw: function () { + if ( + el.clientHeight !== elClientHeight || + el.clientWidth !== elClientWidth + ) { + console.log('resize!!!', el) + elClientHeight = el.clientHeight + elClientWidth = el.clientWidth + renderSingleValueSvg(config, el, extraOptions, this) + } else { + console.log('No action needed') + } }, }, + animation: false, }, credits: { enabled: false }, // exporting: { diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js index 4d8e0292a..99f2247f3 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js @@ -27,7 +27,7 @@ export const generateValueSVG = ({ containerHeight, topMargin = 0, }) => { - console.log('show value') + console.log('show value', renderer) const showIcon = icon && formattedValue !== noData.text const textSize = getTextSize( @@ -56,6 +56,20 @@ export const generateValueSVG = ({ svgValue.setAttribute('y', '50%') svgValue.setAttribute('style', 'overflow: visible') + const box = renderer + .rect(0, 0, containerWidth, containerHeight) + .attr({ + with: '50%', + height: '50%', + x: '50%', + y: '50%', + }) + .css({ + overflow: 'visible', + backgroundColor: 'green', + }) + .add() + let fillColor = colors.grey900 if (valueColor) { From d31e0507258de19b3a8607e937a860e8d2adc374 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 4 Sep 2024 11:46:05 +0200 Subject: [PATCH 33/82] chore: scaling the icon correctly --- src/__demo__/SingleValue.stories.js | 119 ++++++++++---- .../config/generators/dhis/singleValue.js | 1 + .../config/generators/highcharts/index.js | 31 ++-- .../renderSingleValueSvg/generateValueSVG.js | 150 ++++++++---------- .../generateValueSVGOLD.js | 135 ++++++++++++++++ 5 files changed, 304 insertions(+), 132 deletions(-) create mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index ac71e1e1b..0b3bda6fa 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -1,5 +1,5 @@ import { storiesOf } from '@storybook/react' -import React, { useCallback, useState, useMemo, useRef, useEffect } from 'react' +import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react' import { createVisualization } from '../index.js' const constainerStyleBase = { width: 400, @@ -7,6 +7,13 @@ const constainerStyleBase = { border: '1px solid magenta', marginBottom: 14, } +const innerContainerStyle = { + overflow: 'hidden', + display: 'flex', + justifyContent: 'center', + height: '100%', +} + const data = [ { response: { @@ -601,43 +608,23 @@ const layout = { }, axes: [], } -// const icon = -// '' +const icon = + '' const extraOptions = { dashboard: false, animation: 200, legendSets: [], - // icon, + icon, } storiesOf('SingleValue', module).add('default', () => { const newChartRef = useRef(null) + const oldContainerRef = useRef(null) + const newContainerRef = useRef(null) + const [transpose, setTranspose] = useState(false) const [width, setWidth] = useState(constainerStyleBase.width) const [height, setHeight] = useState(constainerStyleBase.height) - const onOldContainerMounted = useCallback((el) => { - createVisualization( - data, - layout, - el, - extraOptions, - undefined, - undefined, - 'dhis' - ) - }, []) - const onNewContainerMounted = useCallback((el) => { - const obj = createVisualization( - data, - layout, - el, - extraOptions, - undefined, - undefined, - 'singleValue' - ) - newChartRef.current = obj.visualization - }, []) const containerStyle = useMemo( () => ({ ...constainerStyleBase, @@ -647,17 +634,47 @@ storiesOf('SingleValue', module).add('default', () => { [width, height] ) useEffect(() => { - if (newChartRef.current) { - console.log('calling reflow') - newChartRef.current.redraw() + if (oldContainerRef.current && newContainerRef.current) { + requestAnimationFrame(() => { + createVisualization( + data, + layout, + oldContainerRef.current, + extraOptions, + undefined, + undefined, + 'dhis' + ) + const newVisualization = createVisualization( + data, + layout, + newContainerRef.current, + extraOptions, + undefined, + undefined, + 'singleValue' + ) + newChartRef.current = newVisualization.visualization + }) } }, [containerStyle]) + const downloadOffline = useCallback(() => { + if (newChartRef.current) { + newChartRef.current.exportChartLocal({ + sourceHeight: 768, + sourceWidth: 1024, + scale: 1, + fallbackToExportServer: false, + filename: 'testOfflineDownload', + showExportInProgress: true, + type: 'image/png', + }) + } + }, []) return ( <> -
-
-
+
{ value={height.toString()} />
+ + +
+
+
+
+
+
+
+
) diff --git a/src/visualizations/config/generators/dhis/singleValue.js b/src/visualizations/config/generators/dhis/singleValue.js index 25ec5bab9..934e14fdb 100644 --- a/src/visualizations/config/generators/dhis/singleValue.js +++ b/src/visualizations/config/generators/dhis/singleValue.js @@ -151,6 +151,7 @@ const generateValueSVG = ({ // embed icon to allow changing color // (elements with fill need to use "currentColor" for this to work) const iconSvgNode = document.createElementNS(svgNS, 'svg') + console.log('old', iconSize) iconSvgNode.setAttribute('viewBox', '0 0 48 48') iconSvgNode.setAttribute('width', iconSize) iconSvgNode.setAttribute('height', iconSize) diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index f9c619c67..7ac83e864 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -91,33 +91,30 @@ export function highcharts(config, el) { } export function singleValue(config, el, extraOptions) { - console.log('el', el) - let elClientHeight, elClientWidth return H.chart(el, { accessibility: { enabled: false }, chart: { backgroundColor: 'transparent', events: { - redraw: function () { - if ( - el.clientHeight !== elClientHeight || - el.clientWidth !== elClientWidth - ) { - console.log('resize!!!', el) - elClientHeight = el.clientHeight - elClientWidth = el.clientWidth - renderSingleValueSvg(config, el, extraOptions, this) - } else { - console.log('No action needed') - } + load: function () { + renderSingleValueSvg(config, el, extraOptions, this) }, }, animation: false, }, credits: { enabled: false }, - // exporting: { - // enabled: false, - // }, + exporting: { + enabled: true, + error: (options, error) => { + console.log('options', options) + console.log(error) + }, + chartOptions: { + title: { + text: null, + }, + }, + }, lang: { noData: null, }, diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js index 99f2247f3..dd7efd382 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js @@ -1,12 +1,12 @@ import { colors } from '@dhis2/ui' import { + svgNS, LETTER_SPACING_MAX_THRESHOLD, LETTER_SPACING_MIN_THRESHOLD, LETTER_SPACING_TEXT_SIZE_FACTOR, SUB_TEXT_SIZE_FACTOR, SUB_TEXT_SIZE_MAX_THRESHOLD, SUB_TEXT_SIZE_MIN_THRESHOLD, - svgNS, } from './constants.js' import { getIconPadding, @@ -15,6 +15,8 @@ import { getTextWidth, } from './textSize.js' +const parser = new DOMParser() + export const generateValueSVG = ({ renderer, formattedValue, @@ -27,8 +29,13 @@ export const generateValueSVG = ({ containerHeight, topMargin = 0, }) => { - console.log('show value', renderer) const showIcon = icon && formattedValue !== noData.text + const group = renderer + .g('value') + .css({ + transform: 'scale(0.5) translate(100%, 100%)', + }) + .add() const textSize = getTextSize( formattedValue, @@ -48,28 +55,6 @@ export const generateValueSVG = ({ ? SUB_TEXT_SIZE_MIN_THRESHOLD : textSize * SUB_TEXT_SIZE_FACTOR - const svgValue = document.createElementNS(svgNS, 'svg') - svgValue.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`) - svgValue.setAttribute('width', '50%') - svgValue.setAttribute('height', '50%') - svgValue.setAttribute('x', '50%') - svgValue.setAttribute('y', '50%') - svgValue.setAttribute('style', 'overflow: visible') - - const box = renderer - .rect(0, 0, containerWidth, containerHeight) - .attr({ - with: '50%', - height: '50%', - x: '50%', - y: '50%', - }) - .css({ - overflow: 'visible', - backgroundColor: 'green', - }) - .add() - let fillColor = colors.grey900 if (valueColor) { @@ -78,72 +63,73 @@ export const generateValueSVG = ({ fillColor = colors.grey600 } + const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) + + const formattedValueText = renderer + .text(formattedValue) + .attr({ + 'font-size': textSize, + 'font-weight': '300', + 'letter-spacing': + letterSpacing < LETTER_SPACING_MIN_THRESHOLD + ? LETTER_SPACING_MIN_THRESHOLD + : letterSpacing > LETTER_SPACING_MAX_THRESHOLD + ? LETTER_SPACING_MAX_THRESHOLD + : letterSpacing, + 'text-anchor': 'middle', + width: '100%', + x: showIcon ? `${iconSize / 2 + getIconPadding(textSize / 2)}` : 0, + y: topMargin / 2 + getTextHeightForNumbers(textSize) / 2, + fill: fillColor, + 'data-test': 'visualization-primary-value', + }) + .add(group) + // show icon if configured in maintenance app if (showIcon) { - // embed icon to allow changing color - // (elements with fill need to use "currentColor" for this to work) - const iconSvgNode = document.createElementNS(svgNS, 'svg') - iconSvgNode.setAttribute('viewBox', '0 0 48 48') - iconSvgNode.setAttribute('width', iconSize) - iconSvgNode.setAttribute('height', iconSize) - iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1) - iconSvgNode.setAttribute( - 'x', - `-${(iconSize + getIconPadding(textSize) + textWidth) / 2}` - ) - iconSvgNode.setAttribute('style', `color: ${fillColor}`) - iconSvgNode.setAttribute('data-test', 'visualization-icon') - - const parser = new DOMParser() const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml') + const iconElHeight = + svgIconDocument.documentElement.getAttribute('height') + const iconElWidth = + svgIconDocument.documentElement.getAttribute('width') + const x = ((iconSize + getIconPadding(textSize) + textWidth) / 2) * -1 + const y = (iconSize / 2 - topMargin / 2) * -1 + const iconGroup = renderer + .g('icon') + .attr('data-test', 'visualization-icon') + .css({ + color: 'green', + // color: fillColor, + }) + /* Force the group element to have the same dimensions as the original + * SVG image by adding this rect. This ensures the icon has the intended + * whitespace around it and makes scaling and translating easier. */ + renderer.rect(0, 0, iconElWidth, iconElHeight).add(iconGroup) Array.from(svgIconDocument.documentElement.children).forEach((node) => - iconSvgNode.appendChild(node) + iconGroup.element.appendChild(node) ) - - svgValue.appendChild(iconSvgNode) + iconGroup.add() + const formattedValueBox = formattedValueText.getBBox() + const targetHeight = textSize / 2 + const scaleFactor = targetHeight / iconElHeight + + console.log(formattedValueBox) + iconGroup.css({ + transform: `scale(${scaleFactor}) translate(16px, 104px)`, + }) } - const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) - - const textNode = document.createElementNS(svgNS, 'text') - textNode.setAttribute('font-size', textSize) - textNode.setAttribute('font-weight', '300') - textNode.setAttribute( - 'letter-spacing', - letterSpacing < LETTER_SPACING_MIN_THRESHOLD - ? LETTER_SPACING_MIN_THRESHOLD - : letterSpacing > LETTER_SPACING_MAX_THRESHOLD - ? LETTER_SPACING_MAX_THRESHOLD - : letterSpacing - ) - textNode.setAttribute('text-anchor', 'middle') - textNode.setAttribute( - 'x', - showIcon ? `${(iconSize + getIconPadding(textSize)) / 2}` : 0 - ) - textNode.setAttribute( - 'y', - topMargin / 2 + getTextHeightForNumbers(textSize) / 2 - ) - textNode.setAttribute('fill', fillColor) - textNode.setAttribute('data-test', 'visualization-primary-value') - - textNode.appendChild(document.createTextNode(formattedValue)) - - svgValue.appendChild(textNode) - if (subText) { - const subTextNode = document.createElementNS(svgNS, 'text') - subTextNode.setAttribute('text-anchor', 'middle') - subTextNode.setAttribute('font-size', subTextSize) - subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2) - subTextNode.setAttribute('dy', subTextSize * 1.7) - subTextNode.setAttribute('fill', textColor) - subTextNode.appendChild(document.createTextNode(subText)) - - svgValue.appendChild(subTextNode) + renderer + .text(subText) + .attr({ + 'text-anchor': 'middle', + 'font-size': subTextSize, + y: iconSize / 2 + topMargin / 2, + dy: subTextSize * 1.7, + fill: textColor, + }) + .add(group) } - - return svgValue } diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js new file mode 100644 index 000000000..1a36f7eda --- /dev/null +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js @@ -0,0 +1,135 @@ +import { colors } from '@dhis2/ui' +import { + svgNS, + LETTER_SPACING_MAX_THRESHOLD, + LETTER_SPACING_MIN_THRESHOLD, + LETTER_SPACING_TEXT_SIZE_FACTOR, + SUB_TEXT_SIZE_FACTOR, + SUB_TEXT_SIZE_MAX_THRESHOLD, + SUB_TEXT_SIZE_MIN_THRESHOLD, +} from './constants.js' +import { + getIconPadding, + getTextHeightForNumbers, + getTextSize, + getTextWidth, +} from './textSize.js' + +export const generateValueSVG = ({ + renderer, + formattedValue, + subText, + valueColor, + textColor, + icon, + noData, + containerWidth, + containerHeight, + topMargin = 0, +}) => { + const showIcon = icon && formattedValue !== noData.text + + const textSize = getTextSize( + formattedValue, + containerWidth, + containerHeight, + showIcon + ) + + const textWidth = getTextWidth(formattedValue, `${textSize}px Roboto`) + + const iconSize = textSize + + const subTextSize = + textSize * SUB_TEXT_SIZE_FACTOR > SUB_TEXT_SIZE_MAX_THRESHOLD + ? SUB_TEXT_SIZE_MAX_THRESHOLD + : textSize * SUB_TEXT_SIZE_FACTOR < SUB_TEXT_SIZE_MIN_THRESHOLD + ? SUB_TEXT_SIZE_MIN_THRESHOLD + : textSize * SUB_TEXT_SIZE_FACTOR + + const svgValue = document.createElementNS(svgNS, 'svg') + svgValue.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`) + svgValue.setAttribute('width', '50%') + svgValue.setAttribute('height', '50%') + svgValue.setAttribute('x', '50%') + svgValue.setAttribute('y', '50%') + svgValue.setAttribute('style', 'overflow: visible') + + let fillColor = colors.grey900 + + if (valueColor) { + fillColor = valueColor + } else if (formattedValue === noData.text) { + fillColor = colors.grey600 + } + + // show icon if configured in maintenance app + if (showIcon) { + // embed icon to allow changing color + // (elements with fill need to use "currentColor" for this to work) + const iconSvgNode = document.createElementNS(svgNS, 'svg') + console.log('old', iconSize) + iconSvgNode.setAttribute('viewBox', '0 0 48 48') + iconSvgNode.setAttribute('width', iconSize) + iconSvgNode.setAttribute('height', iconSize) + iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1) + iconSvgNode.setAttribute( + 'x', + `-${(iconSize + getIconPadding(textSize) + textWidth) / 2}` + ) + iconSvgNode.setAttribute('style', `color: ${fillColor}`) + iconSvgNode.setAttribute('data-test', 'visualization-icon') + + const parser = new DOMParser() + const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml') + + Array.from(svgIconDocument.documentElement.children).forEach((node) => + iconSvgNode.appendChild(node) + ) + + svgValue.appendChild(iconSvgNode) + } + + const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) + + const textNode = document.createElementNS(svgNS, 'text') + textNode.setAttribute('font-size', textSize) + textNode.setAttribute('font-weight', '300') + textNode.setAttribute( + 'letter-spacing', + letterSpacing < LETTER_SPACING_MIN_THRESHOLD + ? LETTER_SPACING_MIN_THRESHOLD + : letterSpacing > LETTER_SPACING_MAX_THRESHOLD + ? LETTER_SPACING_MAX_THRESHOLD + : letterSpacing + ) + textNode.setAttribute('text-anchor', 'middle') + textNode.setAttribute( + 'x', + showIcon ? `${(iconSize + getIconPadding(textSize)) / 2}` : 0 + ) + textNode.setAttribute( + 'y', + topMargin / 2 + getTextHeightForNumbers(textSize) / 2 + ) + textNode.setAttribute('fill', fillColor) + textNode.setAttribute('data-test', 'visualization-primary-value') + + textNode.appendChild(document.createTextNode(formattedValue)) + + svgValue.appendChild(textNode) + + if (subText) { + const subTextNode = document.createElementNS(svgNS, 'text') + subTextNode.setAttribute('text-anchor', 'middle') + subTextNode.setAttribute('font-size', subTextSize) + subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2) + subTextNode.setAttribute('dy', subTextSize * 1.7) + subTextNode.setAttribute('fill', textColor) + subTextNode.appendChild(document.createTextNode(subText)) + + svgValue.appendChild(subTextNode) + } + + renderer.box.appendChild(svgValue) +} From 16280c1a033e2c1662e7292cae7161d7fa2c251a Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 4 Sep 2024 16:32:43 +0200 Subject: [PATCH 34/82] fix: rework old implementation WIP --- src/__demo__/SingleValue.stories.js | 19 +++++++++----- .../renderSingleValueSvg/constants.js | 2 +- .../renderSingleValueSvg/generateValueSVG.js | 26 +++++++++---------- 3 files changed, 26 insertions(+), 21 deletions(-) diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index 0b3bda6fa..f28b775df 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -2,8 +2,8 @@ import { storiesOf } from '@storybook/react' import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react' import { createVisualization } from '../index.js' const constainerStyleBase = { - width: 400, - height: 400, + width: 800, + height: 800, border: '1px solid magenta', marginBottom: 14, } @@ -406,11 +406,16 @@ const data = [ dimensionItemType: 'INDICATOR', valueType: 'NUMBER', totalAggregationType: 'AVERAGE', + // indicatorType: { + // name: 'Per cent', + // displayName: 'Per cent', + // factor: 100, + // number: false, + // }, indicatorType: { - name: 'Per cent', - displayName: 'Per cent', - factor: 100, - number: false, + name: 'Custom', + displayName: 'Custom subtext', + number: true, }, }, }, @@ -720,7 +725,7 @@ storiesOf('SingleValue', module).add('default', () => { ...containerStyle, ...{ opacity: 0.45, - transform: 'translateX(410px)', + transform: `translateX(${width + 10}px)`, zIndex: 10, backgroundColor: 'purple', }, diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js index ce7cea956..06fd79fd0 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js @@ -16,7 +16,7 @@ export const TEXT_WIDTH_CONTAINER_WIDTH_FACTOR = 1.3 // do not allow text size to exceed this export const TEXT_SIZE_CONTAINER_HEIGHT_FACTOR = 0.6 -export const TEXT_SIZE_MAX_THRESHOLD = 400 +export const TEXT_SIZE_MAX_THRESHOLD = 200 // multiply text size with this factor // to get an appropriate letter spacing diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js index dd7efd382..5f365f0b5 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js @@ -33,7 +33,7 @@ export const generateValueSVG = ({ const group = renderer .g('value') .css({ - transform: 'scale(0.5) translate(100%, 100%)', + transform: 'translate(50%, 50%)', }) .add() @@ -92,14 +92,11 @@ export const generateValueSVG = ({ svgIconDocument.documentElement.getAttribute('height') const iconElWidth = svgIconDocument.documentElement.getAttribute('width') - const x = ((iconSize + getIconPadding(textSize) + textWidth) / 2) * -1 - const y = (iconSize / 2 - topMargin / 2) * -1 const iconGroup = renderer .g('icon') .attr('data-test', 'visualization-icon') .css({ - color: 'green', - // color: fillColor, + color: fillColor, }) /* Force the group element to have the same dimensions as the original * SVG image by adding this rect. This ensures the icon has the intended @@ -109,15 +106,18 @@ export const generateValueSVG = ({ Array.from(svgIconDocument.documentElement.children).forEach((node) => iconGroup.element.appendChild(node) ) - iconGroup.add() - const formattedValueBox = formattedValueText.getBBox() - const targetHeight = textSize / 2 - const scaleFactor = targetHeight / iconElHeight + const formattedValueTextBox = formattedValueText.getBBox() + const scaleFactor = textSize / iconElHeight + const textHeight = formattedValueTextBox.height / 2 + const iconHeight = (iconElHeight * scaleFactor) / 2 + const translateY = + (formattedValueTextBox.y + (textHeight - iconHeight)) / scaleFactor - console.log(formattedValueBox) - iconGroup.css({ - transform: `scale(${scaleFactor}) translate(16px, 104px)`, - }) + iconGroup + .css({ + transform: `scale(${scaleFactor}) translate(-98px, ${translateY}px)`, + }) + .add(group) } if (subText) { From 3c67d83cfd16ad3a4209bb622efffea400f436b7 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 10 Sep 2024 17:37:32 +0200 Subject: [PATCH 35/82] chore: align single value store with other highcharts charts --- .../store/adapters/dhis_highcharts/index.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/visualizations/store/adapters/dhis_highcharts/index.js b/src/visualizations/store/adapters/dhis_highcharts/index.js index 22f70cc1d..fa8b85272 100644 --- a/src/visualizations/store/adapters/dhis_highcharts/index.js +++ b/src/visualizations/store/adapters/dhis_highcharts/index.js @@ -112,9 +112,20 @@ function getSeriesFunction(type, categoryIds) { export default function ({ type, data, seriesId, categoryIds, extraOptions }) { categoryIds = categoryIds || [] + // if (type === VIS_TYPE_SINGLE_VALUE) { + // console.log('I want to do things here', data) + // const categoryId = data[0].metaData.dimensions.dx[0] + // return getSingleValue({ + // type, + // data, + // seriesId, + // categoryId, + // }) + // } + const seriesFunction = getSeriesFunction(type, categoryIds) - return data.reduce((acc, res) => { + const returnValue = data.reduce((acc, res) => { const headers = res.headers const metaData = res.metaData const rows = res.rows @@ -146,4 +157,6 @@ export default function ({ type, data, seriesId, categoryIds, extraOptions }) { return acc }, []) + console.log('IS THIS IT THEN?????', returnValue) + return returnValue } From 9b199b591d2a20d12b9f5cfef3f92d2353a39820 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 10 Sep 2024 17:42:18 +0200 Subject: [PATCH 36/82] chore: clean up comments and console log --- .../store/adapters/dhis_highcharts/index.js | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/visualizations/store/adapters/dhis_highcharts/index.js b/src/visualizations/store/adapters/dhis_highcharts/index.js index fa8b85272..22f70cc1d 100644 --- a/src/visualizations/store/adapters/dhis_highcharts/index.js +++ b/src/visualizations/store/adapters/dhis_highcharts/index.js @@ -112,20 +112,9 @@ function getSeriesFunction(type, categoryIds) { export default function ({ type, data, seriesId, categoryIds, extraOptions }) { categoryIds = categoryIds || [] - // if (type === VIS_TYPE_SINGLE_VALUE) { - // console.log('I want to do things here', data) - // const categoryId = data[0].metaData.dimensions.dx[0] - // return getSingleValue({ - // type, - // data, - // seriesId, - // categoryId, - // }) - // } - const seriesFunction = getSeriesFunction(type, categoryIds) - const returnValue = data.reduce((acc, res) => { + return data.reduce((acc, res) => { const headers = res.headers const metaData = res.metaData const rows = res.rows @@ -157,6 +146,4 @@ export default function ({ type, data, seriesId, categoryIds, extraOptions }) { return acc }, []) - console.log('IS THIS IT THEN?????', returnValue) - return returnValue } From 6eaab348a4a5659a2f6f709b169a5e79316f9632 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 11 Sep 2024 10:45:37 +0200 Subject: [PATCH 37/82] chore: switch to highcharts adapter in story --- src/__demo__/SingleValue.stories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index f28b775df..f1dc22d56 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -657,7 +657,7 @@ storiesOf('SingleValue', module).add('default', () => { extraOptions, undefined, undefined, - 'singleValue' + 'highcharts' ) newChartRef.current = newVisualization.visualization }) From f7bfcfba6ae943468a67090717ee924ad5f792c7 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 11 Sep 2024 10:47:54 +0200 Subject: [PATCH 38/82] feat: add generic mechanism for rendering custom SVGs on chart load event --- .../config/adapters/dhis_highcharts/chart.js | 4 +-- .../adapters/dhis_highcharts/custom/index.js | 29 +++++++++++++++++++ .../config/adapters/dhis_highcharts/index.js | 11 ++++++- 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 src/visualizations/config/adapters/dhis_highcharts/custom/index.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/chart.js b/src/visualizations/config/adapters/dhis_highcharts/chart.js index c7245ccff..ed3733648 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/chart.js +++ b/src/visualizations/config/adapters/dhis_highcharts/chart.js @@ -1,6 +1,4 @@ -import { VIS_TYPE_SINGLE_VALUE } from '../../../../modules/visTypes.js' -import { renderCustomSVG } from './customSVGOptions/index.js' -import { getSingleValueBackgroundColor } from './customSVGOptions/singleValue/index.js' +import { renderCustomSVG } from './custom/index.js' import getType from './type.js' const DEFAULT_CHART = { diff --git a/src/visualizations/config/adapters/dhis_highcharts/custom/index.js b/src/visualizations/config/adapters/dhis_highcharts/custom/index.js new file mode 100644 index 000000000..a89d8e42b --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/custom/index.js @@ -0,0 +1,29 @@ +import { VIS_TYPE_SINGLE_VALUE } from '../../../../../modules/visTypes.js' + +export function renderCustomSVG() { + const renderer = this.renderer + const options = this.userOptions.customSVGOptions + + switch (options.visualizationType) { + case VIS_TYPE_SINGLE_VALUE: + console.log('now render SV viz', renderer, options) + break + default: + break + } +} + +export function getCustomSVGOptions({ layout }) { + const baseOptions = { + visualizationType: layout.type, + } + switch (layout.type) { + case VIS_TYPE_SINGLE_VALUE: + return { + ...baseOptions, + test: 1, + } + default: + return null + } +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/index.js b/src/visualizations/config/adapters/dhis_highcharts/index.js index b39a263c2..b71f17493 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/index.js @@ -15,7 +15,7 @@ import { import { defaultMultiAxisTheme1 } from '../../../util/colors/themes.js' import addTrendLines, { isRegressionIneligible } from './addTrendLines.js' import getChart from './chart.js' -import { getCustomSVGOptions } from './customSVGOptions/index.js' +import { getCustomSVGOptions } from './custom/index.js' import getScatterData from './getScatterData.js' import getSortedConfig from './getSortedConfig.js' import getTrimmedConfig from './getTrimmedConfig.js' @@ -241,8 +241,17 @@ export default function ({ store, layout, el, extraConfig, extraOptions }) { ) } + /* The config object passed to the Highcharts Chart constructor + * can contain arbitrary properties, which are made accessible + * under the Chart instance's `userOptions` member. This means + * that in event callback functions the custom SVG options are + * accessible as `this.userOptions.customSVGOptions` */ + config.customSVGOptions = getCustomSVGOptions({ layout }) + // force apply extra config Object.assign(config, extraConfig) + console.log('CONFIG', objectClean(config)) + return objectClean(config) } From 6b6a20422dd91c9c17c62e48d25cfa7869a86de4 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 11 Sep 2024 17:01:32 +0200 Subject: [PATCH 39/82] fix: implement subtitle and title logic for single value chart --- .../config/adapters/dhis_highcharts/chart.js | 17 ++++------------- .../adapters/dhis_highcharts/subtitle/index.js | 4 +--- .../dhis_highcharts/subtitle/singleValue.js | 3 +-- .../title/__tests__/singleValue.spec.js | 17 ++++++++--------- .../adapters/dhis_highcharts/title/index.js | 11 ++--------- .../dhis_highcharts/title/singleValue.js | 3 +-- 6 files changed, 17 insertions(+), 38 deletions(-) diff --git a/src/visualizations/config/adapters/dhis_highcharts/chart.js b/src/visualizations/config/adapters/dhis_highcharts/chart.js index ed3733648..d3c063af3 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/chart.js +++ b/src/visualizations/config/adapters/dhis_highcharts/chart.js @@ -43,19 +43,10 @@ export default function (layout, el, extraOptions, series) { getType(layout.type), { renderTo: el || layout.el }, DEFAULT_CHART, - extraOptions.dashboard ? DASHBOARD_CHART : undefined, + dashboard ? DASHBOARD_CHART : undefined, getEvents(), - layout.type === VIS_TYPE_SINGLE_VALUE - ? { - backgroundColor: getSingleValueBackgroundColor( - extraOptions.legendOptions, - extraOptions.legendSets, - series[0] - ), - } - : undefined, - layout.type === VIS_TYPE_SINGLE_VALUE && extraOptions.dashboard - ? { spacingTop: 7 } - : undefined + { + backgroundColor: 'red', + } ) } diff --git a/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js b/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js index 77a6a2623..479df913a 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js @@ -18,9 +18,7 @@ import { import getFilterText from '../../../../util/getFilterText.js' import { getTextAlignOption } from '../getTextAlignOption.js' import getYearOverYearTitle from '../title/yearOverYear.js' -import getSingleValueSubtitle, { - getSingleValueSubtitleColor, -} from './singleValue.js' +import getSingleValueSubtitle from './singleValue.js' const DASHBOARD_SUBTITLE = { style: { diff --git a/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js index 2ba68032a..15b2b6fa8 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js +++ b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js @@ -1,8 +1,7 @@ import getFilterText from '../../../../util/getFilterText.js' -export { getSingleValueTextColor as getSingleValueSubtitleColor } from '../customSVGOptions/singleValue/index.js' export default function getSingleValueSubtitle(layout, metaData) { - if (layout.hideSubtitle || 1 === 0) { + if (layout.hideSubtitle) { return '' } diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js b/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js index bc8022f81..4f5843c5d 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js +++ b/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js @@ -1,24 +1,25 @@ -import { getSingleValueTitleText } from '../singleValue.js' +import { VIS_TYPE_SINGLE_VALUE } from '../../../../../../modules/visTypes.js' +import getSingleValueTitle from '../singleValue.js' jest.mock('../../../../../util/getFilterText', () => () => 'The filter text') describe('getSingleValueTitle', () => { it('returns empty title when flag hideTitle exists', () => { - expect(getSingleValueTitleText({ hideTitle: true })).toEqual('') + expect(getSingleValueTitle({ hideTitle: true })).toEqual('') }) it('returns the title provided in the layout', () => { const title = 'The title was already set' - expect(getSingleValueTitleText({ title })).toEqual(title) + expect(getSingleValueTitle({ title })).toEqual(title) }) it('returns null when layout does not have columns', () => { - expect(getSingleValueTitleText({})).toEqual('') + expect(getSingleValueTitle({})).toEqual('') }) it('returns the filter text based on column items', () => { expect( - getSingleValueTitleText({ + getSingleValueTitle({ columns: [ { items: [{}], @@ -31,7 +32,7 @@ describe('getSingleValueTitle', () => { describe('not dashboard', () => { it('returns filter text as title', () => { expect( - getSingleValueTitleText( + getSingleValueTitle( { columns: [ { @@ -49,9 +50,7 @@ describe('getSingleValueTitle', () => { describe('dashboard', () => { it('returns empty string', () => { - expect(getSingleValueTitleText({ filters: {} }, {}, true)).toEqual( - '' - ) + expect(getSingleValueTitle({ filters: {} }, {}, true)).toEqual('') }) }) }) diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/index.js b/src/visualizations/config/adapters/dhis_highcharts/title/index.js index 062dda95c..c607469b9 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/title/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/title/index.js @@ -19,10 +19,7 @@ import { import getFilterText from '../../../../util/getFilterText.js' import { getTextAlignOption } from '../getTextAlignOption.js' import getScatterTitle from './scatter.js' -import { - getSingleValueTitleColor, - getSingleValueTitleText, -} from './singleValue.js' +import getSingleValueTitle from './singleValue.js' import getYearOverYearTitle from './yearOverYear.js' const DASHBOARD_TITLE_STYLE = { @@ -95,11 +92,7 @@ export default function (layout, metaData, extraOptions, series) { } else { switch (layout.type) { case VIS_TYPE_SINGLE_VALUE: - title.text = getSingleValueTitleText( - layout, - metaData, - dashboard - ) + title.text = getSingleValueTitle(layout, metaData, dashboard) break case VIS_TYPE_GAUGE: case VIS_TYPE_YEAR_OVER_YEAR_LINE: diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js b/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js index 82ae95417..0d779eed0 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js +++ b/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js @@ -1,7 +1,6 @@ import getFilterText from '../../../../util/getFilterText.js' -export { getSingleValueTextColor as getSingleValueTitleColor } from '../customSVGOptions/singleValue/index.js' -export function getSingleValueTitleText(layout, metaData) { +export default function (layout, metaData, dashboard) { if (layout.hideTitle) { return '' } From 2d5d6272519bc30eab98ca7f0a79b798d303f100 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 12 Sep 2024 16:36:18 +0200 Subject: [PATCH 40/82] refactor: implement single value config via Highcharts adapter --- .../config/adapters/dhis_highcharts/chart.js | 18 +++++-- .../adapters/dhis_highcharts/custom/index.js | 29 ------------ .../dhis_highcharts/customSVGOptions/index.js | 8 ++-- .../getSingleValueBackgroundColor.js | 17 +++++++ .../getSingleValueCustomSVGOptions.js | 47 +++++++++++++++++++ .../getSingleValueFormattedValue.js | 23 +++++++++ .../singleValue/getSingleValueLegendColor.js | 8 ++++ .../singleValue/getSingleValueTextColor.js | 27 +++++++++++ .../customSVGOptions/singleValue/index.js | 6 +-- .../singleValue}/shouldUseContrastColor.js | 0 .../config/adapters/dhis_highcharts/index.js | 9 +--- .../dhis_highcharts/subtitle/index.js | 18 ++----- .../dhis_highcharts/subtitle/singleValue.js | 1 + .../adapters/dhis_highcharts/title/index.js | 15 +++--- .../dhis_highcharts/title/singleValue.js | 3 +- .../config/generators/dhis/singleValue.js | 1 + .../config/generators/highcharts/index.js | 1 + .../highcharts/renderSingleValueSvg/index.js | 2 +- 18 files changed, 161 insertions(+), 72 deletions(-) delete mode 100644 src/visualizations/config/adapters/dhis_highcharts/custom/index.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js rename src/visualizations/config/{generators/highcharts/renderSingleValueSvg => adapters/dhis_highcharts/customSVGOptions/singleValue}/shouldUseContrastColor.js (100%) diff --git a/src/visualizations/config/adapters/dhis_highcharts/chart.js b/src/visualizations/config/adapters/dhis_highcharts/chart.js index d3c063af3..be264f100 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/chart.js +++ b/src/visualizations/config/adapters/dhis_highcharts/chart.js @@ -1,4 +1,6 @@ -import { renderCustomSVG } from './custom/index.js' +import { VIS_TYPE_SINGLE_VALUE } from '../../../../modules/visTypes.js' +import { renderCustomSVG } from './customSVGOptions/index.js' +import { getSingleValueBackgroundColor } from './customSVGOptions/singleValue/index.js' import getType from './type.js' const DEFAULT_CHART = { @@ -43,10 +45,16 @@ export default function (layout, el, extraOptions, series) { getType(layout.type), { renderTo: el || layout.el }, DEFAULT_CHART, - dashboard ? DASHBOARD_CHART : undefined, + extraOptions.dashboard ? DASHBOARD_CHART : undefined, getEvents(), - { - backgroundColor: 'red', - } + layout.type === VIS_TYPE_SINGLE_VALUE + ? { + backgroundColor: getSingleValueBackgroundColor( + extraOptions.legendOptions, + extraOptions.legendSets, + series[0] + ), + } + : undefined ) } diff --git a/src/visualizations/config/adapters/dhis_highcharts/custom/index.js b/src/visualizations/config/adapters/dhis_highcharts/custom/index.js deleted file mode 100644 index a89d8e42b..000000000 --- a/src/visualizations/config/adapters/dhis_highcharts/custom/index.js +++ /dev/null @@ -1,29 +0,0 @@ -import { VIS_TYPE_SINGLE_VALUE } from '../../../../../modules/visTypes.js' - -export function renderCustomSVG() { - const renderer = this.renderer - const options = this.userOptions.customSVGOptions - - switch (options.visualizationType) { - case VIS_TYPE_SINGLE_VALUE: - console.log('now render SV viz', renderer, options) - break - default: - break - } -} - -export function getCustomSVGOptions({ layout }) { - const baseOptions = { - visualizationType: layout.type, - } - switch (layout.type) { - case VIS_TYPE_SINGLE_VALUE: - return { - ...baseOptions, - test: 1, - } - default: - return null - } -} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js index 7ac3bdbc7..2bfb781b9 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js @@ -1,13 +1,13 @@ import { VIS_TYPE_SINGLE_VALUE } from '../../../../../modules/visTypes.js' import { getSingleValueCustomSVGOptions } from './singleValue/index.js' -import { renderSingleValueSVG } from './singleValue/renderer/renderSingleValueSVG.js' export function renderCustomSVG() { - const { visualizationType } = this.userOptions.customSVGOptions + const renderer = this.renderer + const options = this.userOptions.customSVGOptions - switch (visualizationType) { + switch (options.visualizationType) { case VIS_TYPE_SINGLE_VALUE: - renderSingleValueSVG.call(this) + console.log('now render SV viz', this) break default: break diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js new file mode 100644 index 000000000..650c895a5 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js @@ -0,0 +1,17 @@ +import { LEGEND_DISPLAY_STYLE_FILL } from '../../../../../../modules/legends.js' +import { getSingleValueLegendColor } from './getSingleValueLegendColor.js' + +export function getSingleValueBackgroundColor( + legendOptions, + legendSets, + value +) { + const legendColor = getSingleValueLegendColor( + legendOptions, + legendSets, + value + ) + return legendColor && legendOptions.style === LEGEND_DISPLAY_STYLE_FILL + ? legendColor + : 'transparent' +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js new file mode 100644 index 000000000..4e482a333 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js @@ -0,0 +1,47 @@ +import { getSingleValueFormattedValue } from './getSingleValueFormattedValue.js' + +export function getSingleValueCustomSVGOptions({ + extraConfig, + layout, + extraOptions, + metaData, + series, +}) { + const { dashboard, legendSets, fontStyle, noData, legendOptions, icon } = + extraOptions + const value = series[0] + + console.log( + '++++setSingleValueOptions++++', + '\nextraConfig: ', + extraConfig, + '\nlayout: ', + layout, + '\nmetaData: ', + metaData, + '\nseries: ', + series, + '\ndashboard: ', + dashboard, + '\nlegendSets: ', + legendSets, + '\nfontStyle: ', + fontStyle, + '\nnoData: ', + noData, + '\nlegendOptions: ', + legendOptions, + '\nicon: ', + icon, + '\n=============' + ) + const customSVGOptions = { + value, + formattedValue: getSingleValueFormattedValue(value, layout, metaData), + icon, + dashboard, + subText: 'Test', + } + console.log('singleValueCustomSvgOptions', customSVGOptions) + return customSVGOptions +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js new file mode 100644 index 000000000..f0b91dee3 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js @@ -0,0 +1,23 @@ +import { renderValue } from '../../../../../../modules/renderValue.js' +import { VALUE_TYPE_TEXT } from '../../../../../../modules/valueTypes.js' + +export const INDICATOR_FACTOR_100 = 100 + +export function getSingleValueFormattedValue(value, layout, metaData) { + const valueType = metaData.items[metaData.dimensions.dx[0]].valueType + const indicatorType = + metaData.items[metaData.dimensions.dx[0]].indicatorType + + let formattedValue = renderValue(value, valueType || VALUE_TYPE_TEXT, { + digitGroupSeparator: layout.digitGroupSeparator, + skipRounding: layout.skipRounding, + }) + + // only show the percentage symbol for per cent + // for other factors, show the full text under the value + if (indicatorType?.factor === INDICATOR_FACTOR_100) { + formattedValue += '%' + } + + return formattedValue +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js new file mode 100644 index 000000000..9f042fc4d --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js @@ -0,0 +1,8 @@ +import { getColorByValueFromLegendSet } from '../../../../../../modules/legends.js' + +export function getSingleValueLegendColor(legendOptions, legendSets, value) { + const legendSet = legendOptions && legendSets[0] + return legendSet + ? getColorByValueFromLegendSet(legendSet, value) + : undefined +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js new file mode 100644 index 000000000..109c71fb9 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js @@ -0,0 +1,27 @@ +import { colors } from '@dhis2/ui' +import { LEGEND_DISPLAY_STYLE_TEXT } from '../../../../../../modules/legends.js' +import { getSingleValueLegendColor } from './getSingleValueLegendColor.js' +import { shouldUseContrastColor } from './shouldUseContrastColor.js' + +export function getSingleValueTextColor( + baseColor, + value, + legendOptions, + legendSets +) { + const legendColor = getSingleValueLegendColor( + legendOptions, + legendSets, + value + ) + + if (!legendColor) { + return baseColor + } + + if (LEGEND_DISPLAY_STYLE_TEXT) { + return legendColor + } + + return shouldUseContrastColor(legendColor) ? colors.white : baseColor +} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js index a1923d808..892761190 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js @@ -1,3 +1,3 @@ -export { getSingleValueCustomSVGOptions } from './config/getSingleValueCustomSVGOptions.js' -export { getSingleValueBackgroundColor } from './config/getSingleValueBackgroundColor.js' -export { getSingleValueTextColor } from './config/getSingleValueTextColor.js' +export { getSingleValueCustomSVGOptions } from './getSingleValueCustomSVGOptions.js' +export { getSingleValueBackgroundColor } from './getSingleValueBackgroundColor.js' +export { getSingleValueTextColor } from './getSingleValueTextColor.js' diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/shouldUseContrastColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js similarity index 100% rename from src/visualizations/config/generators/highcharts/renderSingleValueSvg/shouldUseContrastColor.js rename to src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/index.js b/src/visualizations/config/adapters/dhis_highcharts/index.js index b71f17493..f9e97469b 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/index.js @@ -15,7 +15,7 @@ import { import { defaultMultiAxisTheme1 } from '../../../util/colors/themes.js' import addTrendLines, { isRegressionIneligible } from './addTrendLines.js' import getChart from './chart.js' -import { getCustomSVGOptions } from './custom/index.js' +import { getCustomSVGOptions } from './customSVGOptions/index.js' import getScatterData from './getScatterData.js' import getSortedConfig from './getSortedConfig.js' import getTrimmedConfig from './getTrimmedConfig.js' @@ -241,13 +241,6 @@ export default function ({ store, layout, el, extraConfig, extraOptions }) { ) } - /* The config object passed to the Highcharts Chart constructor - * can contain arbitrary properties, which are made accessible - * under the Chart instance's `userOptions` member. This means - * that in event callback functions the custom SVG options are - * accessible as `this.userOptions.customSVGOptions` */ - config.customSVGOptions = getCustomSVGOptions({ layout }) - // force apply extra config Object.assign(config, extraConfig) diff --git a/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js b/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js index 479df913a..a317f1a9b 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/subtitle/index.js @@ -18,7 +18,9 @@ import { import getFilterText from '../../../../util/getFilterText.js' import { getTextAlignOption } from '../getTextAlignOption.js' import getYearOverYearTitle from '../title/yearOverYear.js' -import getSingleValueSubtitle from './singleValue.js' +import getSingleValueSubtitle, { + getSingleValueSubtitleColor, +} from './singleValue.js' const DASHBOARD_SUBTITLE = { style: { @@ -112,25 +114,11 @@ export default function (series, layout, metaData, extraOptions) { legendOptions, legendSets ) - if (dashboard) { - // Single value subtitle text should be multiline - /* TODO: The default color of the subtitle now is #4a5768 but the - * original implementation used #666, which is a lighter grey. - * If we want to keep this color, changes are needed here. */ - Object.assign(subtitle.style, { - wordWrap: 'normal', - whiteSpace: 'normal', - overflow: 'visible', - textOverflow: 'initial', - }) - } break default: subtitle.style.color = fontStyle[FONT_STYLE_OPTION_TEXT_COLOR] break } - console.log('subtitle', subtitle) - return subtitle } diff --git a/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js index 15b2b6fa8..4bf8b394a 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js +++ b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js @@ -1,4 +1,5 @@ import getFilterText from '../../../../util/getFilterText.js' +export { getSingleValueTextColor as getSingleValueSubtitleColor } from '../customSVGOptions/singleValue/index.js' export default function getSingleValueSubtitle(layout, metaData) { if (layout.hideSubtitle) { diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/index.js b/src/visualizations/config/adapters/dhis_highcharts/title/index.js index c607469b9..75000cd93 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/title/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/title/index.js @@ -19,7 +19,10 @@ import { import getFilterText from '../../../../util/getFilterText.js' import { getTextAlignOption } from '../getTextAlignOption.js' import getScatterTitle from './scatter.js' -import getSingleValueTitle from './singleValue.js' +import { + getSingleValueTitleColor, + getSingleValueTitleText, +} from './singleValue.js' import getYearOverYearTitle from './yearOverYear.js' const DASHBOARD_TITLE_STYLE = { @@ -92,7 +95,11 @@ export default function (layout, metaData, extraOptions, series) { } else { switch (layout.type) { case VIS_TYPE_SINGLE_VALUE: - title.text = getSingleValueTitle(layout, metaData, dashboard) + title.text = getSingleValueTitleText( + layout, + metaData, + dashboard + ) break case VIS_TYPE_GAUGE: case VIS_TYPE_YEAR_OVER_YEAR_LINE: @@ -116,10 +123,6 @@ export default function (layout, metaData, extraOptions, series) { legendOptions, legendSets ) - if (dashboard) { - // TODO: is this always what we want? - title.style.fontWeight = 'normal' - } break default: title.style.color = fontStyle[FONT_STYLE_OPTION_TEXT_COLOR] diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js b/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js index 0d779eed0..82ae95417 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js +++ b/src/visualizations/config/adapters/dhis_highcharts/title/singleValue.js @@ -1,6 +1,7 @@ import getFilterText from '../../../../util/getFilterText.js' +export { getSingleValueTextColor as getSingleValueTitleColor } from '../customSVGOptions/singleValue/index.js' -export default function (layout, metaData, dashboard) { +export function getSingleValueTitleText(layout, metaData) { if (layout.hideTitle) { return '' } diff --git a/src/visualizations/config/generators/dhis/singleValue.js b/src/visualizations/config/generators/dhis/singleValue.js index 934e14fdb..07c57854f 100644 --- a/src/visualizations/config/generators/dhis/singleValue.js +++ b/src/visualizations/config/generators/dhis/singleValue.js @@ -468,6 +468,7 @@ export default function ( parentEl, { dashboard, legendSets, fontStyle, noData, legendOptions, icon } ) { + console.log('CONFIG OLD', config) const legendSet = legendOptions && legendSets[0] const legendColor = legendSet && getColorByValueFromLegendSet(legendSet, config.value) diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index 7ac83e864..07fca2f68 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -78,6 +78,7 @@ export function highcharts(config, el) { // silence warning about accessibility config.accessibility = { enabled: false } + console.log('Homt ie hier?', config) if (config.lang) { H.setOptions({ lang: config.lang, diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js index be1fc9cde..35c1c4769 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js @@ -3,8 +3,8 @@ import { getColorByValueFromLegendSet, LEGEND_DISPLAY_STYLE_FILL, } from '../../../../../modules/legends.js' +import { shouldUseContrastColor } from '../../../adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js' import { generateDVItem } from './generateDVItem.js' -import { shouldUseContrastColor } from './shouldUseContrastColor.js' export default function ( config, From 4b3284ea6d3c6ef90ddde15f754d73aeb14bc0ae Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 12 Sep 2024 16:58:19 +0200 Subject: [PATCH 41/82] fix: set subtext in customSVGOptions --- .../getSingleValueCustomSVGOptions.js | 36 +++---------------- .../singleValue/getSingleValueSubtext.js | 11 ++++++ 2 files changed, 15 insertions(+), 32 deletions(-) create mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js index 4e482a333..f6452d893 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js @@ -1,47 +1,19 @@ import { getSingleValueFormattedValue } from './getSingleValueFormattedValue.js' +import { getSingleValueSubtext } from './getSingleValueSubtext.js' export function getSingleValueCustomSVGOptions({ - extraConfig, layout, extraOptions, metaData, series, }) { - const { dashboard, legendSets, fontStyle, noData, legendOptions, icon } = - extraOptions + const { dashboard, icon } = extraOptions const value = series[0] - - console.log( - '++++setSingleValueOptions++++', - '\nextraConfig: ', - extraConfig, - '\nlayout: ', - layout, - '\nmetaData: ', - metaData, - '\nseries: ', - series, - '\ndashboard: ', - dashboard, - '\nlegendSets: ', - legendSets, - '\nfontStyle: ', - fontStyle, - '\nnoData: ', - noData, - '\nlegendOptions: ', - legendOptions, - '\nicon: ', - icon, - '\n=============' - ) - const customSVGOptions = { + return { value, formattedValue: getSingleValueFormattedValue(value, layout, metaData), icon, dashboard, - subText: 'Test', + subText: getSingleValueSubtext(metaData), } - console.log('singleValueCustomSvgOptions', customSVGOptions) - return customSVGOptions } diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js new file mode 100644 index 000000000..b14a3f263 --- /dev/null +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js @@ -0,0 +1,11 @@ +import { INDICATOR_FACTOR_100 } from './getSingleValueFormattedValue.js' + +export function getSingleValueSubtext(metaData) { + const indicatorType = + metaData.items[metaData.dimensions.dx[0]].indicatorType + + return indicatorType?.displayName && + indicatorType?.factor !== INDICATOR_FACTOR_100 + ? indicatorType?.displayName + : undefined +} From 8546b95f0e003df31ce5a9d653d49581d545c749 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 17 Sep 2024 17:15:54 +0200 Subject: [PATCH 42/82] chore: positioning WIP --- .../dhis_highcharts/customSVGOptions/index.js | 8 +- .../getSingleValueBackgroundColor.js | 17 --- .../getSingleValueCustomSVGOptions.js | 19 --- .../getSingleValueFormattedValue.js | 23 ---- .../singleValue/getSingleValueLegendColor.js | 8 -- .../singleValue/getSingleValueSubtext.js | 11 -- .../singleValue/getSingleValueTextColor.js | 27 ---- .../customSVGOptions/singleValue/index.js | 6 +- .../singleValue/renderer/addIconElement.js | 5 +- .../renderer/checkIfFitsWithinContainer.js | 12 +- .../singleValue/renderer/positionElements.js | 118 ++++++++++++++---- .../renderer/renderSingleValueSVG.js | 30 +++-- .../singleValue/renderer/styles.js | 44 +++---- .../singleValue/shouldUseContrastColor.js | 17 --- .../dhis_highcharts/subtitle/singleValue.js | 2 +- .../highcharts/renderSingleValueSvg/index.js | 2 +- 16 files changed, 153 insertions(+), 196 deletions(-) delete mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js delete mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js delete mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js delete mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js delete mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js delete mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js delete mode 100644 src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js index 2bfb781b9..7ac3bdbc7 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/index.js @@ -1,13 +1,13 @@ import { VIS_TYPE_SINGLE_VALUE } from '../../../../../modules/visTypes.js' import { getSingleValueCustomSVGOptions } from './singleValue/index.js' +import { renderSingleValueSVG } from './singleValue/renderer/renderSingleValueSVG.js' export function renderCustomSVG() { - const renderer = this.renderer - const options = this.userOptions.customSVGOptions + const { visualizationType } = this.userOptions.customSVGOptions - switch (options.visualizationType) { + switch (visualizationType) { case VIS_TYPE_SINGLE_VALUE: - console.log('now render SV viz', this) + renderSingleValueSVG.call(this) break default: break diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js deleted file mode 100644 index 650c895a5..000000000 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueBackgroundColor.js +++ /dev/null @@ -1,17 +0,0 @@ -import { LEGEND_DISPLAY_STYLE_FILL } from '../../../../../../modules/legends.js' -import { getSingleValueLegendColor } from './getSingleValueLegendColor.js' - -export function getSingleValueBackgroundColor( - legendOptions, - legendSets, - value -) { - const legendColor = getSingleValueLegendColor( - legendOptions, - legendSets, - value - ) - return legendColor && legendOptions.style === LEGEND_DISPLAY_STYLE_FILL - ? legendColor - : 'transparent' -} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js deleted file mode 100644 index f6452d893..000000000 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueCustomSVGOptions.js +++ /dev/null @@ -1,19 +0,0 @@ -import { getSingleValueFormattedValue } from './getSingleValueFormattedValue.js' -import { getSingleValueSubtext } from './getSingleValueSubtext.js' - -export function getSingleValueCustomSVGOptions({ - layout, - extraOptions, - metaData, - series, -}) { - const { dashboard, icon } = extraOptions - const value = series[0] - return { - value, - formattedValue: getSingleValueFormattedValue(value, layout, metaData), - icon, - dashboard, - subText: getSingleValueSubtext(metaData), - } -} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js deleted file mode 100644 index f0b91dee3..000000000 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueFormattedValue.js +++ /dev/null @@ -1,23 +0,0 @@ -import { renderValue } from '../../../../../../modules/renderValue.js' -import { VALUE_TYPE_TEXT } from '../../../../../../modules/valueTypes.js' - -export const INDICATOR_FACTOR_100 = 100 - -export function getSingleValueFormattedValue(value, layout, metaData) { - const valueType = metaData.items[metaData.dimensions.dx[0]].valueType - const indicatorType = - metaData.items[metaData.dimensions.dx[0]].indicatorType - - let formattedValue = renderValue(value, valueType || VALUE_TYPE_TEXT, { - digitGroupSeparator: layout.digitGroupSeparator, - skipRounding: layout.skipRounding, - }) - - // only show the percentage symbol for per cent - // for other factors, show the full text under the value - if (indicatorType?.factor === INDICATOR_FACTOR_100) { - formattedValue += '%' - } - - return formattedValue -} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js deleted file mode 100644 index 9f042fc4d..000000000 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueLegendColor.js +++ /dev/null @@ -1,8 +0,0 @@ -import { getColorByValueFromLegendSet } from '../../../../../../modules/legends.js' - -export function getSingleValueLegendColor(legendOptions, legendSets, value) { - const legendSet = legendOptions && legendSets[0] - return legendSet - ? getColorByValueFromLegendSet(legendSet, value) - : undefined -} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js deleted file mode 100644 index b14a3f263..000000000 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueSubtext.js +++ /dev/null @@ -1,11 +0,0 @@ -import { INDICATOR_FACTOR_100 } from './getSingleValueFormattedValue.js' - -export function getSingleValueSubtext(metaData) { - const indicatorType = - metaData.items[metaData.dimensions.dx[0]].indicatorType - - return indicatorType?.displayName && - indicatorType?.factor !== INDICATOR_FACTOR_100 - ? indicatorType?.displayName - : undefined -} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js deleted file mode 100644 index 109c71fb9..000000000 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/getSingleValueTextColor.js +++ /dev/null @@ -1,27 +0,0 @@ -import { colors } from '@dhis2/ui' -import { LEGEND_DISPLAY_STYLE_TEXT } from '../../../../../../modules/legends.js' -import { getSingleValueLegendColor } from './getSingleValueLegendColor.js' -import { shouldUseContrastColor } from './shouldUseContrastColor.js' - -export function getSingleValueTextColor( - baseColor, - value, - legendOptions, - legendSets -) { - const legendColor = getSingleValueLegendColor( - legendOptions, - legendSets, - value - ) - - if (!legendColor) { - return baseColor - } - - if (LEGEND_DISPLAY_STYLE_TEXT) { - return legendColor - } - - return shouldUseContrastColor(legendColor) ? colors.white : baseColor -} diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js index 892761190..a1923d808 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/index.js @@ -1,3 +1,3 @@ -export { getSingleValueCustomSVGOptions } from './getSingleValueCustomSVGOptions.js' -export { getSingleValueBackgroundColor } from './getSingleValueBackgroundColor.js' -export { getSingleValueTextColor } from './getSingleValueTextColor.js' +export { getSingleValueCustomSVGOptions } from './config/getSingleValueCustomSVGOptions.js' +export { getSingleValueBackgroundColor } from './config/getSingleValueBackgroundColor.js' +export { getSingleValueTextColor } from './config/getSingleValueTextColor.js' diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js index 6248bdeb4..ee3aa0ff9 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js @@ -6,11 +6,10 @@ export function addIconElement(svgString, color) { const iconElWidth = svgIconDocument.documentElement.getAttribute('width') const iconGroup = this.renderer .g('icon') - .attr({ color, 'data-test': 'visualization-icon' }) + .attr('data-test', 'visualization-icon') .css({ - visibility: 'hidden', + color, }) - /* Force the group element to have the same dimensions as the original * SVG image by adding this rect. This ensures the icon has the intended * whitespace around it and makes scaling and translating easier. */ diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js index 355728aef..ada8af973 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js @@ -1,5 +1,3 @@ -import { ACTUAL_NUMBER_HEIGHT_FACTOR } from './constants.js' - export function checkIfFitsWithinContainer( availableSpace, valueElement, @@ -9,17 +7,13 @@ export function checkIfFitsWithinContainer( spacing ) { const valueRect = valueElement.getBBox() - const subTextRect = subText - ? subTextElement.getBBox() - : { width: 0, height: 0 } + const subTextRect = subTextElement.getBBox() const requiredValueWidth = icon ? valueRect.width + spacing.iconGap + spacing.iconSize : valueRect.width const requiredHeight = subText - ? valueRect.height * ACTUAL_NUMBER_HEIGHT_FACTOR + - spacing.subTextTop + - subTextRect.height - : valueRect.height * ACTUAL_NUMBER_HEIGHT_FACTOR + ? valueRect.height + spacing.subTextTop + subTextRect.height + : valueRect.height const fitsHorizontally = availableSpace.width > requiredValueWidth && availableSpace.width > subTextRect.width diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js index 052c86b5b..2c8383946 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js @@ -1,5 +1,4 @@ -import { computeLayoutRect } from './computeLayoutRect.js' -import { ACTUAL_NUMBER_HEIGHT_FACTOR } from './constants.js' +import { computeSpacingTop } from './computeSpacingTop.js' export function positionElements( valueElement, @@ -7,7 +6,18 @@ export function positionElements( iconElement, spacing ) { - const valueElementBox = valueElement.getBBox() + console.log( + '++++positionElements++++', + '\nvalueElement: ', + valueElement, + '\nsubTextElement: ', + subTextElement, + '\niconElement: ', + iconElement, + '\nspacing: ', + spacing, + '\n===============' + ) /* Layout here refers to a virtual rect that wraps * all indiviual parts of the single value visualization * (value, subtext and icon) */ @@ -19,13 +29,37 @@ export function positionElements( spacing ) + // DEBUGGING THE RECT + const debugRect = this.renderer + .rect(layoutRect.x, layoutRect.y, layoutRect.width, layoutRect.height) + .attr({ fill: 'orange', opacity: 0.3 }) + .add() + + const myBBox = debugRect.getBBox() + + // const valueBox = valueElement.getBBox() + // const valueTranslateX = iconElement + // ? layoutRect.x + spacing.iconSize + spacing.iconGap + // : layoutRect.x + // valueElement.css({ + // transform: `translate(${valueTranslateX}px, ${layoutRect.y}px)`, + // }) + // valueElement.attr({ + // // TODO: cover the case where subtext is wider than value + // x: iconElement + // ? layoutRect.x + spacing.iconSize + spacing.iconGap + // : layoutRect.x, + // y: layoutRect.y, + // dy: valueBox.height, + // }) + const valueElementBox = valueElement.getBBox() valueElement.align( { align: 'right', verticalAlign: 'top', alignByTranslate: false, - x: (valueElementBox.width + layoutRect.sideMarginTop) * -1, - y: valueElementBox.height * ACTUAL_NUMBER_HEIGHT_FACTOR, + x: valueElementBox.width * -1, + y: valueElementBox.height * (2 / 3), }, false, layoutRect @@ -33,30 +67,66 @@ export function positionElements( if (iconElement) { const { height } = iconElement.getBBox() - const scale = spacing.iconSize / height - const translateX = layoutRect.x + layoutRect.sideMarginTop - const iconHeight = height * scale - const valueElementHeight = - valueElementBox.height * ACTUAL_NUMBER_HEIGHT_FACTOR - const translateY = layoutRect.y + (valueElementHeight - iconHeight) / 2 - - /* The icon is a with elements that contain coordinates. - * These path-coordinates only scale correctly when using CSS translate */ + const scaleFactor = spacing.iconSize / height + + // This all needs to be done using CSS translate because of the path cooordinates in the SVG icon iconElement.css({ - transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`, + transform: `translate(${layoutRect.x}px, ${layoutRect.y}px) scale(${scaleFactor})`, + }) + } + + if (subTextElement) { + const { height: subTextHeight } = subTextElement.getBBox() + subTextElement.attr({ + x: iconElement + ? layoutRect.x + spacing.iconSize + spacing.iconGap + : layoutRect.x, + y: layoutRect.y + layoutRect.height - subTextHeight, }) } + console.log( + '++++positionElements++++', + '\nvalueElement: ', + valueElement, + '\nsubTextElement: ', + subTextElement, + '\niconElement: ', + iconElement, + '\nspacing: ', + spacing, + '\nlayoutRect: ', + layoutRect, + '\n===============' + ) +} + +function computeLayoutRect(valueElement, subTextElement, iconElement, spacing) { + const valueRect = valueElement.getBBox() + const containerCenterY = this.chartHeight / 2 + const containerCenterX = this.chartWidth / 2 + const minY = computeSpacingTop.call(this, spacing.valueTop) + + let width = valueRect.width + let height = valueRect.height + + if (iconElement) { + width += spacing.iconGap + spacing.iconSize + } + if (subTextElement) { - subTextElement.align( - { - align: 'left', - verticalAlign: 'bottom', - alignByTranslate: false, - x: layoutRect.sideMarginBottom, - }, - false, - layoutRect + const subTextRect = subTextElement.getBBox() + console.log( + `What is bigger? valueWidth: ${width} subTexttWidth ${subTextRect.width}` ) + width = Math.max(width, subTextRect.width) + height += spacing.subTextTop + subTextRect.height + } + + return { + x: containerCenterX - width / 2, + y: Math.max(containerCenterY - height / 2, minY), + width, + height, } } diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js index 7976a4480..d068e488d 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js @@ -6,16 +6,20 @@ import { DynamicStyles } from './styles.js' export function renderSingleValueSVG() { const color = this.title.styles.color - const { formattedValue, icon, subText } = this.userOptions.customSVGOptions + const { dashboard, formattedValue, icon, subText } = + this.userOptions.customSVGOptions const dynamicStyles = new DynamicStyles() const valueElement = this.renderer .text(formattedValue) - .css({ color, visibility: 'hidden' }) + .css({ color, visibility: 'visible' }) .add() const subTextElement = subText - ? this.renderer.text(subText).css({ color, visibility: 'hidden' }).add() + ? this.renderer + .text(subText) + .css({ color, visibility: 'visible' }) + .add() : null - const iconElement = icon ? addIconElement.call(this, icon, color) : null + const iconElement = icon ? addIconElement.call(this, icon) : null let fitsWithinContainer = false let styles = {} @@ -44,7 +48,19 @@ export function renderSingleValueSVG() { styles.spacing ) - valueElement.css({ visibility: 'visible' }) - iconElement?.css({ visibility: 'visible' }) - subTextElement?.css({ visibility: 'visible' }) + console.log( + '+++++Render the SVG++++++', + '\ncolor: ', + color, + '\ndashboard: ', + dashboard, + '\nformattedValue: ', + formattedValue, + '\nicon: ', + icon, + '\nsubText: ', + subText, + '\n=============' + ) + console.log('CHART', this) } diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js index 0bc04f135..f141c285d 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js @@ -20,31 +20,31 @@ const valueStyles = [ ] const subTextStyles = [ - { fontSize: 67, letterSpacing: -2 }, - { fontSize: 61, letterSpacing: -1.8 }, - { fontSize: 55, letterSpacing: -1.6 }, - { fontSize: 49, letterSpacing: -1.4 }, - { fontSize: 43, letterSpacing: -1.2 }, - { fontSize: 37, letterSpacing: -1 }, - { fontSize: 31, letterSpacing: -0.8 }, - { fontSize: 25, letterSpacing: -0.6 }, - { fontSize: 19, letterSpacing: -0.4 }, - { fontSize: 13, letterSpacing: 0.2 }, - { fontSize: 7, letterSpacing: 0 }, + { fontSize: 100, letterSpacing: -3 }, + { fontSize: 91, letterSpacing: -2.7 }, + { fontSize: 82, letterSpacing: -2.4 }, + { fontSize: 73, letterSpacing: -2.1 }, + { fontSize: 64, letterSpacing: -1.8 }, + { fontSize: 55, letterSpacing: -1.5 }, + { fontSize: 46, letterSpacing: -1.2 }, + { fontSize: 37, letterSpacing: -0.9 }, + { fontSize: 28, letterSpacing: -0.6 }, + { fontSize: 19, letterSpacing: 0.3 }, + { fontSize: 10, letterSpacing: 0 }, ] const spacings = [ - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 200 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 182 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 164 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 146 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 128 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 110 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 92 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 74 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 56 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 38 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 20 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 140 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 127 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 115 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 102 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 90 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 77 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 64 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 52 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 39 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 27 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 14 }, ] export const MIN_SIDE_WHITESPACE = 4 diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js deleted file mode 100644 index d01616c9a..000000000 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js +++ /dev/null @@ -1,17 +0,0 @@ -export const shouldUseContrastColor = (inputColor = '') => { - // based on https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color - var color = - inputColor.charAt(0) === '#' ? inputColor.substring(1, 7) : inputColor - var r = parseInt(color.substring(0, 2), 16) // hexToR - var g = parseInt(color.substring(2, 4), 16) // hexToG - var b = parseInt(color.substring(4, 6), 16) // hexToB - var uicolors = [r / 255, g / 255, b / 255] - var c = uicolors.map((col) => { - if (col <= 0.03928) { - return col / 12.92 - } - return Math.pow((col + 0.055) / 1.055, 2.4) - }) - var L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2] - return L <= 0.179 -} diff --git a/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js index 4bf8b394a..2ba68032a 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js +++ b/src/visualizations/config/adapters/dhis_highcharts/subtitle/singleValue.js @@ -2,7 +2,7 @@ import getFilterText from '../../../../util/getFilterText.js' export { getSingleValueTextColor as getSingleValueSubtitleColor } from '../customSVGOptions/singleValue/index.js' export default function getSingleValueSubtitle(layout, metaData) { - if (layout.hideSubtitle) { + if (layout.hideSubtitle || 1 === 0) { return '' } diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js index 35c1c4769..b4cfd8842 100644 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js +++ b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js @@ -3,7 +3,7 @@ import { getColorByValueFromLegendSet, LEGEND_DISPLAY_STYLE_FILL, } from '../../../../../modules/legends.js' -import { shouldUseContrastColor } from '../../../adapters/dhis_highcharts/customSVGOptions/singleValue/shouldUseContrastColor.js' +import { shouldUseContrastColor } from '../../../adapters/dhis_highcharts/customSVGOptions/singleValue/config/shouldUseContrastColor.js' import { generateDVItem } from './generateDVItem.js' export default function ( From ec91ff220b13c829d534319415d2dcee257ed8ae Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 18 Sep 2024 14:39:56 +0200 Subject: [PATCH 43/82] chore: position all SVG elements using Highcharts SVGElement.align() --- .../singleValue/renderer/addIconElement.js | 5 +- .../renderer/checkIfFitsWithinContainer.js | 8 +- .../singleValue/renderer/positionElements.js | 118 ++++-------------- .../renderer/renderSingleValueSVG.js | 27 +--- 4 files changed, 39 insertions(+), 119 deletions(-) diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js index ee3aa0ff9..6248bdeb4 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/addIconElement.js @@ -6,10 +6,11 @@ export function addIconElement(svgString, color) { const iconElWidth = svgIconDocument.documentElement.getAttribute('width') const iconGroup = this.renderer .g('icon') - .attr('data-test', 'visualization-icon') + .attr({ color, 'data-test': 'visualization-icon' }) .css({ - color, + visibility: 'hidden', }) + /* Force the group element to have the same dimensions as the original * SVG image by adding this rect. This ensures the icon has the intended * whitespace around it and makes scaling and translating easier. */ diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js index ada8af973..51bb5c253 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/checkIfFitsWithinContainer.js @@ -1,3 +1,5 @@ +import { ACTUAL_NUMBER_HEIGHT_FACTOR } from './constants.js' + export function checkIfFitsWithinContainer( availableSpace, valueElement, @@ -12,8 +14,10 @@ export function checkIfFitsWithinContainer( ? valueRect.width + spacing.iconGap + spacing.iconSize : valueRect.width const requiredHeight = subText - ? valueRect.height + spacing.subTextTop + subTextRect.height - : valueRect.height + ? valueRect.height * ACTUAL_NUMBER_HEIGHT_FACTOR + + spacing.subTextTop + + subTextRect.height + : valueRect.height * ACTUAL_NUMBER_HEIGHT_FACTOR const fitsHorizontally = availableSpace.width > requiredValueWidth && availableSpace.width > subTextRect.width diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js index 2c8383946..052c86b5b 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/positionElements.js @@ -1,4 +1,5 @@ -import { computeSpacingTop } from './computeSpacingTop.js' +import { computeLayoutRect } from './computeLayoutRect.js' +import { ACTUAL_NUMBER_HEIGHT_FACTOR } from './constants.js' export function positionElements( valueElement, @@ -6,18 +7,7 @@ export function positionElements( iconElement, spacing ) { - console.log( - '++++positionElements++++', - '\nvalueElement: ', - valueElement, - '\nsubTextElement: ', - subTextElement, - '\niconElement: ', - iconElement, - '\nspacing: ', - spacing, - '\n===============' - ) + const valueElementBox = valueElement.getBBox() /* Layout here refers to a virtual rect that wraps * all indiviual parts of the single value visualization * (value, subtext and icon) */ @@ -29,37 +19,13 @@ export function positionElements( spacing ) - // DEBUGGING THE RECT - const debugRect = this.renderer - .rect(layoutRect.x, layoutRect.y, layoutRect.width, layoutRect.height) - .attr({ fill: 'orange', opacity: 0.3 }) - .add() - - const myBBox = debugRect.getBBox() - - // const valueBox = valueElement.getBBox() - // const valueTranslateX = iconElement - // ? layoutRect.x + spacing.iconSize + spacing.iconGap - // : layoutRect.x - // valueElement.css({ - // transform: `translate(${valueTranslateX}px, ${layoutRect.y}px)`, - // }) - // valueElement.attr({ - // // TODO: cover the case where subtext is wider than value - // x: iconElement - // ? layoutRect.x + spacing.iconSize + spacing.iconGap - // : layoutRect.x, - // y: layoutRect.y, - // dy: valueBox.height, - // }) - const valueElementBox = valueElement.getBBox() valueElement.align( { align: 'right', verticalAlign: 'top', alignByTranslate: false, - x: valueElementBox.width * -1, - y: valueElementBox.height * (2 / 3), + x: (valueElementBox.width + layoutRect.sideMarginTop) * -1, + y: valueElementBox.height * ACTUAL_NUMBER_HEIGHT_FACTOR, }, false, layoutRect @@ -67,66 +33,30 @@ export function positionElements( if (iconElement) { const { height } = iconElement.getBBox() - const scaleFactor = spacing.iconSize / height - - // This all needs to be done using CSS translate because of the path cooordinates in the SVG icon + const scale = spacing.iconSize / height + const translateX = layoutRect.x + layoutRect.sideMarginTop + const iconHeight = height * scale + const valueElementHeight = + valueElementBox.height * ACTUAL_NUMBER_HEIGHT_FACTOR + const translateY = layoutRect.y + (valueElementHeight - iconHeight) / 2 + + /* The icon is a with elements that contain coordinates. + * These path-coordinates only scale correctly when using CSS translate */ iconElement.css({ - transform: `translate(${layoutRect.x}px, ${layoutRect.y}px) scale(${scaleFactor})`, - }) - } - - if (subTextElement) { - const { height: subTextHeight } = subTextElement.getBBox() - subTextElement.attr({ - x: iconElement - ? layoutRect.x + spacing.iconSize + spacing.iconGap - : layoutRect.x, - y: layoutRect.y + layoutRect.height - subTextHeight, + transform: `translate(${translateX}px, ${translateY}px) scale(${scale})`, }) } - console.log( - '++++positionElements++++', - '\nvalueElement: ', - valueElement, - '\nsubTextElement: ', - subTextElement, - '\niconElement: ', - iconElement, - '\nspacing: ', - spacing, - '\nlayoutRect: ', - layoutRect, - '\n===============' - ) -} - -function computeLayoutRect(valueElement, subTextElement, iconElement, spacing) { - const valueRect = valueElement.getBBox() - const containerCenterY = this.chartHeight / 2 - const containerCenterX = this.chartWidth / 2 - const minY = computeSpacingTop.call(this, spacing.valueTop) - - let width = valueRect.width - let height = valueRect.height - - if (iconElement) { - width += spacing.iconGap + spacing.iconSize - } - if (subTextElement) { - const subTextRect = subTextElement.getBBox() - console.log( - `What is bigger? valueWidth: ${width} subTexttWidth ${subTextRect.width}` + subTextElement.align( + { + align: 'left', + verticalAlign: 'bottom', + alignByTranslate: false, + x: layoutRect.sideMarginBottom, + }, + false, + layoutRect ) - width = Math.max(width, subTextRect.width) - height += spacing.subTextTop + subTextRect.height - } - - return { - x: containerCenterX - width / 2, - y: Math.max(containerCenterY - height / 2, minY), - width, - height, } } diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js index d068e488d..21f4ea398 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js @@ -11,15 +11,12 @@ export function renderSingleValueSVG() { const dynamicStyles = new DynamicStyles() const valueElement = this.renderer .text(formattedValue) - .css({ color, visibility: 'visible' }) + .css({ color, visibility: 'hidden' }) .add() const subTextElement = subText - ? this.renderer - .text(subText) - .css({ color, visibility: 'visible' }) - .add() + ? this.renderer.text(subText).css({ color, visibility: 'hidden' }).add() : null - const iconElement = icon ? addIconElement.call(this, icon) : null + const iconElement = icon ? addIconElement.call(this, icon, color) : null let fitsWithinContainer = false let styles = {} @@ -48,19 +45,7 @@ export function renderSingleValueSVG() { styles.spacing ) - console.log( - '+++++Render the SVG++++++', - '\ncolor: ', - color, - '\ndashboard: ', - dashboard, - '\nformattedValue: ', - formattedValue, - '\nicon: ', - icon, - '\nsubText: ', - subText, - '\n=============' - ) - console.log('CHART', this) + valueElement.css({ visibility: 'visible' }) + iconElement?.css({ visibility: 'visible' }) + subTextElement?.css({ visibility: 'visible' }) } From 250e7f0263ebffa238c1745f832f017150d0e97d Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 18 Sep 2024 14:51:34 +0200 Subject: [PATCH 44/82] chore: remove files from failed initial re-implementation --- .../config/adapters/dhis_highcharts/index.js | 2 - src/visualizations/config/adapters/index.js | 1 - .../config/generators/dhis/singleValue.js | 2 - .../config/generators/highcharts/index.js | 36 +---- .../renderSingleValueSvg/constants.js | 38 ----- .../renderSingleValueSvg/generateDVItem.js | 134 ----------------- .../renderSingleValueSvg/generateValueSVG.js | 135 ------------------ .../generateValueSVGOLD.js | 135 ------------------ .../getTextAnchorFromTextAlign.js | 17 --- .../renderSingleValueSvg/getXFromTextAlign.js | 17 --- .../highcharts/renderSingleValueSvg/index.js | 76 ---------- .../renderSingleValueSvg/textSize.js | 52 ------- src/visualizations/config/generators/index.js | 3 +- src/visualizations/store/adapters/index.js | 1 - 14 files changed, 2 insertions(+), 647 deletions(-) delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js delete mode 100644 src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js diff --git a/src/visualizations/config/adapters/dhis_highcharts/index.js b/src/visualizations/config/adapters/dhis_highcharts/index.js index f9e97469b..b39a263c2 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/index.js +++ b/src/visualizations/config/adapters/dhis_highcharts/index.js @@ -244,7 +244,5 @@ export default function ({ store, layout, el, extraConfig, extraOptions }) { // force apply extra config Object.assign(config, extraConfig) - console.log('CONFIG', objectClean(config)) - return objectClean(config) } diff --git a/src/visualizations/config/adapters/index.js b/src/visualizations/config/adapters/index.js index e567b54d1..7b49438ee 100644 --- a/src/visualizations/config/adapters/index.js +++ b/src/visualizations/config/adapters/index.js @@ -4,5 +4,4 @@ import dhis_highcharts from './dhis_highcharts/index.js' export default { dhis_highcharts, dhis_dhis, - dhis_singleValue: dhis_dhis, } diff --git a/src/visualizations/config/generators/dhis/singleValue.js b/src/visualizations/config/generators/dhis/singleValue.js index 07c57854f..25ec5bab9 100644 --- a/src/visualizations/config/generators/dhis/singleValue.js +++ b/src/visualizations/config/generators/dhis/singleValue.js @@ -151,7 +151,6 @@ const generateValueSVG = ({ // embed icon to allow changing color // (elements with fill need to use "currentColor" for this to work) const iconSvgNode = document.createElementNS(svgNS, 'svg') - console.log('old', iconSize) iconSvgNode.setAttribute('viewBox', '0 0 48 48') iconSvgNode.setAttribute('width', iconSize) iconSvgNode.setAttribute('height', iconSize) @@ -468,7 +467,6 @@ export default function ( parentEl, { dashboard, legendSets, fontStyle, noData, legendOptions, icon } ) { - console.log('CONFIG OLD', config) const legendSet = legendOptions && legendSets[0] const legendColor = legendSet && getColorByValueFromLegendSet(legendSet, config.value) diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index 07fca2f68..f3222b257 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -72,13 +72,12 @@ function drawLegendSymbolWrap() { ) } -export function highcharts(config, el) { +export default function (config, el) { if (config) { config.chart.renderTo = el || config.chart.renderTo // silence warning about accessibility config.accessibility = { enabled: false } - console.log('Homt ie hier?', config) if (config.lang) { H.setOptions({ lang: config.lang, @@ -90,36 +89,3 @@ export function highcharts(config, el) { return new H.Chart(config) } } - -export function singleValue(config, el, extraOptions) { - return H.chart(el, { - accessibility: { enabled: false }, - chart: { - backgroundColor: 'transparent', - events: { - load: function () { - renderSingleValueSvg(config, el, extraOptions, this) - }, - }, - animation: false, - }, - credits: { enabled: false }, - exporting: { - enabled: true, - error: (options, error) => { - console.log('options', options) - console.log(error) - }, - chartOptions: { - title: { - text: null, - }, - }, - }, - lang: { - noData: null, - }, - noData: {}, - title: null, - }) -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js deleted file mode 100644 index 06fd79fd0..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/constants.js +++ /dev/null @@ -1,38 +0,0 @@ -// TODO: remove this, sch thing it should not be needed -export const svgNS = 'http://www.w3.org/2000/svg' -// multiply text width with this factor -// to get very close to actual text width -// nb: dependent on viewbox etc -export const ACTUAL_TEXT_WIDTH_FACTOR = 0.9 - -// multiply value text size with this factor -// to get very close to the actual number height -// as numbers don't go below the baseline like e.g. "j" and "g" -export const ACTUAL_NUMBER_HEIGHT_FACTOR = 0.67 - -// do not allow text width to exceed this threshold -// a threshold >1 does not really make sense but text width vs viewbox is complicated -export const TEXT_WIDTH_CONTAINER_WIDTH_FACTOR = 1.3 - -// do not allow text size to exceed this -export const TEXT_SIZE_CONTAINER_HEIGHT_FACTOR = 0.6 -export const TEXT_SIZE_MAX_THRESHOLD = 200 - -// multiply text size with this factor -// to get an appropriate letter spacing -export const LETTER_SPACING_TEXT_SIZE_FACTOR = (1 / 35) * -1 -export const LETTER_SPACING_MIN_THRESHOLD = -6 -export const LETTER_SPACING_MAX_THRESHOLD = -1 - -// fixed top margin above title/subtitle -export const TOP_MARGIN_FIXED = 16 - -// multiply text size with this factor -// to get an appropriate sub text size -export const SUB_TEXT_SIZE_FACTOR = 0.5 -export const SUB_TEXT_SIZE_MIN_THRESHOLD = 26 -export const SUB_TEXT_SIZE_MAX_THRESHOLD = 40 - -// multiply text size with this factor -// to get an appropriate icon padding -export const ICON_PADDING_FACTOR = 0.3 diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js deleted file mode 100644 index 2291d341d..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateDVItem.js +++ /dev/null @@ -1,134 +0,0 @@ -import { - defaultFontStyle, - FONT_STYLE_OPTION_BOLD, - FONT_STYLE_OPTION_FONT_SIZE, - FONT_STYLE_OPTION_ITALIC, - FONT_STYLE_OPTION_TEXT_ALIGN, - FONT_STYLE_OPTION_TEXT_COLOR, - FONT_STYLE_VISUALIZATION_SUBTITLE, - FONT_STYLE_VISUALIZATION_TITLE, - mergeFontStyleWithDefault, -} from '../../../../../modules/fontStyle.js' -import { TOP_MARGIN_FIXED } from './constants.js' -import { generateValueSVG } from './generateValueSVG.js' -import { getTextAnchorFromTextAlign } from './getTextAnchorFromTextAlign.js' -import { getXFromTextAlign } from './getXFromTextAlign.js' - -export const generateDVItem = ( - config, - { - renderer, - width, - height, - valueColor, - noData, - backgroundColor, - titleColor, - fontStyle, - icon, - } -) => { - backgroundColor = 'red' - if (backgroundColor) { - renderer - .rect(0, 0, width, height) - .attr({ fill: backgroundColor, width: '100%', height: '100%' }) - .add() - } - - // TITLE - const titleFontStyle = mergeFontStyleWithDefault( - fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_TITLE], - FONT_STYLE_VISUALIZATION_TITLE - ) - - const titleYPosition = - TOP_MARGIN_FIXED + - parseInt(titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]) + - 'px' - - const titleFontSize = `${titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px` - - renderer - .text(config.title) - .attr({ - x: getXFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]), - y: titleYPosition, - 'text-anchor': getTextAnchorFromTextAlign( - titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] - ), - 'font-size': titleFontSize, - 'font-weight': titleFontStyle[FONT_STYLE_OPTION_BOLD] - ? FONT_STYLE_OPTION_BOLD - : 'normal', - 'font-style': titleFontStyle[FONT_STYLE_OPTION_ITALIC] - ? FONT_STYLE_OPTION_ITALIC - : 'normal', - 'data-test': 'visualization-title', - fill: - titleColor && - titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === - defaultFontStyle[FONT_STYLE_VISUALIZATION_TITLE][ - FONT_STYLE_OPTION_TEXT_COLOR - ] - ? titleColor - : titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR], - }) - .add() - - // SUBTITLE - const subtitleFontStyle = mergeFontStyleWithDefault( - fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], - FONT_STYLE_VISUALIZATION_SUBTITLE - ) - const subtitleFontSize = `${subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px` - - if (config.subtitle) { - renderer - .text(config.subtitle) - .attr({ - x: getXFromTextAlign( - subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] - ), - y: titleYPosition, - dy: `${subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE] + 10}`, - 'text-anchor': getTextAnchorFromTextAlign( - subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] - ), - 'font-size': subtitleFontSize, - 'font-weight': subtitleFontStyle[FONT_STYLE_OPTION_BOLD] - ? FONT_STYLE_OPTION_BOLD - : 'normal', - 'font-style': subtitleFontStyle[FONT_STYLE_OPTION_ITALIC] - ? FONT_STYLE_OPTION_ITALIC - : 'normal', - fill: - titleColor && - subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === - defaultFontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE][ - FONT_STYLE_OPTION_TEXT_COLOR - ] - ? titleColor - : subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR], - 'data-test': 'visualization-subtitle', - }) - .add() - } - - generateValueSVG({ - renderer, - formattedValue: config.formattedValue, - subText: config.subText, - valueColor, - textColor: titleColor, - noData, - icon, - containerWidth: width, - containerHeight: height, - topMargin: - TOP_MARGIN_FIXED + - ((config.title ? parseInt(titleFontSize) : 0) + - (config.subtitle ? parseInt(subtitleFontSize) : 0)) * - 2.5, - }) -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js deleted file mode 100644 index 5f365f0b5..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVG.js +++ /dev/null @@ -1,135 +0,0 @@ -import { colors } from '@dhis2/ui' -import { - svgNS, - LETTER_SPACING_MAX_THRESHOLD, - LETTER_SPACING_MIN_THRESHOLD, - LETTER_SPACING_TEXT_SIZE_FACTOR, - SUB_TEXT_SIZE_FACTOR, - SUB_TEXT_SIZE_MAX_THRESHOLD, - SUB_TEXT_SIZE_MIN_THRESHOLD, -} from './constants.js' -import { - getIconPadding, - getTextHeightForNumbers, - getTextSize, - getTextWidth, -} from './textSize.js' - -const parser = new DOMParser() - -export const generateValueSVG = ({ - renderer, - formattedValue, - subText, - valueColor, - textColor, - icon, - noData, - containerWidth, - containerHeight, - topMargin = 0, -}) => { - const showIcon = icon && formattedValue !== noData.text - const group = renderer - .g('value') - .css({ - transform: 'translate(50%, 50%)', - }) - .add() - - const textSize = getTextSize( - formattedValue, - containerWidth, - containerHeight, - showIcon - ) - - const textWidth = getTextWidth(formattedValue, `${textSize}px Roboto`) - - const iconSize = textSize - - const subTextSize = - textSize * SUB_TEXT_SIZE_FACTOR > SUB_TEXT_SIZE_MAX_THRESHOLD - ? SUB_TEXT_SIZE_MAX_THRESHOLD - : textSize * SUB_TEXT_SIZE_FACTOR < SUB_TEXT_SIZE_MIN_THRESHOLD - ? SUB_TEXT_SIZE_MIN_THRESHOLD - : textSize * SUB_TEXT_SIZE_FACTOR - - let fillColor = colors.grey900 - - if (valueColor) { - fillColor = valueColor - } else if (formattedValue === noData.text) { - fillColor = colors.grey600 - } - - const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) - - const formattedValueText = renderer - .text(formattedValue) - .attr({ - 'font-size': textSize, - 'font-weight': '300', - 'letter-spacing': - letterSpacing < LETTER_SPACING_MIN_THRESHOLD - ? LETTER_SPACING_MIN_THRESHOLD - : letterSpacing > LETTER_SPACING_MAX_THRESHOLD - ? LETTER_SPACING_MAX_THRESHOLD - : letterSpacing, - 'text-anchor': 'middle', - width: '100%', - x: showIcon ? `${iconSize / 2 + getIconPadding(textSize / 2)}` : 0, - y: topMargin / 2 + getTextHeightForNumbers(textSize) / 2, - fill: fillColor, - 'data-test': 'visualization-primary-value', - }) - .add(group) - - // show icon if configured in maintenance app - if (showIcon) { - const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml') - const iconElHeight = - svgIconDocument.documentElement.getAttribute('height') - const iconElWidth = - svgIconDocument.documentElement.getAttribute('width') - const iconGroup = renderer - .g('icon') - .attr('data-test', 'visualization-icon') - .css({ - color: fillColor, - }) - /* Force the group element to have the same dimensions as the original - * SVG image by adding this rect. This ensures the icon has the intended - * whitespace around it and makes scaling and translating easier. */ - renderer.rect(0, 0, iconElWidth, iconElHeight).add(iconGroup) - - Array.from(svgIconDocument.documentElement.children).forEach((node) => - iconGroup.element.appendChild(node) - ) - const formattedValueTextBox = formattedValueText.getBBox() - const scaleFactor = textSize / iconElHeight - const textHeight = formattedValueTextBox.height / 2 - const iconHeight = (iconElHeight * scaleFactor) / 2 - const translateY = - (formattedValueTextBox.y + (textHeight - iconHeight)) / scaleFactor - - iconGroup - .css({ - transform: `scale(${scaleFactor}) translate(-98px, ${translateY}px)`, - }) - .add(group) - } - - if (subText) { - renderer - .text(subText) - .attr({ - 'text-anchor': 'middle', - 'font-size': subTextSize, - y: iconSize / 2 + topMargin / 2, - dy: subTextSize * 1.7, - fill: textColor, - }) - .add(group) - } -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js deleted file mode 100644 index 1a36f7eda..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/generateValueSVGOLD.js +++ /dev/null @@ -1,135 +0,0 @@ -import { colors } from '@dhis2/ui' -import { - svgNS, - LETTER_SPACING_MAX_THRESHOLD, - LETTER_SPACING_MIN_THRESHOLD, - LETTER_SPACING_TEXT_SIZE_FACTOR, - SUB_TEXT_SIZE_FACTOR, - SUB_TEXT_SIZE_MAX_THRESHOLD, - SUB_TEXT_SIZE_MIN_THRESHOLD, -} from './constants.js' -import { - getIconPadding, - getTextHeightForNumbers, - getTextSize, - getTextWidth, -} from './textSize.js' - -export const generateValueSVG = ({ - renderer, - formattedValue, - subText, - valueColor, - textColor, - icon, - noData, - containerWidth, - containerHeight, - topMargin = 0, -}) => { - const showIcon = icon && formattedValue !== noData.text - - const textSize = getTextSize( - formattedValue, - containerWidth, - containerHeight, - showIcon - ) - - const textWidth = getTextWidth(formattedValue, `${textSize}px Roboto`) - - const iconSize = textSize - - const subTextSize = - textSize * SUB_TEXT_SIZE_FACTOR > SUB_TEXT_SIZE_MAX_THRESHOLD - ? SUB_TEXT_SIZE_MAX_THRESHOLD - : textSize * SUB_TEXT_SIZE_FACTOR < SUB_TEXT_SIZE_MIN_THRESHOLD - ? SUB_TEXT_SIZE_MIN_THRESHOLD - : textSize * SUB_TEXT_SIZE_FACTOR - - const svgValue = document.createElementNS(svgNS, 'svg') - svgValue.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`) - svgValue.setAttribute('width', '50%') - svgValue.setAttribute('height', '50%') - svgValue.setAttribute('x', '50%') - svgValue.setAttribute('y', '50%') - svgValue.setAttribute('style', 'overflow: visible') - - let fillColor = colors.grey900 - - if (valueColor) { - fillColor = valueColor - } else if (formattedValue === noData.text) { - fillColor = colors.grey600 - } - - // show icon if configured in maintenance app - if (showIcon) { - // embed icon to allow changing color - // (elements with fill need to use "currentColor" for this to work) - const iconSvgNode = document.createElementNS(svgNS, 'svg') - console.log('old', iconSize) - iconSvgNode.setAttribute('viewBox', '0 0 48 48') - iconSvgNode.setAttribute('width', iconSize) - iconSvgNode.setAttribute('height', iconSize) - iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1) - iconSvgNode.setAttribute( - 'x', - `-${(iconSize + getIconPadding(textSize) + textWidth) / 2}` - ) - iconSvgNode.setAttribute('style', `color: ${fillColor}`) - iconSvgNode.setAttribute('data-test', 'visualization-icon') - - const parser = new DOMParser() - const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml') - - Array.from(svgIconDocument.documentElement.children).forEach((node) => - iconSvgNode.appendChild(node) - ) - - svgValue.appendChild(iconSvgNode) - } - - const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) - - const textNode = document.createElementNS(svgNS, 'text') - textNode.setAttribute('font-size', textSize) - textNode.setAttribute('font-weight', '300') - textNode.setAttribute( - 'letter-spacing', - letterSpacing < LETTER_SPACING_MIN_THRESHOLD - ? LETTER_SPACING_MIN_THRESHOLD - : letterSpacing > LETTER_SPACING_MAX_THRESHOLD - ? LETTER_SPACING_MAX_THRESHOLD - : letterSpacing - ) - textNode.setAttribute('text-anchor', 'middle') - textNode.setAttribute( - 'x', - showIcon ? `${(iconSize + getIconPadding(textSize)) / 2}` : 0 - ) - textNode.setAttribute( - 'y', - topMargin / 2 + getTextHeightForNumbers(textSize) / 2 - ) - textNode.setAttribute('fill', fillColor) - textNode.setAttribute('data-test', 'visualization-primary-value') - - textNode.appendChild(document.createTextNode(formattedValue)) - - svgValue.appendChild(textNode) - - if (subText) { - const subTextNode = document.createElementNS(svgNS, 'text') - subTextNode.setAttribute('text-anchor', 'middle') - subTextNode.setAttribute('font-size', subTextSize) - subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2) - subTextNode.setAttribute('dy', subTextSize * 1.7) - subTextNode.setAttribute('fill', textColor) - subTextNode.appendChild(document.createTextNode(subText)) - - svgValue.appendChild(subTextNode) - } - - renderer.box.appendChild(svgValue) -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js deleted file mode 100644 index 5d66ba074..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getTextAnchorFromTextAlign.js +++ /dev/null @@ -1,17 +0,0 @@ -import { - TEXT_ALIGN_LEFT, - TEXT_ALIGN_CENTER, - TEXT_ALIGN_RIGHT, -} from '../../../../../modules/fontStyle.js' - -export const getTextAnchorFromTextAlign = (textAlign) => { - switch (textAlign) { - default: - case TEXT_ALIGN_LEFT: - return 'start' - case TEXT_ALIGN_CENTER: - return 'middle' - case TEXT_ALIGN_RIGHT: - return 'end' - } -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js deleted file mode 100644 index d9383b4e9..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/getXFromTextAlign.js +++ /dev/null @@ -1,17 +0,0 @@ -import { - TEXT_ALIGN_LEFT, - TEXT_ALIGN_CENTER, - TEXT_ALIGN_RIGHT, -} from '../../../../../modules/fontStyle.js' - -export const getXFromTextAlign = (textAlign) => { - switch (textAlign) { - default: - case TEXT_ALIGN_LEFT: - return '1%' - case TEXT_ALIGN_CENTER: - return '50%' - case TEXT_ALIGN_RIGHT: - return '99%' - } -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js deleted file mode 100644 index b4cfd8842..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/index.js +++ /dev/null @@ -1,76 +0,0 @@ -import { colors } from '@dhis2/ui' -import { - getColorByValueFromLegendSet, - LEGEND_DISPLAY_STYLE_FILL, -} from '../../../../../modules/legends.js' -import { shouldUseContrastColor } from '../../../adapters/dhis_highcharts/customSVGOptions/singleValue/config/shouldUseContrastColor.js' -import { generateDVItem } from './generateDVItem.js' - -export default function ( - config, - parentEl, - { dashboard, legendSets, fontStyle, noData, legendOptions, icon }, - chart -) { - const renderer = chart.renderer - const legendSet = legendOptions && legendSets[0] - const legendColor = - legendSet && getColorByValueFromLegendSet(legendSet, config.value) - let valueColor, titleColor, backgroundColor - if (legendColor) { - if (legendOptions.style === LEGEND_DISPLAY_STYLE_FILL) { - backgroundColor = legendColor - valueColor = titleColor = - shouldUseContrastColor(legendColor) && colors.white - } else { - valueColor = legendColor - } - } - - parentEl.style.overflow = 'hidden' - parentEl.style.display = 'flex' - parentEl.style.justifyContent = 'center' - - // We need the inner width so borders etc are excluded - const width = parentEl.clientWidth - const height = parentEl.clientHeight - - const svgContainer = renderer.box - svgContainer.setAttribute('viewBox', `0 0 ${width} ${height}`) - svgContainer.setAttribute('data-test', 'visualization-container') - - chart.setSize(dashboard ? '100%' : width, dashboard ? '100%' : height) - - // if (dashboard) { - // parentEl.style.borderRadius = '3px' - - // return generateDashboardItem(config, { - // svgContainer, - // width, - // height, - // valueColor, - // backgroundColor, - // noData, - // icon, - // ...(legendOptions.style === LEGEND_DISPLAY_STYLE_FILL && - // legendColor && - // shouldUseContrastColor(legendColor) - // ? { titleColor: colors.white } - // : {}), - // }) - // } else { - parentEl.style.height = `100%` - - return generateDVItem(config, { - renderer, - width, - height, - valueColor, - backgroundColor, - titleColor, - noData, - icon, - fontStyle, - }) - // } -} diff --git a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js b/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js deleted file mode 100644 index a94ad7266..000000000 --- a/src/visualizations/config/generators/highcharts/renderSingleValueSvg/textSize.js +++ /dev/null @@ -1,52 +0,0 @@ -// Compute text width before rendering - -import { - ACTUAL_NUMBER_HEIGHT_FACTOR, - ACTUAL_TEXT_WIDTH_FACTOR, - ICON_PADDING_FACTOR, - TEXT_SIZE_CONTAINER_HEIGHT_FACTOR, - TEXT_SIZE_MAX_THRESHOLD, - TEXT_WIDTH_CONTAINER_WIDTH_FACTOR, -} from './constants.js' - -// Not exactly precise but close enough -export const getTextWidth = (text, font) => { - const canvas = document.createElement('canvas') - const context = canvas.getContext('2d') - context.font = font - return Math.round( - context.measureText(text).width * ACTUAL_TEXT_WIDTH_FACTOR - ) -} - -export const getTextHeightForNumbers = (textSize) => - textSize * ACTUAL_NUMBER_HEIGHT_FACTOR - -export const getIconPadding = (textSize) => - Math.round(textSize * ICON_PADDING_FACTOR) - -export const getTextSize = ( - formattedValue, - containerWidth, - containerHeight, - showIcon -) => { - let size = Math.min( - Math.round(containerHeight * TEXT_SIZE_CONTAINER_HEIGHT_FACTOR), - TEXT_SIZE_MAX_THRESHOLD - ) - - const widthThreshold = Math.round( - containerWidth * TEXT_WIDTH_CONTAINER_WIDTH_FACTOR - ) - - const textWidth = - getTextWidth(formattedValue, `${size}px Roboto`) + - (showIcon ? getIconPadding(size) : 0) - - if (textWidth > widthThreshold) { - size = Math.round(size * (widthThreshold / textWidth)) - } - - return size -} diff --git a/src/visualizations/config/generators/index.js b/src/visualizations/config/generators/index.js index 290cac165..bc7a75872 100644 --- a/src/visualizations/config/generators/index.js +++ b/src/visualizations/config/generators/index.js @@ -1,8 +1,7 @@ import dhis from './dhis/index.js' -import { highcharts, singleValue } from './highcharts/index.js' +import highcharts from './highcharts/index.js' export default { highcharts, dhis, - singleValue, } diff --git a/src/visualizations/store/adapters/index.js b/src/visualizations/store/adapters/index.js index e567b54d1..7b49438ee 100644 --- a/src/visualizations/store/adapters/index.js +++ b/src/visualizations/store/adapters/index.js @@ -4,5 +4,4 @@ import dhis_highcharts from './dhis_highcharts/index.js' export default { dhis_highcharts, dhis_dhis, - dhis_singleValue: dhis_dhis, } From 9f50432ca4ad53be6761173c43945b2b635c3540 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 18 Sep 2024 14:56:18 +0200 Subject: [PATCH 45/82] chore: re-enable no-unused-vars ESLint rule and address related issues --- src/visualizations/.eslintrc | 4 +--- .../renderer/renderSingleValueSVG.js | 2 +- .../title/__tests__/singleValue.spec.js | 17 +++++++++-------- .../config/generators/highcharts/index.js | 1 - 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/visualizations/.eslintrc b/src/visualizations/.eslintrc index f8259534e..ce5078472 100644 --- a/src/visualizations/.eslintrc +++ b/src/visualizations/.eslintrc @@ -1,7 +1,5 @@ { "rules": { - "max-params": "off", - // TODO: switch back on before merging - "no-unused-vars": "off" + "max-params": "off" } } diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js index 21f4ea398..715075df2 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/renderSingleValueSVG.js @@ -6,7 +6,7 @@ import { DynamicStyles } from './styles.js' export function renderSingleValueSVG() { const color = this.title.styles.color - const { dashboard, formattedValue, icon, subText } = + const { /* dashboard, */ formattedValue, icon, subText } = this.userOptions.customSVGOptions const dynamicStyles = new DynamicStyles() const valueElement = this.renderer diff --git a/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js b/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js index 4f5843c5d..bc8022f81 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js +++ b/src/visualizations/config/adapters/dhis_highcharts/title/__tests__/singleValue.spec.js @@ -1,25 +1,24 @@ -import { VIS_TYPE_SINGLE_VALUE } from '../../../../../../modules/visTypes.js' -import getSingleValueTitle from '../singleValue.js' +import { getSingleValueTitleText } from '../singleValue.js' jest.mock('../../../../../util/getFilterText', () => () => 'The filter text') describe('getSingleValueTitle', () => { it('returns empty title when flag hideTitle exists', () => { - expect(getSingleValueTitle({ hideTitle: true })).toEqual('') + expect(getSingleValueTitleText({ hideTitle: true })).toEqual('') }) it('returns the title provided in the layout', () => { const title = 'The title was already set' - expect(getSingleValueTitle({ title })).toEqual(title) + expect(getSingleValueTitleText({ title })).toEqual(title) }) it('returns null when layout does not have columns', () => { - expect(getSingleValueTitle({})).toEqual('') + expect(getSingleValueTitleText({})).toEqual('') }) it('returns the filter text based on column items', () => { expect( - getSingleValueTitle({ + getSingleValueTitleText({ columns: [ { items: [{}], @@ -32,7 +31,7 @@ describe('getSingleValueTitle', () => { describe('not dashboard', () => { it('returns filter text as title', () => { expect( - getSingleValueTitle( + getSingleValueTitleText( { columns: [ { @@ -50,7 +49,9 @@ describe('getSingleValueTitle', () => { describe('dashboard', () => { it('returns empty string', () => { - expect(getSingleValueTitle({ filters: {} }, {}, true)).toEqual('') + expect(getSingleValueTitleText({ filters: {} }, {}, true)).toEqual( + '' + ) }) }) }) diff --git a/src/visualizations/config/generators/highcharts/index.js b/src/visualizations/config/generators/highcharts/index.js index f3222b257..731905aaf 100644 --- a/src/visualizations/config/generators/highcharts/index.js +++ b/src/visualizations/config/generators/highcharts/index.js @@ -6,7 +6,6 @@ import HNDTD from 'highcharts/modules/no-data-to-display' import HOE from 'highcharts/modules/offline-exporting' import HPF from 'highcharts/modules/pattern-fill' import HSG from 'highcharts/modules/solid-gauge' -import renderSingleValueSvg from './renderSingleValueSvg/index.js' // apply HM(H) From d1658103b7a466ae21ff5054c8969c07b0687b79 Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Wed, 18 Sep 2024 16:22:33 +0200 Subject: [PATCH 46/82] chore: update background to white when exporting and background is transparent --- src/__demo__/SingleValue.stories.js | 30 ++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index f1dc22d56..a66315afb 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -665,15 +665,27 @@ storiesOf('SingleValue', module).add('default', () => { }, [containerStyle]) const downloadOffline = useCallback(() => { if (newChartRef.current) { - newChartRef.current.exportChartLocal({ - sourceHeight: 768, - sourceWidth: 1024, - scale: 1, - fallbackToExportServer: false, - filename: 'testOfflineDownload', - showExportInProgress: true, - type: 'image/png', - }) + const currentBackgroundColor = + newChartRef.current.userOptions.chart.backgroundColor + newChartRef.current.exportChartLocal( + { + sourceHeight: 768, + sourceWidth: 1024, + scale: 1, + fallbackToExportServer: false, + filename: 'testOfflineDownload', + showExportInProgress: true, + type: 'image/png', + }, + { + chart: { + backgroundColor: + currentBackgroundColor === 'transparent' + ? '#ffffff' + : currentBackgroundColor, + }, + } + ) } }, []) From 543eb3185efecf86f10a42235fa49439340e0d4f Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 19 Sep 2024 09:35:56 +0200 Subject: [PATCH 47/82] chore: give icon the same height as value text --- .../singleValue/renderer/styles.js | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js index f141c285d..e83dfb149 100644 --- a/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js +++ b/src/visualizations/config/adapters/dhis_highcharts/customSVGOptions/singleValue/renderer/styles.js @@ -34,17 +34,17 @@ const subTextStyles = [ ] const spacings = [ - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 140 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 127 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 115 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 102 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 90 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 77 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 64 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 52 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 39 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 27 }, - { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 14 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 200 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 182 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 164 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 146 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 128 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 110 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 92 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 74 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 56 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 38 }, + { valueTop: 8, subTextTop: 4, iconGap: 4, iconSize: 20 }, ] export const MIN_SIDE_WHITESPACE = 4 From 4de5b8b897cbe7dadaf815f20ec34fad9caf1a3f Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Thu, 19 Sep 2024 14:43:53 +0200 Subject: [PATCH 48/82] chore: adjust story so all scenarios can be tested --- src/__demo__/SingleValue.stories.js | 520 +++++++++++++++------------- 1 file changed, 287 insertions(+), 233 deletions(-) diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index a66315afb..0236a7eac 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -14,217 +14,8 @@ const innerContainerStyle = { height: '100%', } -const data = [ - { - response: { - headers: [ - { - name: 'dx', - column: 'Data', - valueType: 'TEXT', - type: 'java.lang.String', - hidden: false, - meta: true, - }, - { - name: 'value', - column: 'Value', - valueType: 'NUMBER', - type: 'java.lang.Double', - hidden: false, - meta: false, - }, - ], - metaData: { - items: { - 202308: { - uid: '202308', - code: '202308', - name: 'August 2023', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2023-08-01T00:00:00.000', - endDate: '2023-08-31T00:00:00.000', - }, - 202309: { - uid: '202309', - code: '202309', - name: 'September 2023', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2023-09-01T00:00:00.000', - endDate: '2023-09-30T00:00:00.000', - }, - 202310: { - uid: '202310', - code: '202310', - name: 'October 2023', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2023-10-01T00:00:00.000', - endDate: '2023-10-31T00:00:00.000', - }, - 202311: { - uid: '202311', - code: '202311', - name: 'November 2023', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2023-11-01T00:00:00.000', - endDate: '2023-11-30T00:00:00.000', - }, - 202312: { - uid: '202312', - code: '202312', - name: 'December 2023', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2023-12-01T00:00:00.000', - endDate: '2023-12-31T00:00:00.000', - }, - 202401: { - uid: '202401', - code: '202401', - name: 'January 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-01-01T00:00:00.000', - endDate: '2024-01-31T00:00:00.000', - }, - 202402: { - uid: '202402', - code: '202402', - name: 'February 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-02-01T00:00:00.000', - endDate: '2024-02-29T00:00:00.000', - }, - 202403: { - uid: '202403', - code: '202403', - name: 'March 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-03-01T00:00:00.000', - endDate: '2024-03-31T00:00:00.000', - }, - 202404: { - uid: '202404', - code: '202404', - name: 'April 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-04-01T00:00:00.000', - endDate: '2024-04-30T00:00:00.000', - }, - 202405: { - uid: '202405', - code: '202405', - name: 'May 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-05-01T00:00:00.000', - endDate: '2024-05-31T00:00:00.000', - }, - 202406: { - uid: '202406', - code: '202406', - name: 'June 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-06-01T00:00:00.000', - endDate: '2024-06-30T00:00:00.000', - }, - 202407: { - uid: '202407', - code: '202407', - name: 'July 2024', - dimensionItemType: 'PERIOD', - valueType: 'TEXT', - totalAggregationType: 'SUM', - startDate: '2024-07-01T00:00:00.000', - endDate: '2024-07-31T00:00:00.000', - }, - ou: { - uid: 'ou', - name: 'Organisation unit', - dimensionType: 'ORGANISATION_UNIT', - }, - O6uvpzGd5pu: { - uid: 'O6uvpzGd5pu', - code: 'OU_264', - name: 'Bo', - dimensionItemType: 'ORGANISATION_UNIT', - valueType: 'TEXT', - totalAggregationType: 'SUM', - }, - LAST_12_MONTHS: { - name: 'Last 12 months', - }, - dx: { - uid: 'dx', - name: 'Data', - dimensionType: 'DATA_X', - }, - pe: { - uid: 'pe', - name: 'Period', - dimensionType: 'PERIOD', - }, - FnYCr2EAzWS: { - uid: 'FnYCr2EAzWS', - code: 'IN_52493', - name: 'BCG Coverage <1y', - legendSet: 'BtxOoQuLyg1', - dimensionItemType: 'INDICATOR', - valueType: 'NUMBER', - totalAggregationType: 'AVERAGE', - indicatorType: { - name: 'Per cent', - displayName: 'Per cent', - factor: 100, - number: false, - }, - }, - }, - dimensions: { - dx: ['FnYCr2EAzWS'], - pe: [ - '202308', - '202309', - '202310', - '202311', - '202312', - '202401', - '202402', - '202403', - '202404', - '202405', - '202406', - '202407', - ], - ou: ['O6uvpzGd5pu'], - co: [], - }, - }, - rowContext: {}, - rows: [['FnYCr2EAzWS', '34.19']], - width: 2, - height: 1, - headerWidth: 2, - }, +const baseDataObj = { + response: { headers: [ { name: 'dx', @@ -233,9 +24,6 @@ const data = [ type: 'java.lang.String', hidden: false, meta: true, - isPrefix: false, - isCollect: false, - index: 0, }, { name: 'value', @@ -244,12 +32,8 @@ const data = [ type: 'java.lang.Double', hidden: false, meta: false, - isPrefix: false, - isCollect: false, - index: 1, }, ], - rows: [['FnYCr2EAzWS', '34.19']], metaData: { items: { 202308: { @@ -406,16 +190,11 @@ const data = [ dimensionItemType: 'INDICATOR', valueType: 'NUMBER', totalAggregationType: 'AVERAGE', - // indicatorType: { - // name: 'Per cent', - // displayName: 'Per cent', - // factor: 100, - // number: false, - // }, indicatorType: { - name: 'Custom', - displayName: 'Custom subtext', - number: true, + name: 'Per cent', + displayName: 'Per cent', + factor: 100, + number: false, }, }, }, @@ -439,8 +218,231 @@ const data = [ co: [], }, }, + rowContext: {}, + rows: [['FnYCr2EAzWS', '34.19']], + width: 2, + height: 1, + headerWidth: 2, + }, + headers: [ + { + name: 'dx', + column: 'Data', + valueType: 'TEXT', + type: 'java.lang.String', + hidden: false, + meta: true, + isPrefix: false, + isCollect: false, + index: 0, + }, + { + name: 'value', + column: 'Value', + valueType: 'NUMBER', + type: 'java.lang.Double', + hidden: false, + meta: false, + isPrefix: false, + isCollect: false, + index: 1, + }, + ], + rows: [['FnYCr2EAzWS', '34.19']], + metaData: { + items: { + 202308: { + uid: '202308', + code: '202308', + name: 'August 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-08-01T00:00:00.000', + endDate: '2023-08-31T00:00:00.000', + }, + 202309: { + uid: '202309', + code: '202309', + name: 'September 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-09-01T00:00:00.000', + endDate: '2023-09-30T00:00:00.000', + }, + 202310: { + uid: '202310', + code: '202310', + name: 'October 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-10-01T00:00:00.000', + endDate: '2023-10-31T00:00:00.000', + }, + 202311: { + uid: '202311', + code: '202311', + name: 'November 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-11-01T00:00:00.000', + endDate: '2023-11-30T00:00:00.000', + }, + 202312: { + uid: '202312', + code: '202312', + name: 'December 2023', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2023-12-01T00:00:00.000', + endDate: '2023-12-31T00:00:00.000', + }, + 202401: { + uid: '202401', + code: '202401', + name: 'January 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-01-01T00:00:00.000', + endDate: '2024-01-31T00:00:00.000', + }, + 202402: { + uid: '202402', + code: '202402', + name: 'February 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-02-01T00:00:00.000', + endDate: '2024-02-29T00:00:00.000', + }, + 202403: { + uid: '202403', + code: '202403', + name: 'March 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-03-01T00:00:00.000', + endDate: '2024-03-31T00:00:00.000', + }, + 202404: { + uid: '202404', + code: '202404', + name: 'April 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-04-01T00:00:00.000', + endDate: '2024-04-30T00:00:00.000', + }, + 202405: { + uid: '202405', + code: '202405', + name: 'May 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-05-01T00:00:00.000', + endDate: '2024-05-31T00:00:00.000', + }, + 202406: { + uid: '202406', + code: '202406', + name: 'June 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-06-01T00:00:00.000', + endDate: '2024-06-30T00:00:00.000', + }, + 202407: { + uid: '202407', + code: '202407', + name: 'July 2024', + dimensionItemType: 'PERIOD', + valueType: 'TEXT', + totalAggregationType: 'SUM', + startDate: '2024-07-01T00:00:00.000', + endDate: '2024-07-31T00:00:00.000', + }, + ou: { + uid: 'ou', + name: 'Organisation unit', + dimensionType: 'ORGANISATION_UNIT', + }, + O6uvpzGd5pu: { + uid: 'O6uvpzGd5pu', + code: 'OU_264', + name: 'Bo', + dimensionItemType: 'ORGANISATION_UNIT', + valueType: 'TEXT', + totalAggregationType: 'SUM', + }, + LAST_12_MONTHS: { + name: 'Last 12 months', + }, + dx: { + uid: 'dx', + name: 'Data', + dimensionType: 'DATA_X', + }, + pe: { + uid: 'pe', + name: 'Period', + dimensionType: 'PERIOD', + }, + FnYCr2EAzWS: { + uid: 'FnYCr2EAzWS', + code: 'IN_52493', + name: 'BCG Coverage <1y', + legendSet: 'BtxOoQuLyg1', + dimensionItemType: 'INDICATOR', + valueType: 'NUMBER', + totalAggregationType: 'AVERAGE', + }, + }, + dimensions: { + dx: ['FnYCr2EAzWS'], + pe: [ + '202308', + '202309', + '202310', + '202311', + '202312', + '202401', + '202402', + '202403', + '202404', + '202405', + '202406', + '202407', + ], + ou: ['O6uvpzGd5pu'], + co: [], + }, }, -] +} +const numberIndicatorType = { + name: 'Plain', + number: true, +} +const subtextIndicatorType = { + name: 'Custom', + displayName: 'Custom subtext', + number: true, +} +const percentIndicatorType = { + name: 'Per cent', + displayName: 'Per cent', + factor: 100, + number: false, +} const layout = { name: 'BCG coverage last 12 months - Bo', created: '2013-10-16T19:50:52.464', @@ -616,18 +618,23 @@ const layout = { const icon = '' -const extraOptions = { - dashboard: false, +const baseExtraOptions = { + dashboard: true, animation: 200, legendSets: [], icon, } +const indicatorTypes = ['plain', 'percent', 'subtext'] + storiesOf('SingleValue', module).add('default', () => { const newChartRef = useRef(null) const oldContainerRef = useRef(null) const newContainerRef = useRef(null) const [transpose, setTranspose] = useState(false) + const [dashboard, setDashboard] = useState(false) + const [showIcon, setShowIcon] = useState(false) + const [indicatorType, setIndicatorType] = useState('plain') const [width, setWidth] = useState(constainerStyleBase.width) const [height, setHeight] = useState(constainerStyleBase.height) const containerStyle = useMemo( @@ -641,8 +648,27 @@ storiesOf('SingleValue', module).add('default', () => { useEffect(() => { if (oldContainerRef.current && newContainerRef.current) { requestAnimationFrame(() => { + const extraOptions = { + ...baseExtraOptions, + dashboard, + icon: showIcon ? icon : undefined, + } + const dataObj = { ...baseDataObj } + + if (indicatorType === 'plain') { + dataObj.metaData.items.FnYCr2EAzWS.indicatorType = + numberIndicatorType + } + if (indicatorType === 'percent') { + dataObj.metaData.items.FnYCr2EAzWS.indicatorType = + percentIndicatorType + } + if (indicatorType === 'subtext') { + dataObj.metaData.items.FnYCr2EAzWS.indicatorType = + subtextIndicatorType + } createVisualization( - data, + [dataObj], layout, oldContainerRef.current, extraOptions, @@ -651,7 +677,7 @@ storiesOf('SingleValue', module).add('default', () => { 'dhis' ) const newVisualization = createVisualization( - data, + [dataObj], layout, newContainerRef.current, extraOptions, @@ -662,7 +688,7 @@ storiesOf('SingleValue', module).add('default', () => { newChartRef.current = newVisualization.visualization }) } - }, [containerStyle]) + }, [containerStyle, dashboard, showIcon, indicatorType]) const downloadOffline = useCallback(() => { if (newChartRef.current) { const currentBackgroundColor = @@ -720,6 +746,34 @@ storiesOf('SingleValue', module).add('default', () => { value={height.toString()} />
+ + + -
-
-
-
() => 'The sv filter title') -jest.mock( - '../../../../../util/getFilterText', - () => () => 'The default filter text' -) - -describe('getSubtitle', () => { - it('returns empty subtitle when flag hideSubtitle exists', () => { - expect(getSubtitle({ hideSubtitle: true })).toEqual('') - }) - - it('returns the subtitle provided in the layout', () => { - const subtitle = 'The subtitle was already set' - expect(getSubtitle({ subtitle })).toEqual(subtitle) - }) - - it('returns subtitle for single value vis', () => { - expect(getSubtitle({ type: VIS_TYPE_SINGLE_VALUE })).toEqual( - 'The sv filter title' - ) - }) - - describe('not dashboard', () => { - describe('layout does not include title', () => { - it('returns empty subtitle', () => { - expect(getSubtitle({ filters: {} }, {}, false)).toEqual('') - }) - }) - - describe('layout includes title', () => { - it('returns filter title as subtitle', () => { - expect( - getSubtitle( - { filters: {}, title: 'Chart title' }, - {}, - false - ) - ).toEqual('The default filter text') - }) - }) - }) - - describe('dashboard', () => { - it('returns filter title as subtitle', () => { - expect(getSubtitle({ filters: {} }, {}, true)).toEqual( - 'The default filter text' - ) - }) - }) -}) diff --git a/src/visualizations/config/adapters/dhis_dhis/subtitle/__tests__/singleValue.spec.js b/src/visualizations/config/adapters/dhis_dhis/subtitle/__tests__/singleValue.spec.js deleted file mode 100644 index 39b497f64..000000000 --- a/src/visualizations/config/adapters/dhis_dhis/subtitle/__tests__/singleValue.spec.js +++ /dev/null @@ -1,15 +0,0 @@ -import getSingleValueSubtitle from '../singleValue.js' - -jest.mock('../../../../../util/getFilterText', () => () => 'The filter text') - -describe('getSingleValueSubtitle', () => { - it('returns null when layout does not have filters', () => { - expect(getSingleValueSubtitle({})).toEqual('') - }) - - it('returns the filter text', () => { - expect(getSingleValueSubtitle({ filters: [] })).toEqual( - 'The filter text' - ) - }) -}) diff --git a/src/visualizations/config/adapters/dhis_dhis/subtitle/index.js b/src/visualizations/config/adapters/dhis_dhis/subtitle/index.js deleted file mode 100644 index 1be507be4..000000000 --- a/src/visualizations/config/adapters/dhis_dhis/subtitle/index.js +++ /dev/null @@ -1,33 +0,0 @@ -import { VIS_TYPE_SINGLE_VALUE } from '../../../../../modules/visTypes.js' -import getFilterText from '../../../../util/getFilterText.js' -import getSingleValueTitle from './singleValue.js' - -function getDefault(layout, dashboard, metaData) { - if (dashboard || typeof layout.title === 'string') { - return getFilterText(layout.filters, metaData) - } - - return '' -} - -export default function (layout, metaData, dashboard) { - if (layout.hideSubtitle) { - return '' - } - - if (typeof layout.subtitle === 'string' && layout.subtitle.length) { - return layout.subtitle - } else { - let subtitle - switch (layout.type) { - case VIS_TYPE_SINGLE_VALUE: - subtitle = getSingleValueTitle(layout, metaData) - - break - default: - subtitle = getDefault(layout, dashboard, metaData) - } - - return subtitle - } -} diff --git a/src/visualizations/config/adapters/dhis_dhis/subtitle/singleValue.js b/src/visualizations/config/adapters/dhis_dhis/subtitle/singleValue.js deleted file mode 100644 index de246ba2f..000000000 --- a/src/visualizations/config/adapters/dhis_dhis/subtitle/singleValue.js +++ /dev/null @@ -1,5 +0,0 @@ -import getFilterText from '../../../../util/getFilterText.js' - -export default function (layout, metaData) { - return layout.filters ? getFilterText(layout.filters, metaData) : '' -} diff --git a/src/visualizations/config/adapters/dhis_dhis/title/__tests__/index.spec.js b/src/visualizations/config/adapters/dhis_dhis/title/__tests__/index.spec.js deleted file mode 100644 index 15a4b8a56..000000000 --- a/src/visualizations/config/adapters/dhis_dhis/title/__tests__/index.spec.js +++ /dev/null @@ -1,36 +0,0 @@ -import { VIS_TYPE_SINGLE_VALUE } from '../../../../../../modules/visTypes.js' -import getTitle from '../index.js' - -jest.mock('../singleValue', () => () => 'The sv filter title') -jest.mock('../../../../../util/getFilterText', () => () => 'The filter text') - -describe('getTitle', () => { - it('returns empty title when flag hideTitle exists', () => { - expect(getTitle({ hideTitle: true })).toEqual('') - }) - - it('returns the title provided in the layout', () => { - const title = 'The title was already set' - expect(getTitle({ title })).toEqual(title) - }) - - it('returns title for single value vis', () => { - expect(getTitle({ type: VIS_TYPE_SINGLE_VALUE })).toEqual( - 'The sv filter title' - ) - }) - - describe('not dashboard', () => { - it('returns filter text as title', () => { - expect(getTitle({ filters: {} }, {}, false)).toEqual( - 'The filter text' - ) - }) - }) - - describe('dashboard', () => { - it('returns empty string', () => { - expect(getTitle({ filters: {} }, {}, true)).toEqual('') - }) - }) -}) diff --git a/src/visualizations/config/adapters/dhis_dhis/title/__tests__/singleValue.spec.js b/src/visualizations/config/adapters/dhis_dhis/title/__tests__/singleValue.spec.js deleted file mode 100644 index 304be7bdb..000000000 --- a/src/visualizations/config/adapters/dhis_dhis/title/__tests__/singleValue.spec.js +++ /dev/null @@ -1,21 +0,0 @@ -import getSingleValueTitle from '../singleValue.js' - -jest.mock('../../../../../util/getFilterText', () => () => 'The filter text') - -describe('getSingleValueTitle', () => { - it('returns null when layout does not have columns', () => { - expect(getSingleValueTitle({})).toEqual('') - }) - - it('returns the filter text based on column items', () => { - expect( - getSingleValueTitle({ - columns: [ - { - items: [{}], - }, - ], - }) - ).toEqual('The filter text') - }) -}) diff --git a/src/visualizations/config/adapters/dhis_dhis/title/index.js b/src/visualizations/config/adapters/dhis_dhis/title/index.js deleted file mode 100644 index fb4c6b040..000000000 --- a/src/visualizations/config/adapters/dhis_dhis/title/index.js +++ /dev/null @@ -1,30 +0,0 @@ -import { VIS_TYPE_SINGLE_VALUE } from '../../../../../modules/visTypes.js' -import getFilterText from '../../../../util/getFilterText.js' -import getSingleValueTitle from './singleValue.js' - -function getDefault(layout, metaData, dashboard) { - return layout.filters && !dashboard - ? getFilterText(layout.filters, metaData) - : '' -} - -export default function (layout, metaData, dashboard) { - if (layout.hideTitle) { - return '' - } - - if (typeof layout.title === 'string' && layout.title.length) { - return layout.title - } else { - let title - switch (layout.type) { - case VIS_TYPE_SINGLE_VALUE: - title = getSingleValueTitle(layout, metaData) - - break - default: - title = getDefault(layout, metaData, dashboard) - } - return title - } -} diff --git a/src/visualizations/config/adapters/dhis_dhis/title/singleValue.js b/src/visualizations/config/adapters/dhis_dhis/title/singleValue.js deleted file mode 100644 index 802c866c0..000000000 --- a/src/visualizations/config/adapters/dhis_dhis/title/singleValue.js +++ /dev/null @@ -1,15 +0,0 @@ -import getFilterText from '../../../../util/getFilterText.js' - -export default function (layout, metaData) { - if (layout.columns) { - const firstItem = layout.columns[0].items[0] - - const column = Object.assign({}, layout.columns[0], { - items: [firstItem], - }) - - return getFilterText([column], metaData) - } - - return '' -} diff --git a/src/visualizations/config/adapters/dhis_dhis/type.js b/src/visualizations/config/adapters/dhis_dhis/type.js deleted file mode 100644 index 412124e58..000000000 --- a/src/visualizations/config/adapters/dhis_dhis/type.js +++ /dev/null @@ -1,10 +0,0 @@ -import { VIS_TYPE_SINGLE_VALUE } from '../../../../modules/visTypes.js' - -export default function (type) { - switch (type) { - case VIS_TYPE_SINGLE_VALUE: - return { type: VIS_TYPE_SINGLE_VALUE } - default: - return { type: VIS_TYPE_SINGLE_VALUE } - } -} diff --git a/src/visualizations/config/adapters/dhis_dhis/value/index.js b/src/visualizations/config/adapters/dhis_dhis/value/index.js deleted file mode 100644 index 508f1c9a4..000000000 --- a/src/visualizations/config/adapters/dhis_dhis/value/index.js +++ /dev/null @@ -1,22 +0,0 @@ -import { renderValue } from '../../../../../modules/renderValue.js' -import { VALUE_TYPE_TEXT } from '../../../../../modules/valueTypes.js' -import { INDICATOR_FACTOR_100 } from '../index.js' - -export default function (value, layout, metaData) { - const valueType = metaData.items[metaData.dimensions.dx[0]].valueType - const indicatorType = - metaData.items[metaData.dimensions.dx[0]].indicatorType - - let formattedValue = renderValue(value, valueType || VALUE_TYPE_TEXT, { - digitGroupSeparator: layout.digitGroupSeparator, - skipRounding: layout.skipRounding, - }) - - // only show the percentage symbol for per cent - // for other factors, show the full text under the value - if (indicatorType?.factor === INDICATOR_FACTOR_100) { - formattedValue += '%' - } - - return formattedValue -} diff --git a/src/visualizations/config/adapters/index.js b/src/visualizations/config/adapters/index.js index 7b49438ee..4db1838e0 100644 --- a/src/visualizations/config/adapters/index.js +++ b/src/visualizations/config/adapters/index.js @@ -1,7 +1,5 @@ -import dhis_dhis from './dhis_dhis/index.js' import dhis_highcharts from './dhis_highcharts/index.js' export default { dhis_highcharts, - dhis_dhis, } diff --git a/src/visualizations/config/generators/dhis/index.js b/src/visualizations/config/generators/dhis/index.js deleted file mode 100644 index b5a6c3958..000000000 --- a/src/visualizations/config/generators/dhis/index.js +++ /dev/null @@ -1,36 +0,0 @@ -import { VIS_TYPE_SINGLE_VALUE } from '../../../../modules/visTypes.js' -import getSingleValueGenerator from './singleValue.js' - -export default function (config, parentEl, extraOptions) { - if (config) { - const node = - typeof parentEl === 'object' - ? parentEl - : typeof parentEl === 'string' - ? document.querySelector(parentEl) - : null - - if (node) { - if (node.lastChild) { - node.removeChild(node.lastChild) - } - - let content - - switch (config.type) { - case VIS_TYPE_SINGLE_VALUE: - default: - content = getSingleValueGenerator( - config, - node, - extraOptions - ) - break - } - - node.appendChild(content) - - return node.innerHTML - } - } -} diff --git a/src/visualizations/config/generators/dhis/singleValue.js b/src/visualizations/config/generators/dhis/singleValue.js deleted file mode 100644 index 25ec5bab9..000000000 --- a/src/visualizations/config/generators/dhis/singleValue.js +++ /dev/null @@ -1,531 +0,0 @@ -import { colors } from '@dhis2/ui' -import { - FONT_STYLE_VISUALIZATION_TITLE, - FONT_STYLE_VISUALIZATION_SUBTITLE, - FONT_STYLE_OPTION_FONT_SIZE, - FONT_STYLE_OPTION_TEXT_COLOR, - FONT_STYLE_OPTION_TEXT_ALIGN, - FONT_STYLE_OPTION_ITALIC, - FONT_STYLE_OPTION_BOLD, - TEXT_ALIGN_LEFT, - TEXT_ALIGN_RIGHT, - TEXT_ALIGN_CENTER, - mergeFontStyleWithDefault, - defaultFontStyle, -} from '../../../../modules/fontStyle.js' -import { - getColorByValueFromLegendSet, - LEGEND_DISPLAY_STYLE_FILL, -} from '../../../../modules/legends.js' - -const svgNS = 'http://www.w3.org/2000/svg' - -// multiply text width with this factor -// to get very close to actual text width -// nb: dependent on viewbox etc -const ACTUAL_TEXT_WIDTH_FACTOR = 0.9 - -// multiply value text size with this factor -// to get very close to the actual number height -// as numbers don't go below the baseline like e.g. "j" and "g" -const ACTUAL_NUMBER_HEIGHT_FACTOR = 0.67 - -// do not allow text width to exceed this threshold -// a threshold >1 does not really make sense but text width vs viewbox is complicated -const TEXT_WIDTH_CONTAINER_WIDTH_FACTOR = 1.3 - -// do not allow text size to exceed this -const TEXT_SIZE_CONTAINER_HEIGHT_FACTOR = 0.6 -const TEXT_SIZE_MAX_THRESHOLD = 400 - -// multiply text size with this factor -// to get an appropriate letter spacing -const LETTER_SPACING_TEXT_SIZE_FACTOR = (1 / 35) * -1 -const LETTER_SPACING_MIN_THRESHOLD = -6 -const LETTER_SPACING_MAX_THRESHOLD = -1 - -// fixed top margin above title/subtitle -const TOP_MARGIN_FIXED = 16 - -// multiply text size with this factor -// to get an appropriate sub text size -const SUB_TEXT_SIZE_FACTOR = 0.5 -const SUB_TEXT_SIZE_MIN_THRESHOLD = 26 -const SUB_TEXT_SIZE_MAX_THRESHOLD = 40 - -// multiply text size with this factor -// to get an appropriate icon padding -const ICON_PADDING_FACTOR = 0.3 - -// Compute text width before rendering -// Not exactly precise but close enough -const getTextWidth = (text, font) => { - const canvas = document.createElement('canvas') - const context = canvas.getContext('2d') - context.font = font - return Math.round( - context.measureText(text).width * ACTUAL_TEXT_WIDTH_FACTOR - ) -} - -const getTextHeightForNumbers = (textSize) => - textSize * ACTUAL_NUMBER_HEIGHT_FACTOR - -const getIconPadding = (textSize) => Math.round(textSize * ICON_PADDING_FACTOR) - -const getTextSize = ( - formattedValue, - containerWidth, - containerHeight, - showIcon -) => { - let size = Math.min( - Math.round(containerHeight * TEXT_SIZE_CONTAINER_HEIGHT_FACTOR), - TEXT_SIZE_MAX_THRESHOLD - ) - - const widthThreshold = Math.round( - containerWidth * TEXT_WIDTH_CONTAINER_WIDTH_FACTOR - ) - - const textWidth = - getTextWidth(formattedValue, `${size}px Roboto`) + - (showIcon ? getIconPadding(size) : 0) - - if (textWidth > widthThreshold) { - size = Math.round(size * (widthThreshold / textWidth)) - } - - return size -} - -const generateValueSVG = ({ - formattedValue, - subText, - valueColor, - textColor, - icon, - noData, - containerWidth, - containerHeight, - topMargin = 0, -}) => { - const showIcon = icon && formattedValue !== noData.text - - const textSize = getTextSize( - formattedValue, - containerWidth, - containerHeight, - showIcon - ) - - const textWidth = getTextWidth(formattedValue, `${textSize}px Roboto`) - - const iconSize = textSize - - const subTextSize = - textSize * SUB_TEXT_SIZE_FACTOR > SUB_TEXT_SIZE_MAX_THRESHOLD - ? SUB_TEXT_SIZE_MAX_THRESHOLD - : textSize * SUB_TEXT_SIZE_FACTOR < SUB_TEXT_SIZE_MIN_THRESHOLD - ? SUB_TEXT_SIZE_MIN_THRESHOLD - : textSize * SUB_TEXT_SIZE_FACTOR - - const svgValue = document.createElementNS(svgNS, 'svg') - svgValue.setAttribute('viewBox', `0 0 ${containerWidth} ${containerHeight}`) - svgValue.setAttribute('width', '50%') - svgValue.setAttribute('height', '50%') - svgValue.setAttribute('x', '50%') - svgValue.setAttribute('y', '50%') - svgValue.setAttribute('style', 'overflow: visible') - - let fillColor = colors.grey900 - - if (valueColor) { - fillColor = valueColor - } else if (formattedValue === noData.text) { - fillColor = colors.grey600 - } - - // show icon if configured in maintenance app - if (showIcon) { - // embed icon to allow changing color - // (elements with fill need to use "currentColor" for this to work) - const iconSvgNode = document.createElementNS(svgNS, 'svg') - iconSvgNode.setAttribute('viewBox', '0 0 48 48') - iconSvgNode.setAttribute('width', iconSize) - iconSvgNode.setAttribute('height', iconSize) - iconSvgNode.setAttribute('y', (iconSize / 2 - topMargin / 2) * -1) - iconSvgNode.setAttribute( - 'x', - `-${(iconSize + getIconPadding(textSize) + textWidth) / 2}` - ) - iconSvgNode.setAttribute('style', `color: ${fillColor}`) - iconSvgNode.setAttribute('data-test', 'visualization-icon') - - const parser = new DOMParser() - const svgIconDocument = parser.parseFromString(icon, 'image/svg+xml') - - Array.from(svgIconDocument.documentElement.children).forEach((node) => - iconSvgNode.appendChild(node) - ) - - svgValue.appendChild(iconSvgNode) - } - - const letterSpacing = Math.round(textSize * LETTER_SPACING_TEXT_SIZE_FACTOR) - - const textNode = document.createElementNS(svgNS, 'text') - textNode.setAttribute('font-size', textSize) - textNode.setAttribute('font-weight', '300') - textNode.setAttribute( - 'letter-spacing', - letterSpacing < LETTER_SPACING_MIN_THRESHOLD - ? LETTER_SPACING_MIN_THRESHOLD - : letterSpacing > LETTER_SPACING_MAX_THRESHOLD - ? LETTER_SPACING_MAX_THRESHOLD - : letterSpacing - ) - textNode.setAttribute('text-anchor', 'middle') - textNode.setAttribute( - 'x', - showIcon ? `${(iconSize + getIconPadding(textSize)) / 2}` : 0 - ) - textNode.setAttribute( - 'y', - topMargin / 2 + getTextHeightForNumbers(textSize) / 2 - ) - textNode.setAttribute('fill', fillColor) - textNode.setAttribute('data-test', 'visualization-primary-value') - - textNode.appendChild(document.createTextNode(formattedValue)) - - svgValue.appendChild(textNode) - - if (subText) { - const subTextNode = document.createElementNS(svgNS, 'text') - subTextNode.setAttribute('text-anchor', 'middle') - subTextNode.setAttribute('font-size', subTextSize) - subTextNode.setAttribute('y', iconSize / 2 + topMargin / 2) - subTextNode.setAttribute('dy', subTextSize * 1.7) - subTextNode.setAttribute('fill', textColor) - subTextNode.appendChild(document.createTextNode(subText)) - - svgValue.appendChild(subTextNode) - } - - return svgValue -} - -const generateDashboardItem = ( - config, - { - svgContainer, - width, - height, - valueColor, - titleColor, - backgroundColor, - noData, - icon, - } -) => { - svgContainer.appendChild( - generateValueSVG({ - formattedValue: config.formattedValue, - subText: config.subText, - valueColor, - textColor: titleColor, - noData, - icon, - containerWidth: width, - containerHeight: height, - }) - ) - - const container = document.createElement('div') - container.setAttribute( - 'style', - `display: flex; flex-direction: column; align-items: center; justify-content: center; width: 100%; height: 100%; padding-top: 8px; ${ - backgroundColor ? `background-color:${backgroundColor};` : '' - }` - ) - - const titleStyle = `padding: 0 8px; text-align: center; font-size: 12px; color: ${ - titleColor || '#666' - };` - - const title = document.createElement('span') - title.setAttribute('style', titleStyle) - if (config.title) { - title.appendChild(document.createTextNode(config.title)) - - container.appendChild(title) - } - - if (config.subtitle) { - const subtitle = document.createElement('span') - subtitle.setAttribute('style', titleStyle + ' margin-top: 4px;') - - subtitle.appendChild(document.createTextNode(config.subtitle)) - - container.appendChild(subtitle) - } - - container.appendChild(svgContainer) - - return container -} - -const getTextAnchorFromTextAlign = (textAlign) => { - switch (textAlign) { - default: - case TEXT_ALIGN_LEFT: - return 'start' - case TEXT_ALIGN_CENTER: - return 'middle' - case TEXT_ALIGN_RIGHT: - return 'end' - } -} - -const getXFromTextAlign = (textAlign) => { - switch (textAlign) { - default: - case TEXT_ALIGN_LEFT: - return '1%' - case TEXT_ALIGN_CENTER: - return '50%' - case TEXT_ALIGN_RIGHT: - return '99%' - } -} - -const generateDVItem = ( - config, - { - svgContainer, - width, - height, - valueColor, - noData, - backgroundColor, - titleColor, - fontStyle, - icon, - } -) => { - if (backgroundColor) { - svgContainer.setAttribute( - 'style', - `background-color: ${backgroundColor};` - ) - - const background = document.createElementNS(svgNS, 'rect') - background.setAttribute('width', '100%') - background.setAttribute('height', '100%') - background.setAttribute('fill', backgroundColor) - svgContainer.appendChild(background) - } - - const svgWrapper = document.createElementNS(svgNS, 'svg') - - // title - const title = document.createElementNS(svgNS, 'text') - - const titleFontStyle = mergeFontStyleWithDefault( - fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_TITLE], - FONT_STYLE_VISUALIZATION_TITLE - ) - - const titleYPosition = - TOP_MARGIN_FIXED + - parseInt(titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]) + - 'px' - - const titleAttributes = { - x: getXFromTextAlign(titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]), - y: titleYPosition, - 'text-anchor': getTextAnchorFromTextAlign( - titleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] - ), - 'font-size': `${titleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px`, - 'font-weight': titleFontStyle[FONT_STYLE_OPTION_BOLD] - ? FONT_STYLE_OPTION_BOLD - : 'normal', - 'font-style': titleFontStyle[FONT_STYLE_OPTION_ITALIC] - ? FONT_STYLE_OPTION_ITALIC - : 'normal', - 'data-test': 'visualization-title', - fill: - titleColor && - titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === - defaultFontStyle[FONT_STYLE_VISUALIZATION_TITLE][ - FONT_STYLE_OPTION_TEXT_COLOR - ] - ? titleColor - : titleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR], - } - - Object.entries(titleAttributes).forEach(([key, value]) => - title.setAttribute(key, value) - ) - - if (config.title) { - title.appendChild(document.createTextNode(config.title)) - svgWrapper.appendChild(title) - } - - // subtitle - const subtitle = document.createElementNS(svgNS, 'text') - - const subtitleFontStyle = mergeFontStyleWithDefault( - fontStyle && fontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE], - FONT_STYLE_VISUALIZATION_SUBTITLE - ) - - const subtitleAttributes = { - x: getXFromTextAlign(subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN]), - y: titleYPosition, - dy: `${subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE] + 10}`, - 'text-anchor': getTextAnchorFromTextAlign( - subtitleFontStyle[FONT_STYLE_OPTION_TEXT_ALIGN] - ), - 'font-size': `${subtitleFontStyle[FONT_STYLE_OPTION_FONT_SIZE]}px`, - 'font-weight': subtitleFontStyle[FONT_STYLE_OPTION_BOLD] - ? FONT_STYLE_OPTION_BOLD - : 'normal', - 'font-style': subtitleFontStyle[FONT_STYLE_OPTION_ITALIC] - ? FONT_STYLE_OPTION_ITALIC - : 'normal', - fill: - titleColor && - subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR] === - defaultFontStyle[FONT_STYLE_VISUALIZATION_SUBTITLE][ - FONT_STYLE_OPTION_TEXT_COLOR - ] - ? titleColor - : subtitleFontStyle[FONT_STYLE_OPTION_TEXT_COLOR], - 'data-test': 'visualization-subtitle', - } - - Object.entries(subtitleAttributes).forEach(([key, value]) => - subtitle.setAttribute(key, value) - ) - - if (config.subtitle) { - subtitle.appendChild(document.createTextNode(config.subtitle)) - svgWrapper.appendChild(subtitle) - } - - svgContainer.appendChild(svgWrapper) - - svgContainer.appendChild( - generateValueSVG({ - formattedValue: config.formattedValue, - subText: config.subText, - valueColor, - textColor: titleColor, - noData, - icon, - containerWidth: width, - containerHeight: height, - topMargin: - TOP_MARGIN_FIXED + - ((config.title - ? parseInt(title.getAttribute('font-size')) - : 0) + - (config.subtitle - ? parseInt(subtitle.getAttribute('font-size')) - : 0)) * - 2.5, - }) - ) - - return svgContainer -} - -const shouldUseContrastColor = (inputColor = '') => { - // based on https://stackoverflow.com/questions/3942878/how-to-decide-font-color-in-white-or-black-depending-on-background-color - var color = - inputColor.charAt(0) === '#' ? inputColor.substring(1, 7) : inputColor - var r = parseInt(color.substring(0, 2), 16) // hexToR - var g = parseInt(color.substring(2, 4), 16) // hexToG - var b = parseInt(color.substring(4, 6), 16) // hexToB - var uicolors = [r / 255, g / 255, b / 255] - var c = uicolors.map((col) => { - if (col <= 0.03928) { - return col / 12.92 - } - return Math.pow((col + 0.055) / 1.055, 2.4) - }) - var L = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2] - return L <= 0.179 -} - -export default function ( - config, - parentEl, - { dashboard, legendSets, fontStyle, noData, legendOptions, icon } -) { - const legendSet = legendOptions && legendSets[0] - const legendColor = - legendSet && getColorByValueFromLegendSet(legendSet, config.value) - let valueColor, titleColor, backgroundColor - if (legendColor) { - if (legendOptions.style === LEGEND_DISPLAY_STYLE_FILL) { - backgroundColor = legendColor - valueColor = titleColor = - shouldUseContrastColor(legendColor) && colors.white - } else { - valueColor = legendColor - } - } - - parentEl.style.overflow = 'hidden' - parentEl.style.display = 'flex' - parentEl.style.justifyContent = 'center' - - const parentElBBox = parentEl.getBoundingClientRect() - const width = parentElBBox.width - const height = parentElBBox.height - - const svgContainer = document.createElementNS(svgNS, 'svg') - svgContainer.setAttribute('xmlns', svgNS) - svgContainer.setAttribute('viewBox', `0 0 ${width} ${height}`) - svgContainer.setAttribute('width', dashboard ? '100%' : width) - svgContainer.setAttribute('height', dashboard ? '100%' : height) - svgContainer.setAttribute('data-test', 'visualization-container') - - if (dashboard) { - parentEl.style.borderRadius = '3px' - - return generateDashboardItem(config, { - svgContainer, - width, - height, - valueColor, - backgroundColor, - noData, - icon, - ...(legendOptions.style === LEGEND_DISPLAY_STYLE_FILL && - legendColor && - shouldUseContrastColor(legendColor) - ? { titleColor: colors.white } - : {}), - }) - } else { - parentEl.style.height = `100%` - - return generateDVItem(config, { - svgContainer, - width, - height, - valueColor, - backgroundColor, - titleColor, - noData, - icon, - fontStyle, - }) - } -} diff --git a/src/visualizations/config/generators/index.js b/src/visualizations/config/generators/index.js index bc7a75872..5c0f9cfc9 100644 --- a/src/visualizations/config/generators/index.js +++ b/src/visualizations/config/generators/index.js @@ -1,7 +1,5 @@ -import dhis from './dhis/index.js' import highcharts from './highcharts/index.js' export default { highcharts, - dhis, } diff --git a/src/visualizations/store/adapters/dhis_dhis/index.js b/src/visualizations/store/adapters/dhis_dhis/index.js deleted file mode 100644 index 62afa2342..000000000 --- a/src/visualizations/store/adapters/dhis_dhis/index.js +++ /dev/null @@ -1,102 +0,0 @@ -import { VIS_TYPE_SINGLE_VALUE } from '../../../../modules/visTypes.js' -import getSingleValue from './singleValue.js' - -const VALUE_ID = 'value' - -function getHeaderIdIndexMap(headers) { - const map = new Map() - - headers.forEach((header, index) => { - map.set(header.name, index) - }) - - return map -} - -function getPrefixedId(row, header) { - return (header.isPrefix ? header.name + '_' : '') + row[header.index] -} - -function getIdValueMap(rows, seriesHeader, categoryHeader, valueIndex) { - const map = new Map() - - let key - let value - - rows.forEach((row) => { - key = [ - ...(seriesHeader ? [getPrefixedId(row, seriesHeader)] : []), - ...(categoryHeader ? [getPrefixedId(row, categoryHeader)] : []), - ].join('-') - - value = row[valueIndex] - - map.set(key, value) - }) - - return map -} - -function getDefault(acc, seriesIds, categoryIds, idValueMap, metaData) { - seriesIds.forEach((seriesId) => { - const serieData = [] - - categoryIds.forEach((categoryId) => { - const value = idValueMap.get(`${seriesId}-${categoryId}`) - - // DHIS2-1261: 0 is a valid value - // undefined value means the key was not found within the rows - // in that case null is returned as value in the serie - serieData.push(value === undefined ? null : parseFloat(value)) - }) - - acc.push({ - id: seriesId, - name: metaData.items[seriesId].name, - data: serieData, - }) - }) - - return acc -} - -function getValueFunction(type) { - switch (type) { - case VIS_TYPE_SINGLE_VALUE: - return getSingleValue - default: - return getDefault - } -} - -export default function ({ type, data, seriesId, categoryId }) { - const valueFunction = getValueFunction(type) - - return data.reduce((acc, res) => { - const headers = res.headers - const metaData = res.metaData - const rows = res.rows - const headerIdIndexMap = getHeaderIdIndexMap(headers) - - const seriesIndex = headerIdIndexMap.get(seriesId) - const categoryIndex = headerIdIndexMap.get(categoryId) - const valueIndex = headerIdIndexMap.get(VALUE_ID) - - const seriesHeader = headers[seriesIndex] - const categoryHeader = headers[categoryIndex] - - const idValueMap = getIdValueMap( - rows, - seriesHeader, - categoryHeader, - valueIndex - ) - - const seriesIds = metaData.dimensions[seriesId] - const categoryIds = metaData.dimensions[categoryId] - - valueFunction(acc, seriesIds, categoryIds, idValueMap, metaData) - - return acc - }, []) -} diff --git a/src/visualizations/store/adapters/dhis_dhis/singleValue.js b/src/visualizations/store/adapters/dhis_dhis/singleValue.js deleted file mode 100644 index 159838d82..000000000 --- a/src/visualizations/store/adapters/dhis_dhis/singleValue.js +++ /dev/null @@ -1,5 +0,0 @@ -export default function (acc, seriesIds, categoryIds, idValueMap) { - const seriesId = seriesIds[0] - - acc.push(idValueMap.get(seriesId)) -} diff --git a/src/visualizations/store/adapters/index.js b/src/visualizations/store/adapters/index.js index 7b49438ee..4db1838e0 100644 --- a/src/visualizations/store/adapters/index.js +++ b/src/visualizations/store/adapters/index.js @@ -1,7 +1,5 @@ -import dhis_dhis from './dhis_dhis/index.js' import dhis_highcharts from './dhis_highcharts/index.js' export default { dhis_highcharts, - dhis_dhis, } From 2f2217bf79897463f1efafcf149d61baebedaa9f Mon Sep 17 00:00:00 2001 From: HendrikThePendric Date: Tue, 22 Oct 2024 16:28:56 +0200 Subject: [PATCH 82/82] chore: adjust initial subtext state in story, so it matches dropdown --- src/__demo__/SingleValue.stories.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/__demo__/SingleValue.stories.js b/src/__demo__/SingleValue.stories.js index 1e68c5ef4..c47b82cbd 100644 --- a/src/__demo__/SingleValue.stories.js +++ b/src/__demo__/SingleValue.stories.js @@ -632,7 +632,7 @@ storiesOf('SingleValue', module).add('default', () => { const newContainerRef = useRef(null) const [dashboard, setDashboard] = useState(false) const [showIcon, setShowIcon] = useState(true) - const [indicatorType, setIndicatorType] = useState('subtext') + const [indicatorType, setIndicatorType] = useState('plain') const [exportAsPdf, setExportAsPdf] = useState(true) const [width, setWidth] = useState(constainerStyleBase.width) const [height, setHeight] = useState(constainerStyleBase.height)