diff --git a/public/locales/en/charts.json b/public/locales/en/charts.json new file mode 100644 index 000000000000..2a593d33b601 --- /dev/null +++ b/public/locales/en/charts.json @@ -0,0 +1,6 @@ +{ + "goToLibrary": "Go to library", + "title": "Playground", + "apply": "Apply", + "reset": "Reset" +} diff --git a/public/locales/en/libraries-info.json b/public/locales/en/libraries-info.json index 0370f0d2a2b0..d23fd612d252 100644 --- a/public/locales/en/libraries-info.json +++ b/public/locales/en/libraries-info.json @@ -37,5 +37,6 @@ "description_table": "Library for visualizing data in tabular format.", "description_timeline": "A React-based library for building interactive timeline visualizations with canvas rendering.", "description_page-constructor-builder": "A powerful command-line utility for building static pages from YAML configurations", - "description_aikit": "A flexible React component library for building AI chats with Atomic Design principles." + "description_aikit": "A flexible React component library for building AI chats with Atomic Design principles.", + "description_charts": "A flexible JavaScript library for data visualization and chart rendering using React." } diff --git a/public/locales/ru/charts.json b/public/locales/ru/charts.json new file mode 100644 index 000000000000..018c13d497f1 --- /dev/null +++ b/public/locales/ru/charts.json @@ -0,0 +1,6 @@ +{ + "goToLibrary": "К библиотеке", + "title": "Редактор", + "apply": "Применить", + "reset": "Сбросить" +} diff --git a/public/locales/ru/libraries-info.json b/public/locales/ru/libraries-info.json index 30aab809f7e7..151b7026d0dd 100644 --- a/public/locales/ru/libraries-info.json +++ b/public/locales/ru/libraries-info.json @@ -37,5 +37,6 @@ "description_table": "Библиотека для отображения таблиц.", "description_timeline": "Библиотека на основе React для создания интерактивных визуализаций временной шкалы с отрисовкой на canvas.", "description_page-constructor-builder": "Мощная утилита командной строки для создания статических страниц из YAML-конфигураций", - "description_aikit": "Гибкая библиотека React-компонентов для создания AI-чатов, построенная на принципах Atomic Design." + "description_aikit": "Гибкая библиотека React-компонентов для создания AI-чатов, построенная на принципах Atomic Design.", + "description_charts": "Гибкая JavaScript-библиотека для визуализации данных и построения диаграмм с использованием React." } diff --git a/src/components/ChartsPlayground/ChartsPlayground.tsx b/src/components/ChartsPlayground/ChartsPlayground.tsx new file mode 100644 index 000000000000..93fe0945d5ed --- /dev/null +++ b/src/components/ChartsPlayground/ChartsPlayground.tsx @@ -0,0 +1,18 @@ +import {useTranslation} from 'next-i18next'; +import {memo} from 'react'; + +import {PlaygroundWrap} from '../PlaygroundWrap'; + +import {Playground} from './Playground'; + +export const ChartsPlayground = memo(() => { + const {t} = useTranslation('charts'); + + return ( + + + + ); +}); + +ChartsPlayground.displayName = 'ChartsPlayground'; diff --git a/src/components/ChartsPlayground/Playground/Editor.scss b/src/components/ChartsPlayground/Playground/Editor.scss new file mode 100644 index 000000000000..5ed86479e206 --- /dev/null +++ b/src/components/ChartsPlayground/Playground/Editor.scss @@ -0,0 +1,21 @@ +@use '~@gravity-ui/page-constructor/styles/variables.scss' as pcVariables; +@use '~@gravity-ui/uikit/styles/mixins' as ukitMixins; +@use '../../../variables.scss'; + +$block: '.#{variables.$ns}editor'; + +#{$block} { + overflow: hidden; + + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 10px; + + &__actions { + border-top: 1px solid rgba(255, 255, 255, 0.2); + padding: var(--g-spacing-3); + + .g-button { + --_--border-radius: 8px; + } + } +} diff --git a/src/components/ChartsPlayground/Playground/Editor.tsx b/src/components/ChartsPlayground/Playground/Editor.tsx new file mode 100644 index 000000000000..707b4132f22d --- /dev/null +++ b/src/components/ChartsPlayground/Playground/Editor.tsx @@ -0,0 +1,114 @@ +import {ChartData} from '@gravity-ui/charts'; +import {Button, Flex, Hotkey, Text} from '@gravity-ui/uikit'; +import {Editor as MonacoEditor, OnMount} from '@monaco-editor/react'; +import {KeyCode, KeyMod, type editor} from 'monaco-editor'; +import {useTranslation} from 'next-i18next'; +import React, {FC, useCallback, useRef, useState} from 'react'; + +import {block} from '../../../utils'; + +import './Editor.scss'; +import {GravityTheme, defineTheme} from './theme'; + +const b = block('editor'); + +const MONACO_OPTIONS: editor.IStandaloneEditorConstructionOptions = { + contextmenu: false, + lineNumbersMinChars: 4, + glyphMargin: false, + colorDecorators: true, + minimap: {enabled: false}, + smoothScrolling: true, + bracketPairColorization: {enabled: true}, +}; + +type Props = { + data: ChartData; + onApplyChanges: (data: ChartData) => void; + onReset: () => ChartData; +}; + +export const Editor: FC = ({data, onApplyChanges, onReset}) => { + const {t} = useTranslation('charts'); + const editorRef = useRef[0]>(); + const [errorMarker, setErrorMarker] = useState(); + + const handleApplyChanges = useCallback(() => { + const model = editorRef.current?.getModel(); + + if (model) { + onApplyChanges(JSON.parse(model.getValue())); + } + }, []); + + const handleReset = useCallback(() => { + const newData = onReset(); + editorRef.current?.setValue(JSON.stringify(newData, null, 2)); + }, [handleApplyChanges]); + + const handleErrorMarkerClick = useCallback(() => { + if (errorMarker) { + const {startColumn, startLineNumber, endColumn, endLineNumber} = errorMarker; + + editorRef.current?.revealLinesInCenter(startLineNumber, endLineNumber, 0); + + editorRef.current?.setSelection({ + startColumn, + startLineNumber, + endColumn, + endLineNumber, + }); + } + }, [errorMarker]); + + const handleValidate = useCallback( + (markers: editor.IMarker[]) => + setErrorMarker(markers.filter(({severity}) => severity === 8)[0]), + [], + ); + + const handleMount = useCallback( + (editor: editor.IStandaloneCodeEditor) => { + editorRef.current = editor; + editor.setValue(JSON.stringify(data, null, 2)); + // eslint-disable-next-line no-bitwise + editor.addCommand(KeyMod.CtrlCmd | KeyCode.Enter, handleApplyChanges); + }, + [data], + ); + + return ( + + + + + + {errorMarker && ( + + + + {errorMarker.message} + + + + )} + + + ); +}; diff --git a/src/components/ChartsPlayground/Playground/Playground.tsx b/src/components/ChartsPlayground/Playground/Playground.tsx new file mode 100644 index 000000000000..e9d49643e9d9 --- /dev/null +++ b/src/components/ChartsPlayground/Playground/Playground.tsx @@ -0,0 +1,85 @@ +import {Chart, ChartData, ChartSeries} from '@gravity-ui/charts'; +import {Flex, Select} from '@gravity-ui/uikit'; +import React, {useCallback, useState} from 'react'; + +import {ErrorBoundary} from '../../ErrorBoundary'; + +import {Editor} from './Editor'; +import * as mocks from './mocks'; + +type ChartType = ChartSeries['type']; +type SelectOption = {value: ChartType; content: string}; + +const chartTypeOptions: SelectOption[] = [ + {value: 'area', content: 'Area'}, + {value: 'bar-x', content: 'Bar-X'}, + {value: 'bar-y', content: 'Bar-Y'}, + {value: 'funnel', content: 'Funnel'}, + {value: 'heatmap', content: 'Heatmap'}, + {value: 'line', content: 'Line'}, + {value: 'pie', content: 'Pie'}, + {value: 'radar', content: 'Radar'}, + {value: 'sankey', content: 'Sankey'}, + {value: 'scatter', content: 'Scatter'}, + {value: 'treemap', content: 'Treemap'}, + {value: 'waterfall', content: 'Waterfall'}, +]; + +const chartMocks: Record = { + area: mocks.areaData, + 'bar-x': mocks.barXData, + 'bar-y': mocks.barYData, + funnel: mocks.funnelData, + heatmap: mocks.heatmapData, + line: mocks.lineData, + pie: mocks.pieData, + radar: mocks.radarData, + sankey: mocks.sankeyData, + scatter: mocks.scatterData, + treemap: mocks.treemapData, + waterfall: mocks.waterfallData, +}; + +export const Playground = () => { + const defaultChartType = chartTypeOptions[0].value; + const [chartData, setChartData] = useState(chartMocks[defaultChartType]); + const [chartType, setChartType] = useState(defaultChartType); + + const handleUpdateChartType = useCallback((value: string[]) => { + const type = value[0] as ChartType; + setChartType(type); + setChartData(chartMocks[type]); + }, []); + + const handleReset = useCallback(() => { + const newData = chartMocks[chartType]; + + setChartData(newData); + + return newData; + }, [chartType]); + + return ( + + +