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 (