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) +}