@chart-plugins/superset-indicator-chart
Version:
Indicator chart plugin for Apache Superset
350 lines (327 loc) • 11.8 kB
JavaScript
import _pt from "prop-types";
let _ = t => t,
_t,
_t2,
_t3,
_t4,
_t5;
import React from 'react';
import { styled, getNumberFormatter } from '@superset-ui/core';
// Дефолтный цвет для защиты от undefined
const DEFAULT_COLOR = {
r: 11,
g: 218,
b: 81,
a: 1
}; // Зеленый цвет #0BDA51
// Стили для контейнера
const Container = styled.div(_t || (_t = _`
width: 100%;
height: 100%;
padding: 16px;
display: flex;
flex-direction: ${0};
justify-content: center;
align-items: center;
background-color: ${0};
color: ${0}; /* Support custom color values or dark/light keywords */
border-radius: ${0};
overflow: hidden;
`), ({
orientation
}) => orientation === 'vertical' ? 'column' : 'row', ({
backgroundColor
}) => {
// Защита от undefined
if (!backgroundColor || backgroundColor.r === undefined) {
return `rgba(11, 218, 81, 1)`;
}
const {
r,
g,
b,
a
} = backgroundColor;
return `rgba(${r}, ${g}, ${b}, ${a || 1})`;
}, ({
textColor
}) => textColor === 'dark' ? '#333' : textColor === 'light' ? '#fff' : textColor, ({
roundedCorners
}) => roundedCorners ? '8px' : '0');
// Стили для внешнего контейнера, который будет содержать все блоки индикаторов
const MultiBlockContainer = styled.div(_t2 || (_t2 = _`
width: 100%;
height: 100%;
display: flex;
flex-direction: ${0};
flex-wrap: wrap;
justify-content: space-between;
align-items: stretch;
overflow: auto;
`), ({
orientation
}) => orientation === 'vertical' ? 'column' : 'row');
// Стили для блока индикатора
const IndicatorBlock = styled.div(_t3 || (_t3 = _`
flex: 1;
min-width: 200px;
margin: 0 10px 10px 0;
padding: 16px;
display: flex;
flex-direction: ${0};
justify-content: center;
align-items: center;
background-color: ${0};
color: ${0};
border-radius: ${0};
overflow: hidden;
`), ({
orientation
}) => orientation === 'vertical' ? 'column' : 'row', ({
backgroundColor
}) => {
// Защита от undefined
if (!backgroundColor || backgroundColor.r === undefined) {
return `rgba(11, 218, 81, 1)`;
}
const {
r,
g,
b,
a
} = backgroundColor;
return `rgba(${r}, ${g}, ${b}, ${a || 1})`;
}, ({
textColor
}) => textColor === 'dark' ? '#404040' : textColor === 'light' ? 'gainsboro' : textColor, ({
roundedCorners
}) => roundedCorners ? '0.5em' : '0');
// Стили для отображения значений
const ValueDisplay = styled.div(_t4 || (_t4 = _`
font-size: 2rem;
font-weight: bold;
margin: 8px;
text-align: center;
width: 100%;
color: inherit; /* Ensure color is inherited from parent container */
`));
// Стили для форматированного текста
const FormattedText = styled.div(_t5 || (_t5 = _`
width: 100%;
height: 100%;
overflow: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: inherit; /* Ensure color is inherited from parent container */
/* Apply text color to all HTML elements rendered within the markdown */
* {
color: inherit;
}
`));
// Преобразуем Markdown в HTML с подстановкой значений
const renderMarkdown = (markdown, data, valueName, textColor, numberFormat) => {
let html = markdown;
try {
// Используем getNumberFormatter для форматирования чисел
// Используем 'SMART_NUMBER' только если формат не указан
const numberFormatter = getNumberFormatter(numberFormat || 'SMART_NUMBER');
// Заменяем метки значений на их значения
if (data) {
// Ищем все плейсхолдеры {{метрика}}
const regexp = /({{(.*?)}})/g;
const matches = [];
let match;
while ((match = regexp.exec(html)) !== null) {
matches.push(match);
}
// Обрабатываем каждое совпадение
for (const _match of matches) {
const metricLabel = _match[2].trim();
let metricValue = data[metricLabel];
if (metricValue !== undefined) {
// Применяем форматирование для числовых значений
if (typeof metricValue === 'number') {
metricValue = numberFormatter(metricValue);
}
html = html.replace(_match[1], String(metricValue));
}
}
// Заменяем специальный плейсхолдер {{value}} на отформатированное значение
if (data.displayValue) {
html = html.replace(/{{value}}/g, data.displayValue);
}
}
// Определяем стиль текста на основе textColor
let textStyle = 'color: inherit;';
if (textColor === 'dark') {
textStyle += ' text-shadow: 0 0 1px rgba(255, 255, 255, 0.3);';
} else if (textColor === 'light') {
textStyle += ' text-shadow: 0 0 1px rgba(0, 0, 0, 0.3);';
}
// Добавляем обертку с inline стилем для обеспечения применения цвета текста
return `<div style="${textStyle}">${html}</div>`;
} catch (error) {
console.error('Error processing markdown template:', error);
return `<div>Error processing template</div>`;
}
};
// Определяем функцию для получения цвета из форматтеров
const getColorFromFormatters = (record, colorThresholdFormatters) => {
// Проверяем, есть ли форматтеры
const hasThresholdColorFormatter = Array.isArray(colorThresholdFormatters) && colorThresholdFormatters.length > 0;
if (!hasThresholdColorFormatter) {
console.log('No formatters available, using default color');
return DEFAULT_COLOR;
}
// Если backgroundColor уже предварительно рассчитан в записи, используем его
if (record.backgroundColor && record.backgroundColor.r !== undefined) {
console.log('Using pre-calculated backgroundColor from record:', record.backgroundColor);
return record.backgroundColor;
}
// Перебираем форматтеры и применяем первый подходящий
let resultColor;
colorThresholdFormatters.forEach((formatter, index) => {
try {
console.log(`Applying formatter ${index} with column:`, formatter.column);
let formatterResult;
// Проверяем тип форматтера и применяем соответствующим образом
if (formatter.isJsFormatter) {
// Для JS-форматтера передаем всю запись
formatterResult = formatter.getColorFromValue(record);
} else {
// Для стандартного форматтера передаем значение колонки
const columnValue = record[formatter.column];
if (columnValue !== undefined) {
formatterResult = formatter.getColorFromValue(columnValue);
}
}
console.log(`Formatter ${index} result:`, formatterResult);
if (formatterResult) {
resultColor = formatterResult;
console.log(`Using color from formatter ${index}:`, resultColor);
}
} catch (error) {
console.error(`Error applying color formatter ${index}:`, error);
}
});
if (!resultColor) {
console.log('No valid color result from formatters, using default');
return DEFAULT_COLOR;
}
return resultColor;
};
// Компонент для рендеринга одного блока индикатора
const IndicatorItem = ({
record,
colorThresholdFormatters,
textColor,
orientation,
roundedCorners,
markdown,
valueName,
numberFormat
}) => {
// Определяем цвет фона из форматтеров
const backgroundColor = getColorFromFormatters(record, colorThresholdFormatters);
// Подставляем значения в markdown или показываем просто значение
const content = markdown ? /*#__PURE__*/React.createElement(FormattedText, {
dangerouslySetInnerHTML: {
__html: renderMarkdown(markdown, record, valueName, textColor, numberFormat)
}
}) : /*#__PURE__*/React.createElement(ValueDisplay, null, record.displayValue || 'No value');
return /*#__PURE__*/React.createElement(IndicatorBlock, {
orientation: orientation,
backgroundColor: backgroundColor,
textColor: textColor,
roundedCorners: roundedCorners
}, content);
};
export default function Chart(props) {
const {
data,
colorThresholdFormatters = [],
textColor = 'dark',
orientation = 'horizontal',
roundedCorners = false,
markdown = '',
valueName = '',
width,
height,
numberFormat
} = props;
// Determine which text color to use
const getActualTextColor = bgColor => {
// If textColor is explicitly specified as 'dark' or 'light', use it
if (textColor === 'dark' || textColor === 'light') {
return textColor;
}
// If textColor is a custom value (not 'dark' or 'light'), use that
if (textColor !== 'dark' && textColor !== 'light') {
return textColor;
}
// Default color logic based on background brightness
if (bgColor && bgColor.r !== undefined) {
// Calculate perceived brightness
// Formula: (0.299*R + 0.587*G + 0.114*B)
const brightness = 0.299 * bgColor.r + 0.587 * bgColor.g + 0.114 * bgColor.b;
// If background is bright, use dark text, otherwise use light text
return brightness > 128 ? 'dark' : 'light';
}
return textColor;
};
// Получаем цвет по умолчанию для основного контейнера
const defaultBackgroundColor = getColorFromFormatters({}, colorThresholdFormatters);
const actualTextColor = getActualTextColor(defaultBackgroundColor);
if (!data || data.length === 0) {
return /*#__PURE__*/React.createElement(Container, {
orientation: orientation,
backgroundColor: DEFAULT_COLOR,
textColor: actualTextColor,
roundedCorners: roundedCorners
}, /*#__PURE__*/React.createElement(ValueDisplay, null, "No data"));
}
// Вместо использования только первой записи, используем все записи
return /*#__PURE__*/React.createElement("div", {
style: {
width: width || '100%',
height: height || '100%',
overflow: 'auto',
boxSizing: 'border-box',
// Ensure padding is included in dimensions
padding: '0',
// Remove any default padding
display: 'flex' // Use flex to ensure child fills the space
}
}, /*#__PURE__*/React.createElement(MultiBlockContainer, {
orientation: orientation,
width: width || 0,
height: height || 0,
style: {
flex: 1
} // Ensure it takes up all available space
}, data.map((record, index) => /*#__PURE__*/React.createElement(IndicatorItem, {
key: index,
record: record,
colorThresholdFormatters: colorThresholdFormatters,
textColor: actualTextColor,
orientation: orientation,
roundedCorners: roundedCorners,
markdown: markdown,
valueName: valueName,
numberFormat: numberFormat
}))));
}
Chart.propTypes = {
width: _pt.number.isRequired,
height: _pt.number.isRequired,
data: _pt.arrayOf(_pt.any).isRequired,
colorThresholdFormatters: _pt.arrayOf(_pt.any).isRequired,
textColor: _pt.string,
orientation: _pt.string,
roundedCorners: _pt.bool,
markdown: _pt.string,
valueName: _pt.string,
numberFormat: _pt.string
};