├── CODEOWNERS ├── Makefile ├── src ├── hooks │ ├── index.ts │ └── misc.ts ├── plugins │ ├── highcharts │ │ ├── types │ │ │ ├── lib.ts │ │ │ ├── index.ts │ │ │ ├── misc.ts │ │ │ ├── comments.ts │ │ │ └── highcharts-extends.d.ts │ │ ├── renderer │ │ │ ├── components │ │ │ │ ├── HighchartsComponent.scss │ │ │ │ ├── withSplitPane │ │ │ │ │ └── WithSplitPane.scss │ │ │ │ └── useElementSize.ts │ │ │ ├── helpers │ │ │ │ ├── highcharts │ │ │ │ │ ├── utils │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── calcucalteClosestPointManually.ts │ │ │ │ │ │ └── calculateClosestPointManually.test.ts │ │ │ │ │ └── colors.ts │ │ │ │ ├── config │ │ │ │ │ ├── utils │ │ │ │ │ │ ├── concatStrings.ts │ │ │ │ │ │ ├── isNavigatorSeries.ts │ │ │ │ │ │ ├── isSafari.ts │ │ │ │ │ │ ├── mergeArrayWithObject.ts │ │ │ │ │ │ ├── buildNavigatorFallback.ts │ │ │ │ │ │ ├── localStorage.ts │ │ │ │ │ │ ├── getFormatOptionsFromLine.test.ts │ │ │ │ │ │ ├── getFormatOptionsFromLine.ts │ │ │ │ │ │ ├── calculatePrecision.ts │ │ │ │ │ │ ├── numberFormat.ts │ │ │ │ │ │ ├── getXAxisThresholdValue.ts │ │ │ │ │ │ ├── getChartKitFormattedValue.ts │ │ │ │ │ │ ├── getXAxisThresholdValue.test.ts │ │ │ │ │ │ ├── numberFormat.test.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── tooltip.ts │ │ │ │ │ │ ├── buildNavigatorFallback.test.ts │ │ │ │ │ │ ├── setNavigatorDefaultPeriod.ts │ │ │ │ │ │ ├── setNavigatorDefaultPeriod.test.ts │ │ │ │ │ │ ├── addShowInNavigatorToSeries.ts │ │ │ │ │ │ ├── tooltip.test.ts │ │ │ │ │ │ └── calculatePrecision.test.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── tooltip │ │ │ │ │ ├── render-shape-icon │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ ├── utils.ts │ │ │ │ │ │ ├── template-icons │ │ │ │ │ │ │ ├── SolidLineIcon.ts │ │ │ │ │ │ │ ├── LongDashLineIcon.ts │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── LongDashDotLineIcon.ts │ │ │ │ │ │ │ ├── DashDotLineIcon.ts │ │ │ │ │ │ │ ├── LongDashDotDotLineIcon.ts │ │ │ │ │ │ │ ├── ShortDashDotLineIcon.ts │ │ │ │ │ │ │ ├── ShortDashDotDotLineIcon.ts │ │ │ │ │ │ │ ├── DotLineIcon.ts │ │ │ │ │ │ │ ├── ShortDashLineIcon.ts │ │ │ │ │ │ │ ├── DashLineIcon.ts │ │ │ │ │ │ │ └── ShortDotLineIcon.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── helpers.ts │ │ │ │ │ └── constants.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── types.ts │ │ │ │ ├── add-holidays.ts │ │ │ │ └── graph.scss │ │ │ └── HighchartsWidget.tsx │ │ ├── index.ts │ │ ├── __stories__ │ │ │ ├── Line.stories.tsx │ │ │ ├── Pie.stories.tsx │ │ │ ├── column │ │ │ │ ├── Vertical.stories.tsx │ │ │ │ ├── HorizontalStacked.stories.tsx │ │ │ │ ├── VerticalStacked.stories.tsx │ │ │ │ └── VerticalStackedSplitTooltip.stories.tsx │ │ │ ├── pie │ │ │ │ └── WithTotals.stories.tsx │ │ │ ├── complex │ │ │ │ └── TwoAxis.stories.tsx │ │ │ ├── UnsafeTooltip.stories.tsx │ │ │ ├── area │ │ │ │ ├── Stacked.stories.tsx │ │ │ │ ├── Range.stories.tsx │ │ │ │ └── WithThreshold.stories.tsx │ │ │ ├── combined │ │ │ │ ├── ComboChartWithSameLegendValues.stories.tsx │ │ │ │ └── AreaLine.stories.tsx │ │ │ ├── Venn.stories.tsx │ │ │ ├── constants │ │ │ │ └── story-settings.ts │ │ │ ├── no-data │ │ │ │ └── no-data.stories.tsx │ │ │ ├── components │ │ │ │ └── ChartStory.tsx │ │ │ ├── scatter │ │ │ │ └── PerformanceIssue.stories.tsx │ │ │ └── custom-error-render │ │ │ │ └── custom-error-render.stories.tsx │ │ ├── __tests__ │ │ │ └── prepare-data.test.ts │ │ └── mocks │ │ │ ├── pie.ts │ │ │ ├── pie-with-totals.ts │ │ │ ├── venn.ts │ │ │ ├── column-ver.ts │ │ │ ├── area-range.ts │ │ │ ├── no-data.ts │ │ │ └── custom-error-render.ts │ ├── yagr │ │ ├── renderer │ │ │ ├── tooltip │ │ │ │ ├── index.ts │ │ │ │ ├── helpers │ │ │ │ │ └── escapeHTML.ts │ │ │ │ └── types.ts │ │ │ ├── YagrWidget.scss │ │ │ ├── polyfills.js │ │ │ └── useWidgetData.ts │ │ ├── index.ts │ │ ├── types.ts │ │ ├── __tests__ │ │ │ └── utils.test.ts │ │ └── __stories__ │ │ │ └── Playground.stories.tsx │ ├── shared │ │ ├── index.ts │ │ └── format-number │ │ │ ├── types.ts │ │ │ ├── i18n │ │ │ ├── en.json │ │ │ ├── ru.json │ │ │ └── i18n.ts │ │ │ └── format-number.test.ts │ ├── gravity-charts │ │ ├── types.ts │ │ ├── index.ts │ │ └── renderer │ │ │ ├── __stories__ │ │ │ ├── StoryWrapper.tsx │ │ │ └── SplitTooltip.stories.tsx │ │ │ ├── utils.ts │ │ │ ├── __tests__ │ │ │ ├── D3Widget.visual.test.tsx │ │ │ ├── TestStory.visual.tsx │ │ │ └── utils.test.ts │ │ │ ├── withSplitPane │ │ │ ├── TooltipContent.tsx │ │ │ └── useWithSplitPaneState.ts │ │ │ └── GravityChartsWidget.tsx │ └── indicator │ │ ├── index.ts │ │ ├── types.ts │ │ ├── renderer │ │ ├── IndicatorItem.tsx │ │ ├── IndicatorWidget.scss │ │ └── IndicatorWidget.tsx │ │ └── __stories__ │ │ └── Indicator.stories.tsx ├── components │ ├── SplitPane │ │ ├── constants.ts │ │ ├── types.ts │ │ ├── index.ts │ │ ├── Pane.tsx │ │ ├── StyledSplitPane.scss │ │ ├── StyledSplitPane.tsx │ │ └── Resizer.tsx │ ├── Loader │ │ ├── Loader.scss │ │ └── Loader.tsx │ ├── ChartKit.scss │ ├── ErrorBoundary │ │ └── ErrorBoundary.tsx │ └── ChartKit.tsx ├── constants │ ├── index.ts │ ├── common.ts │ ├── misc.ts │ └── widget-data.ts ├── types │ ├── misc.ts │ ├── widget.ts │ └── index.ts ├── utils │ ├── index.ts │ ├── cn.ts │ ├── react.ts │ ├── getErrorMessage.ts │ ├── __tests__ │ │ └── common.test.ts │ ├── common.ts │ ├── misc.ts │ └── performance.ts ├── libs │ ├── index.ts │ ├── settings │ │ ├── __tests__ │ │ │ ├── settings.test.ts │ │ │ └── settings-update.test.ts │ │ ├── eventEmitter.ts │ │ ├── mergeSettingStrategy.ts │ │ └── settings.ts │ └── chartkit-error │ │ ├── chartkit-error.ts │ │ └── __tests__ │ │ └── chartkit-error.ts ├── index.ts └── i18n │ └── index.ts ├── test-utils ├── style.mock.ts └── globals.mock.ts ├── .npmrc ├── .prettierrc.js ├── .eslintignore ├── .prettierignore ├── .stylelintrc ├── tests ├── playwright │ ├── index.tsx │ └── index.html └── playwright.config.ts ├── tsconfig.publish.json ├── .storybook ├── manager.ts ├── decorators │ ├── withTheme.tsx │ ├── withMobile.tsx │ ├── withLang.tsx │ └── DocsDecorator │ │ ├── DocsDecorator.tsx │ │ └── DocsDecorator.scss ├── theme-addon │ └── register.tsx ├── main.ts ├── theme.ts └── preview.tsx ├── .gitignore ├── .editorconfig ├── .github └── workflows │ ├── pr-preview-build.yml │ ├── release.yml │ ├── pr-preview-deploy.yml │ ├── main-preview.yml │ ├── ci.yml │ └── release-beta.yml ├── .babelrc.json ├── tsconfig.json ├── .eslintrc ├── jest.config.js ├── LICENSE ├── gulpfile.js ├── CONTRIBUTING.md ├── README.md └── README-ru.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @korvin89 @kuzmadom 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | npm run build 3 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './misc'; 2 | -------------------------------------------------------------------------------- /test-utils/style.mock.ts: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org 2 | legacy-peer-deps=true 3 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@gravity-ui/prettier-config'); 2 | -------------------------------------------------------------------------------- /src/plugins/highcharts/types/lib.ts: -------------------------------------------------------------------------------- 1 | export * as Highcharts from 'highcharts'; 2 | -------------------------------------------------------------------------------- /src/plugins/yagr/renderer/tooltip/index.ts: -------------------------------------------------------------------------------- 1 | export {getRenderTooltip} from './renderTooltip'; 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | src/i18n/keysets 4 | test-utils 5 | tests 6 | jest.config.js 7 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | storybook-static 2 | build 3 | CHANGELOG.md 4 | CONTRIBUTING.md 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@gravity-ui/stylelint-config", "@gravity-ui/stylelint-config/prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/playwright/index.tsx: -------------------------------------------------------------------------------- 1 | // Import styles, initialize component theme here. 2 | // import '../src/common.css'; 3 | -------------------------------------------------------------------------------- /tsconfig.publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "ES2015", 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/components/HighchartsComponent.scss: -------------------------------------------------------------------------------- 1 | .chartkit-graph { 2 | flex: 1; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /src/components/SplitPane/constants.ts: -------------------------------------------------------------------------------- 1 | export const SplitLayout = { 2 | HORIZONTAL: 'horizontal', 3 | VERTICAL: 'vertical', 4 | } as const; 5 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/highcharts/utils/index.ts: -------------------------------------------------------------------------------- 1 | export {calculateClosestPointManually} from './calcucalteClosestPointManually'; 2 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export {CHARTKIT_SCROLLABLE_NODE_CLASSNAME} from './common'; 2 | 3 | export * from './widget-data'; 4 | export * from './misc'; 5 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/components/withSplitPane/WithSplitPane.scss: -------------------------------------------------------------------------------- 1 | .with-split-pane { 2 | position: relative; 3 | height: 100%; 4 | } 5 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/concatStrings.ts: -------------------------------------------------------------------------------- 1 | export const concatStrings = (...strs: unknown[]) => strs.filter(Boolean).join(' '); 2 | -------------------------------------------------------------------------------- /src/plugins/shared/index.ts: -------------------------------------------------------------------------------- 1 | export {formatNumber} from './format-number/format-number'; 2 | export type {FormatNumberOptions} from './format-number/types'; 3 | -------------------------------------------------------------------------------- /.storybook/manager.ts: -------------------------------------------------------------------------------- 1 | import {addons} from '@storybook/addons'; 2 | import {themes} from './theme'; 3 | 4 | addons.setConfig({ 5 | theme: themes.light, 6 | }); 7 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/types.ts: -------------------------------------------------------------------------------- 1 | export type CommonIconProps = { 2 | width: string; 3 | height: string; 4 | }; 5 | -------------------------------------------------------------------------------- /src/components/SplitPane/types.ts: -------------------------------------------------------------------------------- 1 | import type {SplitLayout} from './constants'; 2 | 3 | export type SplitLayoutType = (typeof SplitLayout)[keyof typeof SplitLayout]; 4 | -------------------------------------------------------------------------------- /src/types/misc.ts: -------------------------------------------------------------------------------- 1 | export type ChartKitHolidays = { 2 | holiday: Record>; 3 | weekend: Record>; 4 | }; 5 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export {getRandomCKId, randomString} from './common'; 2 | export {typedMemo} from './react'; 3 | export * from './performance'; 4 | export * from './misc'; 5 | -------------------------------------------------------------------------------- /src/plugins/yagr/renderer/YagrWidget.scss: -------------------------------------------------------------------------------- 1 | .yagr-tooltip { 2 | background-color: var(--g-color-infographics-tooltip-bg); 3 | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15); 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.scss: -------------------------------------------------------------------------------- 1 | .chartkit-loader { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | -------------------------------------------------------------------------------- /src/constants/common.ts: -------------------------------------------------------------------------------- 1 | // This css class should be added for DOM element for correct calculation of scrollHeight 2 | export const CHARTKIT_SCROLLABLE_NODE_CLASSNAME = 'chartkit-scrollable-node'; 3 | -------------------------------------------------------------------------------- /src/plugins/gravity-charts/types.ts: -------------------------------------------------------------------------------- 1 | import type {ChartData} from '@gravity-ui/charts'; 2 | 3 | export type * from '@gravity-ui/charts'; 4 | 5 | export type GravityChartsWidgetData = { 6 | data: ChartData; 7 | }; 8 | -------------------------------------------------------------------------------- /src/plugins/yagr/renderer/tooltip/helpers/escapeHTML.ts: -------------------------------------------------------------------------------- 1 | export function escapeHTML(html: string) { 2 | const elem = document.createElement('span'); 3 | 4 | elem.innerText = html; 5 | return elem.innerHTML; 6 | } 7 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/helpers.ts: -------------------------------------------------------------------------------- 1 | export const escapeHTML = (html = '') => { 2 | const elem = document.createElement('span'); 3 | elem.innerText = html; 4 | 5 | return elem.innerHTML; 6 | }; 7 | -------------------------------------------------------------------------------- /src/utils/cn.ts: -------------------------------------------------------------------------------- 1 | import {withNaming} from '@bem-react/classname'; 2 | 3 | export const NAMESPACE = 'chartkit-'; 4 | 5 | export const cn = withNaming({e: '__', m: '_'}); 6 | export const block = withNaming({n: NAMESPACE, e: '__', m: '_'}); 7 | -------------------------------------------------------------------------------- /src/libs/index.ts: -------------------------------------------------------------------------------- 1 | export {CHARTKIT_ERROR_CODE, ChartKitError, isChartKitError} from './chartkit-error/chartkit-error'; 2 | export type {ChartKitErrorArgs} from './chartkit-error/chartkit-error'; 3 | export {settings} from './settings/settings'; 4 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/types.ts: -------------------------------------------------------------------------------- 1 | export type NavigatorPeriod = { 2 | type: string; 3 | value: string; 4 | period: Period; 5 | }; 6 | 7 | export type Period = 'month' | 'year' | 'day' | 'hour' | 'week' | 'quarter'; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | .nyc_output 4 | .DS_Store 5 | *.log 6 | .vscode 7 | .idea 8 | dist 9 | build 10 | compiled 11 | .awcache 12 | .rpt2_cache 13 | /test-results/ 14 | /playwright-report/ 15 | /blob-report/ 16 | /tests/playwright/.cache/ 17 | -------------------------------------------------------------------------------- /src/hooks/misc.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function usePrevious(value: T) { 4 | const ref = React.useRef(); 5 | 6 | React.useEffect(() => { 7 | ref.current = value; 8 | }, [value]); 9 | 10 | return ref.current; 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [{*.json,*.yml,*.md}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /.github/workflows/pr-preview-build.yml: -------------------------------------------------------------------------------- 1 | name: PR Preview Build 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | name: Build 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: gravity-ui/preview-build-action@v2 12 | with: 13 | node-version: 18 14 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/isNavigatorSeries.ts: -------------------------------------------------------------------------------- 1 | import type {Highcharts} from '../../../../types'; 2 | 3 | export const isNavigatorSeries = (series?: Highcharts.Series | Highcharts.Point) => { 4 | return series?.options.className === 'highcharts-navigator-series'; 5 | }; 6 | -------------------------------------------------------------------------------- /src/plugins/yagr/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {ChartKitPlugin} from '../../types'; 4 | 5 | export * from './types'; 6 | 7 | export const YagrPlugin: ChartKitPlugin = { 8 | type: 'yagr', 9 | renderer: React.lazy(() => import('./renderer/YagrWidget')), 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/react.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // For some reason React.memo drops the generic prop type and creates a regular union type 4 | // https://github.com/DefinitelyTyped/DefinitelyTyped/issues/37087#issuecomment-542793243 5 | export const typedMemo: (component: T) => T = React.memo; 6 | -------------------------------------------------------------------------------- /src/plugins/highcharts/types/index.ts: -------------------------------------------------------------------------------- 1 | export type {Highcharts} from './lib'; 2 | export type {HighchartsWidgetData, CkHighchartsSeriesOptionsType} from './widget'; 3 | export type {DrillDownConfig, ExtendedHChart, StringParams, XAxisItem} from './misc'; 4 | export type {HighchartsComment} from './comments'; 5 | -------------------------------------------------------------------------------- /.babelrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "sourceType": "unambiguous", 3 | "presets": [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | "targets": { 8 | "chrome": 100 9 | } 10 | } 11 | ], 12 | "@babel/preset-typescript", 13 | "@babel/preset-react" 14 | ], 15 | "plugins": [] 16 | } -------------------------------------------------------------------------------- /src/plugins/indicator/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {ChartKitPlugin} from '../../types'; 4 | 5 | export * from './types'; 6 | 7 | export const IndicatorPlugin: ChartKitPlugin = { 8 | type: 'indicator', 9 | renderer: React.lazy(() => import('./renderer/IndicatorWidget')), 10 | }; 11 | -------------------------------------------------------------------------------- /src/utils/getErrorMessage.ts: -------------------------------------------------------------------------------- 1 | import {i18n} from '../i18n'; 2 | import type {ChartKitError} from '../libs'; 3 | 4 | export function getErrorMessage(error: ChartKitError | Error) { 5 | const code = 'code' in error && error.code; 6 | return (error.message || code || i18n('error', 'label_unknown-error')).toString(); 7 | } 8 | -------------------------------------------------------------------------------- /src/utils/__tests__/common.test.ts: -------------------------------------------------------------------------------- 1 | import {getRandomCKId} from '../common'; 2 | 3 | // length of "ck." + 10 random symbols 4 | const ID_LENGTH = 13; 5 | 6 | describe('utils/getRandomCKId', () => { 7 | it('Id should have 13 symbols', () => { 8 | const result = getRandomCKId(); 9 | expect(result.length).toBe(ID_LENGTH); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /src/utils/common.ts: -------------------------------------------------------------------------------- 1 | export const randomString = (length: number, chars: string) => { 2 | let result = ''; 3 | for (let i = length; i > 0; --i) { 4 | result += chars[Math.floor(Math.random() * chars.length)]; 5 | } 6 | return result; 7 | }; 8 | 9 | export const getRandomCKId = () => `ck.${randomString(10, '0123456789abcdefghijklmnopqrstuvwxyz')}`; 10 | -------------------------------------------------------------------------------- /tests/playwright/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Testing Page 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/plugins/highcharts/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import type {ChartKitPlugin} from '../../types'; 4 | 5 | export * from './types'; 6 | export {HighchartsReact} from './renderer/components/HighchartsReact'; 7 | 8 | export const HighchartsPlugin: ChartKitPlugin = { 9 | type: 'highcharts', 10 | renderer: React.lazy(() => import('./renderer/HighchartsWidget')), 11 | }; 12 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/utils.ts: -------------------------------------------------------------------------------- 1 | export const getParsedRect = ({width, height}: {width?: string; height?: string}) => { 2 | const parsedWidth = typeof width === 'string' ? Number.parseInt(width, 10) : width; 3 | const parsedHeight = typeof height === 'string' ? Number.parseInt(height, 10) : height; 4 | 5 | return {parsedWidth, parsedHeight}; 6 | }; 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@gravity-ui/tsconfig", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "outDir": "build/esm", 6 | "module": "esnext", 7 | "jsx": "react", 8 | "baseUrl": ".", 9 | "importHelpers": true, 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true 12 | }, 13 | "include": ["src/**/*.ts", "src/**/*.tsx", "tests/*"] 14 | } 15 | -------------------------------------------------------------------------------- /src/utils/misc.ts: -------------------------------------------------------------------------------- 1 | import {AVAILABLE_SCREEN_ORIENTATIONS} from '../constants'; 2 | import type {ScreenOrientationType} from '../constants'; 3 | 4 | export function isScreenOrientationEventType(value: unknown): value is ScreenOrientationType { 5 | if (typeof value !== 'string') { 6 | return false; 7 | } 8 | 9 | return AVAILABLE_SCREEN_ORIENTATIONS.includes(value as ScreenOrientationType); 10 | } 11 | -------------------------------------------------------------------------------- /test-utils/globals.mock.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/42685938 2 | Object.defineProperty(window, 'matchMedia', { 3 | writable: true, 4 | value: jest.fn().mockImplementation((query) => ({ 5 | matches: false, 6 | media: query, 7 | onchange: null, 8 | addEventListener: jest.fn(), 9 | removeEventListener: jest.fn(), 10 | dispatchEvent: jest.fn(), 11 | })), 12 | }); 13 | -------------------------------------------------------------------------------- /.storybook/decorators/withTheme.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type {Decorator} from '@storybook/react'; 3 | import {ThemeProvider} from '@gravity-ui/uikit'; 4 | 5 | export const WithTheme: Decorator = (Story, context) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | - release/v* 6 | 7 | name: Release 8 | 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: gravity-ui/release-action@v1 14 | with: 15 | github-token: ${{ secrets.GRAVITY_UI_BOT_GITHUB_TOKEN }} 16 | npm-token: ${{ secrets.GRAVITY_UI_BOT_NPM_TOKEN }} 17 | node-version: 18 18 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/constants.ts: -------------------------------------------------------------------------------- 1 | export enum LineShapeType { 2 | Solid = 'Solid', 3 | ShortDash = 'ShortDash', 4 | ShortDot = 'ShortDot', 5 | ShortDashDot = 'ShortDashDot', 6 | ShortDashDotDot = 'ShortDashDotDot', 7 | Dot = 'Dot', 8 | Dash = 'Dash', 9 | LongDash = 'LongDash', 10 | DashDot = 'DashDot', 11 | LongDashDot = 'LongDashDot', 12 | LongDashDotDot = 'LongDashDotDot', 13 | } 14 | -------------------------------------------------------------------------------- /src/plugins/indicator/types.ts: -------------------------------------------------------------------------------- 1 | export type IndicatorWidgetDataItem = { 2 | content: { 3 | current: { 4 | value: string | number; 5 | } & Record; 6 | }; 7 | color?: string; 8 | size?: 's' | 'm' | 'l' | 'xl'; 9 | title?: string; 10 | nowrap?: boolean; 11 | }; 12 | 13 | export type IndicatorWidgetData = { 14 | data?: IndicatorWidgetDataItem[]; 15 | defaultColor?: string; 16 | }; 17 | -------------------------------------------------------------------------------- /src/plugins/shared/format-number/types.ts: -------------------------------------------------------------------------------- 1 | export type FormatOptions = { 2 | precision?: number | 'auto'; 3 | unitRate?: number; 4 | showRankDelimiter?: boolean; 5 | lang?: string; 6 | labelMode?: string; 7 | }; 8 | 9 | export type FormatNumberOptions = FormatOptions & { 10 | format?: 'number' | 'percent'; 11 | multiplier?: number; 12 | prefix?: string; 13 | postfix?: string; 14 | unit?: 'auto' | 'k' | 'm' | 'b' | 't' | null; 15 | }; 16 | -------------------------------------------------------------------------------- /src/plugins/gravity-charts/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {ChartKitPlugin} from '../../types'; 4 | 5 | export {CustomShapeRenderer} from '@gravity-ui/charts'; 6 | export * from './types'; 7 | /** 8 | * It is an experemental plugin 9 | * 10 | * DO NOT USE IT IN YOUR PRODUCTION 11 | * */ 12 | export const GravityChartsPlugin: ChartKitPlugin = { 13 | type: 'gravity-charts', 14 | renderer: React.lazy(() => import('./renderer/GravityChartsWidget')), 15 | }; 16 | -------------------------------------------------------------------------------- /.storybook/decorators/withMobile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type {Decorator} from '@storybook/react'; 3 | import {MobileProvider} from '@gravity-ui/uikit'; 4 | 5 | export const withMobile: Decorator = (Story, context) => { 6 | const platform = context.globals.platform; 7 | 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/isSafari.ts: -------------------------------------------------------------------------------- 1 | // https://stackoverflow.com/a/9851769/5806646 2 | // @ts-ignore 3 | export const isSafari = 4 | // @ts-ignore 5 | /constructor/i.test(window.HTMLElement) || 6 | (function (p) { 7 | return p.toString() === '[object SafariRemoteNotification]'; 8 | })( 9 | // @ts-ignore 10 | !window['safari'] || 11 | // @ts-ignore 12 | (typeof safari !== 'undefined' && safari.pushNotification), 13 | ); 14 | -------------------------------------------------------------------------------- /src/plugins/shared/format-number/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "chartkit-units": { 3 | "value_short-bytes": "B", 4 | "value_short-kilobytes": "KB", 5 | "value_short-megabytes": "MB", 6 | "value_short-milliseconds": "ms", 7 | "value_short-seconds": "s", 8 | "value_short-minutes": "m", 9 | "value_short-empty": "", 10 | "value_short-k": "K", 11 | "value_short-m": "M", 12 | "value_short-b": "B", 13 | "value_short-t": "T", 14 | "value_space-delimiter": " ", 15 | "value_number-delimiter": "" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/plugins/shared/format-number/i18n/ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "chartkit-units": { 3 | "value_short-bytes": "Б", 4 | "value_short-kilobytes": "КБ", 5 | "value_short-megabytes": "МБ", 6 | "value_short-milliseconds": "мс", 7 | "value_short-seconds": "с", 8 | "value_short-minutes": "м", 9 | "value_short-empty": "", 10 | "value_short-k": "K", 11 | "value_short-m": "M", 12 | "value_short-b": "B", 13 | "value_short-t": "T", 14 | "value_space-delimiter": " ", 15 | "value_number-delimiter": "" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/mergeArrayWithObject.ts: -------------------------------------------------------------------------------- 1 | import merge from 'lodash/merge'; 2 | 3 | export const mergeArrayWithObject = (a: unknown[] | unknown, b: unknown[] | unknown) => { 4 | if (Array.isArray(a) && b && typeof b === 'object' && !Array.isArray(b)) { 5 | return a.map((value) => merge(value, b)); 6 | } 7 | 8 | if (Array.isArray(b) && a && typeof a === 'object' && !Array.isArray(a)) { 9 | return b.map((value) => merge({}, a, value)); 10 | } 11 | 12 | return undefined; 13 | }; 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {ChartKit} from './components/ChartKit'; 2 | import {settings} from './libs'; 3 | 4 | export * from './libs/chartkit-error/chartkit-error'; 5 | 6 | export type { 7 | ChartKitLang, 8 | ChartKitOnLoadData, 9 | ChartKitOnRenderData, 10 | ChartKitOnChartLoad, 11 | ChartKitOnError, 12 | ChartKitPlugin, 13 | ChartKitProps, 14 | ChartKitRef, 15 | ChartKitWidgetRef, 16 | ChartKitType, 17 | ChartKitWidget, 18 | } from './types'; 19 | 20 | export {settings}; 21 | 22 | export default ChartKit; 23 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/Line.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../components/ChartKit'; 6 | import {data} from '../mocks/line'; 7 | 8 | import {ChartStory} from './components/ChartStory'; 9 | 10 | export default { 11 | title: 'Plugins/Highcharts/Line', 12 | component: ChartKit, 13 | } as Meta; 14 | 15 | const Template: Story = () => { 16 | return ; 17 | }; 18 | 19 | export const Line = Template.bind({}); 20 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/Pie.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../components/ChartKit'; 6 | import {data} from '../mocks/pie'; 7 | 8 | import {ChartStory} from './components/ChartStory'; 9 | 10 | export default { 11 | title: 'Plugins/Highcharts/Pie', 12 | component: ChartKit, 13 | } as Meta; 14 | 15 | const Template: Story = () => { 16 | return ; 17 | }; 18 | 19 | export const Pie = Template.bind({}); 20 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@gravity-ui/eslint-config", 4 | "@gravity-ui/eslint-config/client", 5 | "@gravity-ui/eslint-config/prettier", 6 | "@gravity-ui/eslint-config/import-order" 7 | ], 8 | "rules": { 9 | "valid-jsdoc": 0, 10 | "no-console": ["error", {"allow": ["warn", "error"]}] 11 | }, 12 | "root": true, 13 | "overrides": [{ 14 | "files": ["*.ts", "*.tsx"], 15 | "parserOptions": { 16 | "project": ["./tsconfig.json"] 17 | } 18 | }] 19 | } 20 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/HighchartsWidget.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import type {ChartKitProps, ChartKitWidgetRef} from '../../../types'; 4 | 5 | import {HighchartsComponent} from './components/HighchartsComponent'; 6 | 7 | const HighchartsWidget = React.forwardRef< 8 | ChartKitWidgetRef | undefined, 9 | ChartKitProps<'highcharts'> 10 | >(function HighchartsWidgetInner(props, ref) { 11 | return } {...props} />; 12 | }); 13 | 14 | export default HighchartsWidget; 15 | -------------------------------------------------------------------------------- /src/plugins/shared/format-number/i18n/i18n.ts: -------------------------------------------------------------------------------- 1 | import {I18N} from '@gravity-ui/i18n'; 2 | 3 | type KeysetData = Parameters['registerKeysets']>[1]; 4 | 5 | const i18nInstance = new I18N(); 6 | 7 | i18nInstance.setLang('ru'); 8 | 9 | const makeInstance = (keysetName: string, keysetsData: Record) => { 10 | Object.entries(keysetsData).forEach(([key, value]) => i18nInstance.registerKeysets(key, value)); 11 | return i18nInstance.i18n.bind(i18nInstance, keysetName); 12 | }; 13 | 14 | export {i18nInstance, makeInstance}; 15 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/column/Vertical.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | import {data} from '../../mocks/column-ver'; 7 | import {ChartStory} from '../components/ChartStory'; 8 | 9 | export default { 10 | title: 'Plugins/Highcharts/Column', 11 | component: ChartKit, 12 | } as Meta; 13 | 14 | const Template: Story = () => { 15 | return ; 16 | }; 17 | 18 | export const Vertical = Template.bind({}); 19 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import {I18N, I18NFn} from '@gravity-ui/i18n'; 2 | 3 | import type {ChartKitLang} from '../types'; 4 | 5 | import en from './keysets/en.json'; 6 | import ru from './keysets/ru.json'; 7 | 8 | type Keysets = typeof en; 9 | type TypedI18n = I18NFn; 10 | 11 | const i18nFactory = new I18N(); 12 | const EN: ChartKitLang = 'en'; 13 | const RU: ChartKitLang = 'ru'; 14 | 15 | i18nFactory.registerKeysets(EN, en); 16 | i18nFactory.registerKeysets(RU, ru); 17 | 18 | const i18n = i18nFactory.i18n.bind(i18nFactory) as TypedI18n; 19 | 20 | export {i18nFactory, i18n}; 21 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/pie/WithTotals.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | import {data} from '../../mocks/pie-with-totals'; 7 | import {ChartStory} from '../components/ChartStory'; 8 | 9 | export default { 10 | title: 'Plugins/Highcharts/Pie', 11 | component: ChartKit, 12 | } as Meta; 13 | 14 | const Template: Story = () => { 15 | return ; 16 | }; 17 | 18 | export const WithTotals = Template.bind({}); 19 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/complex/TwoAxis.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | import {data} from '../../mocks/complex'; 7 | import {ChartStory} from '../components/ChartStory'; 8 | 9 | export default { 10 | title: 'Plugins/Highcharts/TwoAxis', 11 | component: ChartKit, 12 | } as Meta; 13 | 14 | const Template: Story = () => { 15 | return ; 16 | }; 17 | 18 | export const TwoAxis = Template.bind({}); 19 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/UnsafeTooltip.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../components/ChartKit'; 6 | import {data} from '../mocks/unsafe-tooltip'; 7 | 8 | import {ChartStory} from './components/ChartStory'; 9 | 10 | export default { 11 | title: 'Plugins/Highcharts/UnsafeTooltip', 12 | component: ChartKit, 13 | } as Meta; 14 | 15 | const Template: Story = () => { 16 | return ; 17 | }; 18 | 19 | export const UnsafeTooltip = Template.bind({}); 20 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/area/Stacked.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | import {data} from '../../mocks/area-stacked'; 7 | import {ChartStory} from '../components/ChartStory'; 8 | 9 | export default { 10 | title: 'Plugins/Highcharts/Area', 11 | component: ChartKit, 12 | } as Meta; 13 | 14 | const Template: Story = () => { 15 | return ; 16 | }; 17 | 18 | export const AreaStacked = Template.bind({}); 19 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/column/HorizontalStacked.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | import {data} from '../../mocks/column-hor-stacked'; 7 | import {ChartStory} from '../components/ChartStory'; 8 | 9 | export default { 10 | title: 'Plugins/Highcharts/Column', 11 | component: ChartKit, 12 | } as Meta; 13 | 14 | const Template: Story = () => { 15 | return ; 16 | }; 17 | 18 | export const HorizontalStacked = Template.bind({}); 19 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/column/VerticalStacked.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | import {data} from '../../mocks/column-ver-stacked'; 7 | import {ChartStory} from '../components/ChartStory'; 8 | 9 | export default { 10 | title: 'Plugins/Highcharts/Column', 11 | component: ChartKit, 12 | } as Meta; 13 | 14 | const Template: Story = () => { 15 | return ; 16 | }; 17 | 18 | export const VerticalStacked = Template.bind({}); 19 | -------------------------------------------------------------------------------- /src/plugins/yagr/renderer/polyfills.js: -------------------------------------------------------------------------------- 1 | /* @see https://github.com/leeoniya/uPlot/issues/538#issuecomment-870711531 */ 2 | // eslint-disable-next-line 3 | const oMatchMedia = window.matchMedia; 4 | // eslint-disable-next-line 5 | window.matchMedia = (query) => { 6 | const mql = oMatchMedia(query); 7 | 8 | if (!mql.addEventListener) { 9 | mql.addEventListener = (_, handler) => { 10 | mql.addListener(handler); 11 | }; 12 | mql.removeEventListener = (_, handler) => { 13 | mql.removeListener(handler); 14 | }; 15 | } 16 | 17 | return mql; 18 | }; 19 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/column/VerticalStackedSplitTooltip.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | import {data} from '../../mocks/column-ver-stacked'; 7 | import {ChartStory} from '../components/ChartStory'; 8 | 9 | export default { 10 | title: 'Plugins/Highcharts/Column', 11 | component: ChartKit, 12 | } as Meta; 13 | 14 | const Template: Story = () => { 15 | return ; 16 | }; 17 | 18 | export const VerticalStackedSplitTooltip = Template.bind({}); 19 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/combined/ComboChartWithSameLegendValues.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | import {data} from '../../mocks/combo-chart-with-same-legend-titles'; 7 | import {ChartStory} from '../components/ChartStory'; 8 | export default { 9 | title: 'Plugins/Highcharts/Combined Charts', 10 | component: ChartKit, 11 | } as Meta; 12 | 13 | const Template: Story = () => { 14 | return ; 15 | }; 16 | 17 | export const ComboChart = Template.bind({}); 18 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/highcharts/colors.ts: -------------------------------------------------------------------------------- 1 | const colors = [ 2 | ['#4DA2F1', '#84D1EE', '#1F68A9', '#52A6C5'], 3 | ['#FF3D64', '#FF91A1', '#ED65A9', '#BE2443'], 4 | ['#8AD554', '#54A520', '#0FA08D', '#70C1AF'], 5 | ['#FFC636', '#DB9100', '#FF7E00', '#FFB46C'], 6 | ['#FFB9DD', '#BA74B3', '#E8B0A4', '#DCA3D7'], 7 | ]; 8 | 9 | function mergeColors() { 10 | const result = []; 11 | for (let i = 0; i < colors[0].length; i++) { 12 | for (let j = 0; j < colors.length; j++) { 13 | result.push(colors[j][i]); 14 | } 15 | } 16 | return result; 17 | } 18 | 19 | export default mergeColors().slice(); 20 | -------------------------------------------------------------------------------- /src/libs/settings/__tests__/settings.test.ts: -------------------------------------------------------------------------------- 1 | import {settings} from '../settings'; 2 | 3 | const resetSettings = () => settings.set({lang: 'en'}); 4 | 5 | describe('libs/settings', () => { 6 | it('Default lang should be equal to en', () => { 7 | const result = settings.get('lang'); 8 | 9 | expect(result).toBe('en'); 10 | }); 11 | 12 | it('Changed lang should be equal to ru', () => { 13 | settings.set({ 14 | lang: 'ru', 15 | }); 16 | const result = settings.get('lang'); 17 | 18 | expect(result).toBe('ru'); 19 | }); 20 | 21 | beforeAll(resetSettings); 22 | afterEach(resetSettings); 23 | }); 24 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/Venn.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | import Highcharts from 'highcharts'; 5 | import venn from 'highcharts/modules/venn'; 6 | 7 | import {ChartKit} from '../../../components/ChartKit'; 8 | import {data} from '../mocks/venn'; 9 | 10 | import {ChartStory} from './components/ChartStory'; 11 | 12 | export default { 13 | title: 'Plugins/Highcharts/Venn', 14 | component: ChartKit, 15 | } as Meta; 16 | 17 | venn(Highcharts); 18 | 19 | const Template: Story = () => { 20 | return ; 21 | }; 22 | 23 | export const UnsafeTooltip = Template.bind({}); 24 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/SolidLineIcon.ts: -------------------------------------------------------------------------------- 1 | import type {CommonIconProps} from '../types'; 2 | import {getParsedRect} from '../utils'; 3 | 4 | export const SolidLineIcon = ({width, height}: CommonIconProps) => { 5 | const {parsedWidth, parsedHeight} = getParsedRect({width, height}); 6 | 7 | return ` 14 | 15 | `; 16 | }; 17 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/buildNavigatorFallback.ts: -------------------------------------------------------------------------------- 1 | export const buildNavigatorFallback = (graphs: Record[], baseSeriesName?: string) => { 2 | if (baseSeriesName) { 3 | graphs.forEach((item) => { 4 | if (typeof item.showInNavigator === 'undefined') { 5 | item.showInNavigator = 6 | item.sname === baseSeriesName || 7 | item.name === baseSeriesName || 8 | item.title === baseSeriesName; 9 | } 10 | }); 11 | } else { 12 | graphs.forEach((item) => { 13 | item.showInNavigator = true; 14 | }); 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /.github/workflows/pr-preview-deploy.yml: -------------------------------------------------------------------------------- 1 | name: PR Preview Deploy 2 | 3 | on: 4 | workflow_run: 5 | workflows: ['PR Preview Build'] 6 | types: 7 | - completed 8 | 9 | jobs: 10 | deploy: 11 | name: Deploy 12 | if: > 13 | github.event.workflow_run.event == 'pull_request' && 14 | github.event.workflow_run.conclusion == 'success' 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: gravity-ui/preview-deploy-action@v2 18 | with: 19 | project: chartkit 20 | github-token: ${{ secrets.GRAVITY_UI_BOT_GITHUB_TOKEN }} 21 | s3-key-id: ${{ secrets.STORYBOOK_S3_KEY_ID }} 22 | s3-secret-key: ${{ secrets.STORYBOOK_S3_SECRET_KEY }} 23 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/LongDashLineIcon.ts: -------------------------------------------------------------------------------- 1 | import type {CommonIconProps} from '../types'; 2 | import {getParsedRect} from '../utils'; 3 | 4 | export const LongDashLineIcon = ({height, width}: CommonIconProps) => { 5 | const {parsedWidth, parsedHeight} = getParsedRect({width, height}); 6 | 7 | return ` 14 | 15 | 16 | 17 | `; 18 | }; 19 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/area/Range.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | import Highcharts from 'highcharts'; 5 | import highchartsMore from 'highcharts/highcharts-more'; 6 | 7 | import {ChartKit} from '../../../../components/ChartKit'; 8 | import {data} from '../../mocks/area-range'; 9 | import {ChartStory} from '../components/ChartStory'; 10 | 11 | highchartsMore(Highcharts); 12 | 13 | export default { 14 | title: 'Plugins/Highcharts/Area', 15 | component: ChartKit, 16 | } as Meta; 17 | 18 | const Template: Story = () => { 19 | return ; 20 | }; 21 | 22 | export const AreaRange = Template.bind({}); 23 | -------------------------------------------------------------------------------- /src/plugins/gravity-charts/renderer/__stories__/StoryWrapper.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {settings} from '../../../../libs'; 4 | import {GravityChartsPlugin} from '../../index'; 5 | 6 | settings.set({plugins: [GravityChartsPlugin]}); 7 | 8 | type Props = { 9 | children?: React.ReactNode; 10 | style?: React.CSSProperties; 11 | }; 12 | 13 | export const StoryWrapper = (props: Props) => { 14 | const {children, style} = props; 15 | 16 | return ( 17 |
24 | {children} 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/index.ts: -------------------------------------------------------------------------------- 1 | export {DashDotLineIcon} from './DashDotLineIcon'; 2 | export {DashLineIcon} from './DashLineIcon'; 3 | export {DotLineIcon} from './DotLineIcon'; 4 | export {LongDashDotDotLineIcon} from './LongDashDotDotLineIcon'; 5 | export {LongDashDotLineIcon} from './LongDashDotLineIcon'; 6 | export {LongDashLineIcon} from './LongDashLineIcon'; 7 | export {ShortDashDotDotLineIcon} from './ShortDashDotDotLineIcon'; 8 | export {ShortDashDotLineIcon} from './ShortDashDotLineIcon'; 9 | export {ShortDashLineIcon} from './ShortDashLineIcon'; 10 | export {ShortDotLineIcon} from './ShortDotLineIcon'; 11 | export {SolidLineIcon} from './SolidLineIcon'; 12 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/localStorage.ts: -------------------------------------------------------------------------------- 1 | class LocalStorage { 2 | static restore(key: string) { 3 | try { 4 | const data = window.localStorage.getItem(key); 5 | if (data === null) { 6 | return null; 7 | } 8 | return JSON.parse(data); 9 | } catch (err) { 10 | return null; 11 | } 12 | } 13 | 14 | static store(key: string, data: unknown) { 15 | try { 16 | window.localStorage.setItem(key, JSON.stringify(data)); 17 | } catch (err) { 18 | console.error(`data not saved in localeStorage: ${err}`); 19 | } 20 | } 21 | } 22 | 23 | export default LocalStorage; 24 | -------------------------------------------------------------------------------- /src/utils/performance.ts: -------------------------------------------------------------------------------- 1 | export const markChartPerformance = (name: string) => { 2 | window.performance.mark(`${name}-mark`); 3 | }; 4 | 5 | export const getChartPerformanceDuration = (name: string) => { 6 | const measureName = `${name}-measure`; 7 | 8 | window.performance.measure(measureName, `${name}-mark`); 9 | 10 | const entry = window.performance.getEntriesByName(measureName)[0]; 11 | 12 | if (entry) { 13 | return entry.duration; 14 | } 15 | 16 | return undefined; 17 | }; 18 | 19 | export function measurePerformance() { 20 | const timestamp = performance.now(); 21 | 22 | return { 23 | end() { 24 | return performance.now() - timestamp; 25 | }, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /.storybook/decorators/withLang.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import type {Decorator} from '@storybook/react'; 3 | import {Lang, configure} from '@gravity-ui/uikit'; 4 | import {settings as dateUtilsSettings} from '@gravity-ui/date-utils'; 5 | import {settings as chartkitSettings} from '../../src/libs'; 6 | 7 | const setDateUtilsLocale = async (lang: string) => { 8 | await dateUtilsSettings.loadLocale(lang); 9 | dateUtilsSettings.setLocale(lang); 10 | }; 11 | 12 | export const withLang: Decorator = (Story, context) => { 13 | const lang = context.globals.lang; 14 | chartkitSettings.set({lang}); 15 | setDateUtilsLocale(lang); 16 | configure({lang: lang as Lang}); 17 | 18 | return ; 19 | }; 20 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/getFormatOptionsFromLine.test.ts: -------------------------------------------------------------------------------- 1 | import type {FormatNumberOptions} from '../../../../../shared'; 2 | import type {TooltipLine} from '../../tooltip/types'; 3 | 4 | import {getFormatOptionsFromLine} from './getFormatOptionsFromLine'; 5 | 6 | describe('plugins/highcharts/config', () => { 7 | test.each<[Partial | undefined, FormatNumberOptions | undefined]>([ 8 | [{chartKitFormat: 'percent'}, {format: 'percent'}], 9 | [{}, undefined], 10 | [undefined, undefined], 11 | ])('getFormatOptionsFromLine (line: %j)', (line, expected) => { 12 | const result = getFormatOptionsFromLine(line); 13 | expect(result).toEqual(expected); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/components/SplitPane/index.ts: -------------------------------------------------------------------------------- 1 | import {ScreenOrientationType} from '../../constants'; 2 | 3 | import {SplitLayoutType} from './types'; 4 | 5 | export {SplitLayout} from './constants'; 6 | export * from './Pane'; 7 | export * from './SplitPane'; 8 | export * from './StyledSplitPane'; 9 | export * from './types'; 10 | 11 | export function mapScreenOrientationTypeToSplitLayout( 12 | type: ScreenOrientationType, 13 | ): SplitLayoutType { 14 | switch (type) { 15 | case 'landscape-primary': 16 | case 'landscape-secondary': { 17 | return 'vertical'; 18 | } 19 | case 'portrait-primary': 20 | case 'portrait-secondary': 21 | default: { 22 | return 'horizontal'; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/plugins/shared/format-number/format-number.test.ts: -------------------------------------------------------------------------------- 1 | import {formatNumber} from './format-number'; 2 | import {i18nInstance} from './i18n/i18n'; 3 | import type {FormatNumberOptions} from './types'; 4 | 5 | i18nInstance.setLang('en'); 6 | 7 | describe('plugins/shared', () => { 8 | test.each<[unknown, FormatNumberOptions | undefined, string]>([ 9 | ['not-a-number', undefined, 'NaN'], 10 | [NaN, undefined, 'NaN'], 11 | ['0.2211556', undefined, '0.2211556'], 12 | ['0.2211556', {precision: 4}, '0.2212'], 13 | ])('formatNumber (args: {value: %p, options: %p})', (value, options, expected) => { 14 | const result = formatNumber(value as number, options); 15 | expect(result).toEqual(expected); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/plugins/highcharts/types/misc.ts: -------------------------------------------------------------------------------- 1 | import type {HighchartsComment} from './comments'; 2 | 3 | export type StringParams = Record; 4 | 5 | export type DrillDownConfig = { 6 | breadcrumbs: string[]; 7 | }; 8 | 9 | export type XAxisItem = Highcharts.Axis & { 10 | dataMin: number; 11 | dataMax: number; 12 | setExtremes: (...args: any[]) => void; 13 | }; 14 | 15 | export type ExtendedHChart = Highcharts.Chart & { 16 | userOptions: Highcharts.Options & { 17 | _internalComments: HighchartsComment[]; 18 | _externalComments: HighchartsComment[]; 19 | _getComments: () => HighchartsComment[]; 20 | _config?: {region?: string}; 21 | }; 22 | xAxis: XAxisItem[]; 23 | navigator?: Highcharts.Options['navigator']; 24 | }; 25 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__tests__/prepare-data.test.ts: -------------------------------------------------------------------------------- 1 | import {data} from '../mocks/line'; 2 | import {prepareData} from '../renderer/helpers/prepare-data'; 3 | import {ConfigOptions} from '../renderer/helpers/types'; 4 | 5 | describe('plugins/highcharts/helpers', () => { 6 | describe('prepareData', () => { 7 | it('should not throw an error', () => { 8 | expect(() => prepareData(data.data, data.config)).not.toThrowError(); 9 | }); 10 | 11 | it('should throw an error', () => { 12 | const configWithLinesLimit: Partial = { 13 | ...data.config, 14 | linesLimit: 1, 15 | }; 16 | expect(() => prepareData(data.data, configWithLinesLimit)).toThrowError(); 17 | }); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/LongDashDotLineIcon.ts: -------------------------------------------------------------------------------- 1 | import type {CommonIconProps} from '../types'; 2 | import {getParsedRect} from '../utils'; 3 | 4 | export const LongDashDotLineIcon = ({height, width}: CommonIconProps) => { 5 | const {parsedWidth, parsedHeight} = getParsedRect({width, height}); 6 | 7 | return ` 14 | 20 | `; 21 | }; 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const esModules = [ 2 | '@gravity-ui/date-utils', 3 | '@gravity-ui/yagr', 4 | 'uplot', 5 | 'd3', 6 | 'd3-array', 7 | 'internmap', 8 | 'delaunator', 9 | 'robust-predicates', 10 | ].join('|'); 11 | 12 | module.exports = { 13 | verbose: true, 14 | preset: 'ts-jest', 15 | testEnvironment: 'jsdom', 16 | transform: { 17 | '^.+\\.(js|ts)?$': 'ts-jest', 18 | }, 19 | modulePathIgnorePatterns: ['/build/', '/node_modules/'], 20 | transformIgnorePatterns: [`/node_modules/(?!${esModules})`], 21 | moduleNameMapper: { 22 | '^.+\\.(css|scss)$': '/test-utils/style.mock.ts', 23 | }, 24 | setupFiles: ['/test-utils/globals.mock.ts'], 25 | testPathIgnorePatterns: ['.visual.'], 26 | }; 27 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/DashDotLineIcon.ts: -------------------------------------------------------------------------------- 1 | import type {CommonIconProps} from '../types'; 2 | import {getParsedRect} from '../utils'; 3 | 4 | export const DashDotLineIcon = ({height, width}: CommonIconProps) => { 5 | const {parsedWidth, parsedHeight} = getParsedRect({width, height}); 6 | 7 | return ` 8 | 15 | 21 | 22 | `; 23 | }; 24 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/LongDashDotDotLineIcon.ts: -------------------------------------------------------------------------------- 1 | import type {CommonIconProps} from '../types'; 2 | import {getParsedRect} from '../utils'; 3 | 4 | export const LongDashDotDotLineIcon = ({width, height}: CommonIconProps) => { 5 | const {parsedWidth, parsedHeight} = getParsedRect({width, height}); 6 | 7 | return ` 14 | 20 | `; 21 | }; 22 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/ShortDashDotLineIcon.ts: -------------------------------------------------------------------------------- 1 | import type {CommonIconProps} from '../types'; 2 | import {getParsedRect} from '../utils'; 3 | 4 | export const ShortDashDotLineIcon = ({height, width}: CommonIconProps) => { 5 | const {parsedWidth, parsedHeight} = getParsedRect({width, height}); 6 | 7 | return ` 14 | 20 | `; 21 | }; 22 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/ShortDashDotDotLineIcon.ts: -------------------------------------------------------------------------------- 1 | import type {CommonIconProps} from '../types'; 2 | import {getParsedRect} from '../utils'; 3 | 4 | export const ShortDashDotDotLineIcon = ({height, width}: CommonIconProps) => { 5 | const {parsedWidth, parsedHeight} = getParsedRect({width, height}); 6 | 7 | return ` 14 | 20 | `; 21 | }; 22 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/constants.ts: -------------------------------------------------------------------------------- 1 | export enum HighchartsType { 2 | Area = 'area', 3 | Arearange = 'arearange', 4 | Bar = 'bar', 5 | Boxplot = 'boxplot', 6 | Bubble = 'bubble', 7 | Column = 'column', 8 | Columnrange = 'columnrange', 9 | Funnel = 'funnel', 10 | Heatmap = 'heatmap', 11 | Line = 'line', 12 | Map = 'map', 13 | Networkgraph = 'networkgraph', 14 | Pie = 'pie', 15 | Sankey = 'sankey', 16 | Scatter = 'scatter', 17 | Streamgraph = 'streamgraph', 18 | Timeline = 'timeline', 19 | Treemap = 'treemap', 20 | Variwide = 'variwide', 21 | Waterfall = 'waterfall', 22 | Wordcloud = 'wordcloud', 23 | Xrange = 'xrange', 24 | } 25 | 26 | export enum NavigatorLinesMode { 27 | All = 'all', 28 | Selected = 'selected', 29 | } 30 | 31 | export const DEFAULT_LINES_LIMIT = 50; 32 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/getFormatOptionsFromLine.ts: -------------------------------------------------------------------------------- 1 | import type {FormatNumberOptions} from '../../../../../shared'; 2 | import type {TooltipLine} from '../../tooltip/types'; 3 | 4 | export const getFormatOptionsFromLine = ( 5 | line?: Partial, 6 | ): FormatNumberOptions | undefined => { 7 | if (!line) { 8 | return undefined; 9 | } 10 | 11 | const options: FormatNumberOptions = { 12 | format: line.chartKitFormat, 13 | postfix: line.chartKitPostfix, 14 | precision: line.chartKitPrecision, 15 | prefix: line.chartKitPrefix, 16 | showRankDelimiter: line.chartKitShowRankDelimiter, 17 | unit: line.chartKitUnit, 18 | }; 19 | const hasValues = Object.values(options).some((value) => typeof value !== 'undefined'); 20 | 21 | return hasValues ? options : undefined; 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/main-preview.yml: -------------------------------------------------------------------------------- 1 | name: Main Preview 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | main: 9 | name: Build and Deploy 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | - name: Setup Node 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 18 20 | - name: Install Packages 21 | run: npm ci 22 | shell: bash 23 | - name: Build Storybook 24 | run: npx sb build 25 | shell: bash 26 | - name: Upload to S3 27 | uses: gravity-ui/preview-upload-to-s3-action@v1 28 | with: 29 | src-path: storybook-static 30 | dest-path: /chartkit/main/ 31 | s3-key-id: ${{ secrets.STORYBOOK_S3_KEY_ID }} 32 | s3-secret-key: ${{ secrets.STORYBOOK_S3_SECRET_KEY }} 33 | -------------------------------------------------------------------------------- /src/plugins/gravity-charts/renderer/utils.ts: -------------------------------------------------------------------------------- 1 | import {CHARTKIT_ERROR_CODE, ChartKitError} from '../../../libs'; 2 | import type {ChartKitProps} from '../../../types'; 3 | 4 | function validateSeriesCountLimit( 5 | series?: ChartKitProps<'gravity-charts'>['data']['series']['data'], 6 | seriesCountLimit?: number, 7 | ) { 8 | if (typeof seriesCountLimit !== 'number') { 9 | return; 10 | } 11 | 12 | const seriesCount = series?.length ?? 0; 13 | 14 | if (seriesCount > seriesCountLimit) { 15 | throw new ChartKitError({code: CHARTKIT_ERROR_CODE.TOO_MANY_LINES}); 16 | } 17 | } 18 | 19 | export function vaildateData(props: ChartKitProps<'gravity-charts'>) { 20 | const {data, validation} = props; 21 | const seriesCountLimit = validation?.seriesCountLimit; 22 | const series = data?.series?.data; 23 | validateSeriesCountLimit(series, seriesCountLimit); 24 | } 25 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/constants/story-settings.ts: -------------------------------------------------------------------------------- 1 | export const defaultChartKitPropsControlsState = { 2 | ref: { 3 | table: { 4 | disable: true, 5 | }, 6 | }, 7 | hoistConfigError: { 8 | table: { 9 | disable: true, 10 | }, 11 | }, 12 | onError: { 13 | table: { 14 | disable: true, 15 | }, 16 | }, 17 | data: { 18 | table: { 19 | disable: true, 20 | }, 21 | }, 22 | type: { 23 | table: { 24 | disable: true, 25 | }, 26 | }, 27 | id: { 28 | table: { 29 | disable: true, 30 | }, 31 | }, 32 | isMobile: { 33 | table: { 34 | disable: true, 35 | }, 36 | }, 37 | onLoad: { 38 | table: { 39 | disable: true, 40 | }, 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/DotLineIcon.ts: -------------------------------------------------------------------------------- 1 | import type {CommonIconProps} from '../types'; 2 | import {getParsedRect} from '../utils'; 3 | 4 | export const DotLineIcon = ({height, width}: CommonIconProps) => { 5 | const {parsedWidth, parsedHeight} = getParsedRect({width, height}); 6 | 7 | return ` 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | `; 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Loader as BaseLoader, LoaderProps as BaseLoaderProps} from '@gravity-ui/uikit'; 4 | 5 | import type {ChartKitRenderPluginLoader} from '../../types'; 6 | import {block} from '../../utils/cn'; 7 | 8 | import './Loader.scss'; 9 | 10 | const b = block('loader'); 11 | 12 | type LoaderProps = BaseLoaderProps & {renderPluginLoader?: ChartKitRenderPluginLoader}; 13 | 14 | export const Loader = ({renderPluginLoader, ...props}: LoaderProps) => { 15 | const pluginLoader = renderPluginLoader?.(); 16 | 17 | // React.Suspense complains about possible undefined in "fallback" property 18 | if (typeof pluginLoader !== 'undefined') { 19 | return pluginLoader as React.JSX.Element; 20 | } 21 | 22 | return ( 23 |
24 | 25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /.storybook/theme-addon/register.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {addons, types} from '@storybook/addons'; 3 | import {useGlobals} from '@storybook/api'; 4 | import {FORCE_RE_RENDER} from '@storybook/core-events'; 5 | import {getThemeType} from '@gravity-ui/uikit'; 6 | import {themes} from '../theme'; 7 | 8 | const ADDON_ID = 'yc-theme-addon'; 9 | const TOOL_ID = `${ADDON_ID}tool`; 10 | 11 | addons.register(ADDON_ID, (api) => { 12 | addons.add(TOOL_ID, { 13 | type: types.TOOL, 14 | title: 'Theme', 15 | render: () => { 16 | return ; 17 | }, 18 | }); 19 | }); 20 | 21 | function Tool({api}) { 22 | const [{theme}] = useGlobals(); 23 | React.useEffect(() => { 24 | api.setOptions({theme: themes[getThemeType(theme)]}); 25 | addons.getChannel().emit(FORCE_RE_RENDER); 26 | }, [theme]); 27 | return null; 28 | } 29 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/highcharts/utils/calcucalteClosestPointManually.ts: -------------------------------------------------------------------------------- 1 | export function calculateClosestPointManually(this: any): number | undefined { 2 | const series = this.series; 3 | 4 | const xValues: number[] = series.reduce((values: number[], currSeries: any) => { 5 | return values.concat(currSeries.processedXData); 6 | }, []); 7 | 8 | xValues.sort((a, b) => b - a); 9 | 10 | let closestPointRange: number | undefined; 11 | 12 | xValues.forEach((xValue: number, index: number) => { 13 | const nextXValue = xValues[index + 1]; 14 | 15 | if (nextXValue) { 16 | const distance = xValue - nextXValue; 17 | 18 | if ( 19 | distance > 0 && 20 | (typeof closestPointRange === 'undefined' || distance < closestPointRange) 21 | ) { 22 | closestPointRange = distance; 23 | } 24 | } 25 | }); 26 | 27 | return closestPointRange; 28 | } 29 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/types.ts: -------------------------------------------------------------------------------- 1 | import type {FormatNumberOptions} from '../../../shared'; 2 | import type {DrillDownConfig, HighchartsWidgetData} from '../../types'; 3 | 4 | export type ConfigOptions = { 5 | highcharts: HighchartsWidgetData['libraryConfig']; 6 | nonBodyScroll?: boolean; 7 | splitTooltip?: boolean; 8 | drillDownData?: DrillDownConfig; 9 | extremes?: { 10 | min?: number; 11 | max?: number; 12 | }; 13 | } & HighchartsWidgetData['config']; 14 | 15 | export type ChartKitFormatNumberSettings = { 16 | chartKitFormatting?: boolean; 17 | chartKitFormat?: FormatNumberOptions['format']; 18 | chartKitPostfix?: FormatNumberOptions['postfix']; 19 | chartKitPrecision?: FormatNumberOptions['precision']; 20 | chartKitPrefix?: FormatNumberOptions['prefix']; 21 | chartKitShowRankDelimiter?: FormatNumberOptions['showRankDelimiter']; 22 | chartKitUnit?: FormatNumberOptions['unit']; 23 | chartKitLabelMode?: FormatNumberOptions['labelMode']; 24 | }; 25 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/no-data/no-data.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Button} from '@gravity-ui/uikit'; 4 | import {Meta, Story} from '@storybook/react'; 5 | 6 | import {ChartKit} from '../../../../components/ChartKit'; 7 | import {filledData, noData} from '../../mocks/no-data'; 8 | import {ChartStory} from '../components/ChartStory'; 9 | 10 | export default { 11 | title: 'Plugins/Highcharts/NoData', 12 | component: ChartKit, 13 | } as Meta; 14 | 15 | const Template: Story = () => { 16 | const [data, setData] = React.useState(noData); 17 | 18 | const handleUpdateData = React.useCallback(() => { 19 | setData(filledData); 20 | }, []); 21 | 22 | return ( 23 |
24 |
25 | 26 |
27 | 28 |
29 | ); 30 | }; 31 | 32 | export const NoData = Template.bind({}); 33 | -------------------------------------------------------------------------------- /.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import type {StorybookConfig} from '@storybook/react-webpack5'; 2 | 3 | const config: StorybookConfig = { 4 | framework: { 5 | name: '@storybook/react-webpack5', 6 | options: {fastRefresh: true}, 7 | }, 8 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(ts|tsx)'], 9 | docs: { 10 | autodocs: false, 11 | }, 12 | addons: [ 13 | '@storybook/preset-scss', 14 | '@storybook/addon-knobs', 15 | {name: '@storybook/addon-essentials', options: {backgrounds: false}}, 16 | './theme-addon/register.tsx', 17 | ], 18 | refs: (_config, {configType}) => { 19 | if (configType !== 'PRODUCTION') { 20 | return {} as Record; 21 | } 22 | 23 | return { 24 | 'gravity-charts': { 25 | title: 'Gravity Charts', 26 | url: 'https://preview.gravity-ui.com/charts', 27 | }, 28 | }; 29 | }, 30 | }; 31 | 32 | export default config; 33 | -------------------------------------------------------------------------------- /src/libs/settings/eventEmitter.ts: -------------------------------------------------------------------------------- 1 | type EventObject = { 2 | id: string; 3 | action: (args: T) => void; 4 | }; 5 | 6 | export class EventEmitter { 7 | private events = {} as Record[]>; 8 | 9 | on(type: MapKey, event: EventObject) { 10 | if (this.events[type]) { 11 | this.events[type].push(event); 12 | } else { 13 | this.events[type] = [event]; 14 | } 15 | } 16 | 17 | off(type: MapKey, eventId: string) { 18 | if (this.events[type]) { 19 | this.events[type] = this.events[type].filter(({id}) => id !== eventId); 20 | } 21 | } 22 | 23 | dispatch(type: MapKey, args: EventsMap[MapKey]) { 24 | if (this.events[type]) { 25 | this.events[type].forEach(({action}) => { 26 | action(args); 27 | }); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/constants/misc.ts: -------------------------------------------------------------------------------- 1 | function checkWindowAvailability() { 2 | return typeof window === 'object'; 3 | } 4 | 5 | export const IS_WINDOW_AVAILABLE = checkWindowAvailability(); 6 | 7 | function checkScreenOrientationAvailability() { 8 | // W3C spec implementation 9 | return ( 10 | IS_WINDOW_AVAILABLE && 11 | typeof window.ScreenOrientation === 'function' && 12 | typeof screen.orientation.addEventListener === 'function' && 13 | typeof screen.orientation.type === 'string' 14 | ); 15 | } 16 | 17 | export const IS_SCREEN_ORIENTATION_AVAILABLE = checkScreenOrientationAvailability(); 18 | 19 | export const ScreenOrientation = { 20 | PORTRAIT_PRIMARY: 'portrait-primary', 21 | PORTRAIT_SECONDARY: 'portrait-secondary', 22 | LANDSCAPE_PRIMARY: 'landscape-primary', 23 | LANDSCAPE_SECONDARY: 'landscape-secondary', 24 | } as const; 25 | 26 | export type ScreenOrientationType = (typeof ScreenOrientation)[keyof typeof ScreenOrientation]; 27 | 28 | export const AVAILABLE_SCREEN_ORIENTATIONS = Object.values(ScreenOrientation); 29 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/calculatePrecision.ts: -------------------------------------------------------------------------------- 1 | import isInteger from 'lodash/isInteger'; 2 | 3 | export const calculatePrecision = ( 4 | alternativePrecision: number | null, 5 | options: {normalizeDiv: boolean; normalizeSub: boolean; precision?: number}, 6 | originalValue?: number, 7 | ) => { 8 | const hasPrecisionOption = options.precision || options.precision === 0; 9 | const hasAlternativePrecision = alternativePrecision || alternativePrecision === 0; 10 | const hasFloat = originalValue && !isInteger(originalValue); 11 | 12 | let precision; 13 | 14 | if (options.normalizeDiv || options.normalizeSub) { 15 | precision = 2; 16 | } 17 | 18 | if (hasPrecisionOption) { 19 | precision = options.precision; 20 | } 21 | 22 | if (!precision && precision !== 0 && hasAlternativePrecision) { 23 | precision = alternativePrecision; 24 | } 25 | 26 | if (hasFloat && !precision && precision !== 0 && !hasAlternativePrecision) { 27 | precision = 2; 28 | } 29 | 30 | return precision; 31 | }; 32 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/numberFormat.ts: -------------------------------------------------------------------------------- 1 | import {formatNumber} from '../../../../../shared'; 2 | import type {FormatNumberOptions} from '../../../../../shared'; 3 | 4 | export const numberFormat = (val: number, round?: number, options: FormatNumberOptions = {}) => { 5 | if (parseInt(val as unknown as string, 10) === val) { 6 | if (typeof round === 'number') { 7 | return formatNumber(val, { 8 | precision: Math.min(round, 20), 9 | ...options, 10 | }); 11 | } else { 12 | return formatNumber(val, { 13 | precision: 0, 14 | ...options, 15 | }); 16 | } 17 | } else if (val) { 18 | let resultRound = round; 19 | 20 | if (typeof resultRound !== 'number') { 21 | resultRound = val.toString().split('.')[1].length; 22 | } 23 | 24 | return formatNumber(val, { 25 | precision: Math.min(resultRound, 20), 26 | ...options, 27 | }); 28 | } 29 | 30 | return null; 31 | }; 32 | -------------------------------------------------------------------------------- /src/plugins/gravity-charts/renderer/__tests__/D3Widget.visual.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import type {ChartData} from '@gravity-ui/charts'; 4 | import {expect, test} from '@playwright/experimental-ct-react17'; 5 | 6 | import {TestStory} from './TestStory.visual'; 7 | 8 | const VALID_CHART_DATA = { 9 | series: { 10 | data: [ 11 | { 12 | type: 'line', 13 | data: [{x: 0, y: 100}], 14 | }, 15 | ], 16 | }, 17 | xAxis: { 18 | type: 'category', 19 | categories: ['A'], 20 | }, 21 | } as ChartData; 22 | 23 | test('Validation should work when updating chart data (empty series)', async ({mount}) => { 24 | const component = await mount(); 25 | 26 | const emptyData = { 27 | series: { 28 | data: [], 29 | }, 30 | xAxis: { 31 | type: 'category', 32 | }, 33 | }; 34 | await component.locator('input').fill(JSON.stringify(emptyData)); 35 | await expect(component).toHaveText('No data'); 36 | }); 37 | -------------------------------------------------------------------------------- /tests/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path'; 2 | import {defineConfig, devices} from '@playwright/experimental-ct-react17'; 3 | 4 | function pathFromRoot(p: string) { 5 | return resolve(__dirname, '../', p); 6 | } 7 | 8 | /** 9 | * See https://playwright.dev/docs/test-configuration. 10 | */ 11 | export default defineConfig({ 12 | testDir: pathFromRoot('src'), 13 | testMatch: '**/__tests__/*.visual.test.tsx', 14 | snapshotPathTemplate: 15 | '{testDir}/{testFileDir}/../__snapshots__/{testFileName}-snapshots/{arg}{-projectName}-linux{ext}', 16 | timeout: 10 * 1000, 17 | fullyParallel: true, 18 | forbidOnly: Boolean(process.env.CI), 19 | retries: process.env.CI ? 2 : 0, 20 | workers: process.env.CI ? 8 : undefined, 21 | reporter: 'html', 22 | use: { 23 | testIdAttribute: 'data-qa', 24 | trace: 'on', 25 | headless: true, 26 | timezoneId: 'UTC', 27 | }, 28 | 29 | projects: [ 30 | { 31 | name: 'chromium', 32 | use: {...devices['Desktop Chrome']}, 33 | }, 34 | ], 35 | }); 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | verify_files: 10 | name: Verify Files 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | with: 16 | fetch-depth: 0 17 | - name: Setup Node 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: 18 21 | cache: 'npm' 22 | - name: Install Packages 23 | run: npm ci 24 | - name: Lint Files 25 | run: npm run lint 26 | - name: Typecheck 27 | run: npm run typecheck 28 | 29 | tests: 30 | name: Tests 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v2 35 | with: 36 | fetch-depth: 0 37 | - name: Setup Node 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: 18 41 | cache: 'npm' 42 | - name: Install Packages 43 | run: npm ci 44 | - name: Unit Tests 45 | run: npm run test 46 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/getXAxisThresholdValue.ts: -------------------------------------------------------------------------------- 1 | import type {Highcharts} from '../../../../types'; 2 | 3 | const VALUES_LIMIT = 1000; 4 | 5 | export const getXAxisThresholdValue = ( 6 | graphs: Record[], 7 | operation: 'min' | 'max', 8 | ): number | null => { 9 | const xAxisValues = graphs.reduce((acc: number[], series: Record) => { 10 | const data = series.data || []; 11 | 12 | return [...acc, ...data.map((point: Highcharts.Point) => point.x)]; 13 | }, [] as number[]); 14 | const fn = operation === 'min' ? Math.min : Math.max; 15 | let index = 0; 16 | let limited = xAxisValues.slice(0, VALUES_LIMIT); 17 | let xAxisValue; 18 | 19 | do { 20 | if (typeof xAxisValue === 'number') { 21 | limited.push(xAxisValue); 22 | } 23 | xAxisValue = fn(...limited); 24 | index += 1; 25 | limited = xAxisValues.slice(index * VALUES_LIMIT, index * VALUES_LIMIT + VALUES_LIMIT); 26 | } while (limited.length); 27 | 28 | return isFinite(xAxisValue) ? xAxisValue : null; 29 | }; 30 | -------------------------------------------------------------------------------- /src/libs/chartkit-error/chartkit-error.ts: -------------------------------------------------------------------------------- 1 | export type ChartKitErrorArgs = { 2 | code?: number | string; 3 | originalError?: Error; 4 | message?: string; 5 | }; 6 | 7 | export const CHARTKIT_ERROR_CODE = { 8 | NO_DATA: 'ERR.CK.NO_DATA', 9 | INVALID_DATA: 'ERR.CK.INVALID_DATA', 10 | UNKNOWN: 'ERR.CK.UNKNOWN_ERROR', 11 | UNKNOWN_PLUGIN: 'ERR.CK.UNKNOWN_PLUGIN', 12 | TOO_MANY_LINES: 'ERR.CK.TOO_MANY_LINES', 13 | }; 14 | 15 | export class ChartKitError extends Error { 16 | readonly code: number | string; 17 | readonly isCustomError = true; 18 | 19 | constructor({ 20 | originalError, 21 | message, 22 | code = CHARTKIT_ERROR_CODE.UNKNOWN, 23 | }: ChartKitErrorArgs = {}) { 24 | super(message); 25 | 26 | this.code = code; 27 | 28 | if (originalError) { 29 | this.name = originalError.name; 30 | this.stack = originalError.stack; 31 | } 32 | } 33 | } 34 | 35 | export const isChartKitError = (error: unknown): error is ChartKitError => { 36 | return error instanceof Error && 'isCustomError' in error; 37 | }; 38 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/getChartKitFormattedValue.ts: -------------------------------------------------------------------------------- 1 | import {formatNumber} from '../../../../../shared'; 2 | import type {ChartKitFormatNumberSettings} from '../../types'; 3 | 4 | export const getChartKitFormattedValue = ( 5 | chartKitFormatSettings: ChartKitFormatNumberSettings, 6 | value: number, 7 | percentage: number, 8 | ) => { 9 | const { 10 | chartKitPrecision, 11 | chartKitPrefix, 12 | chartKitPostfix, 13 | chartKitUnit, 14 | chartKitFormat, 15 | chartKitLabelMode, 16 | chartKitShowRankDelimiter, 17 | } = chartKitFormatSettings; 18 | 19 | const formatOptions = { 20 | precision: chartKitPrecision, 21 | prefix: chartKitPrefix, 22 | postfix: chartKitPostfix, 23 | format: chartKitFormat, 24 | showRankDelimiter: chartKitShowRankDelimiter, 25 | unit: chartKitUnit, 26 | labelMode: chartKitLabelMode, 27 | }; 28 | 29 | const valueToFormat = chartKitLabelMode === 'percent' ? percentage : value; 30 | 31 | return formatNumber(valueToFormat, formatOptions); 32 | }; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 YANDEX LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/getXAxisThresholdValue.test.ts: -------------------------------------------------------------------------------- 1 | import type {Highcharts} from '../../../../types'; 2 | 3 | import {getXAxisThresholdValue} from './getXAxisThresholdValue'; 4 | 5 | const MOCKED_SERIES = [ 6 | {data: [{x: 1}, {x: 2}, {x: -11}, {x: 0}, {x: 1}]}, 7 | {data: [{x: 100}, {x: -1232}]}, 8 | {data: []}, 9 | ] as Highcharts.Series[]; 10 | 11 | describe('plugins/highcharts/config/getXAxisThresholdValue', () => { 12 | it("should return maximun value from x axis in case of 'max' operation", () => { 13 | const result = getXAxisThresholdValue(MOCKED_SERIES, 'max'); 14 | expect(result).toEqual(100); 15 | }); 16 | 17 | it("should return minimum value from x axis in case of 'min' operation", () => { 18 | const result = getXAxisThresholdValue(MOCKED_SERIES, 'min'); 19 | expect(result).toEqual(-1232); 20 | }); 21 | 22 | it.each([['min'], ['max']])('should return null in case of empty series array', (operation) => { 23 | const result = getXAxisThresholdValue([], operation as 'min' | 'max'); 24 | expect(result).toBeNull(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/numberFormat.test.ts: -------------------------------------------------------------------------------- 1 | import type {FormatNumberOptions} from '../../../../../shared'; 2 | import {i18nInstance} from '../../../../../shared/format-number/i18n/i18n'; 3 | 4 | import {numberFormat} from './numberFormat'; 5 | 6 | i18nInstance.setLang('en'); 7 | 8 | describe('plugins/highcharts/config', () => { 9 | test.each<[number, number | undefined, FormatNumberOptions | undefined, string | null]>([ 10 | [100, undefined, undefined, '100'], 11 | [100, 0, undefined, '100'], 12 | [100, 2, undefined, '100.00'], 13 | [100, 21, undefined, '100.00000000000000000000'], 14 | [NaN, 0, undefined, null], 15 | [100, undefined, undefined, '100'], 16 | [100, 2, {precision: 3}, '100.000'], 17 | [100.234, 2, {precision: 3}, '100.234'], 18 | [100000, undefined, {unit: 'k'}, '100K'], 19 | [100000, 2, {unit: 'k'}, '100.00K'], 20 | ])('numberFormat (args: {value: %p, round: %p})', (value, round, options, expected) => { 21 | const result = numberFormat(value, round, options); 22 | expect(result).toEqual(expected); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/plugins/gravity-charts/renderer/withSplitPane/TooltipContent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {ChartTooltipContent} from '@gravity-ui/charts'; 4 | import type {ChartTooltipContentProps} from '@gravity-ui/charts'; 5 | 6 | export type TooltipContentRef = { 7 | redraw: (updates?: Omit) => void; 8 | }; 9 | 10 | type TooltipContentProps = Pick; 11 | 12 | export const TooltipContent = React.forwardRef( 13 | function TooltipContent(props, forwardedRef) { 14 | const {renderer} = props; 15 | const [tooltipProps, setTooltipProps] = React.useState< 16 | Omit | undefined 17 | >(); 18 | 19 | React.useImperativeHandle( 20 | forwardedRef, 21 | () => ({ 22 | redraw(updates?: ChartTooltipContentProps) { 23 | setTooltipProps(updates); 24 | }, 25 | }), 26 | [], 27 | ); 28 | 29 | return ; 30 | }, 31 | ); 32 | -------------------------------------------------------------------------------- /src/constants/widget-data.ts: -------------------------------------------------------------------------------- 1 | export const SeriesType = { 2 | Area: 'area', 3 | BarX: 'bar-x', 4 | BarY: 'bar-y', 5 | Line: 'line', 6 | Pie: 'pie', 7 | Scatter: 'scatter', 8 | Treemap: 'treemap', 9 | Waterfall: 'waterfall', 10 | } as const; 11 | 12 | export enum DashStyle { 13 | Dash = 'Dash', 14 | DashDot = 'DashDot', 15 | Dot = 'Dot', 16 | LongDash = 'LongDash', 17 | LongDashDot = 'LongDashDot', 18 | LongDashDotDot = 'LongDashDotDot', 19 | ShortDash = 'ShortDash', 20 | ShortDashDot = 'ShortDashDot', 21 | ShortDashDotDot = 'ShortDashDotDot', 22 | ShortDot = 'ShortDot', 23 | Solid = 'Solid', 24 | } 25 | 26 | export enum SymbolType { 27 | Circle = 'circle', 28 | Diamond = 'diamond', 29 | Square = 'square', 30 | Triangle = 'triangle', 31 | TriangleDown = 'triangle-down', 32 | } 33 | 34 | export enum LineCap { 35 | Butt = 'butt', 36 | Round = 'round', 37 | Square = 'square', 38 | None = 'none', 39 | } 40 | 41 | export enum LayoutAlgorithm { 42 | Binary = 'binary', 43 | Dice = 'dice', 44 | Slice = 'slice', 45 | SliceDice = 'slice-dice', 46 | Squarify = 'squarify', 47 | } 48 | -------------------------------------------------------------------------------- /src/plugins/gravity-charts/renderer/__tests__/TestStory.visual.tsx: -------------------------------------------------------------------------------- 1 | import React, {ChangeEventHandler} from 'react'; 2 | 3 | import {ChartKit} from '../../../../components/ChartKit'; 4 | import {settings} from '../../../../libs'; 5 | import {GravityChartsPlugin} from '../../index'; 6 | 7 | export const TestStory = (props: any) => { 8 | const [chartData, setChartData] = React.useState(props.data); 9 | const [loading, setLoading] = React.useState(true); 10 | 11 | React.useEffect(() => { 12 | settings.set({plugins: [GravityChartsPlugin]}); 13 | setLoading(false); 14 | }, []); 15 | 16 | const updateData: ChangeEventHandler = (event) => { 17 | const value = event.target.value; 18 | setChartData(JSON.parse(value)); 19 | }; 20 | 21 | if (loading) { 22 | return loading; 23 | } 24 | 25 | return ( 26 |
32 | 33 | 34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/ShortDashLineIcon.ts: -------------------------------------------------------------------------------- 1 | import type {CommonIconProps} from '../types'; 2 | import {getParsedRect} from '../utils'; 3 | 4 | export const ShortDashLineIcon = ({height, width}: CommonIconProps) => { 5 | const {parsedWidth, parsedHeight} = getParsedRect({width, height}); 6 | 7 | return ` 14 | 20 | `; 21 | }; 22 | -------------------------------------------------------------------------------- /src/plugins/yagr/types.ts: -------------------------------------------------------------------------------- 1 | import type {MinimalValidConfig, RawSerieData, YagrConfig} from '@gravity-ui/yagr'; 2 | import type Yagr from '@gravity-ui/yagr'; 3 | 4 | import {ChartKitProps} from 'src/types'; 5 | 6 | export type {default as Yagr} from '@gravity-ui/yagr'; 7 | export type {YagrReactRef} from '@gravity-ui/yagr/react'; 8 | export * from '@gravity-ui/yagr/dist/types'; 9 | 10 | export interface CustomTooltipProps { 11 | yagr: Yagr | undefined; 12 | } 13 | 14 | export type YagrWidgetProps = ChartKitProps<'yagr'> & { 15 | id: string; 16 | }; 17 | 18 | export type YagrWidgetData = { 19 | data: { 20 | graphs: RawSerieData[]; 21 | timeline: number[]; 22 | /** 23 | * Allow to setup timezone for X axis and tooltip's header. 24 | * 25 | * Format example: "UTC", "Europe/Moscow". 26 | * 27 | * For more examples check [wiki](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) 28 | */ 29 | timeZone?: string; 30 | }; 31 | libraryConfig: Partial; 32 | sources?: Record< 33 | number, 34 | { 35 | data: { 36 | program: string; 37 | }; 38 | } 39 | >; 40 | }; 41 | -------------------------------------------------------------------------------- /src/libs/chartkit-error/__tests__/chartkit-error.ts: -------------------------------------------------------------------------------- 1 | import {CHARTKIT_ERROR_CODE, ChartKitError, isChartKitError} from '../chartkit-error'; 2 | import type {ChartKitErrorArgs} from '../chartkit-error'; 3 | 4 | describe('libs/chartkit-error', () => { 5 | test.each<[unknown, boolean] /* [error, expected] */>([ 6 | [new ChartKitError(), true], 7 | [new Error(), false], 8 | [null, false], 9 | [undefined, false], 10 | ])('isChartKitError (args: %j)', (error, expected) => { 11 | const result = isChartKitError(error); 12 | expect(result).toEqual(expected); 13 | }); 14 | 15 | test.each<[ChartKitErrorArgs | undefined, ChartKitErrorArgs['code']] /* [args, expected] */>([ 16 | [undefined, CHARTKIT_ERROR_CODE.UNKNOWN], 17 | [{code: CHARTKIT_ERROR_CODE.NO_DATA}, CHARTKIT_ERROR_CODE.NO_DATA], 18 | ])('check ChartKitError code (args: %j)', (args, expected) => { 19 | let result: ChartKitErrorArgs['code'] = ''; 20 | 21 | try { 22 | throw new ChartKitError(args); 23 | } catch (error) { 24 | if (isChartKitError(error)) { 25 | result = error.code; 26 | } 27 | } 28 | 29 | expect(result).toEqual(expected); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/plugins/indicator/renderer/IndicatorItem.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import type {ChartKitProps} from '../../../types'; 4 | import {block} from '../../../utils/cn'; 5 | import type {IndicatorWidgetDataItem} from '../types'; 6 | 7 | const b = block('indicator'); 8 | 9 | export const IndicatorItem = ( 10 | props: IndicatorWidgetDataItem & { 11 | defaultColor?: string; 12 | formatNumber?: ChartKitProps<'indicator'>['formatNumber']; 13 | }, 14 | ) => { 15 | const {formatNumber, content, color, defaultColor, size, title, nowrap} = props; 16 | const mods = {size, nowrap}; 17 | const style: React.CSSProperties = {color: color || defaultColor}; 18 | 19 | let value = content.current.value; 20 | 21 | if (formatNumber && typeof value === 'number') { 22 | value = formatNumber(value, content.current); 23 | } 24 | 25 | return ( 26 |
27 | {title && ( 28 |
29 | {title} 30 |
31 | )} 32 |
33 | {value} 34 |
35 |
36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/DashLineIcon.ts: -------------------------------------------------------------------------------- 1 | import type {CommonIconProps} from '../types'; 2 | import {getParsedRect} from '../utils'; 3 | 4 | export const DashLineIcon = ({height, width}: CommonIconProps) => { 5 | const {parsedWidth, parsedHeight} = getParsedRect({width, height}); 6 | 7 | return ` 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | `; 28 | }; 29 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/index.ts: -------------------------------------------------------------------------------- 1 | import orderBy from 'lodash/orderBy'; 2 | 3 | import type {HighchartsSortData} from '../../../../types/widget'; 4 | 5 | export {addShowInNavigatorToSeries} from './addShowInNavigatorToSeries'; 6 | export {buildNavigatorFallback} from './buildNavigatorFallback'; 7 | export {calculatePrecision} from './calculatePrecision'; 8 | export {concatStrings} from './concatStrings'; 9 | export {getChartKitFormattedValue} from './getChartKitFormattedValue'; 10 | export {getFormatOptionsFromLine} from './getFormatOptionsFromLine'; 11 | export {getXAxisThresholdValue} from './getXAxisThresholdValue'; 12 | export * from './tooltip'; 13 | export {isNavigatorSeries} from './isNavigatorSeries'; 14 | export {isSafari} from './isSafari'; 15 | export {mergeArrayWithObject} from './mergeArrayWithObject'; 16 | export {numberFormat} from './numberFormat'; 17 | export {setNavigatorDefaultPeriod} from './setNavigatorDefaultPeriod'; 18 | 19 | export const getSortedData = >( 20 | data: T[], 21 | sort: HighchartsSortData = {}, 22 | ) => { 23 | const {enabled = false, order = 'desc', iteratee = 'y'} = sort; 24 | 25 | if (!enabled) { 26 | return [...data]; 27 | } 28 | 29 | return orderBy(data, iteratee, order); 30 | }; 31 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/tooltip.ts: -------------------------------------------------------------------------------- 1 | import {HighchartsWidgetData} from '../../../../types'; 2 | 3 | // In case of using 'sankey' or 'xrange', the shared property must be set to false, otherwise the tooltip behaves incorrectly: 4 | // Point.onMouseOver -> Highcharts.Pointer.runPointActions -> H.Tooltip.refresh -> Cannot read property 'series' of undefined 5 | export const isTooltipShared = (chartType: string) => { 6 | if (['sankey', 'xrange'].includes(chartType)) { 7 | return false; 8 | } 9 | 10 | return true; 11 | }; 12 | 13 | export const checkTooltipPinningAvailability = ( 14 | args: { 15 | tooltip?: HighchartsWidgetData['config']['tooltip']; 16 | altKey?: boolean; 17 | metaKey?: boolean; 18 | } = {}, 19 | ) => { 20 | const {tooltip, altKey, metaKey} = args; 21 | const enabled = tooltip?.pin?.enabled ?? true; 22 | const shouldAltKeyBePressed = tooltip?.pin?.altKey ?? false; 23 | const shouldMetaKeyBePressed = tooltip?.pin?.metaKey ?? false; 24 | 25 | if (!enabled) { 26 | return false; 27 | } 28 | 29 | if (shouldAltKeyBePressed && !altKey) { 30 | return false; 31 | } 32 | 33 | if (shouldMetaKeyBePressed && !metaKey) { 34 | return false; 35 | } 36 | 37 | return true; 38 | }; 39 | -------------------------------------------------------------------------------- /.storybook/theme.ts: -------------------------------------------------------------------------------- 1 | import {create} from '@storybook/theming'; 2 | 3 | export const CloudThemeLight = create({ 4 | base: 'light', 5 | 6 | colorPrimary: '#027bf3', 7 | colorSecondary: 'rgba(2, 123, 243, 0.6)', 8 | 9 | // Typography 10 | fontBase: '"Helvetica Neue", Arial, Helvetica, sans-serif', 11 | fontCode: 12 | '"SF Mono", "Menlo", "Monaco", "Consolas", "Ubuntu Mono", "Liberation Mono", "DejaVu Sans Mono", "Courier New", "Courier", monospace', 13 | 14 | // Text colors 15 | textColor: 'black', 16 | textInverseColor: 'black', 17 | 18 | // Toolbar default and active colors 19 | barTextColor: 'silver', 20 | barSelectedColor: '#027bf3', 21 | // barBg: '#027bf3', 22 | 23 | // Form colors 24 | inputBg: 'white', 25 | inputBorder: 'silver', 26 | inputTextColor: 'black', 27 | inputBorderRadius: 4, 28 | 29 | brandUrl: 'https://github.com/gravity-ui/chartkit', 30 | brandTitle: `
ChartKit
31 |
ChartKit Plugins
`, 32 | }); 33 | 34 | export const CloudThemeDark = create({ 35 | base: 'dark', 36 | }); 37 | 38 | export const themes = { 39 | light: CloudThemeLight, 40 | dark: CloudThemeDark, 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/SplitPane/Pane.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2015 tomkp 2 | // Copyright 2022 YANDEX LLC 3 | 4 | import React from 'react'; 5 | 6 | import type {SplitLayoutType} from './types'; 7 | 8 | type Props = { 9 | className?: string; 10 | children?: React.ReactNode; 11 | size?: number | string; 12 | split?: SplitLayoutType; 13 | style?: React.CSSProperties; 14 | eleRef?: (node: HTMLDivElement) => void; 15 | }; 16 | 17 | export class Pane extends React.PureComponent { 18 | render() { 19 | const {children, className, split, style: styleProps, size, eleRef} = this.props; 20 | const classes = ['Pane', split, className]; 21 | 22 | let style: React.CSSProperties = { 23 | flex: 1, 24 | position: 'relative', 25 | outline: 'none', 26 | }; 27 | 28 | if (size !== undefined) { 29 | if (split === 'vertical') { 30 | style.width = size; 31 | } else { 32 | style.height = size; 33 | style.display = 'flex'; 34 | } 35 | style.flex = 'none'; 36 | } 37 | 38 | style = Object.assign({}, style, styleProps || {}); 39 | 40 | return ( 41 |
42 | {children} 43 |
44 | ); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/plugins/highcharts/types/comments.ts: -------------------------------------------------------------------------------- 1 | interface CommentBase { 2 | id: string; 3 | feed: string; 4 | creatorLogin: string; 5 | createdDate: string; 6 | modifierLogin: string; 7 | modifiedDate: string; 8 | data: string; 9 | dateUntil: string | null; 10 | type: 'band-x' | 'dot-x-y' | 'flag-x' | 'line-x'; 11 | text: string; 12 | params: object | null; 13 | meta: object; 14 | } 15 | 16 | interface CommentDotXY extends CommentBase { 17 | type: 'dot-x-y'; 18 | meta: { 19 | color: string; 20 | graphId: string; 21 | visible: boolean; 22 | fillColor: string; 23 | textColor: string; 24 | }; 25 | } 26 | 27 | interface CommentLineX extends CommentBase { 28 | type: 'line-x'; 29 | meta: { 30 | color: string; 31 | width: number; 32 | dashStyle: string; 33 | }; 34 | } 35 | 36 | interface CommentBandX extends CommentBase { 37 | dateUntil: string; 38 | type: 'band-x'; 39 | meta: { 40 | color: string; 41 | zIndex: number; 42 | visible: boolean; 43 | }; 44 | } 45 | 46 | interface CommentFlagX extends CommentBase { 47 | type: 'flag-x'; 48 | meta: { 49 | y: number; 50 | color: string; 51 | shape: string; 52 | }; 53 | } 54 | 55 | export type HighchartsComment = CommentDotXY | CommentBandX | CommentLineX | CommentFlagX; 56 | -------------------------------------------------------------------------------- /src/plugins/highcharts/mocks/pie.ts: -------------------------------------------------------------------------------- 1 | import {HighchartsWidgetData} from '../types'; 2 | 3 | export const data: HighchartsWidgetData = { 4 | data: { 5 | graphs: [ 6 | { 7 | name: 'Number of requests', 8 | tooltip: { 9 | chartKitFormatting: true, 10 | chartKitPrecision: 0, 11 | }, 12 | dataLabels: { 13 | format: null, 14 | chartKitFormatting: true, 15 | chartKitPrecision: 0, 16 | chartKitPrefix: '', 17 | chartKitPostfix: '', 18 | chartKitLabelMode: 'absolute', 19 | chartKitFormat: 'number', 20 | chartKitShowRankDelimiter: true, 21 | }, 22 | data: [ 23 | {name: 'Furniture', y: 14344, label: 14344}, 24 | {name: 'Domestic chemistry', y: 14244, label: 14244}, 25 | {name: 'Household goods', y: 14181, label: 14181}, 26 | ], 27 | }, 28 | ], 29 | categories: ['Furniture', 'Domestic chemistry', 'Household goods'], 30 | }, 31 | config: { 32 | showPercentInTooltip: true, 33 | }, 34 | libraryConfig: { 35 | chart: { 36 | type: 'pie', 37 | }, 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/plugins/yagr/renderer/useWidgetData.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {useThemeValue} from '@gravity-ui/uikit'; 4 | import type {YagrChartProps} from '@gravity-ui/yagr/react'; 5 | 6 | import type {MinimalValidConfig, YagrTheme, YagrWidgetProps} from '../types'; 7 | 8 | import {shapeYagrConfig} from './utils'; 9 | 10 | export const useWidgetData = ( 11 | props: YagrWidgetProps, 12 | id: string, 13 | ): {config: MinimalValidConfig; debug: YagrChartProps['debug']} => { 14 | const {data, sources, libraryConfig} = props.data; 15 | const theme = useThemeValue() as YagrTheme; 16 | const config: MinimalValidConfig = React.useMemo( 17 | () => 18 | shapeYagrConfig({ 19 | data, 20 | libraryConfig, 21 | theme, 22 | customTooltip: Boolean(props.tooltip), 23 | }), 24 | [data, libraryConfig, theme, props.tooltip], 25 | ); 26 | const debug: YagrChartProps['debug'] = React.useMemo(() => { 27 | const filename = sources 28 | ? Object.values(sources) 29 | .map((source) => { 30 | return source?.data?.program; 31 | }) 32 | .filter(Boolean) 33 | .join(', ') || id 34 | : id; 35 | return {filename}; 36 | }, [id, sources]); 37 | 38 | return {config, debug}; 39 | }; 40 | -------------------------------------------------------------------------------- /src/libs/settings/mergeSettingStrategy.ts: -------------------------------------------------------------------------------- 1 | import isObject from 'lodash/isObject'; 2 | import mergeWith from 'lodash/mergeWith'; 3 | 4 | import {ChartKitPlugin} from 'src/types'; 5 | 6 | // @ts-ignore 7 | export function mergeSettingStrategy(objValue: any, srcValue: any, key: string): any { 8 | if (key === 'plugins') { 9 | const currentPlugins: ChartKitPlugin[] = [...objValue]; 10 | const newPlugins: ChartKitPlugin[] = [...srcValue]; 11 | // modify existing plugins 12 | let newSettingsPlugins = currentPlugins.map((currentPlugin) => { 13 | const newPluginIndex = newPlugins.findIndex(({type}) => type === currentPlugin.type); 14 | 15 | if (newPluginIndex !== -1) { 16 | const newPlugin = newPlugins[newPluginIndex]; 17 | newPlugins.splice(newPluginIndex, 1); 18 | 19 | return { 20 | type: currentPlugin.type, 21 | renderer: newPlugin.renderer, 22 | }; 23 | } 24 | 25 | return currentPlugin; 26 | }); 27 | 28 | // add new plugins if it exist after modified 29 | if (newPlugins.length > 0) { 30 | newSettingsPlugins = [...newSettingsPlugins, ...newPlugins]; 31 | } 32 | 33 | return newSettingsPlugins; 34 | } 35 | 36 | if (isObject(objValue)) { 37 | return mergeWith(objValue, srcValue, mergeSettingStrategy); 38 | } 39 | 40 | return srcValue; 41 | } 42 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/index.ts: -------------------------------------------------------------------------------- 1 | import {LineShapeType} from '../constants'; 2 | 3 | import { 4 | DashDotLineIcon, 5 | DashLineIcon, 6 | DotLineIcon, 7 | LongDashDotDotLineIcon, 8 | LongDashDotLineIcon, 9 | LongDashLineIcon, 10 | ShortDashDotDotLineIcon, 11 | ShortDashDotLineIcon, 12 | ShortDashLineIcon, 13 | ShortDotLineIcon, 14 | SolidLineIcon, 15 | } from './template-icons'; 16 | 17 | const DEFAULT_ICON_WIDTH = '38px'; 18 | const DEFAULT_ICON_HEIGHT = '2px'; 19 | const TEMPLATE_ICONS = { 20 | [LineShapeType.DashDot]: DashDotLineIcon, 21 | [LineShapeType.Dash]: DashLineIcon, 22 | [LineShapeType.Dot]: DotLineIcon, 23 | [LineShapeType.LongDashDotDot]: LongDashDotDotLineIcon, 24 | [LineShapeType.LongDashDot]: LongDashDotLineIcon, 25 | [LineShapeType.LongDash]: LongDashLineIcon, 26 | [LineShapeType.ShortDashDotDot]: ShortDashDotDotLineIcon, 27 | [LineShapeType.ShortDashDot]: ShortDashDotLineIcon, 28 | [LineShapeType.ShortDash]: ShortDashLineIcon, 29 | [LineShapeType.ShortDot]: ShortDotLineIcon, 30 | [LineShapeType.Solid]: SolidLineIcon, 31 | }; 32 | 33 | export const renderShapeIcon = (args: {type?: LineShapeType; width?: string; height?: string}) => { 34 | const {type, width = DEFAULT_ICON_WIDTH, height = DEFAULT_ICON_HEIGHT} = args; 35 | const templateIcon = type && TEMPLATE_ICONS[type]; 36 | 37 | return templateIcon ? templateIcon({width, height}) : ''; 38 | }; 39 | -------------------------------------------------------------------------------- /src/plugins/gravity-charts/renderer/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/cloneDeep'; 2 | import merge from 'lodash/merge'; 3 | 4 | import type {ChartKitProps} from '../../../../types'; 5 | import {vaildateData} from '../utils'; 6 | 7 | const BASIC_PROPS: ChartKitProps<'gravity-charts'> = { 8 | data: { 9 | series: { 10 | data: [ 11 | {data: [{x: 0, y: 0}], name: 'Line 1', type: 'line'}, 12 | {data: [{x: 1, y: 1}], name: 'Line 2', type: 'line'}, 13 | ], 14 | }, 15 | }, 16 | type: 'gravity-charts', 17 | }; 18 | 19 | describe('plugins/gravity-charts/utils', () => { 20 | describe('validateData', () => { 21 | it('should not throw an error without series count limit', () => { 22 | expect(() => vaildateData(BASIC_PROPS)).not.toThrowError(); 23 | }); 24 | it('should not throw an error with sufficient series count limit', () => { 25 | const result = merge(cloneDeep(BASIC_PROPS), { 26 | validation: {seriesCountLimit: 3}, 27 | }); 28 | expect(() => vaildateData(result)).not.toThrowError(); 29 | }); 30 | it('should throw an error with insufficient series count limit', () => { 31 | const result = merge(cloneDeep(BASIC_PROPS), { 32 | validation: {seriesCountLimit: 1}, 33 | }); 34 | expect(() => vaildateData(result)).toThrowError(); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/plugins/highcharts/mocks/pie-with-totals.ts: -------------------------------------------------------------------------------- 1 | import {HighchartsWidgetData} from '../types'; 2 | 3 | export const data: HighchartsWidgetData = { 4 | data: { 5 | graphs: [ 6 | { 7 | name: 'Number of requests', 8 | tooltip: { 9 | chartKitFormatting: true, 10 | chartKitPrecision: 0, 11 | }, 12 | dataLabels: { 13 | format: null, 14 | chartKitFormatting: true, 15 | chartKitPrecision: 0, 16 | chartKitPrefix: '', 17 | chartKitPostfix: '', 18 | chartKitLabelMode: 'absolute', 19 | chartKitFormat: 'number', 20 | chartKitShowRankDelimiter: true, 21 | }, 22 | data: [ 23 | {name: 'Furniture', y: 14344, label: 14344}, 24 | {name: 'Domestic chemistry', y: 14244, label: 14244}, 25 | {name: 'Household goods', y: 14181, label: 14181}, 26 | ], 27 | }, 28 | ], 29 | categories: ['Furniture', 'Domestic chemistry', 'Household goods'], 30 | totals: 42769, 31 | }, 32 | config: { 33 | showPercentInTooltip: true, 34 | }, 35 | libraryConfig: { 36 | chart: { 37 | type: 'pie', 38 | }, 39 | plotOptions: { 40 | pie: { 41 | innerSize: '50%', 42 | }, 43 | }, 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /src/components/SplitPane/StyledSplitPane.scss: -------------------------------------------------------------------------------- 1 | // Copyright 2015 tomkp 2 | // Copyright 2022 YANDEX LLC 3 | 4 | .styled-split-pane { 5 | &__pane-resizer { 6 | background: var(--g-color-base-generic); 7 | z-index: 1; 8 | box-sizing: border-box; 9 | position: relative; 10 | 11 | &_hovered { 12 | opacity: 0.5; 13 | } 14 | 15 | &.horizontal { 16 | height: 24px; 17 | min-height: 24px; 18 | width: 100%; 19 | 20 | &:before { 21 | content: ''; 22 | width: 28px; 23 | margin-left: -14px; 24 | height: 1px; 25 | background-color: var(--highcarts-navigator-body); 26 | left: 50%; 27 | top: 10px; 28 | position: absolute; 29 | } 30 | 31 | &:after { 32 | content: ''; 33 | width: 28px; 34 | margin-left: -14px; 35 | height: 1px; 36 | background-color: var(--highcarts-navigator-body); 37 | left: 50%; 38 | top: 13px; 39 | position: absolute; 40 | } 41 | } 42 | 43 | &.vertical { 44 | height: 100%; 45 | width: 1px; 46 | min-width: 1px; 47 | background: var(--data-table-border-color); 48 | } 49 | } 50 | 51 | .Pane { 52 | height: 100%; 53 | background: var(--ck-split-pane-background); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/types/widget.ts: -------------------------------------------------------------------------------- 1 | import type {ChartData} from '@gravity-ui/charts'; 2 | 3 | import type {SplitLayoutType} from '../components/SplitPane/types'; 4 | import type {Highcharts, HighchartsWidgetData, StringParams} from '../plugins/highcharts/types'; 5 | import type {IndicatorWidgetData} from '../plugins/indicator/types'; 6 | import type {CustomTooltipProps, Yagr, YagrWidgetData} from '../plugins/yagr/types'; 7 | 8 | export interface ChartKitWidget { 9 | yagr: { 10 | data: YagrWidgetData; 11 | widget: Yagr; 12 | tooltip?: (props: T) => React.ReactNode; 13 | }; 14 | indicator: { 15 | data: IndicatorWidgetData; 16 | widget: never; 17 | formatNumber?: (value: number, options?: T) => string; 18 | }; 19 | highcharts: { 20 | data: HighchartsWidgetData; 21 | widget: Highcharts.Chart | null; 22 | hoistConfigError?: boolean; 23 | nonBodyScroll?: boolean; 24 | splitTooltip?: boolean; 25 | paneSplitOrientation?: SplitLayoutType; 26 | onSplitPaneOrientationChange?: (orientation: SplitLayoutType) => void; 27 | onChange?: ( 28 | data: {type: 'PARAMS_CHANGED'; data: {params: StringParams}}, 29 | state: {forceUpdate: boolean}, 30 | callExternalOnChange?: boolean, 31 | ) => void; 32 | }; 33 | 'gravity-charts': { 34 | data: ChartData; 35 | widget: never; 36 | tooltip?: { 37 | splitted?: boolean; 38 | }; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/components/ChartKit.scss: -------------------------------------------------------------------------------- 1 | .chartkit { 2 | height: 100%; 3 | width: 100%; 4 | 5 | &_mobile .chartkit-scrollable-node { 6 | max-height: 3000px; 7 | } 8 | } 9 | 10 | .chartkit-theme_common { 11 | --highcarts-navigator-border: var(--g-color-line-generic); 12 | --highcarts-navigator-track: var(--g-color-base-generic); 13 | --highcarts-navigator-body: var(--g-color-scroll-handle); 14 | --highcharts-series-border: var(--g-color-base-background); 15 | --highcharts-grid-line: var(--g-color-line-generic); 16 | --highcharts-axis-line: var(--g-color-line-generic); 17 | --highcharts-tick: var(--g-color-line-generic); 18 | --highcharts-title: var(--g-color-text-primary); 19 | --highcharts-axis-labels: var(--g-color-text-secondary); 20 | --highcharts-data-labels: var(--g-color-text-secondary); 21 | --highcharts-plot-line-label: var(--g-color-text-secondary); 22 | --highcharts-legend-item: var(--g-color-text-secondary); 23 | --highcharts-legend-item-hover: var(--g-color-text-primary); 24 | --highcharts-legend-item-hidden: var(--g-color-text-hint); 25 | --highcharts-floating-bg: var(--g-color-infographics-tooltip-bg); 26 | --highcharts-tooltip-text: var(--g-color-text-primary); 27 | --highcharts-tooltip-bg: var(--highcharts-floating-bg); 28 | --highcharts-tooltip-alternate-bg: var(--g-color-base-generic); 29 | --highcharts-tooltip-selected-bg: var(--g-color-base-info-medium); 30 | --highcharts-tooltip-text-complementary: var(--g-color-text-secondary); 31 | --highcharts-holiday-band: var(--g-color-base-generic); 32 | } 33 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/buildNavigatorFallback.test.ts: -------------------------------------------------------------------------------- 1 | import {buildNavigatorFallback} from './buildNavigatorFallback'; 2 | 3 | const MOCKED_GRAPHS = [{name: 'Test'}, {name: 'Test1'}, {name: 'Test2'}]; 4 | const baseSeriesName = 'Test2'; 5 | const missedSeriesName = 'Test3'; 6 | 7 | describe('plugins/highcharts/config/buildNavigatorFallback', () => { 8 | it('should set {showInNavigator: true} to current series in case of initialized baseSeriesName', () => { 9 | const expectedResult = [ 10 | {name: 'Test', showInNavigator: false}, 11 | {name: 'Test1', showInNavigator: false}, 12 | {name: 'Test2', showInNavigator: true}, 13 | ]; 14 | buildNavigatorFallback(MOCKED_GRAPHS, baseSeriesName); 15 | 16 | expect(MOCKED_GRAPHS).toEqual(expectedResult); 17 | }); 18 | 19 | it('should set {showInNavigator: true} to all series in case of baseSeriesName are not initialized', () => { 20 | const expectedResult = [ 21 | {name: 'Test', showInNavigator: true}, 22 | {name: 'Test1', showInNavigator: true}, 23 | {name: 'Test2', showInNavigator: true}, 24 | ]; 25 | 26 | buildNavigatorFallback(MOCKED_GRAPHS); 27 | 28 | expect(MOCKED_GRAPHS).toEqual(expectedResult); 29 | }); 30 | 31 | it('should not set {showInNavigator: true} to current series in case of baseSeriesName are not finded in graphs', () => { 32 | buildNavigatorFallback(MOCKED_GRAPHS, missedSeriesName); 33 | 34 | expect(MOCKED_GRAPHS).toEqual(MOCKED_GRAPHS); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /src/plugins/indicator/renderer/IndicatorWidget.scss: -------------------------------------------------------------------------------- 1 | .chartkit-indicator { 2 | $class: &; 3 | 4 | width: 100%; 5 | height: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | 9 | &__content { 10 | width: 100%; 11 | overflow: auto; 12 | } 13 | 14 | &__item { 15 | padding: 15px; 16 | font-size: inherit; 17 | box-sizing: border-box; 18 | 19 | &_nowrap { 20 | #{$class}__item-title { 21 | white-space: nowrap; 22 | text-overflow: ellipsis; 23 | overflow: hidden; 24 | } 25 | } 26 | 27 | &_size_s #{$class}__item-title { 28 | font-size: 13px; 29 | } 30 | 31 | &_size_s #{$class}__item-value { 32 | font-size: 24px; 33 | } 34 | 35 | &_size_l #{$class}__item-title { 36 | font-size: 20px; 37 | } 38 | 39 | &_size_l #{$class}__item-value { 40 | font-size: 64px; 41 | } 42 | 43 | &_size_xl #{$class}__item-title { 44 | font-size: 24px; 45 | } 46 | 47 | &_size_xl #{$class}__item-value { 48 | font-size: 80px; 49 | } 50 | } 51 | 52 | &__item-title { 53 | font-weight: 500; 54 | line-height: 1.2; 55 | color: var(--g-color-text-primary); 56 | font-size: 16px; 57 | padding-bottom: 0.125em; 58 | } 59 | 60 | &__item-value { 61 | font-weight: 500; 62 | line-height: 1.2; 63 | font-size: 48px; 64 | white-space: nowrap; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/area/WithThreshold.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | import type {HighchartsWidgetData} from '../../types'; 7 | import {ChartStory} from '../components/ChartStory'; 8 | 9 | export default { 10 | title: 'Plugins/Highcharts/Area', 11 | component: ChartKit, 12 | } as Meta; 13 | 14 | const data = { 15 | data: { 16 | graphs: [ 17 | { 18 | data: [ 19 | 29.9, 71.5, 106.4, 129.2, 144.0, 176.0, 135.6, 148.5, 216.4, 194.1, 95.6, 54.4, 20 | ], 21 | }, 22 | ], 23 | }, 24 | config: { 25 | hideHolidaysBands: true, 26 | }, 27 | libraryConfig: { 28 | chart: { 29 | type: 'area', 30 | }, 31 | plotOptions: { 32 | series: { 33 | threshold: 100, 34 | }, 35 | }, 36 | xAxis: { 37 | categories: [ 38 | 'Jan', 39 | 'Feb', 40 | 'Mar', 41 | 'Apr', 42 | 'May', 43 | 'Jun', 44 | 'Jul', 45 | 'Aug', 46 | 'Sep', 47 | 'Oct', 48 | 'Nov', 49 | 'Dec', 50 | ], 51 | }, 52 | }, 53 | } as HighchartsWidgetData; 54 | 55 | const Template: Story = () => { 56 | return ; 57 | }; 58 | 59 | export const WithThreshold = Template.bind({}); 60 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/setNavigatorDefaultPeriod.ts: -------------------------------------------------------------------------------- 1 | import {dateTime} from '@gravity-ui/date-utils'; 2 | 3 | import type {Highcharts} from '../../../../types'; 4 | import type {NavigatorPeriod} from '../types'; 5 | 6 | import {getXAxisThresholdValue} from './getXAxisThresholdValue'; 7 | 8 | type SetNavigatorDefaultPeriod = { 9 | params: Record; 10 | periodSettings: NavigatorPeriod; 11 | }; 12 | 13 | type NavigatorPeriodInMS = { 14 | minRange: number; 15 | range: number; 16 | }; 17 | const HOUR_IN_MS = 1000 * 60 * 60; 18 | const DAY_IN_MS = HOUR_IN_MS * 24; 19 | 20 | export const setNavigatorDefaultPeriod = ({params, periodSettings}: SetNavigatorDefaultPeriod) => { 21 | const periodInMS = getDefaultPeriodInMS(periodSettings, params.series); 22 | 23 | if (!periodInMS) { 24 | return; 25 | } 26 | 27 | const {range, minRange} = periodInMS; 28 | params.xAxis.range = range; 29 | params.xAxis.minRange = minRange; 30 | }; 31 | 32 | export const getDefaultPeriodInMS = ( 33 | periodSettings: NavigatorPeriod, 34 | series: Highcharts.Series[], 35 | ): NavigatorPeriodInMS | null => { 36 | const {type, value, period} = periodSettings; 37 | const minRange = type === 'date' ? DAY_IN_MS : HOUR_IN_MS; 38 | const maxXValue = getXAxisThresholdValue(series, 'max'); 39 | 40 | if (maxXValue === null) { 41 | return null; 42 | } 43 | 44 | const minXValue = dateTime({input: maxXValue}).subtract(value, period); 45 | const range = maxXValue - minXValue.valueOf(); 46 | 47 | return { 48 | minRange, 49 | range, 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /src/plugins/highcharts/mocks/venn.ts: -------------------------------------------------------------------------------- 1 | import type {HighchartsWidgetData} from '../types'; 2 | 3 | export const data: HighchartsWidgetData = { 4 | data: { 5 | graphs: [ 6 | { 7 | name: 'hey', 8 | data: [ 9 | { 10 | sets: ['Good'], 11 | value: 2, 12 | }, 13 | { 14 | sets: ['Fast'], 15 | value: 2, 16 | }, 17 | { 18 | sets: ['Cheap'], 19 | value: 2, 20 | }, 21 | { 22 | sets: ['Good', 'Fast'], 23 | value: 1, 24 | name: 'More expensive', 25 | }, 26 | { 27 | sets: ['Good', 'Cheap'], 28 | value: 1, 29 | name: 'Will take time to deliver', 30 | }, 31 | { 32 | sets: ['Fast', 'Cheap'], 33 | value: 1, 34 | name: 'Not the best quality', 35 | }, 36 | { 37 | sets: ['Fast', 'Cheap', 'Good'], 38 | value: 1, 39 | name: "They're dreaming", 40 | }, 41 | ], 42 | }, 43 | ], 44 | }, 45 | config: {}, 46 | libraryConfig: { 47 | chart: { 48 | type: 'venn', 49 | }, 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const path = require('path'); 3 | 4 | const {task, src, dest, series} = require('gulp'); 5 | const sass = require('gulp-dart-sass'); 6 | const replace = require('gulp-replace'); 7 | const ts = require('gulp-typescript'); 8 | const rimraf = require('rimraf'); 9 | 10 | const BUILD_DIR = path.resolve('build'); 11 | 12 | task('clean', (done) => { 13 | rimraf.sync(BUILD_DIR); 14 | rimraf.sync('styles/**/*.css'); 15 | done(); 16 | }); 17 | 18 | function compileTs() { 19 | const tsProject = ts.createProject('tsconfig.json', { 20 | declaration: true, 21 | module: 'esnext', 22 | }); 23 | 24 | return src(['src/**/*.{js,jsx,ts,tsx}']) 25 | .pipe(replace(/import '.+\.scss';/g, (match) => match.replace('.scss', '.css'))) 26 | .pipe(tsProject()) 27 | .pipe(dest(path.resolve(BUILD_DIR))); 28 | } 29 | 30 | task('compile-to-esm', () => { 31 | return compileTs(); 32 | }); 33 | 34 | task('copy-js-declarations', () => { 35 | return src(['src/**/*.d.ts']).pipe(dest(path.resolve(BUILD_DIR))); 36 | }); 37 | 38 | task('copy-i18n', () => { 39 | return src(['src/**/*.json']).pipe(dest(path.resolve(BUILD_DIR))); 40 | }); 41 | 42 | task('styles-components', () => { 43 | return src('src/**/*.scss') 44 | .pipe( 45 | sass({ 46 | includePaths: ['node_modules'], 47 | }), 48 | ) 49 | .pipe(sass().on('error', sass.logError)) 50 | .pipe(dest(path.resolve(BUILD_DIR))); 51 | }); 52 | 53 | task( 54 | 'build', 55 | series(['clean', 'compile-to-esm', 'copy-js-declarations', 'copy-i18n', 'styles-components']), 56 | ); 57 | 58 | task('default', series(['build'])); 59 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/tooltip/render-shape-icon/template-icons/ShortDotLineIcon.ts: -------------------------------------------------------------------------------- 1 | import type {CommonIconProps} from '../types'; 2 | import {getParsedRect} from '../utils'; 3 | 4 | export const ShortDotLineIcon = ({height, width}: CommonIconProps) => { 5 | const {parsedWidth, parsedHeight} = getParsedRect({width, height}); 6 | 7 | return ` 8 | 15 | 21 | 22 | `; 23 | }; 24 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/highcharts/utils/calculateClosestPointManually.test.ts: -------------------------------------------------------------------------------- 1 | import {calculateClosestPointManually} from './calcucalteClosestPointManually'; 2 | 3 | describe('calculateClosestPointManually', () => { 4 | it('Должна вернуть наименьшее расстояние между точками и если оно больше 0', () => { 5 | const MOCKED_SERIES = [ 6 | { 7 | processedXData: [1, 5, 12], 8 | }, 9 | { 10 | processedXData: [12, 65], 11 | }, 12 | { 13 | processedXData: [3, 140], 14 | }, 15 | ] as any[]; 16 | 17 | const MOCKED_CONTEXT = { 18 | series: MOCKED_SERIES, 19 | }; 20 | 21 | const expectedResult = 2; 22 | const result = calculateClosestPointManually.apply(MOCKED_CONTEXT); 23 | 24 | expect(result).toEqual(expectedResult); 25 | }); 26 | 27 | it('Должна вернуть undefined, если передан пустой массив', () => { 28 | const MOCKED_CONTEXT = {series: []}; 29 | const result = calculateClosestPointManually.apply(MOCKED_CONTEXT); 30 | 31 | expect(result).toBeUndefined(); 32 | }); 33 | 34 | it('Должна вернуть undefined, если наименьшее расстояние это 0', () => { 35 | const MOCKED_SERIES = [ 36 | { 37 | processedXData: [12], 38 | }, 39 | { 40 | processedXData: [12], 41 | }, 42 | ] as any[]; 43 | 44 | const MOCKED_CONTEXT = { 45 | series: MOCKED_SERIES, 46 | }; 47 | 48 | const result = calculateClosestPointManually.apply(MOCKED_CONTEXT); 49 | 50 | expect(result).toBeUndefined(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /.github/workflows/release-beta.yml: -------------------------------------------------------------------------------- 1 | # Build and publish -beta tag for @gravity-ui/chartkit 2 | # Runs manually in Actions tabs in github 3 | # Runs on any branch except main 4 | 5 | name: Release beta version 6 | 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | version: 11 | type: string 12 | required: false 13 | description: 'If your build failed and the version is already exists you can set version of package manually, e.g. 3.0.0-beta.0. Use the prefix `beta` otherwise you will get error.' 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - run: | 20 | if [ "${{ github.event.inputs.version }}" != "" ]; then 21 | if [[ "${{ github.event.inputs.version }}" != *"beta"* ]]; then 22 | echo "version set incorrectly! Check that is contains beta in it's name" 23 | exit 1 24 | fi 25 | fi 26 | - uses: actions/checkout@v2 27 | - uses: actions/setup-node@v1 28 | with: 29 | node-version: 18 30 | registry-url: 'https://registry.npmjs.org' 31 | - run: npm ci 32 | shell: bash 33 | - run: npm test 34 | shell: bash 35 | - name: Bump and commit version 36 | run: | 37 | echo ${{ github.event.inputs.version }} 38 | 39 | if [ "${{ github.event.inputs.version }}" == "" ]; then 40 | npm version prerelease --preid=beta --git-tag-version=false 41 | else 42 | npm version ${{ github.event.inputs.version }} --git-tag-version=false 43 | fi 44 | - name: Publish version 45 | run: npm publish --tag beta --access public 46 | env: 47 | NODE_AUTH_TOKEN: ${{ secrets.GRAVITY_UI_BOT_NPM_TOKEN }} 48 | shell: bash 49 | -------------------------------------------------------------------------------- /.storybook/decorators/DocsDecorator/DocsDecorator.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {DocsContainer, DocsContainerProps} from '@storybook/addon-docs'; 3 | import {ThemeProvider, MobileProvider, getThemeType} from '@gravity-ui/uikit'; 4 | import {themes} from '../../../.storybook/theme'; 5 | import {cn} from '../../../src/utils/cn'; 6 | 7 | import './DocsDecorator.scss'; 8 | 9 | export interface DocsDecoratorProps extends React.PropsWithChildren {} 10 | 11 | const b = cn('docs-decorator'); 12 | 13 | export function DocsDecorator({children, context}: DocsDecoratorProps) { 14 | const storyContext = context.getStoryContext(context.storyById(context.id)); 15 | const theme = storyContext.globals.theme; 16 | return ( 17 |
18 | {/* @ts-ignore */} 19 | { 23 | const story = context.storyById(id); 24 | return { 25 | ...story, 26 | parameters: { 27 | ...story?.parameters, 28 | docs: { 29 | ...story?.parameters?.docs, 30 | theme: themes[getThemeType(theme)], 31 | }, 32 | }, 33 | }; 34 | }, 35 | }} 36 | > 37 | 38 | {children} 39 | 40 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/plugins/highcharts/types/highcharts-extends.d.ts: -------------------------------------------------------------------------------- 1 | import 'highcharts'; 2 | 3 | import type {StringParams} from './misc'; 4 | 5 | declare module 'highcharts' { 6 | interface Axis { 7 | closestPointRange: number; 8 | } 9 | 10 | interface Chart { 11 | afterRedrawCallback: () => void; 12 | updateParams: (params: StringParams) => void; 13 | getParams: () => StringParams; 14 | plotSizeY: number; 15 | pointsForInitialRefresh: Highcharts.Point[] | Highcharts.Point; 16 | } 17 | 18 | interface Tooltip { 19 | fixed: boolean; 20 | splitTooltip: boolean; 21 | isHidden: boolean; 22 | lastVisibleRowIndex: number; 23 | preFixationHeight?: number; 24 | getTooltipContainer: Function; 25 | hideFixedTooltip: Function; 26 | yagrChart?: {height: number}; 27 | } 28 | 29 | interface PointOptionsObject extends Record { 30 | url?: string; 31 | params?: Array; 32 | } 33 | 34 | interface Options extends Record { 35 | url?: string; 36 | } 37 | 38 | interface SeriesOptionsRegistry extends Record> {} 39 | 40 | // for Stock chart from https://github.com/highcharts/highcharts/blob/master/ts/Stock/Navigator/NavigatorComposition.ts#L65 41 | interface Series { 42 | // https://github.com/highcharts/highcharts/blob/master/ts/Core/Series/Series.ts#L1023 43 | getPointsCollection: () => Point[]; 44 | xData: number[]; 45 | baseSeries?: Series; 46 | navigatorSeries?: Series; 47 | } 48 | 49 | interface SeriesClickEventObject { 50 | // https://github.com/highcharts/highcharts/blob/818eb62b9d1a0efc3c9ec705e95b13849e2040fa/ts/Core/Series/Series.ts#L5039 51 | metaKey?: boolean; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/SplitPane/StyledSplitPane.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {cn} from '../../utils/cn'; 4 | 5 | import {Pane} from './Pane'; 6 | import {SplitPane} from './SplitPane'; 7 | import type {SplitPaneProps} from './SplitPane'; 8 | 9 | import './StyledSplitPane.scss'; 10 | 11 | const b = cn('styled-split-pane'); 12 | const resizerClassName = b('pane-resizer'); 13 | 14 | type Props = SplitPaneProps & { 15 | paneOneRender: () => React.ReactNode; 16 | paneTwoRender: () => React.ReactNode; 17 | }; 18 | 19 | export const StyledSplitPane = ({paneOneRender, paneTwoRender, ...splitPaneProps}: Props) => { 20 | const splitPaneRef = React.useRef(null); 21 | 22 | React.useEffect(() => { 23 | const resizer = 24 | splitPaneRef.current?.splitPane?.getElementsByClassName(resizerClassName)[0]; 25 | const hoveredClassName = `${resizerClassName}_hovered`; 26 | 27 | const onTouchStart = () => { 28 | resizer?.classList.add(hoveredClassName); 29 | }; 30 | 31 | const onTouchEnd = () => { 32 | resizer?.classList.remove(hoveredClassName); 33 | }; 34 | 35 | resizer?.addEventListener('touchstart', onTouchStart); 36 | resizer?.addEventListener('touchend', onTouchEnd); 37 | 38 | return function cleanup() { 39 | resizer?.removeEventListener('touchstart', onTouchStart); 40 | resizer?.removeEventListener('touchend', onTouchEnd); 41 | }; 42 | }, []); 43 | 44 | return ( 45 | 51 | {paneOneRender()} 52 | {paneTwoRender()} 53 | 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /src/plugins/gravity-charts/renderer/__stories__/SplitTooltip.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import type {StoryObj} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | 7 | import {StoryWrapper} from './StoryWrapper'; 8 | 9 | function getPieSegmentData(name: string, color: string, index: number) { 10 | return { 11 | name, 12 | value: index * 10, 13 | label: name, 14 | color: color, 15 | }; 16 | } 17 | 18 | export const SplitTooltipBasic: StoryObj = { 19 | name: 'Basic', 20 | render: () => { 21 | return ( 22 | 23 | 46 | 47 | ); 48 | }, 49 | }; 50 | 51 | export default { 52 | title: 'Plugins/Gravity Chart/Split tooltip', 53 | }; 54 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/components/ChartStory.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Button} from '@gravity-ui/uikit'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | import {settings} from '../../../../libs'; 7 | import {ChartKitRef, RenderError} from '../../../../types'; 8 | import {HighchartsPlugin} from '../../index'; 9 | import {HighchartsWidgetData} from '../../types'; 10 | 11 | const DEFAULT_STORY_HEIGHT = '300px'; 12 | const DEFAULT_STORY_WIDTH = '100%'; 13 | 14 | export type ChartStoryProps = { 15 | data: HighchartsWidgetData; 16 | 17 | withoutPlugin?: boolean; 18 | visible?: boolean; 19 | splitTooltip?: boolean; 20 | height?: string; 21 | width?: string; 22 | renderError?: RenderError; 23 | }; 24 | export const ChartStory: React.FC = (props: ChartStoryProps) => { 25 | const {height, width, data} = props; 26 | 27 | const initRef = React.useRef(false); 28 | const [visible, setVisible] = React.useState(Boolean(props.visible)); 29 | const chartKitRef = React.useRef(); 30 | 31 | if (!initRef.current) { 32 | if (!props.withoutPlugin) { 33 | settings.set({plugins: [HighchartsPlugin]}); 34 | } 35 | initRef.current = true; 36 | } 37 | 38 | if (!visible) { 39 | return ; 40 | } 41 | 42 | return ( 43 |
49 | 56 |
57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/setNavigatorDefaultPeriod.test.ts: -------------------------------------------------------------------------------- 1 | import type {Highcharts} from '../../../../types'; 2 | import type {NavigatorPeriod} from '../types'; 3 | 4 | import {getDefaultPeriodInMS} from './setNavigatorDefaultPeriod'; 5 | 6 | const date1 = new Date('2021-01-01'); 7 | const date2 = new Date('2021-01-02'); 8 | const date3 = new Date('2021-01-03'); 9 | 10 | const MOCKED_SERIES = [ 11 | { 12 | data: [{x: date1.valueOf()}, {x: date3.valueOf()}], 13 | }, 14 | { 15 | data: [{x: date2.valueOf()}], 16 | }, 17 | ] as Highcharts.Series[]; 18 | 19 | const DAY_MIN_RANGE = 60 * 60 * 1000 * 24; 20 | const HOUR_MIN_RANGE = 60 * 60 * 1000; 21 | 22 | describe('plugins/highcharts/config/getDefaultPeriodInMS', () => { 23 | let settings: NavigatorPeriod; 24 | beforeEach(() => { 25 | settings = { 26 | type: 'date', 27 | value: '2', 28 | period: 'day', 29 | }; 30 | }); 31 | it('should return range & minRange for date in ms', () => { 32 | const result = getDefaultPeriodInMS(settings, MOCKED_SERIES); 33 | 34 | const expectedResult = { 35 | minRange: DAY_MIN_RANGE, 36 | range: date3.valueOf() - date1.valueOf(), 37 | }; 38 | 39 | expect(result).toEqual(expectedResult); 40 | }); 41 | 42 | it(`should set {minRange: ${HOUR_MIN_RANGE}} in case of settings.type !== 'date'`, () => { 43 | settings.type = 'datetime'; 44 | const result = getDefaultPeriodInMS(settings, MOCKED_SERIES); 45 | 46 | const expectedResult = { 47 | minRange: HOUR_MIN_RANGE, 48 | range: date3.valueOf() - date1.valueOf(), 49 | }; 50 | 51 | expect(result).toEqual(expectedResult); 52 | }); 53 | 54 | it('should return null in case of empty series', () => { 55 | const result = getDefaultPeriodInMS(settings, []); 56 | 57 | expect(result).toBeNull(); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Notice to external contributors 2 | 3 | ## General info 4 | 5 | Hello! In order for us (YANDEX LLC) to accept patches and other contributions from you, you will have to adopt our Yandex Contributor License Agreement (the “**CLA**”). The current version of the CLA can be found here: 6 | 7 | 1. https://yandex.ru/legal/cla/?lang=en (in English) and 8 | 2. https://yandex.ru/legal/cla/?lang=ru (in Russian). 9 | 10 | By adopting the CLA, you state the following: 11 | 12 | - You obviously wish and are willingly licensing your contributions to us for our open source projects under the terms of the CLA, 13 | - You have read the terms and conditions of the CLA and agree with them in full, 14 | - You are legally able to provide and license your contributions as stated, 15 | - We may use your contributions for our open source projects and for any other our project too, 16 | - We rely on your assurances concerning the rights of third parties in relation to your contributions. 17 | 18 | If you agree with these principles, please read and adopt our CLA. By providing us your contributions, you hereby declare that you have already read and adopt our CLA, and we may freely merge your contributions with our corresponding open source project and use it in further in accordance with terms and conditions of the CLA. 19 | 20 | ## Provide contributions 21 | 22 | If you have already adopted terms and conditions of the CLA, you are able to provide your contributions. When you submit your pull request, please add the following information into it: 23 | 24 | ``` 25 | I hereby agree to the terms of the CLA available at: [link]. 26 | ``` 27 | 28 | Replace the bracketed text as follows: 29 | 30 | - [link] is the link to the current version of the CLA: https://yandex.ru/legal/cla/?lang=en (in English) or https://yandex.ru/legal/cla/?lang=ru (in Russian). 31 | 32 | It is enough to provide us such notification once. 33 | 34 | ## Other questions 35 | 36 | If you have any questions, please mail us at opensource@yandex-team.ru. 37 | -------------------------------------------------------------------------------- /src/libs/settings/settings.ts: -------------------------------------------------------------------------------- 1 | import {configure} from '@gravity-ui/uikit'; 2 | import get from 'lodash/get'; 3 | import mergeWith from 'lodash/mergeWith'; 4 | 5 | import {i18nFactory} from '../../i18n'; 6 | import type {ChartKitHolidays, ChartKitLang, ChartKitPlugin} from '../../types'; 7 | 8 | import {EventEmitter} from './eventEmitter'; 9 | import {mergeSettingStrategy} from './mergeSettingStrategy'; 10 | 11 | interface Settings { 12 | plugins: ChartKitPlugin[]; 13 | lang: ChartKitLang; 14 | extra?: { 15 | holidays?: ChartKitHolidays; 16 | }; 17 | } 18 | 19 | type SettingKey = keyof Settings; 20 | type SettingsEventsMap = { 21 | 'change-lang': ChartKitLang; 22 | }; 23 | 24 | export const settingsEventEmitter = new EventEmitter(); 25 | 26 | const removeUndefinedValues = >(data: T) => { 27 | return Object.entries(data).reduce((acc, [key, value]) => { 28 | if (typeof value !== 'undefined') { 29 | acc[key as keyof T] = value; 30 | } 31 | 32 | return acc; 33 | }, {} as T); 34 | }; 35 | 36 | const updateLang = (lang: ChartKitLang) => { 37 | configure({lang}); 38 | i18nFactory.setLang(lang); 39 | }; 40 | 41 | class ChartKitSettings { 42 | private settings: Settings = { 43 | plugins: [], 44 | lang: 'en', 45 | }; 46 | 47 | constructor() { 48 | updateLang(this.get('lang')); 49 | } 50 | 51 | get(key: T) { 52 | return get(this.settings, key); 53 | } 54 | 55 | set(updates: Partial) { 56 | const filteredUpdates = removeUndefinedValues(updates); 57 | 58 | this.settings = mergeWith(this.settings, filteredUpdates, mergeSettingStrategy); 59 | 60 | if (filteredUpdates.lang) { 61 | const lang = filteredUpdates.lang || this.get('lang'); 62 | updateLang(lang); 63 | settingsEventEmitter.dispatch('change-lang', lang); 64 | } 65 | } 66 | } 67 | 68 | export const settings = new ChartKitSettings(); 69 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/addShowInNavigatorToSeries.ts: -------------------------------------------------------------------------------- 1 | import {NavigatorLinesMode} from '../../constants'; 2 | 3 | import {getXAxisThresholdValue} from './getXAxisThresholdValue'; 4 | 5 | type AddShowInNavigatorToSeriesArgs = { 6 | linesMode: NavigatorLinesMode; 7 | graphs: Record[]; 8 | baseSeriesName: string; 9 | params: Record; 10 | selectedLines: string[]; 11 | }; 12 | 13 | export const addShowInNavigatorToSeries = ({ 14 | linesMode, 15 | graphs, 16 | baseSeriesName, 17 | params, 18 | selectedLines, 19 | }: AddShowInNavigatorToSeriesArgs) => { 20 | if (linesMode === NavigatorLinesMode.All) { 21 | graphs.forEach((item) => { 22 | item.showInNavigator = true; 23 | }); 24 | } else { 25 | const mergedLines = [...selectedLines]; 26 | 27 | if (baseSeriesName) { 28 | mergedLines.push(baseSeriesName); 29 | } 30 | 31 | if (mergedLines.length) { 32 | graphs.forEach((item) => { 33 | const itemName = item.sname || item.name || item.title; 34 | if (typeof item.showInNavigator === 'undefined') { 35 | item.showInNavigator = mergedLines.includes(itemName); 36 | } 37 | }); 38 | } else { 39 | graphs.forEach((item) => { 40 | item.showInNavigator = false; 41 | }); 42 | 43 | const xMinValue = getXAxisThresholdValue(graphs, 'min'); 44 | const xMaxValue = getXAxisThresholdValue(graphs, 'max'); 45 | 46 | const navigatorParams = {...params.navigator} || {}; 47 | 48 | if (navigatorParams.xAxis) { 49 | navigatorParams.xAxis.min = xMinValue; 50 | navigatorParams.xAxis.max = xMaxValue; 51 | } else { 52 | navigatorParams.xAxis = { 53 | min: xMinValue, 54 | max: xMaxValue, 55 | }; 56 | } 57 | 58 | params.navigator = navigatorParams; 59 | } 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /src/components/SplitPane/Resizer.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2015 tomkp 2 | // Copyright 2022 YANDEX LLC 3 | 4 | import React from 'react'; 5 | 6 | import type {SplitLayoutType} from './types'; 7 | 8 | export const RESIZER_DEFAULT_CLASSNAME = 'Resizer'; 9 | 10 | type Props = { 11 | onMouseDown: React.MouseEventHandler; 12 | onTouchStart: React.TouchEventHandler; 13 | onTouchEnd: React.TouchEventHandler; 14 | className?: string; 15 | split?: SplitLayoutType; 16 | style?: React.CSSProperties; 17 | resizerClassName?: string; 18 | onClick?: React.MouseEventHandler; 19 | onDoubleClick?: React.MouseEventHandler; 20 | }; 21 | 22 | export class Resizer extends React.Component { 23 | render() { 24 | const { 25 | className, 26 | onClick, 27 | onDoubleClick, 28 | onMouseDown, 29 | onTouchEnd, 30 | onTouchStart, 31 | resizerClassName = RESIZER_DEFAULT_CLASSNAME, 32 | split, 33 | style, 34 | } = this.props; 35 | const classes = [resizerClassName, split, className]; 36 | 37 | return ( 38 | onMouseDown(event)} 43 | onTouchStart={(event) => { 44 | onTouchStart(event); 45 | }} 46 | onTouchEnd={(event) => { 47 | event.preventDefault(); 48 | onTouchEnd(event); 49 | }} 50 | onClick={(event) => { 51 | if (onClick) { 52 | event.preventDefault(); 53 | onClick(event); 54 | } 55 | }} 56 | onDoubleClick={(event) => { 57 | if (onDoubleClick) { 58 | event.preventDefault(); 59 | onDoubleClick(event); 60 | } 61 | }} 62 | /> 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.storybook/decorators/DocsDecorator/DocsDecorator.scss: -------------------------------------------------------------------------------- 1 | @use '@gravity-ui/uikit/styles/mixins'; 2 | 3 | $offset-xs: 4px; 4 | $offset-s: 8px; 5 | $offset-m: 16px; 6 | $offset-l: 40px; 7 | 8 | $root: '.docs-decorator'; 9 | 10 | #{$root}#{$root}#{$root}#{$root}#{$root} { 11 | .sbdocs-p, 12 | .sbdocs-li, 13 | .sbdocs-a { 14 | @include mixins.text-body-2; 15 | } 16 | 17 | .sbdocs-wrapper { 18 | padding: 0 $offset-l; 19 | } 20 | 21 | .sbdocs-content { 22 | max-width: 800px; 23 | } 24 | 25 | .sbdocs-p, 26 | .sbdocs-ul, 27 | .sbdocs-ol { 28 | margin: $offset-xs 0; 29 | 30 | & + .sbdocs-p, 31 | & + .sbdocs-ul, 32 | & + .sbdocs-ol { 33 | margin-top: $offset-s; 34 | } 35 | } 36 | 37 | .sbdocs-li + .sbdocs-li { 38 | margin-top: $offset-xs; 39 | } 40 | 41 | .sbdocs-ul, 42 | .sbdocs-ol { 43 | padding-left: $offset-m; 44 | } 45 | 46 | .sbdocs-h1 { 47 | @include mixins.text-display-3; 48 | } 49 | 50 | .sbdocs-h2 { 51 | @include mixins.text-display-1; 52 | border: 0; 53 | padding: 0; 54 | } 55 | 56 | .sbdocs-h3 { 57 | @include mixins.text-header-2; 58 | } 59 | 60 | .sbdocs-h1, 61 | .sbdocs-h2, 62 | .sbdocs-h3 { 63 | margin-top: $offset-l; 64 | margin-bottom: $offset-m; 65 | } 66 | 67 | .sbdocs-a { 68 | text-decoration: none; 69 | touch-action: manipulation; 70 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 71 | cursor: pointer; 72 | 73 | color: var(--yc-color-text-link); 74 | 75 | &:hover { 76 | color: var(--yc-color-text-link-hover); 77 | } 78 | } 79 | 80 | .sbdocs-p code { 81 | @include mixins.text-code-inline-2; 82 | line-height: 1; 83 | padding: 1px 4px; 84 | background: var(--yc-color-base-misc); 85 | color: var(--yc-color-text-misc); 86 | } 87 | 88 | .docblock-source { 89 | margin: $offset-m 0; 90 | } 91 | 92 | .docs-example { 93 | margin: $offset-m 0; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @gravity-ui/chartkit · [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) [![npm package](https://img.shields.io/npm/v/@gravity-ui/chartkit)](https://www.npmjs.com/package/@gravity-ui/chartkit) [![storybook](https://img.shields.io/badge/Storybook-deployed-ff4685)](https://preview.gravity-ui.com/chartkit/) 2 | 3 | React component used to render charts based on any sources you need 4 | 5 | ## Install 6 | 7 | ```shell 8 | npm i --save-dev @gravity-ui/chartkit @gravity-ui/uikit 9 | ``` 10 | 11 | Make sure you have `@gravity-ui/uikit` styles enabled in your project. 12 | 13 | ```typescript 14 | import '@gravity-ui/uikit/styles/styles.scss'; 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```typescript 20 | import {ThemeProvider} from '@gravity-ui/uikit'; 21 | import ChartKit, {settings} from '@gravity-ui/chartkit'; 22 | import {YagrPlugin} from '@gravity-ui/chartkit/yagr'; 23 | import type {YagrWidgetData} from '@gravity-ui/chartkit/yagr'; 24 | 25 | import '@gravity-ui/uikit/styles/styles.scss'; 26 | 27 | settings.set({plugins: [YagrPlugin]}); 28 | 29 | const data: YagrWidgetData = { 30 | data: { 31 | timeline: [ 32 | 1636838612441, 1636925012441, 1637011412441, 1637097812441, 1637184212441, 1637270612441, 33 | 1637357012441, 1637443412441, 1637529812441, 1637616212441, 34 | ], 35 | graphs: [ 36 | { 37 | id: '0', 38 | name: 'Serie 1', 39 | color: '#6c59c2', 40 | data: [25, 52, 89, 72, 39, 49, 82, 59, 36, 5], 41 | }, 42 | { 43 | id: '1', 44 | name: 'Serie 2', 45 | color: '#6e8188', 46 | data: [37, 6, 51, 10, 65, 35, 72, 0, 94, 54], 47 | }, 48 | ], 49 | }, 50 | libraryConfig: { 51 | chart: { 52 | series: { 53 | type: 'line', 54 | }, 55 | }, 56 | title: { 57 | text: 'line: random 10 pts', 58 | }, 59 | }, 60 | }; 61 | 62 | function App() { 63 | return ( 64 | 65 |
66 | 67 |
68 |
69 | ); 70 | } 71 | 72 | export default App; 73 | ``` 74 | -------------------------------------------------------------------------------- /README-ru.md: -------------------------------------------------------------------------------- 1 | # @gravity-ui/chartkit · [![license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) [![npm package](https://img.shields.io/npm/v/@gravity-ui/chartkit)](https://www.npmjs.com/package/@gravity-ui/chartkit) [![storybook](https://img.shields.io/badge/Storybook-deployed-ff4685)](https://preview.gravity-ui.com/chartkit/) 2 | 3 | React-компонент для рендеринга графиков на основе любых доступных источников данных. 4 | 5 | ## Установка 6 | 7 | ```shell 8 | npm i --save-dev @gravity-ui/chartkit @gravity-ui/uikit 9 | ``` 10 | 11 | В проекте должны быть активированы стили `@gravity-ui/uikit`. 12 | 13 | ```typescript 14 | import '@gravity-ui/uikit/styles/styles.scss'; 15 | ``` 16 | 17 | ## Использование 18 | 19 | ```typescript 20 | import {ThemeProvider} from '@gravity-ui/uikit'; 21 | import ChartKit, {settings} from '@gravity-ui/chartkit'; 22 | import {YagrPlugin} from '@gravity-ui/chartkit/yagr'; 23 | import type {YagrWidgetData} from '@gravity-ui/chartkit/yagr'; 24 | 25 | import '@gravity-ui/uikit/styles/styles.scss'; 26 | 27 | settings.set({plugins: [YagrPlugin]}); 28 | 29 | const data: YagrWidgetData = { 30 | data: { 31 | timeline: [ 32 | 1636838612441, 1636925012441, 1637011412441, 1637097812441, 1637184212441, 1637270612441, 33 | 1637357012441, 1637443412441, 1637529812441, 1637616212441, 34 | ], 35 | graphs: [ 36 | { 37 | id: '0', 38 | name: 'Serie 1', 39 | color: '#6c59c2', 40 | data: [25, 52, 89, 72, 39, 49, 82, 59, 36, 5], 41 | }, 42 | { 43 | id: '1', 44 | name: 'Serie 2', 45 | color: '#6e8188', 46 | data: [37, 6, 51, 10, 65, 35, 72, 0, 94, 54], 47 | }, 48 | ], 49 | }, 50 | libraryConfig: { 51 | chart: { 52 | series: { 53 | type: 'line', 54 | }, 55 | }, 56 | title: { 57 | text: 'line: random 10 pts', 58 | }, 59 | }, 60 | }; 61 | 62 | function App() { 63 | return ( 64 | 65 |
66 | 67 |
68 |
69 | ); 70 | } 71 | 72 | export default App; 73 | ``` 74 | -------------------------------------------------------------------------------- /src/components/ErrorBoundary/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import type {ChartKitError} from '../../libs'; 4 | import {CHARTKIT_ERROR_CODE} from '../../libs'; 5 | import type {ChartKitOnError, ChartKitType, ChartKitWidget, RenderError} from '../../types'; 6 | import {getErrorMessage} from '../../utils/getErrorMessage'; 7 | 8 | type Props = { 9 | onError?: ChartKitOnError; 10 | data: ChartKitWidget[ChartKitType]['data']; 11 | renderError?: RenderError; 12 | }; 13 | 14 | type State = { 15 | error?: ChartKitError | Error; 16 | }; 17 | 18 | export class ErrorBoundary extends React.Component { 19 | static getDerivedStateFromError(error: Error) { 20 | return {error}; 21 | } 22 | 23 | state: State = { 24 | error: undefined, 25 | }; 26 | 27 | componentDidCatch() { 28 | const {error} = this.state; 29 | 30 | if (error) { 31 | this.props.onError?.({error}); 32 | } 33 | } 34 | 35 | componentDidUpdate(prevProps: Readonly) { 36 | if (prevProps.data !== this.props.data) { 37 | const {error} = this.state; 38 | if ( 39 | error && 40 | 'code' in error && 41 | [CHARTKIT_ERROR_CODE.NO_DATA, CHARTKIT_ERROR_CODE.INVALID_DATA].includes( 42 | String(error.code), 43 | ) 44 | ) { 45 | this.resetError(); 46 | } 47 | } 48 | } 49 | 50 | render() { 51 | const {error} = this.state; 52 | 53 | if (error) { 54 | const message = getErrorMessage(error); 55 | 56 | if (this.props.renderError) { 57 | return this.props.renderError({ 58 | error, 59 | message, 60 | resetError: this.resetError, 61 | }); 62 | } 63 | 64 | return
{message}
; 65 | } 66 | 67 | return this.props.children; 68 | } 69 | 70 | resetError = () => { 71 | if (this.state.error) { 72 | this.setState({error: undefined}); 73 | } 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/tooltip.test.ts: -------------------------------------------------------------------------------- 1 | import {HighchartsType} from '../../constants'; 2 | 3 | import {checkTooltipPinningAvailability, isTooltipShared} from './tooltip'; 4 | 5 | const chartTypes: [HighchartsType, boolean][] = [ 6 | [HighchartsType.Sankey, false], 7 | [HighchartsType.Xrange, false], 8 | [HighchartsType.Line, true], 9 | [HighchartsType.Area, true], 10 | [HighchartsType.Arearange, true], 11 | [HighchartsType.Bar, true], 12 | [HighchartsType.Column, true], 13 | [HighchartsType.Columnrange, true], 14 | [HighchartsType.Funnel, true], 15 | [HighchartsType.Pie, true], 16 | [HighchartsType.Map, true], 17 | [HighchartsType.Scatter, true], 18 | [HighchartsType.Bubble, true], 19 | [HighchartsType.Heatmap, true], 20 | [HighchartsType.Treemap, true], 21 | [HighchartsType.Networkgraph, true], 22 | [HighchartsType.Variwide, true], 23 | [HighchartsType.Waterfall, true], 24 | [HighchartsType.Streamgraph, true], 25 | [HighchartsType.Wordcloud, true], 26 | [HighchartsType.Boxplot, true], 27 | [HighchartsType.Timeline, true], 28 | ]; 29 | 30 | describe('plugins/highcharts/config', () => { 31 | test.each(chartTypes)(`calculatePrecision for %s return %s`, (chartType, expected) => { 32 | expect(isTooltipShared(chartType)).toBe(expected); 33 | }); 34 | 35 | test.each([ 36 | [undefined, true], 37 | [{tooltip: {pin: {altKey: true}}, altKey: true}, true], 38 | [{tooltip: {pin: {metaKey: true}}, metaKey: true}, true], 39 | [{tooltip: {pin: {altKey: true, metaKey: true}}, altKey: true, metaKey: true}, true], 40 | [{tooltip: {pin: {enabled: false}}}, false], 41 | [{tooltip: {pin: {altKey: true}}, altKey: false}, false], 42 | [{tooltip: {pin: {metaKey: true}}, metaKey: false}, false], 43 | [{tooltip: {pin: {altKey: true, metaKey: true}}, altKey: false, metaKey: true}, false], 44 | [{tooltip: {pin: {altKey: true, metaKey: true}}, altKey: true, metaKey: false}, false], 45 | ])(`checkTooltipPinningAvailability (args: %j)`, (args, expected) => { 46 | const result = checkTooltipPinningAvailability(args); 47 | expect(result).toBe(expected); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/plugins/highcharts/mocks/column-ver.ts: -------------------------------------------------------------------------------- 1 | import {HighchartsWidgetData} from '../types'; 2 | 3 | export const data: HighchartsWidgetData = { 4 | data: { 5 | graphs: [ 6 | { 7 | data: [ 8 | { 9 | y: 50.55, 10 | color: 'rgb(255, 61, 9)', 11 | }, 12 | { 13 | y: 80.45, 14 | color: 'rgb(255, 65, 9)', 15 | }, 16 | { 17 | y: 100.34, 18 | color: 'rgb(255, 83, 9)', 19 | }, 20 | ], 21 | name: 'Profit', 22 | }, 23 | { 24 | data: [ 25 | { 26 | y: 350.65, 27 | color: 'rgb(208, 189, 48)', 28 | }, 29 | { 30 | y: 119.82, 31 | color: 'rgb(255, 95, 88)', 32 | }, 33 | { 34 | y: 452.15, 35 | color: 'rgb(84, 165, 32)', 36 | }, 37 | ], 38 | name: 'Sales', 39 | }, 40 | ], 41 | categories: ['Furniture', 'Office Supplies', 'Technology'], 42 | }, 43 | config: { 44 | enableSum: true, 45 | precision: 2, 46 | }, 47 | libraryConfig: { 48 | chart: { 49 | type: 'column', 50 | }, 51 | legend: { 52 | title: { 53 | text: 'Measure Values', 54 | }, 55 | enabled: true, 56 | }, 57 | colorAxis: { 58 | startOnTick: false, 59 | endOnTick: false, 60 | min: 50.55, 61 | max: 452.72057380654326, 62 | stops: [ 63 | [0, 'rgb(255, 61, 100)'], 64 | [0.5, 'rgb(255, 198, 54)'], 65 | [1, 'rgb(84, 165, 32)'], 66 | ], 67 | }, 68 | plotOptions: { 69 | column: { 70 | maxPointWidth: 50, 71 | }, 72 | }, 73 | enableSum: true, 74 | }, 75 | }; 76 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/scatter/PerformanceIssue.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Button} from '@gravity-ui/uikit'; 4 | import {action} from '@storybook/addon-actions'; 5 | import {Meta, Story} from '@storybook/react'; 6 | import {randomNormal} from 'd3'; 7 | 8 | import {HighchartsPlugin, HighchartsWidgetData} from '../..'; 9 | import {ChartKit} from '../../../../components/ChartKit'; 10 | import {settings} from '../../../../libs'; 11 | import type {ChartKitRef} from '../../../../types'; 12 | 13 | const Template: Story = () => { 14 | const [shown, setShown] = React.useState(false); 15 | const chartkitRef = React.useRef(); 16 | 17 | const widgetData = React.useMemo(() => { 18 | const categories = Array.from({length: 5000}).map((_, i) => String(i)); 19 | const randomFn = randomNormal(0, 10); 20 | 21 | return { 22 | data: { 23 | graphs: [ 24 | { 25 | type: 'scatter', 26 | name: 'Series 1', 27 | data: categories.map((_, i) => ({ 28 | x: i, 29 | y: randomFn(), 30 | })), 31 | }, 32 | ], 33 | categories: categories, 34 | }, 35 | libraryConfig: { 36 | chart: { 37 | type: 'scatter', 38 | }, 39 | }, 40 | } as unknown as HighchartsWidgetData; 41 | }, []); 42 | 43 | if (!shown) { 44 | settings.set({plugins: [HighchartsPlugin]}); 45 | return ; 46 | } 47 | 48 | return ( 49 |
50 | 57 |
58 | ); 59 | }; 60 | 61 | export const PerformanceIssue = Template.bind({}); 62 | 63 | const meta: Meta = { 64 | title: 'Plugins/Highcharts/Scatter', 65 | }; 66 | 67 | export default meta; 68 | -------------------------------------------------------------------------------- /src/plugins/indicator/__stories__/Indicator.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Button} from '@gravity-ui/uikit'; 4 | import {action} from '@storybook/addon-actions'; 5 | import {boolean, color as colorKnob, radios, text, withKnobs} from '@storybook/addon-knobs'; 6 | import {Meta, Story} from '@storybook/react'; 7 | import cloneDeep from 'lodash/cloneDeep'; 8 | 9 | import {IndicatorPlugin} from '../'; 10 | import {ChartKit} from '../../../components/ChartKit'; 11 | import {settings} from '../../../libs'; 12 | import type {ChartKitRef} from '../../../types'; 13 | import type {IndicatorWidgetData, IndicatorWidgetDataItem} from '../types'; 14 | 15 | const data: IndicatorWidgetData = { 16 | data: [ 17 | { 18 | content: { 19 | current: { 20 | value: 1539577973, 21 | }, 22 | }, 23 | }, 24 | ], 25 | }; 26 | 27 | const Template: Story = () => { 28 | const [shown, setShown] = React.useState(false); 29 | const chartkitRef = React.useRef(); 30 | const color = colorKnob('color', '#4da2f1'); 31 | const size = radios( 32 | 'size', 33 | {s: 's', m: 'm', l: 'l', xl: 'xl'}, 34 | 'm', 35 | ); 36 | const title = text('title', 'Value title'); 37 | const nowrap = boolean('nowrap', false); 38 | const resultData = cloneDeep(data); 39 | 40 | if (resultData.data) { 41 | resultData.data[0].size = size; 42 | resultData.data[0].color = color; 43 | resultData.data[0].title = title; 44 | resultData.data[0].nowrap = nowrap; 45 | } 46 | 47 | if (!shown) { 48 | settings.set({plugins: [IndicatorPlugin]}); 49 | return ; 50 | } 51 | 52 | return ( 53 |
54 | 61 |
62 | ); 63 | }; 64 | 65 | export const Showcase = Template.bind({}); 66 | 67 | const meta: Meta = { 68 | title: 'Plugins/Indicator', 69 | decorators: [withKnobs], 70 | }; 71 | 72 | export default meta; 73 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/combined/AreaLine.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Meta, Story} from '@storybook/react'; 4 | 5 | import {ChartKit} from '../../../../components/ChartKit'; 6 | import type {HighchartsWidgetData} from '../../types'; 7 | import {ChartStory} from '../components/ChartStory'; 8 | 9 | export default { 10 | title: 'Plugins/Highcharts/Combined Charts', 11 | component: ChartKit, 12 | } as Meta; 13 | 14 | const data = { 15 | data: { 16 | graphs: [ 17 | { 18 | data: [29.9, 40, 30.4, 50, 60, 176.0, 135.6, 148.5, 216.4, 194.1, 95.6, 54.4], 19 | }, 20 | { 21 | type: 'area', 22 | data: [ 23 | 144.0, 176.0, 135.6, 148.5, 216.4, 194.1, 95.6, 54.4, 29.9, 71.5, 106.4, 129.2, 24 | ], 25 | yAxis: 1, 26 | }, 27 | ], 28 | }, 29 | config: { 30 | hideHolidaysBands: true, 31 | }, 32 | libraryConfig: { 33 | chart: { 34 | marginRight: 80, 35 | zoomType: 'xy', 36 | }, 37 | xAxis: { 38 | categories: [ 39 | 'Jan', 40 | 'Feb', 41 | 'Mar', 42 | 'Apr', 43 | 'May', 44 | 'Jun', 45 | 'Jul', 46 | 'Aug', 47 | 'Sep', 48 | 'Oct', 49 | 'Nov', 50 | 'Dec', 51 | ], 52 | endOnTick: false, 53 | startOnTick: false, 54 | }, 55 | yAxis: [ 56 | { 57 | lineWidth: 1, 58 | title: { 59 | text: 'Primary Axis', 60 | }, 61 | labels: { 62 | enabled: true, 63 | }, 64 | }, 65 | { 66 | lineWidth: 1, 67 | opposite: true, 68 | title: { 69 | text: 'Secondary Axis', 70 | }, 71 | }, 72 | ], 73 | tooltip: { 74 | shared: true, 75 | valueDecimals: 0, 76 | }, 77 | }, 78 | } as HighchartsWidgetData; 79 | 80 | const Template: Story = () => { 81 | return ; 82 | }; 83 | 84 | export const AreaLine = Template.bind({}); 85 | -------------------------------------------------------------------------------- /src/plugins/highcharts/mocks/area-range.ts: -------------------------------------------------------------------------------- 1 | import type {HighchartsWidgetData} from '../types'; 2 | 3 | export const data: HighchartsWidgetData = { 4 | data: { 5 | graphs: [ 6 | { 7 | name: 'Temperatures', 8 | data: [ 9 | [1246406400000, 10.4, 17], 10 | [1246492800000, 10.3, 28.6], 11 | [1246579200000, 14.8, 18.4], 12 | [1246665600000, 11.5, 25.8], 13 | [1246752000000, 11.1, 24.4], 14 | [1246838400000, 17.7, 19.6], 15 | [1246924800000, 15.1, 18.1], 16 | [1247011200000, 15.1, 27.2], 17 | [1247097600000, 17, 17.5], 18 | [1247184000000, 12.6, 18.5], 19 | [1247270400000, 12.2, 26], 20 | [1247356800000, 15.9, 22.9], 21 | [1247443200000, 17.1, 18.1], 22 | [1247529600000, 13.3, 24.2], 23 | [1247616000000, 17, 28.1], 24 | [1247702400000, 16.2, 22.6], 25 | [1247788800000, 10.6, 19], 26 | [1247875200000, 11.3, 19.7], 27 | [1247961600000, 14.1, 24.6], 28 | [1248048000000, 14.2, 22.5], 29 | [1248134400000, 14.1, 28.5], 30 | [1248220800000, 14, 27], 31 | [1248307200000, 10.2, 20.6], 32 | [1248393600000, 13.1, 29.9], 33 | [1248480000000, 13.7, 21.1], 34 | [1248566400000, 15, 28.6], 35 | [1248652800000, 12, 17.5], 36 | [1248739200000, 17.8, 24.4], 37 | [1248825600000, 11.7, 25.9], 38 | [1248912000000, 13.6, 25.6], 39 | [1248998400000, 17.3, 22.2], 40 | ], 41 | }, 42 | ], 43 | }, 44 | config: { 45 | hideHolidays: false, 46 | normalizeDiv: false, 47 | normalizeSub: false, 48 | }, 49 | libraryConfig: { 50 | chart: { 51 | type: 'arearange', 52 | }, 53 | title: { 54 | text: 'Temperature variation by day', 55 | }, 56 | xAxis: { 57 | type: 'datetime', 58 | }, 59 | tooltip: { 60 | valueSuffix: '°C', 61 | }, 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/components/useElementSize.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import debounce from 'lodash/debounce'; 4 | import round from 'lodash/round'; 5 | 6 | const RESIZE_DEBOUNCE = 200; 7 | const ROUND_PRESICION = 2; 8 | 9 | export interface UseElementSizeResult { 10 | width: number; 11 | height: number; 12 | } 13 | 14 | export function useElementSize( 15 | ref: React.MutableRefObject | null, 16 | // can be used, when it is needed to force reassign observer to element 17 | // in order to get correct measures. might be related to below 18 | // https://github.com/WICG/resize-observer/issues/65 19 | key?: string, 20 | ) { 21 | const [size, setSize] = React.useState({ 22 | width: 0, 23 | height: 0, 24 | }); 25 | 26 | React.useLayoutEffect(() => { 27 | if (!ref?.current) { 28 | return undefined; 29 | } 30 | 31 | const handleResize: ResizeObserverCallback = (entries) => { 32 | if (!Array.isArray(entries)) { 33 | return; 34 | } 35 | 36 | const entry = entries[0]; 37 | 38 | if (entry && entry.borderBoxSize) { 39 | const borderBoxSize = entry.borderBoxSize[0] 40 | ? entry.borderBoxSize[0] 41 | : (entry.borderBoxSize as unknown as ResizeObserverSize); 42 | // ...but old versions of Firefox treat it as a single item 43 | // https://github.com/mdn/dom-examples/blob/main/resize-observer/resize-observer-text.html#L88 44 | 45 | setSize({ 46 | width: round(borderBoxSize.inlineSize, ROUND_PRESICION), 47 | height: round(borderBoxSize.blockSize, ROUND_PRESICION), 48 | }); 49 | } else if (entry) { 50 | const target = entry.target as HTMLElement; 51 | setSize({ 52 | width: round(target.offsetWidth, ROUND_PRESICION), 53 | height: round(target.offsetHeight, ROUND_PRESICION), 54 | }); 55 | } 56 | }; 57 | 58 | const observer = new ResizeObserver(debounce(handleResize, RESIZE_DEBOUNCE)); 59 | observer.observe(ref.current); 60 | 61 | return () => { 62 | observer.disconnect(); 63 | }; 64 | }, [ref, key]); 65 | 66 | return size; 67 | } 68 | -------------------------------------------------------------------------------- /src/plugins/gravity-charts/renderer/GravityChartsWidget.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Chart} from '@gravity-ui/charts'; 4 | import type {ChartProps, ChartRef} from '@gravity-ui/charts'; 5 | import afterFrame from 'afterframe'; 6 | 7 | import {settings} from '../../../libs'; 8 | import type {ChartKitProps, ChartKitWidgetRef} from '../../../types'; 9 | import {measurePerformance} from '../../../utils'; 10 | 11 | import {vaildateData} from './utils'; 12 | import {withSplitPane} from './withSplitPane/withSplitPane'; 13 | 14 | const ChartWithSplitPane = withSplitPane(Chart); 15 | 16 | export const GravityChartsWidget = React.forwardRef< 17 | ChartKitWidgetRef | undefined, 18 | ChartKitProps<'gravity-charts'> 19 | >(function GravityChartsWidget(props, forwardedRef) { 20 | const {data, tooltip, onLoad, onRender, onChartLoad} = props; 21 | vaildateData(props); 22 | const lang = settings.get('lang'); 23 | const performanceMeasure = React.useRef | null>( 24 | measurePerformance(), 25 | ); 26 | const chartRef = React.useRef(null); 27 | const ChartComponent = tooltip?.splitted ? ChartWithSplitPane : Chart; 28 | 29 | const handleResize: NonNullable = React.useCallback( 30 | ({dimensions}) => { 31 | if (!dimensions) { 32 | return; 33 | } 34 | 35 | if (!performanceMeasure.current) { 36 | performanceMeasure.current = measurePerformance(); 37 | } 38 | 39 | afterFrame(() => { 40 | const renderTime = performanceMeasure.current?.end(); 41 | onRender?.({ 42 | renderTime, 43 | }); 44 | onLoad?.({ 45 | widgetRendering: renderTime, 46 | }); 47 | performanceMeasure.current = null; 48 | }); 49 | }, 50 | [onRender, onLoad], 51 | ); 52 | 53 | React.useImperativeHandle( 54 | forwardedRef, 55 | () => ({ 56 | reflow() { 57 | chartRef.current?.reflow(); 58 | }, 59 | }), 60 | [], 61 | ); 62 | 63 | React.useLayoutEffect(() => { 64 | if (onChartLoad) { 65 | onChartLoad({}); 66 | } 67 | }, [onChartLoad]); 68 | 69 | return ; 70 | }); 71 | 72 | export default GravityChartsWidget; 73 | -------------------------------------------------------------------------------- /src/plugins/gravity-charts/renderer/withSplitPane/useWithSplitPaneState.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {SplitLayout} from '../../../../components/SplitPane'; 4 | import type {SplitLayoutType} from '../../../../components/SplitPane'; 5 | import {IS_WINDOW_AVAILABLE} from '../../../../constants'; 6 | 7 | const CHART_SECTION_PERCENTAGE = 0.6; 8 | export const RESIZER_HEIGHT = 24; 9 | 10 | type WithSplitPaneState = { 11 | allowResize: boolean; 12 | tooltipHeight: number; 13 | setTooltipHeight: (value: number) => void; 14 | split: SplitLayoutType; 15 | setSplit: (value: SplitLayoutType) => void; 16 | size: number | string; 17 | setSize: (value: number | string) => void; 18 | maxSize?: number; 19 | minSize?: number; 20 | }; 21 | 22 | function getInitialSplit(): SplitLayoutType { 23 | if (!IS_WINDOW_AVAILABLE) { 24 | return SplitLayout.HORIZONTAL; 25 | } 26 | 27 | return window.innerWidth > window.innerHeight ? SplitLayout.VERTICAL : SplitLayout.HORIZONTAL; 28 | } 29 | 30 | type UseWithSplitPaneProps = { 31 | containerHeight: number; 32 | }; 33 | 34 | export function getVerticalSize() { 35 | return window.innerWidth * CHART_SECTION_PERCENTAGE; 36 | } 37 | 38 | function getInitialSize(split: SplitLayoutType, containerHeight: number) { 39 | const defaultSize = containerHeight - RESIZER_HEIGHT; 40 | 41 | if (!IS_WINDOW_AVAILABLE) { 42 | return defaultSize; 43 | } 44 | 45 | return split === SplitLayout.VERTICAL ? getVerticalSize() : defaultSize; 46 | } 47 | 48 | export function useWithSplitPaneState(props: UseWithSplitPaneProps): WithSplitPaneState { 49 | const {containerHeight} = props; 50 | const [tooltipHeight, setTooltipHeight] = React.useState(0); 51 | const [split, setSplit] = React.useState(getInitialSplit()); 52 | const [size, setSize] = React.useState(getInitialSize(split, containerHeight)); 53 | const allowResize = split === SplitLayout.HORIZONTAL; 54 | let maxSize: number | undefined; 55 | let minSize: number | undefined; 56 | 57 | if (IS_WINDOW_AVAILABLE && split === SplitLayout.HORIZONTAL) { 58 | maxSize = containerHeight - RESIZER_HEIGHT - tooltipHeight; 59 | minSize = containerHeight / 3; 60 | } 61 | 62 | return { 63 | allowResize, 64 | maxSize, 65 | minSize, 66 | tooltipHeight, 67 | setTooltipHeight, 68 | split, 69 | setSplit, 70 | size, 71 | setSize, 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/add-holidays.ts: -------------------------------------------------------------------------------- 1 | import type {ChartKitHolidays} from '../../../../types'; 2 | import type {ExtendedHChart, Highcharts} from '../../types'; 3 | 4 | const HALF_DAY = 43200000; 5 | 6 | const calculateConsistentClosestPointRange = ( 7 | type: string, 8 | closestPointRange: number, 9 | series: Highcharts.Series[], 10 | ) => { 11 | const isDatetimeAxis = type === 'datetime'; 12 | 13 | return ( 14 | isDatetimeAxis && 15 | closestPointRange === 86400000 && 16 | series.every(({xData}) => { 17 | let consistent = true; 18 | for (let i = 2; i < xData.length - 1; i++) { 19 | if ((xData[i] - xData[i - 1]) % closestPointRange !== 0) { 20 | consistent = false; 21 | break; 22 | } 23 | } 24 | return consistent; 25 | }) 26 | ); 27 | }; 28 | 29 | export function addHolidays(chart: ExtendedHChart, holidays: ChartKitHolidays) { 30 | const { 31 | userOptions: {_config: {region: configRegion = 'TOT'} = {}}, 32 | xAxis: [xAxis], 33 | } = chart; 34 | 35 | const {dataMin, dataMax, closestPointRange, series} = xAxis; 36 | const isConsistentClosestPointRange = calculateConsistentClosestPointRange( 37 | xAxis.options.type || '', 38 | closestPointRange, 39 | series, 40 | ); 41 | 42 | let needRedraw = false; 43 | 44 | if (isConsistentClosestPointRange) { 45 | const region = configRegion.toLowerCase(); 46 | 47 | for (let passed = 0; dataMin + passed <= dataMax; passed += closestPointRange) { 48 | const timestamp = dataMin + passed; 49 | 50 | const pointDate = Number(chart.time.dateFormat('%Y%m%d', timestamp)); 51 | 52 | const holidayByRegion = holidays.holiday[region]; 53 | const weekendByRegion = holidays.weekend[region]; 54 | 55 | if ( 56 | (holidayByRegion && holidayByRegion[pointDate]) || 57 | (weekendByRegion && weekendByRegion[pointDate]) 58 | ) { 59 | const bandStart = timestamp - HALF_DAY; 60 | const bandStop = timestamp + HALF_DAY; 61 | 62 | xAxis.addPlotBand({ 63 | color: 'var(--highcharts-holiday-band)', 64 | from: bandStart, 65 | to: bandStop, 66 | }); 67 | 68 | needRedraw = true; 69 | } 70 | } 71 | } 72 | 73 | return needRedraw; 74 | } 75 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/config/utils/calculatePrecision.test.ts: -------------------------------------------------------------------------------- 1 | import {calculatePrecision} from './calculatePrecision'; 2 | 3 | describe('plugins/highcharts/config/calculatePrecision', () => { 4 | test('should return undefined', () => { 5 | expect(calculatePrecision(null, {normalizeDiv: false, normalizeSub: false})).toEqual( 6 | undefined, 7 | ); 8 | 9 | expect(calculatePrecision(null, {normalizeDiv: false, normalizeSub: false}, 1)).toEqual( 10 | undefined, 11 | ); 12 | }); 13 | 14 | test('should return 2 in case of some of normalized options are initialized', () => { 15 | expect(calculatePrecision(null, {normalizeDiv: true, normalizeSub: false}, 99)).toEqual(2); 16 | 17 | expect(calculatePrecision(null, {normalizeDiv: false, normalizeSub: true}, 99.99)).toEqual( 18 | 2, 19 | ); 20 | 21 | expect(calculatePrecision(null, {normalizeDiv: false, normalizeSub: true})).toEqual(2); 22 | 23 | expect(calculatePrecision(10, {normalizeDiv: true, normalizeSub: true})).toEqual(2); 24 | }); 25 | 26 | test('should return precision value from func arguments', () => { 27 | expect( 28 | calculatePrecision(null, {normalizeDiv: false, normalizeSub: false, precision: 3}), 29 | ).toEqual(3); 30 | 31 | expect( 32 | calculatePrecision( 33 | null, 34 | {normalizeDiv: true, normalizeSub: false, precision: 4}, 35 | 99.99, 36 | ), 37 | ).toEqual(4); 38 | 39 | expect( 40 | calculatePrecision(null, {normalizeDiv: false, normalizeSub: true, precision: 5}, 99), 41 | ).toEqual(5); 42 | 43 | expect( 44 | calculatePrecision(10, {normalizeDiv: false, normalizeSub: false, precision: 3}), 45 | ).toEqual(3); 46 | }); 47 | 48 | test('should return alternativePrecision value from func arguments', () => { 49 | expect(calculatePrecision(10, {normalizeDiv: false, normalizeSub: false})).toEqual(10); 50 | 51 | expect(calculatePrecision(10, {normalizeDiv: false, normalizeSub: false}, 99)).toEqual(10); 52 | 53 | expect(calculatePrecision(10, {normalizeDiv: false, normalizeSub: false}, 99.99)).toEqual( 54 | 10, 55 | ); 56 | }); 57 | 58 | test('should return 2 for decimal number by default', () => { 59 | expect( 60 | calculatePrecision(null, {normalizeDiv: false, normalizeSub: false}, 0.1111111), 61 | ).toEqual(2); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/libs/settings/__tests__/settings-update.test.ts: -------------------------------------------------------------------------------- 1 | import {ChartKitPlugin} from 'src/types'; 2 | 3 | import {settings} from '../settings'; 4 | 5 | const resetSettings = () => settings.set({lang: 'en'}); 6 | 7 | const getMockedPlugin = (type: string, renderer: string) => 8 | ({ 9 | type, 10 | renderer, 11 | }) as unknown as ChartKitPlugin; 12 | 13 | // Order test is important because settings module is singleton and we can't delete plugins 14 | describe('libs/settings update plugins', () => { 15 | it('Update plugins when it is empty', () => { 16 | settings.set({ 17 | plugins: [getMockedPlugin('highcharts', 'initial'), getMockedPlugin('d3', 'initial')], 18 | }); 19 | 20 | expect(settings.get('plugins')).toEqual([ 21 | { 22 | type: 'highcharts', 23 | renderer: 'initial', 24 | }, 25 | { 26 | type: 'd3', 27 | renderer: 'initial', 28 | }, 29 | ]); 30 | }); 31 | 32 | it('Update existing plugin d3', () => { 33 | const initial = settings.get('plugins'); 34 | 35 | expect(initial).toEqual([ 36 | { 37 | type: 'highcharts', 38 | renderer: 'initial', 39 | }, 40 | { 41 | type: 'd3', 42 | renderer: 'initial', 43 | }, 44 | ]); 45 | 46 | settings.set({ 47 | plugins: [getMockedPlugin('d3', 'update')], 48 | }); 49 | 50 | const result = settings.get('plugins'); 51 | 52 | expect(result).toEqual([ 53 | { 54 | type: 'highcharts', 55 | renderer: 'initial', 56 | }, 57 | { 58 | type: 'd3', 59 | renderer: 'update', 60 | }, 61 | ]); 62 | }); 63 | 64 | it('Add new plugin', () => { 65 | settings.set({ 66 | plugins: [getMockedPlugin('yagr', 'update')], 67 | }); 68 | 69 | const result = settings.get('plugins'); 70 | 71 | expect(result).toEqual([ 72 | { 73 | type: 'highcharts', 74 | renderer: 'initial', 75 | }, 76 | { 77 | type: 'd3', 78 | renderer: 'update', 79 | }, 80 | { 81 | type: 'yagr', 82 | renderer: 'update', 83 | }, 84 | ]); 85 | }); 86 | 87 | beforeAll(resetSettings); 88 | afterEach(resetSettings); 89 | }); 90 | -------------------------------------------------------------------------------- /src/plugins/highcharts/__stories__/custom-error-render/custom-error-render.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Button} from '@gravity-ui/uikit'; 4 | import {Meta, Story} from '@storybook/react'; 5 | 6 | import {ChartKit} from '../../../../components/ChartKit'; 7 | import {CHARTKIT_ERROR_CODE, settings} from '../../../../libs'; 8 | import {RenderError} from '../../../../types'; 9 | import {HighchartsPlugin} from '../../index'; 10 | import {filledData, noData} from '../../mocks/custom-error-render'; 11 | import {ChartStory} from '../components/ChartStory'; 12 | 13 | export default { 14 | title: 'Plugins/Highcharts/CustomErrorRender', 15 | component: ChartKit, 16 | } as Meta; 17 | 18 | const Template: Story = () => { 19 | const [data, setData] = React.useState(noData); 20 | 21 | const renderErrorView: RenderError = React.useCallback(({error, message, resetError}) => { 22 | function renderFixButton() { 23 | if (!('code' in error)) { 24 | return null; 25 | } 26 | 27 | switch (error.code) { 28 | case CHARTKIT_ERROR_CODE.UNKNOWN_PLUGIN: 29 | return ( 30 | 38 | ); 39 | case CHARTKIT_ERROR_CODE.NO_DATA: 40 | return ( 41 | 48 | ); 49 | default: 50 | return null; 51 | } 52 | } 53 | 54 | return ( 55 |
56 |

{message}

57 | {renderFixButton()} 58 |
59 | ); 60 | }, []); 61 | 62 | return ( 63 |
64 | 70 |
71 | ); 72 | }; 73 | 74 | export const CustomErrorRender = Template.bind({}); 75 | -------------------------------------------------------------------------------- /src/plugins/indicator/renderer/IndicatorWidget.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import isEmpty from 'lodash/isEmpty'; 4 | 5 | import {CHARTKIT_SCROLLABLE_NODE_CLASSNAME} from '../../../constants'; 6 | import {i18n} from '../../../i18n'; 7 | import {CHARTKIT_ERROR_CODE, ChartKitError} from '../../../libs'; 8 | import type {ChartKitProps, ChartKitWidgetRef} from '../../../types'; 9 | import {getChartPerformanceDuration, getRandomCKId, markChartPerformance} from '../../../utils'; 10 | import {block} from '../../../utils/cn'; 11 | 12 | import {IndicatorItem} from './IndicatorItem'; 13 | 14 | import './IndicatorWidget.scss'; 15 | 16 | const b = block('indicator'); 17 | 18 | const IndicatorWidget = React.forwardRef>( 19 | // _ref needs to avoid this React warning: 20 | // "forwardRef render functions accept exactly two parameters: props and ref" 21 | function IndicatorWidgetInner(props, _ref) { 22 | const { 23 | onLoad, 24 | formatNumber, 25 | data: {data = [], defaultColor}, 26 | id, 27 | onRender, 28 | } = props; 29 | 30 | const generatedId = React.useMemo( 31 | () => `${id}_${getRandomCKId()}`, 32 | [data, defaultColor, formatNumber, id], 33 | ); 34 | 35 | markChartPerformance(generatedId); 36 | 37 | React.useLayoutEffect(() => { 38 | if (onRender) { 39 | onRender({renderTime: getChartPerformanceDuration(generatedId)}); 40 | return; 41 | } 42 | onLoad?.({widgetRendering: getChartPerformanceDuration(generatedId)}); 43 | }, [onLoad, onRender, generatedId]); 44 | 45 | if (isEmpty(data)) { 46 | throw new ChartKitError({ 47 | code: CHARTKIT_ERROR_CODE.NO_DATA, 48 | message: i18n('error', 'label_no-data'), 49 | }); 50 | } 51 | 52 | return ( 53 |
54 |
55 | {data.map((item, index) => ( 56 | 62 | ))} 63 |
64 |
65 | ); 66 | }, 67 | ); 68 | 69 | export default IndicatorWidget; 70 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {ChartKitError} from '../libs'; 4 | 5 | import type {ChartKitWidget} from './widget'; 6 | 7 | export type {ChartKitHolidays} from './misc'; 8 | 9 | export type ChartKitLang = 'ru' | 'en'; 10 | 11 | export type ChartKitType = keyof ChartKitWidget; 12 | 13 | export type ChartKitRef = { 14 | reflow: (details?: unknown) => void; 15 | }; 16 | 17 | export type ChartKitWidgetRef = { 18 | reflow?: ChartKitRef['reflow']; 19 | }; 20 | 21 | export type ChartKitOnLoadData = { 22 | widget?: ChartKitWidget[T]['widget']; 23 | widgetRendering?: number; 24 | }; 25 | 26 | export type ChartKitOnRenderData = { 27 | renderTime?: number; 28 | }; 29 | 30 | export type ChartKitOnChartLoad = { 31 | widget?: ChartKitWidget[T]['widget'] | null; 32 | }; 33 | 34 | export type ChartKitOnError = (data: {error: any}) => void; 35 | 36 | export type ChartKitRenderPluginLoader = () => React.ReactNode; 37 | 38 | export type ChartKitProps = { 39 | type: T; 40 | data: ChartKitWidget[T]['data']; 41 | id?: string; 42 | isMobile?: boolean; 43 | onLoad?: (data?: ChartKitOnLoadData) => void; 44 | /** Fires on each chartkit plugin's component render */ 45 | onRender?: (data: ChartKitOnRenderData) => void; 46 | /** Fires on chartkit plugin's component mount */ 47 | onChartLoad?: (data: ChartKitOnChartLoad) => void; 48 | /** Fires in case of unhandled plugin's exception */ 49 | onError?: ChartKitOnError; 50 | /** Used to render user's error component */ 51 | renderError?: RenderError; 52 | /** Used to render user's plugin loader component */ 53 | renderPluginLoader?: ChartKitRenderPluginLoader; 54 | validation?: { 55 | /** 56 | * Series count limit. 57 | * 58 | * If you have series more than limit, your chart will throw an error `'ERR.CK.TOO_MANY_LINES'`. 59 | * 60 | * - This setting applies only when provided 61 | * - Supported only for the `gravity-charts` plugin 62 | */ 63 | seriesCountLimit?: number; 64 | }; 65 | } & { 66 | [key in keyof Omit]: ChartKitWidget[T][key]; 67 | }; 68 | 69 | export type ChartKitPlugin = { 70 | type: ChartKitType; 71 | renderer: React.LazyExoticComponent; 72 | }; 73 | 74 | export type RenderErrorOpts = { 75 | message: string; 76 | error: ChartKitError | Error; 77 | resetError: () => void; 78 | }; 79 | 80 | export type RenderError = (opts: RenderErrorOpts) => React.ReactNode; 81 | 82 | export type {ChartKitWidget}; 83 | -------------------------------------------------------------------------------- /.storybook/preview.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {MINIMAL_VIEWPORTS} from '@storybook/addon-viewport'; 3 | import type {DecoratorFn} from '@storybook/react'; 4 | import {themes} from './theme'; 5 | import {withMobile} from './decorators/withMobile'; 6 | import {withLang} from './decorators/withLang'; 7 | import {DocsDecorator} from './decorators/DocsDecorator/DocsDecorator'; 8 | import {ThemeProvider, MobileProvider, configure, Lang} from '@gravity-ui/uikit'; 9 | 10 | import '@gravity-ui/uikit/styles/styles.scss'; 11 | 12 | configure({ 13 | lang: Lang.En, 14 | }); 15 | 16 | const withContextProvider: DecoratorFn = (Story, context) => { 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export const decorators = [withMobile, withLang, withContextProvider]; 29 | 30 | export const parameters = { 31 | docs: { 32 | theme: themes.light, 33 | container: DocsDecorator, 34 | }, 35 | jsx: {showFunctions: true}, // To show functions in sources 36 | viewport: { 37 | viewports: MINIMAL_VIEWPORTS, 38 | }, 39 | options: { 40 | storySort: { 41 | order: ['Showcase'], 42 | includeNames: true, 43 | }, 44 | }, 45 | }; 46 | 47 | export const globalTypes = { 48 | theme: { 49 | name: 'Theme', 50 | defaultValue: 'light', 51 | toolbar: { 52 | icon: 'mirror', 53 | items: [ 54 | {value: 'light', right: '☼', title: 'Light'}, 55 | {value: 'dark', right: '☾', title: 'Dark'}, 56 | {value: 'light-hc', right: '☼', title: 'High Contrast Light'}, 57 | {value: 'dark-hc', right: '☾', title: 'High Contrast Dark'}, 58 | ], 59 | }, 60 | }, 61 | lang: { 62 | name: 'Language', 63 | defaultValue: 'en', 64 | toolbar: { 65 | icon: 'globe', 66 | items: [ 67 | {value: 'en', right: '🇬🇧', title: 'En'}, 68 | {value: 'ru', right: '🇷🇺', title: 'Ru'}, 69 | ], 70 | }, 71 | }, 72 | platform: { 73 | name: 'Platform', 74 | defaultValue: 'desktop', 75 | toolbar: { 76 | items: [ 77 | {value: 'desktop', title: 'Desktop', icon: 'browser'}, 78 | {value: 'mobile', title: 'Mobile', icon: 'mobile'}, 79 | ], 80 | }, 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /src/plugins/highcharts/mocks/no-data.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import type {HighchartsWidgetData} from '../types'; 4 | 5 | const baseData: Omit = { 6 | config: { 7 | hideHolidays: false, 8 | normalizeDiv: false, 9 | normalizeSub: false, 10 | }, 11 | libraryConfig: { 12 | chart: { 13 | type: 'arearange', 14 | }, 15 | title: { 16 | text: 'Temperature variation by day', 17 | }, 18 | xAxis: { 19 | type: 'datetime', 20 | }, 21 | tooltip: { 22 | valueSuffix: '°C', 23 | }, 24 | }, 25 | }; 26 | 27 | export const noData: HighchartsWidgetData = { 28 | ...baseData, 29 | data: { 30 | graphs: [ 31 | { 32 | name: 'Temperatures', 33 | data: [], 34 | }, 35 | ], 36 | }, 37 | }; 38 | 39 | export const filledData: HighchartsWidgetData = { 40 | ...baseData, 41 | data: { 42 | graphs: [ 43 | { 44 | name: 'Temperatures', 45 | data: [ 46 | [1246406400000, 10.4, 17], 47 | [1246492800000, 10.3, 28.6], 48 | [1246579200000, 14.8, 18.4], 49 | [1246665600000, 11.5, 25.8], 50 | [1246752000000, 11.1, 24.4], 51 | [1246838400000, 17.7, 19.6], 52 | [1246924800000, 15.1, 18.1], 53 | [1247011200000, 15.1, 27.2], 54 | [1247097600000, 17, 17.5], 55 | [1247184000000, 12.6, 18.5], 56 | [1247270400000, 12.2, 26], 57 | [1247356800000, 15.9, 22.9], 58 | [1247443200000, 17.1, 18.1], 59 | [1247529600000, 13.3, 24.2], 60 | [1247616000000, 17, 28.1], 61 | [1247702400000, 16.2, 22.6], 62 | [1247788800000, 10.6, 19], 63 | [1247875200000, 11.3, 19.7], 64 | [1247961600000, 14.1, 24.6], 65 | [1248048000000, 14.2, 22.5], 66 | [1248134400000, 14.1, 28.5], 67 | [1248220800000, 14, 27], 68 | [1248307200000, 10.2, 20.6], 69 | [1248393600000, 13.1, 29.9], 70 | [1248480000000, 13.7, 21.1], 71 | [1248566400000, 15, 28.6], 72 | [1248652800000, 12, 17.5], 73 | [1248739200000, 17.8, 24.4], 74 | [1248825600000, 11.7, 25.9], 75 | [1248912000000, 13.6, 25.6], 76 | [1248998400000, 17.3, 22.2], 77 | ], 78 | }, 79 | ], 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /src/plugins/highcharts/mocks/custom-error-render.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | 3 | import type {HighchartsWidgetData} from '../types'; 4 | 5 | const baseData: Omit = { 6 | config: { 7 | hideHolidays: false, 8 | normalizeDiv: false, 9 | normalizeSub: false, 10 | }, 11 | libraryConfig: { 12 | chart: { 13 | type: 'arearange', 14 | }, 15 | title: { 16 | text: 'Temperature variation by day', 17 | }, 18 | xAxis: { 19 | type: 'datetime', 20 | }, 21 | tooltip: { 22 | valueSuffix: '°C', 23 | }, 24 | }, 25 | }; 26 | 27 | export const noData: HighchartsWidgetData = { 28 | ...baseData, 29 | data: { 30 | graphs: [ 31 | { 32 | name: 'Temperatures', 33 | data: [], 34 | }, 35 | ], 36 | }, 37 | }; 38 | 39 | export const filledData: HighchartsWidgetData = { 40 | ...baseData, 41 | data: { 42 | graphs: [ 43 | { 44 | name: 'Temperatures', 45 | data: [ 46 | [1246406400000, 10.4, 17], 47 | [1246492800000, 10.3, 28.6], 48 | [1246579200000, 14.8, 18.4], 49 | [1246665600000, 11.5, 25.8], 50 | [1246752000000, 11.1, 24.4], 51 | [1246838400000, 17.7, 19.6], 52 | [1246924800000, 15.1, 18.1], 53 | [1247011200000, 15.1, 27.2], 54 | [1247097600000, 17, 17.5], 55 | [1247184000000, 12.6, 18.5], 56 | [1247270400000, 12.2, 26], 57 | [1247356800000, 15.9, 22.9], 58 | [1247443200000, 17.1, 18.1], 59 | [1247529600000, 13.3, 24.2], 60 | [1247616000000, 17, 28.1], 61 | [1247702400000, 16.2, 22.6], 62 | [1247788800000, 10.6, 19], 63 | [1247875200000, 11.3, 19.7], 64 | [1247961600000, 14.1, 24.6], 65 | [1248048000000, 14.2, 22.5], 66 | [1248134400000, 14.1, 28.5], 67 | [1248220800000, 14, 27], 68 | [1248307200000, 10.2, 20.6], 69 | [1248393600000, 13.1, 29.9], 70 | [1248480000000, 13.7, 21.1], 71 | [1248566400000, 15, 28.6], 72 | [1248652800000, 12, 17.5], 73 | [1248739200000, 17.8, 24.4], 74 | [1248825600000, 11.7, 25.9], 75 | [1248912000000, 13.6, 25.6], 76 | [1248998400000, 17.3, 22.2], 77 | ], 78 | }, 79 | ], 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /src/components/ChartKit.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {i18n} from '../i18n'; 4 | import {CHARTKIT_ERROR_CODE, ChartKitError, settings} from '../libs'; 5 | import type {ChartKitProps, ChartKitRef, ChartKitType, ChartKitWidgetRef} from '../types'; 6 | import {getRandomCKId, typedMemo} from '../utils'; 7 | import {cn} from '../utils/cn'; 8 | 9 | import {ErrorBoundary} from './ErrorBoundary/ErrorBoundary'; 10 | import {Loader} from './Loader/Loader'; 11 | 12 | import './ChartKit.scss'; 13 | 14 | const b = cn('chartkit'); 15 | 16 | type ChartKitComponentProps = Omit, 'onError'> & { 17 | instanceRef?: React.ForwardedRef; 18 | }; 19 | 20 | const ChartKitComponent = (props: ChartKitComponentProps) => { 21 | const widgetRef = React.useRef(); 22 | const {instanceRef, id: propsId, type, isMobile, renderPluginLoader, ...restProps} = props; 23 | 24 | const ckId = React.useMemo(() => getRandomCKId(), []); 25 | const id = propsId || ckId; 26 | 27 | const lang = settings.get('lang'); 28 | const plugins = settings.get('plugins'); 29 | const plugin = plugins.find((iteratedPlugin) => iteratedPlugin.type === type); 30 | 31 | if (!plugin) { 32 | throw new ChartKitError({ 33 | code: CHARTKIT_ERROR_CODE.UNKNOWN_PLUGIN, 34 | message: i18n('error', 'label_unknown-plugin', {type}), 35 | }); 36 | } 37 | 38 | const ChartComponent = plugin.renderer; 39 | 40 | React.useImperativeHandle( 41 | instanceRef, 42 | () => ({ 43 | reflow(details) { 44 | if (widgetRef.current?.reflow) { 45 | widgetRef.current.reflow(details); 46 | } 47 | }, 48 | }), 49 | [], 50 | ); 51 | 52 | return ( 53 | }> 54 |
55 | 56 |
57 |
58 | ); 59 | }; 60 | 61 | const ChartKitComponentWithErrorBoundary = React.forwardRef< 62 | ChartKitRef | undefined, 63 | ChartKitProps 64 | >(function ChartKitComponentWithErrorBoundary(props, ref) { 65 | return ( 66 | 67 | 68 | 69 | ); 70 | }) /* https://stackoverflow.com/a/58473012 */ as ( 71 | props: ChartKitProps & {ref?: React.ForwardedRef}, 72 | ) => ReturnType; 73 | 74 | export const ChartKit = typedMemo(ChartKitComponentWithErrorBoundary); 75 | -------------------------------------------------------------------------------- /src/plugins/highcharts/renderer/helpers/graph.scss: -------------------------------------------------------------------------------- 1 | .chartkit-graph { 2 | & .highcharts-plot-line-label, 3 | .highcharts-plot-band-label { 4 | fill: var(--highcharts-plot-line-label); 5 | } 6 | 7 | & .highcharts-plot-band_comment { 8 | opacity: 0.5; 9 | } 10 | 11 | & .highcharts-legend { 12 | & .highcharts-coloraxis-labels { 13 | text { 14 | // stylelint-disable-next-line declaration-no-important 15 | fill: var(--g-color-text-secondary) !important; 16 | // stylelint-disable-next-line declaration-no-important 17 | color: var(--g-color-text-secondary) !important; 18 | } 19 | } 20 | 21 | & .highcharts-coloraxis-grid { 22 | path { 23 | stroke: var(--g-color-base-background); 24 | } 25 | } 26 | } 27 | } 28 | 29 | .chartkit-tooltip { 30 | $class: &; 31 | 32 | border: 1px solid var(--highcharts-grid-line); 33 | border-radius: 3px; 34 | background: var(--highcharts-tooltip-bg); 35 | padding: 10px 14px; 36 | color: var(--highcharts-tooltip-text); 37 | font-size: 12px; 38 | box-sizing: border-box; 39 | box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15); 40 | 41 | &_split-tooltip { 42 | border: none; 43 | border-radius: 0; 44 | box-shadow: none; 45 | border-bottom: 1px solid var(--g-color-line-generic); 46 | } 47 | 48 | &_yandex-map { 49 | position: absolute; 50 | } 51 | 52 | &__header { 53 | white-space: nowrap; 54 | font-size: 13px; 55 | font-weight: 600; 56 | 57 | & + #{$class}__row #{$class}__cell { 58 | padding-top: 10px; 59 | } 60 | } 61 | 62 | &__footer { 63 | color: #aaaaaa; 64 | font-size: 10px; 65 | 66 | &:not(:only-child) { 67 | margin-top: 8px; 68 | } 69 | } 70 | 71 | &__row { 72 | display: table-row; 73 | } 74 | 75 | &__cell { 76 | display: table-cell; 77 | padding: 2px 7px 2px 0; 78 | max-width: 370px; 79 | text-overflow: ellipsis; 80 | overflow: hidden; 81 | 82 | &_yandex-map { 83 | white-space: nowrap; 84 | } 85 | } 86 | 87 | &__color { 88 | position: relative; 89 | top: -1px; 90 | width: 12px; 91 | height: 6px; 92 | margin-right: 3px; 93 | border-radius: 1px; 94 | display: inline-block; 95 | } 96 | 97 | &__series-name { 98 | padding-right: 45px; 99 | } 100 | 101 | h3 { 102 | margin: 0; 103 | } 104 | 105 | &__point-container_type_timeline { 106 | max-width: 350px; 107 | white-space: normal; 108 | padding-top: 5px; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/plugins/yagr/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import type {YagrChartOptions} from '@gravity-ui/yagr'; 2 | 3 | import {getUplotTimezoneAligner, shapeYagrConfig} from '../renderer/utils'; 4 | import type {MinimalValidConfig, YagrWidgetData} from '../types'; 5 | 6 | const DATA: YagrWidgetData['data'] = { 7 | timeline: [1], 8 | graphs: [{data: [45]}], 9 | }; 10 | 11 | jest.mock('@gravity-ui/date-utils', () => { 12 | const originalModule = jest.requireActual('@gravity-ui/date-utils'); 13 | return { 14 | __esModule: true, 15 | ...originalModule, 16 | dateTime: ({input, timeZone}: {input: number; timeZone?: string}) => { 17 | const browserMockedTimezone = 'Europe/Moscow'; 18 | return originalModule.dateTime({ 19 | input, 20 | timeZone: timeZone || browserMockedTimezone, 21 | }); 22 | }, 23 | }; 24 | }); 25 | 26 | describe('plugins/yagr/utils', () => { 27 | describe('shapeYagrConfig > check chart property', () => { 28 | test.each<[Partial, Partial]>([ 29 | [{}, {appearance: {locale: 'en', theme: 'dark'}}], 30 | [{appearance: {locale: 'ru'}}, {appearance: {locale: 'ru', theme: 'dark'}}], 31 | [{appearance: {theme: 'light'}}, {appearance: {locale: 'en', theme: 'light'}}], 32 | [ 33 | {series: {type: 'dots'}, select: {zoom: false}, timeMultiplier: 1}, 34 | { 35 | appearance: {locale: 'en', theme: 'dark'}, 36 | series: {type: 'dots'}, 37 | select: {zoom: false}, 38 | timeMultiplier: 1, 39 | }, 40 | ], 41 | ])('(args: %j)', (chart, expected) => { 42 | const config = shapeYagrConfig({data: DATA, libraryConfig: {chart}, theme: 'dark'}); 43 | expect(config.chart).toEqual(expected); 44 | }); 45 | }); 46 | 47 | describe('GetUplotTimezoneAligner', () => { 48 | test.each<[YagrChartOptions | undefined, string | undefined, number, number]>([ 49 | // UTC 50 | [{}, 'UTC', 1706659878000, 1706649078000], 51 | // UTC + 1 52 | [{}, 'Europe/Belgrade', 1706659878000, 1706652678000], 53 | // UTC - 1 54 | [{}, 'America/Scoresbysund', 1706659878000, 1706645478000], 55 | // UTC + 4 56 | [{}, 'Asia/Muscat', 1706659878000, 1706663478000], 57 | ])( 58 | 'should return timestamp with subtracted timezone diff', 59 | (chart, timeZone, timestamp, expectedResult) => { 60 | const uplotTimezoneAligener = getUplotTimezoneAligner(chart, timeZone); 61 | 62 | // timestamp is UTC Wed Jan 31 2024 00:11:18 63 | const result = uplotTimezoneAligener(timestamp); 64 | 65 | expect(result.getTime()).toEqual(expectedResult); 66 | }, 67 | ); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/plugins/yagr/__stories__/Playground.stories.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import {Button} from '@gravity-ui/uikit'; 4 | import {StoryObj} from '@storybook/react'; 5 | 6 | import {ChartKit} from '../../../components/ChartKit'; 7 | import {settings} from '../../../libs'; 8 | import {ChartKitRef} from '../../../types'; 9 | import {YagrPlugin, YagrWidgetData} from '../index'; 10 | 11 | function prepareData(): YagrWidgetData { 12 | return { 13 | data: { 14 | timeline: [1705525200, 1705611600], 15 | timeZone: 'UTC', 16 | graphs: [ 17 | { 18 | id: 'a', 19 | name: 'a', 20 | color: 'rgb(255,255,0)', 21 | data: [43.97782069570251, 42.474166396151084], 22 | }, 23 | { 24 | id: 'b', 25 | name: 'b', 26 | color: 'rgb(255,0,0)', 27 | data: [42.814983190371834, 41.47785724489535], 28 | }, 29 | ], 30 | }, 31 | libraryConfig: { 32 | axes: { 33 | x: {label: 'UTC', labelSize: 25}, 34 | y: {label: '', precision: 'auto', scale: 'y', side: 'left'}, 35 | }, 36 | chart: { 37 | appearance: {drawOrder: ['plotLines', 'series', 'axes']}, 38 | series: {type: 'area', interpolation: 'linear'}, 39 | select: {zoom: false}, 40 | timeMultiplier: 0.001, 41 | }, 42 | cursor: {snapToValues: false, x: {style: '1px solid #ffa0a0'}, y: {visible: false}}, 43 | legend: {show: true}, 44 | scales: { 45 | x: {}, 46 | y: {normalize: false, stacking: true, type: 'linear'}, 47 | yRight: {normalize: false, stacking: true, type: 'linear'}, 48 | }, 49 | tooltip: { 50 | show: true, 51 | hideNoData: false, 52 | maxLines: 15, 53 | percent: false, 54 | precision: 2, 55 | sum: false, 56 | tracking: 'area', 57 | }, 58 | }, 59 | }; 60 | } 61 | 62 | const ChartStory = ({data}: {data: YagrWidgetData}) => { 63 | const [shown, setShown] = React.useState(false); 64 | const chartkitRef = React.useRef(); 65 | 66 | if (!shown) { 67 | settings.set({plugins: [YagrPlugin]}); 68 | return ; 69 | } 70 | 71 | return ( 72 |
73 | 74 |
75 | ); 76 | }; 77 | 78 | export const PlaygroundLineChartStory: StoryObj = { 79 | name: 'Playground', 80 | args: { 81 | data: prepareData(), 82 | }, 83 | argTypes: { 84 | data: { 85 | control: 'object', 86 | }, 87 | }, 88 | }; 89 | 90 | export default { 91 | title: 'Plugins/Yagr', 92 | component: ChartStory, 93 | }; 94 | -------------------------------------------------------------------------------- /src/plugins/yagr/renderer/tooltip/types.ts: -------------------------------------------------------------------------------- 1 | export type TooltipData = { 2 | /** Tooltip lines data */ 3 | lines: Array; 4 | /** Tooltip comments */ 5 | xComments?: Array<{ 6 | text: string; 7 | color: string; 8 | }>; 9 | commentDateText?: string; 10 | /** 11 | * Indicating that active line duplicated by displaying it on top of the main list 12 | * default behavior - the active line is displayed on top of the main list only if it"does not fit" in the tooltip 13 | */ 14 | activeRowAlwaysFirstInTooltip?: boolean; 15 | /** Indicating that the chart is displayed in "split tooltip" mode */ 16 | splitTooltip?: boolean; 17 | /** Text of the header of the tooltip */ 18 | tooltipHeader?: string; 19 | /** Indicating that a column with the line name is displayed in the tooltip */ 20 | shared?: boolean; 21 | /** Indicating that a column with a percentage value is displayed in the tooltip */ 22 | withPercent?: boolean; 23 | /** Indicating that a column with a diff is displayed in the tooltip */ 24 | useCompareFrom?: boolean; 25 | /** Indicating that the tooltip displays a block with information about the holiday */ 26 | holiday?: boolean; 27 | /** Name of the holiday */ 28 | holidayText?: string; 29 | /** Region for which the holiday is relevant */ 30 | region?: string; 31 | /** Sum of the values of the rows displayed in the tooltip */ 32 | sum?: number | string; 33 | /** Number of hidden lines "not fit" in the tooltip */ 34 | hiddenRowsNumber: number; 35 | /** Sum of the values of the hidden ("not fit" in the tooltip) rows */ 36 | hiddenRowsSum?: number | string; 37 | }; 38 | 39 | export type TooltipExtraData = { 40 | lastVisibleRowIndex: number; 41 | }; 42 | 43 | export type TooltipLine = { 44 | /** Color displayed in a separate cell */ 45 | seriesColor: string; 46 | /** Series name */ 47 | seriesName: string; 48 | /** Series index */ 49 | seriesIdx?: number; 50 | /** Indicating whether the series name should be displayed */ 51 | hideSeriesName?: boolean; 52 | /** Percentage value displayed in a separate cell */ 53 | percentValue?: number | string; 54 | /** Diff value displayed in the separate cell */ 55 | diff?: string; 56 | /** Formatted numeric value for the current series displayed in a separate cell */ 57 | value: string; 58 | /** Comment to the line (displayed under the corresponding line), set via manageTooltipConfig */ 59 | commentText?: string; 60 | /** Comment to the line (displayed under the corresponding line) */ 61 | xyCommentText?: string; 62 | /** Indicating that line is active */ 63 | selectedSeries?: boolean; 64 | /** Custom renderer of the line (a string with text or html markup) */ 65 | customRender?: string; 66 | replaceCellAt?: Record string>; 67 | insertCellAt?: Record string>; 68 | }; 69 | 70 | export type RowRenderingConfig = { 71 | cellsRenderers: Array<(line: TooltipLine) => string>; 72 | isSelectedLine?: boolean; 73 | allowComment?: boolean; 74 | withDarkBackground?: boolean; 75 | isSingleLine?: boolean; 76 | rowIndex?: number; 77 | }; 78 | --------------------------------------------------------------------------------