Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions public/locales/en/charts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"goToLibrary": "Go to library",
"title": "Playground",
"apply": "Apply",
"reset": "Reset"
}
3 changes: 2 additions & 1 deletion public/locales/en/libraries-info.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
6 changes: 6 additions & 0 deletions public/locales/ru/charts.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"goToLibrary": "К библиотеке",
"title": "Редактор",
"apply": "Применить",
"reset": "Сбросить"
}
3 changes: 2 additions & 1 deletion public/locales/ru/libraries-info.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
18 changes: 18 additions & 0 deletions src/components/ChartsPlayground/ChartsPlayground.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<PlaygroundWrap title={t('title')} libraryId="charts" goToLibraryText={t('goToLibrary')}>
<Playground />
</PlaygroundWrap>
);
});

ChartsPlayground.displayName = 'ChartsPlayground';
21 changes: 21 additions & 0 deletions src/components/ChartsPlayground/Playground/Editor.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
114 changes: 114 additions & 0 deletions src/components/ChartsPlayground/Playground/Editor.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({data, onApplyChanges, onReset}) => {
const {t} = useTranslation('charts');
const editorRef = useRef<Parameters<OnMount>[0]>();
const [errorMarker, setErrorMarker] = useState<editor.IMarker>();

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 (
<Flex direction="column" width="100%" className={b()}>
<MonacoEditor
beforeMount={defineTheme}
onMount={handleMount}
onValidate={handleValidate}
language="json"
theme={GravityTheme}
options={MONACO_OPTIONS}
/>
<Flex gap={2} className={b('actions')}>
<Button
size="l"
view="action"
disabled={Boolean(errorMarker)}
onClick={handleApplyChanges}
>
{t('apply')} <Hotkey view="light" value="mod+enter" />
</Button>
<Button size="l" onClick={handleReset}>
{t('reset')}
</Button>
{errorMarker && (
<Flex grow={1} alignItems="center" justifyContent="flex-end">
<Text color="danger">
<span style={{cursor: 'pointer'}} onClick={handleErrorMarkerClick}>
{errorMarker.message}
</span>
</Text>
</Flex>
)}
</Flex>
</Flex>
);
};
85 changes: 85 additions & 0 deletions src/components/ChartsPlayground/Playground/Playground.tsx
Original file line number Diff line number Diff line change
@@ -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<ChartType, ChartData> = {
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<ChartData>(chartMocks[defaultChartType]);
const [chartType, setChartType] = useState<ChartType>(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 (
<Flex gap={6} direction="column" width="100%" height="100%">
<Flex justifyContent="center">
<Select
options={chartTypeOptions}
defaultValue={[defaultChartType]}
onUpdate={handleUpdateChartType}
size="xl"
width={200}
/>
</Flex>
<Flex gap={8} justifyContent="space-between" height="100%" key={chartType}>
<Flex width="calc(60% - 16px)">
<ErrorBoundary>
<Chart data={chartData} />
</ErrorBoundary>
</Flex>
<Flex width="calc(40% - 16px)">
<Editor data={chartData} onApplyChanges={setChartData} onReset={handleReset} />
</Flex>
</Flex>
</Flex>
);
};
1 change: 1 addition & 0 deletions src/components/ChartsPlayground/Playground/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {Playground} from './Playground';
29 changes: 29 additions & 0 deletions src/components/ChartsPlayground/Playground/mocks/area.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {ChartData} from '@gravity-ui/charts';

import nintendoGames from './nintendo-games';

const prepareData = (): ChartData => {
const data = nintendoGames
.filter((d) => d.date && d.user_score && d.genres.includes('Puzzle'))
.map((d) => ({
x: Number(d.date),
y: Number(d.user_score),
}));

return {
series: {
data: [
{
name: 'User score',
type: 'area',
data,
},
],
},
xAxis: {
type: 'datetime',
},
};
};

export const areaData = prepareData();
40 changes: 40 additions & 0 deletions src/components/ChartsPlayground/Playground/mocks/bar-x.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {ChartData} from '@gravity-ui/charts';

import nintendoGames from './nintendo-games';

const prepareData = (): ChartData => {
const gamesByPlatform: Record<string, number> = {};

for (const game of nintendoGames) {
const key = game.platform;

gamesByPlatform[key] = (gamesByPlatform[key] || 0) + 1;
}

const categories = Object.keys(gamesByPlatform);

return {
series: {
data: [
{
type: 'bar-x',
data: categories.map((key) => ({
x: key,
y: gamesByPlatform[key],
})),
name: 'Games released',
},
],
},
xAxis: {
type: 'category',
categories,
title: {
text: 'Game Platforms',
},
},
yAxis: [{title: {text: 'Number of games released'}}],
};
};

export const barXData = prepareData();
42 changes: 42 additions & 0 deletions src/components/ChartsPlayground/Playground/mocks/bar-y.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {ChartData} from '@gravity-ui/charts';

import nintendoGames from './nintendo-games';

const prepareData = (): ChartData => {
const gamesByPlatform: Record<string, number> = {};

for (const game of nintendoGames) {
const key = game.platform;

gamesByPlatform[key] = (gamesByPlatform[key] || 0) + 1;
}

const categories = Object.keys(gamesByPlatform);

return {
series: {
data: [
{
type: 'bar-y',
data: categories.map((key) => ({
x: gamesByPlatform[key],
y: key,
})),
name: 'Games released',
},
],
},
xAxis: {title: {text: 'Number of games released'}},
yAxis: [
{
type: 'category',
categories,
title: {
text: 'Game Platforms',
},
},
],
};
};

export const barYData = prepareData();
Loading
Loading