// Licensed under the MIT License
// https://github.com/craigahobbs/markdown-up/blob/main/LICENSE
/** @module lib/lineChart */
import {formatValue, parameterValue, valueParameter} from './dataUtil.js';
import {parseSchemaMarkdown} from 'schema-markdown/lib/parser.js';
import {validateType} from 'schema-markdown/lib/schema.js';
import {valueCompare} from 'bare-script/lib/value.js';
// The line chart model's Schema Markdown
export const lineChartTypes = parseSchemaMarkdown(`\
group "Line Chart"
# A line chart model
struct LineChart
# The chart title
optional string title
# The chart width
optional int width
# The chart height
optional int height
# The numeric formatting precision (default is 2)
optional int(>= 0) precision
# The datetime format
optional LineChartDatetimeFormat datetime
# The line chart's X-axis field
string x
# The line chart's Y-axis fields
string[len > 0] y
# The color encoding field
optional string color
# The X-axis tick marks
optional LineChartAxisTicks xTicks
# The Y-axis tick marks
optional LineChartAxisTicks yTicks
# The X-axis annotations
optional LineChartAxisAnnotation[len > 0] xLines
# The Y-axis annotations
optional LineChartAxisAnnotation[len > 0] yLines
# The axis tick mark model
struct LineChartAxisTicks
# The count of evenly-spaced tick marks. The default is 3.
optional int(>= 0) count
# The value of the first tick mark. Default is the minimum axis value.
optional object start
# The value of the last tick mark. Default is the maximum axis value.
optional object end
# The number of tick mark labels to skip after a rendered label
optional int(> 0) skip
# An axis annotation
struct LineChartAxisAnnotation
# The axis value
object value
# The annotation label
optional string label
# A datetime format
enum LineChartDatetimeFormat
# ISO datetime year format
year
# ISO datetime month format
month
# ISO datetime day format
day
`);
/**
* Validate a line chart model
*
* @param {Object} lineChart - The
* [line chart model]{@link https://craigahobbs.github.io/markdown-up/library/model.html#var.vName='LineChart'}
* @returns {Object}
* @throws [ValidationError]{@link https://craigahobbs.github.io/schema-markdown-js/module-lib_schema.ValidationError.html}
*/
export function validateLineChart(lineChart) {
return validateType(lineChartTypes, 'LineChart', lineChart);
}
// Line chart defaults
const defaultWidth = 640;
const defaultHeight = 320;
const defaultXAxisTickCount = 3;
const defaultYAxisTickCount = 3;
// The categorical color palette
const categoricalColors = [
'#1f77b4',
'#ff7f0e',
'#2ca02c',
'#d62728',
'#9467bd',
'#8c564b',
'#e377c2',
'#7f7f7f',
'#bcbd22',
'#17becf',
'#aec7e8',
'#ffbb78',
'#98df8a',
'#ff9896',
'#c5b0d5',
'#c49c94',
'#f7b6d2',
'#c7c7c7',
'#dbdb8d',
'#9edae5'
];
// Line chart constants (all numbers in pixels)
const pixelsPerPoint = 4 / 3;
const svgPrecision = 3;
const chartFontFamily = 'Arial, Helvetica, sans-serif';
const chartFontWidthRatio = 0.6;
const chartFontDefaultSize = 12;
const chartBackgroundColor = 'white';
const axisColor = 'black';
const axisTickLineColor = 'lightgray';
const axisLineWidth = 1;
const axisTickWidth = 1;
const axisTickLength = 5;
const annotationBackgroundColor = '#ffffffa0';
const annotationTextColor = 'black';
const annotationLineColor = 'black';
const annotationLineWidth = 2;
const chartLineWidth = 3;
/**
* The line chart options object
*
* @typedef {Object} LineChartOptions
* @property {number} [fontSize] - The font size, in points
*/
/**
* Render a line chart
*
* @param {Object[]} data - The data array
* @param {Object} lineChart - The
* [line chart model]{@link https://craigahobbs.github.io/markdown-up/library/model.html#var.vName='LineChart'}
* @param {?Object} [options = null] - The [line chart options]{@link module:lib/lineChart~LineChartOptions}
* @returns {Object} The line chart [element model]{@link https://github.com/craigahobbs/element-model#readme}
*/
export function lineChartElements(data, lineChart, options = null) {
const chartFontSize = pixelsPerPoint * (options !== null && 'fontSize' in options ? options.fontSize : chartFontDefaultSize);
const xField = lineChart.x;
const yFields = lineChart.y;
const colorField = lineChart.color ?? null;
// Sort the rows by the X field
data.sort((row1, row2) => {
const x1 = row1[xField] ?? null;
const x2 = row2[xField] ?? null;
return valueCompare(x1, x2);
});
// Generate the line points - [(label, color, points), ...]
const linePoints = [];
let xMin = null;
let yMin = null;
let xMax = null;
let yMax = null;
if (colorField !== null) {
// Determine the set of color encoding values
const colorValueSet = new Set();
const pointsMap = {};
for (const row of data) {
for (const yField of yFields) {
const colorValue = formatValue(row[colorField] ?? null, lineChart.precision, lineChart.datetime);
const rowKey = (yFields.length === 1 ? colorValue : `${yField}, ${colorValue}`);
colorValueSet.add(rowKey);
const xRow = row[xField] ?? null;
const yRow = row[yField] ?? null;
if (xRow !== null && yRow !== null) {
if (!(rowKey in pointsMap)) {
pointsMap[rowKey] = [];
}
pointsMap[rowKey].push([xRow, yRow]);
xMin = (xMin === null ? xRow : xMin);
yMin = (yMin === null ? yRow : (yRow < yMin ? yRow : yMin));
xMax = xRow;
yMax = (yMax === null ? yRow : (yRow > yMax ? yRow : yMax));
}
}
}
// Add the points
const colorValues = Array.from(colorValueSet.values()).sort();
const colorValueCount = colorValues.length;
for (let ixColorValue = 0; ixColorValue < colorValueCount; ixColorValue += 1) {
const colorValue = colorValues[ixColorValue];
const color = categoricalColors[ixColorValue % categoricalColors.length];
const points = pointsMap[colorValue];
linePoints.push({'label': colorValue, color, points});
}
} else {
// Create a line for each y-field
const fieldCount = yFields.length;
for (let ixField = 0; ixField < fieldCount; ixField += 1) {
const yField = yFields[ixField];
const color = categoricalColors[ixField % categoricalColors.length];
const points = [];
linePoints.push({'label': yField, color, points});
// Add the points
for (const row of data) {
const xRow = row[xField] ?? null;
const yRow = row[yField] ?? null;
if (xRow !== null && yRow !== null) {
points.push([xRow, yRow]);
xMin = (xMin === null ? xRow : xMin);
yMin = (yMin === null ? yRow : (yRow < yMin ? yRow : yMin));
xMax = xRow;
yMax = (yMax === null ? yRow : (yRow > yMax ? yRow : yMax));
}
}
}
}
// No data?
if (xMin === null) {
throw new Error('No data');
}
// Compute the chart title, width, and height
const chartTitle = lineChart.title ?? null;
const chartWidth = lineChart.width ?? defaultWidth;
const chartHeight = lineChart.height ?? defaultHeight;
// Compute Y-axis tick values
const yAxisTicks = [];
const yTickCount = ('yTicks' in lineChart && 'count' in lineChart.yTicks ? lineChart.yTicks.count : defaultYAxisTickCount);
const yTickSkip = ('yTicks' in lineChart && 'skip' in lineChart.yTicks ? lineChart.yTicks.skip + 1 : 1);
const yTickStart = ('yTicks' in lineChart && 'start' in lineChart.yTicks ? lineChart.yTicks.start : yMin);
const yTickEnd = ('yTicks' in lineChart && 'end' in lineChart.yTicks ? lineChart.yTicks.end : yMax);
for (let ixTick = 0; ixTick < yTickCount; ixTick++) {
const yTickParam = yTickCount === 1 ? 0 : ixTick / (yTickCount - 1);
const yTickValue = parameterValue(yTickParam, yTickStart, yTickEnd);
yAxisTicks.push([yTickValue, (ixTick % yTickSkip) !== 0 ? '' : formatValue(yTickValue, lineChart.precision, lineChart.datetime)]);
yMin = (yTickValue < yMin ? yTickValue : yMin);
yMax = (yTickValue > yMax ? yTickValue : yMax);
}
// Compute X-axis tick values
const xAxisTicks = [];
const xTickCount = ('xTicks' in lineChart && 'count' in lineChart.xTicks ? lineChart.xTicks.count : defaultXAxisTickCount);
const xTickSkip = ('xTicks' in lineChart && 'skip' in lineChart.xTicks ? lineChart.xTicks.skip + 1 : 1);
const xTickStart = ('xTicks' in lineChart && 'start' in lineChart.xTicks ? lineChart.xTicks.start : xMin);
const xTickEnd = ('xTicks' in lineChart && 'end' in lineChart.xTicks ? lineChart.xTicks.end : xMax);
for (let ixTick = 0; ixTick < xTickCount; ixTick++) {
const xTickParam = xTickCount === 1 ? 0 : ixTick / (xTickCount - 1);
const xTickValue = parameterValue(xTickParam, xTickStart, xTickEnd);
xAxisTicks.push([xTickValue, (ixTick % xTickSkip) !== 0 ? '' : formatValue(xTickValue, lineChart.precision, lineChart.datetime)]);
xMin = (xTickValue < xMin ? xTickValue : xMin);
xMax = (xTickValue > xMax ? xTickValue : xMax);
}
// Compute Y-axis annotations
const yAxisAnnotations = [];
if ('yLines' in lineChart) {
for (const annotation of lineChart.yLines) {
const yAnnotationValue = annotation.value;
yAxisAnnotations.push([
yAnnotationValue,
annotation.label ?? formatValue(yAnnotationValue, lineChart.precision, lineChart.datetime)
]);
yMin = (yAnnotationValue < yMin ? yAnnotationValue : yMin);
yMax = (yAnnotationValue > yMax ? yAnnotationValue : yMax);
}
}
// Compute X-axis annotations
const xAxisAnnotations = [];
if ('xLines' in lineChart) {
for (const annotation of lineChart.xLines) {
const xAnnotationValue = annotation.value;
xAxisAnnotations.push([
xAnnotationValue,
annotation.label ?? formatValue(xAnnotationValue, lineChart.precision, lineChart.datetime)
]);
xMin = (xAnnotationValue < xMin ? xAnnotationValue : xMin);
xMax = (xAnnotationValue > xMax ? xAnnotationValue : xMax);
}
}
// Chart title calculations
const chartBorderSize = chartFontSize;
const chartTitleFontSize = 1.1 * chartFontSize;
const chartTitleHeight = (chartTitle !== null ? 1.5 * chartTitleFontSize : 0);
// Y-axis calculations
const axisTitleFontSize = 1 * chartFontSize;
const axisLabelFontSize = chartFontSize;
const yAxisTitle = (yFields.length === 1 ? yFields[0] : null);
const yAxisTitleWidth = (yAxisTitle !== null ? 1.8 * axisTitleFontSize : 0);
const yAxisLabelWidth = yAxisTicks.reduce((labelMax, [, label]) => {
const labelWidth = label.length * chartFontWidthRatio * axisLabelFontSize;
return labelWidth > labelMax ? labelWidth : labelMax;
}, 0);
const yAxisTickGap = 0.75 * axisTickLength;
const yAxisX = Math.min(
chartBorderSize + yAxisTitleWidth + (yAxisTicks.length === 0 ? 0 : yAxisLabelWidth + yAxisTickGap + axisTickLength),
0.4 * chartWidth
);
// X-axis calculations
const xAxisTitleHeight = 1.8 * axisTitleFontSize;
const xAxisTickGap = 0.75 * axisTickLength;
const xAxisY = chartHeight - chartBorderSize - xAxisTitleHeight -
(xAxisTicks.length === 0 ? 0 : axisLabelFontSize - xAxisTickGap + axisTickLength);
// Y-axis annotation calculations
const annotationLabelFontSize = axisLabelFontSize;
const annotationLabelMargin = 0.25 * annotationLabelFontSize;
const annotationLabelHeight = annotationLabelFontSize + 2 * annotationLabelMargin;
const yAnnotationLabelOffsetX = 0.2 * annotationLabelFontSize;
const yAnnotationLabelOffsetY = 0.1 * annotationLabelFontSize;
// Y-axis annotation calculations
const xAnnotationLabelOffsetX = 0.2 * annotationLabelFontSize;
const xAnnotationLabelOffsetY = 0.1 * annotationLabelFontSize;
// Color legend calculations
const colorLegendFontSize = chartFontSize;
const colorLegendGap = 0.5 * colorLegendFontSize;
const colorLegendLabelHeight = colorLegendFontSize;
const colorLegendLabelGap = 0.35 * colorLegendLabelHeight;
const colorLegendSampleWidth = 1.35 * colorLegendLabelHeight;
const colorLegendLabelWidth = linePoints.reduce((labelMax, {label}) => {
const labelWidth = label.length * chartFontWidthRatio * colorLegendFontSize;
return labelWidth > labelMax ? labelWidth : labelMax;
}, 0);
const colorLegendX = yFields.length === 1 && !('color' in lineChart) ? null : Math.max(
chartWidth - chartBorderSize - colorLegendLabelWidth - colorLegendSampleWidth,
0.6 * chartWidth
);
// Chart area calculations
const chartTop = chartBorderSize + chartTitleHeight + 0.5 * chartLineWidth;
const chartLeft = yAxisX + 0.5 * axisLineWidth + 0.5 * chartLineWidth;
const chartBottom = xAxisY - 0.5 * axisLineWidth - 0.5 * chartLineWidth;
const chartRight = (colorLegendX !== null ? colorLegendX - colorLegendGap : chartWidth - chartBorderSize);
// Axis label limits
const xAxisLabelLeft = chartLeft + 0.5 * axisLabelFontSize;
const xAxisLabelRight = chartRight - 0.5 * axisLabelFontSize;
const yAxisLabelTop = chartTop + 0.5 * axisLabelFontSize;
const yAxisLabelBottom = chartBottom - 0.5 * axisLabelFontSize;
// Helper functions to compute chart coordindate points
const chartPointX = (xCoord) => parameterValue(valueParameter(xCoord, xMin, xMax), chartLeft, chartRight);
const chartPointY = (yCoord) => parameterValue(valueParameter(yCoord, yMin, yMax), chartBottom, chartTop);
const svgValue = (value) => value.toFixed(svgPrecision);
const chartPathPoint = ([xCoord, yCoord], ixPoint, points) => {
const xPoint = chartPointX(xCoord);
const yPoint = chartPointY(yCoord);
if (points.length === 1) {
return `M ${svgValue(xPoint - 0.5 * chartLineWidth)} ${svgValue(yPoint)} ` +
`L ${svgValue(xPoint + 0.5 * chartLineWidth)} ${svgValue(yPoint)}`;
}
return `${ixPoint === 0 ? 'M' : 'L'} ${svgValue(xPoint)} ${svgValue(yPoint)}`;
};
// Render the chart
return {
'svg': 'svg',
'attr': {
'width': chartWidth,
'height': chartHeight
},
'elem': [
// Background
{
'svg': 'rect',
'attr': {
'width': chartWidth,
'height': chartHeight,
'fill': chartBackgroundColor
}
},
// Chart title
chartTitle === null ? null : {
'svg': 'text',
'attr': {
'font-family': chartFontFamily,
'font-size': `${svgValue(chartTitleFontSize)}px`,
'fill': axisColor,
'style': 'font-weight: bold',
'x': svgValue(0.5 * (chartLeft + chartRight)),
'y': svgValue(chartBorderSize),
'text-anchor': 'middle',
'dominant-baseline': 'hanging'
},
'elem': {'text': chartTitle}
},
// Y-Axis title
yAxisTitle === null ? null : {
'svg': 'text',
'attr': {
'font-family': chartFontFamily,
'font-size': `${svgValue(axisTitleFontSize)}px`,
'fill': axisColor,
'style': 'font-weight: bold',
'x': svgValue(chartBorderSize),
'y': svgValue(0.5 * (chartTop + chartBottom)),
'transform': `rotate(-90 ${svgValue(chartBorderSize)}, ${svgValue(0.5 * (chartTop + chartBottom))})`,
'text-anchor': 'middle',
'dominant-baseline': 'hanging'
},
'elem': {'text': yAxisTitle}
},
// Y-axis ticks
yAxisTicks.map(([yCoord, yLabel]) => {
const yPoint = chartPointY(yCoord);
const hasLabel = yLabel !== '';
const hasLine = !hasLabel || (yPoint > yAxisLabelTop && yPoint < yAxisLabelBottom);
return [
!hasLabel ? null : {
'svg': 'path',
'attr': {
'stroke': axisColor,
'stroke-width': svgValue(axisTickWidth),
'fill': 'none',
'd': `M ${svgValue(yAxisX)} ${svgValue(yPoint)} H ${svgValue(yAxisX - axisTickLength)}`
}
},
!hasLine ? null : {
'svg': 'path',
'attr': {
'stroke': axisTickLineColor,
'stroke-width': svgValue(axisTickWidth),
'fill': 'none',
'd': `M ${svgValue(yAxisX)} ${svgValue(yPoint)} H ${svgValue(chartRight)}`
}
}
];
}),
// Y-axis labels
yAxisTicks.map(([yCoord, yLabel]) => {
const yPoint = chartPointY(yCoord);
const hasLabel = yLabel !== '';
return !hasLabel ? null : {
'svg': 'text',
'attr': {
'font-family': chartFontFamily,
'font-size': `${svgValue(axisLabelFontSize)}px`,
'fill': axisColor,
'x': svgValue(yAxisX - axisTickLength - yAxisTickGap),
'y': svgValue(yPoint),
'text-anchor': 'end',
'dominant-baseline': (yPoint > yAxisLabelBottom ? 'auto' : (yPoint < yAxisLabelTop ? 'hanging' : 'middle'))
},
'elem': {'text': yLabel}
};
}),
// X-Axis title
{
'svg': 'text',
'attr': {
'font-family': chartFontFamily,
'font-size': `${svgValue(axisTitleFontSize)}px`,
'fill': axisColor,
'style': 'font-weight: bold',
'x': svgValue(0.5 * (chartLeft + chartRight)),
'y': svgValue(chartHeight - chartBorderSize),
'text-anchor': 'middle',
'dominant-baseline': 'auto'
},
'elem': {'text': xField}
},
// X-axis ticks
xAxisTicks.map(([xCoord, xLabel]) => {
const xPoint = chartPointX(xCoord);
const hasLabel = xLabel !== '';
const hasLine = !hasLabel || (xPoint > xAxisLabelLeft && xPoint < xAxisLabelRight);
return [
!hasLabel ? null : {
'svg': 'path',
'attr': {
'stroke': axisColor,
'stroke-width': svgValue(axisTickWidth),
'fill': 'none',
'd': `M ${svgValue(xPoint)} ${svgValue(xAxisY)} V ${svgValue(xAxisY + axisTickLength)}`
}
},
!hasLine ? null : {
'svg': 'path',
'attr': {
'stroke': axisTickLineColor,
'stroke-width': svgValue(axisTickWidth),
'fill': 'none',
'd': `M ${svgValue(xPoint)} ${svgValue(xAxisY)} V ${svgValue(chartTop)}`
}
}
];
}),
// X-axis labels
xAxisTicks.map(([xCoord, xLabel]) => {
const xPoint = chartPointX(xCoord);
const hasLabel = xLabel !== '';
return !hasLabel ? null : {
'svg': 'text',
'attr': {
'font-family': chartFontFamily,
'font-size': `${svgValue(axisLabelFontSize)}px`,
'fill': axisColor,
'x': svgValue(xPoint),
'y': svgValue(xAxisY + axisTickLength + xAxisTickGap),
'text-anchor': (xPoint < xAxisLabelLeft ? 'start' : (xPoint > xAxisLabelRight ? 'end' : 'middle')),
'dominant-baseline': 'hanging'
},
'elem': {'text': xLabel}
};
}),
// Axis lines
{
'svg': 'path',
'attr': {
'stroke': axisColor,
'stroke-width': svgValue(axisLineWidth),
'fill': 'none',
'd': `M ${svgValue(yAxisX)} ${svgValue(chartTop - 0.5 * axisTickWidth)} ` +
`V ${svgValue(xAxisY)} H ${svgValue(chartRight + 0.5 * axisTickWidth)}`
}
},
// Lines
linePoints.map(({color, points}) => ({
'svg': 'path',
'attr': {
'stroke': color,
'stroke-width': svgValue(chartLineWidth),
'fill': 'none',
'd': points.map(chartPathPoint).join(' ')
}
})),
// Y-axis annotations
yAxisAnnotations.map(([yCoord, yLabel]) => {
const yPoint = chartPointY(yCoord);
const isUnder = yPoint < 0.5 * (chartTop + chartBottom);
const labelWidth = 2 * annotationLabelMargin + yLabel.length * chartFontWidthRatio * annotationLabelFontSize;
const labelY = isUnder
? yPoint + annotationLineWidth + yAnnotationLabelOffsetY
: yPoint - annotationLineWidth - yAnnotationLabelOffsetY - annotationLabelHeight;
return [
yLabel === '' ? null : {
'svg': 'rect',
'attr': {
'x': svgValue(chartLeft + yAnnotationLabelOffsetX),
'y': svgValue(labelY),
'width': svgValue(labelWidth),
'height': svgValue(annotationLabelHeight),
'fill': annotationBackgroundColor
}
},
yLabel === '' ? null : {
'svg': 'text',
'attr': {
'font-family': chartFontFamily,
'font-size': `${svgValue(annotationLabelFontSize)}px`,
'fill': annotationTextColor,
'x': svgValue(chartLeft + yAnnotationLabelOffsetX + annotationLabelMargin),
'y': svgValue(labelY + annotationLabelMargin + 0.5 * annotationLabelFontSize),
'text-anchor': 'start',
'dominant-baseline': 'middle'
},
'elem': {'text': yLabel}
},
{
'svg': 'path',
'attr': {
'stroke': annotationLineColor,
'stroke-width': svgValue(annotationLineWidth),
'fill': 'none',
'd': `M ${svgValue(yAxisX)} ${svgValue(yPoint)} H ${svgValue(chartRight)}`
}
}
];
}),
// X-axis annotations
xAxisAnnotations.map(([xCoord, xLabel]) => {
const xPoint = chartPointX(xCoord);
const isLeft = xPoint > 0.5 * (chartLeft + chartRight);
const labelWidth = 2 * annotationLabelMargin + xLabel.length * chartFontWidthRatio * annotationLabelFontSize;
const labelY = chartBottom - xAnnotationLabelOffsetY - annotationLabelHeight;
return [
xLabel === '' ? null : {
'svg': 'rect',
'attr': {
'x': svgValue(isLeft
? xPoint - 0.5 * annotationLineWidth - xAnnotationLabelOffsetX - labelWidth
: xPoint + 0.5 * annotationLineWidth + xAnnotationLabelOffsetX),
'y': svgValue(labelY),
'width': svgValue(labelWidth),
'height': svgValue(annotationLabelHeight),
'fill': annotationBackgroundColor
}
},
xLabel === '' ? null : {
'svg': 'text',
'attr': {
'font-family': chartFontFamily,
'font-size': `${svgValue(annotationLabelFontSize)}px`,
'fill': annotationTextColor,
'x': svgValue(isLeft
? xPoint - 0.5 * annotationLineWidth - xAnnotationLabelOffsetX - annotationLabelMargin
: xPoint + 0.5 * annotationLineWidth + xAnnotationLabelOffsetX + annotationLabelMargin),
'y': svgValue(labelY + annotationLabelMargin + 0.5 * annotationLabelFontSize),
'text-anchor': (isLeft ? 'end' : 'start'),
'dominant-baseline': 'middle'
},
'elem': {'text': xLabel}
},
{
'svg': 'path',
'attr': {
'stroke': annotationLineColor,
'stroke-width': svgValue(annotationLineWidth),
'fill': 'none',
'd': `M ${svgValue(xPoint)} ${svgValue(xAxisY)} V ${svgValue(chartTop)}`
}
}
];
}),
// Color legend
colorLegendX === null ? null : linePoints.map(({label, color}, ix) => [
{
'svg': 'rect',
'attr': {
'x': svgValue(colorLegendX),
'y': svgValue(chartTop + ix * (colorLegendLabelHeight + colorLegendLabelGap)),
'width': svgValue(colorLegendLabelHeight),
'height': svgValue(colorLegendLabelHeight),
'stroke': 'none',
'fill': color
}
},
{
'svg': 'text',
'attr': {
'font-family': chartFontFamily,
'font-size': `${svgValue(colorLegendFontSize)}px`,
'fill': axisColor,
'x': svgValue(colorLegendX + colorLegendSampleWidth),
'y': svgValue(chartTop + ix * (colorLegendLabelHeight + colorLegendLabelGap) + 0.5 * colorLegendLabelHeight),
'text-anchor': 'start',
'dominant-baseline': 'middle'
},
'elem': {'text': label}
}
])
]
};
}