Skip to content

Commit

Permalink
feat: single value as a highcharts instance WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
HendrikThePendric committed Aug 27, 2024
1 parent 59f717e commit 7cae17e
Show file tree
Hide file tree
Showing 14 changed files with 1,157 additions and 3 deletions.
639 changes: 639 additions & 0 deletions src/__demo__/SingleValue.stories.js

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion src/visualizations/.eslintrc
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
{
"rules": {
"max-params": "off"
"max-params": "off",
// TODO: switch back on before merging
"no-unused-vars": "off"
}
}
1 change: 1 addition & 0 deletions src/visualizations/config/adapters/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ import dhis_highcharts from './dhis_highcharts/index.js'
export default {
dhis_highcharts,
dhis_dhis,
dhis_singleValue: dhis_dhis,
}
26 changes: 25 additions & 1 deletion src/visualizations/config/generators/highcharts/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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,
})
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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,
})
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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'
}
}
Loading

0 comments on commit 7cae17e

Please sign in to comment.