UNPKG

@chart-plugins/superset-indicator-chart

Version:

Indicator chart plugin for Apache Superset

350 lines (327 loc) 11.8 kB
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 };