2022-07-11 12:54:08 +05:00
|
|
|
|
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
|
|
|
|
import { useElementSize } from 'usehooks-ts'
|
|
|
|
|
import { Property } from 'csstype'
|
|
|
|
|
import { Empty } from 'antd'
|
|
|
|
|
import * as d3 from 'd3'
|
|
|
|
|
|
|
|
|
|
import LoaderPortal from '@components/LoaderPortal'
|
2022-08-15 17:03:42 +05:00
|
|
|
|
import { isDev, usePartialProps, useUserSettings } from '@utils'
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
ChartAxis,
|
|
|
|
|
ChartDataset,
|
|
|
|
|
ChartOffset,
|
|
|
|
|
ChartRegistry,
|
|
|
|
|
ChartTick,
|
|
|
|
|
MinMax
|
2022-08-14 15:03:48 +05:00
|
|
|
|
} from '@components/d3/types'
|
2022-07-11 12:54:08 +05:00
|
|
|
|
import {
|
|
|
|
|
BasePluginSettings,
|
|
|
|
|
D3ContextMenu,
|
|
|
|
|
D3ContextMenuSettings,
|
2022-08-14 15:03:48 +05:00
|
|
|
|
} from '@components/d3/plugins'
|
|
|
|
|
import D3MouseZone from '@components/d3/D3MouseZone'
|
|
|
|
|
import { getByAccessor, getChartClass, getGroupClass, getTicks } from '@components/d3/functions'
|
|
|
|
|
import { renderArea, renderLine, renderNeedle, renderPoint, renderRectArea } from '@components/d3/renders'
|
|
|
|
|
|
2022-08-15 06:57:20 +05:00
|
|
|
|
import D3MonitoringEditor from './D3MonitoringEditor'
|
2022-08-22 06:32:05 +05:00
|
|
|
|
import D3MonitoringCurrentValues from './D3MonitoringCurrentValues'
|
2022-08-14 15:03:48 +05:00
|
|
|
|
import { D3HorizontalCursor, D3HorizontalCursorSettings } from './D3HorizontalCursor'
|
2022-08-15 06:57:20 +05:00
|
|
|
|
import D3MonitoringLimitChart, { TelemetryRegulators } from './D3MonitoringLimitChart'
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
2022-07-11 14:04:24 +05:00
|
|
|
|
const roundTo = (v: number, to: number = 50) => {
|
2022-08-11 17:14:36 +05:00
|
|
|
|
if (v === 0) return v
|
|
|
|
|
return (v > 0 ? Math.ceil : Math.round)(v / to) * to
|
2022-07-11 14:04:24 +05:00
|
|
|
|
}
|
|
|
|
|
|
2022-08-11 17:14:36 +05:00
|
|
|
|
const calculateDomain = (mm: MinMax): Required<MinMax> => {
|
|
|
|
|
let round = Math.abs((mm.max ?? 0) - (mm.min ?? 0))
|
|
|
|
|
if (round < 10) round = 10
|
|
|
|
|
else if (round < 100) round = roundTo(round, 10)
|
|
|
|
|
else if (round < 1000) round = roundTo(round, 100)
|
|
|
|
|
else if (round < 10000) round = roundTo(round, 1000)
|
|
|
|
|
else round = 0
|
2022-07-11 14:04:24 +05:00
|
|
|
|
let min = roundTo(mm.min ?? 0, round)
|
|
|
|
|
let max = roundTo(mm.max ?? round, round)
|
2022-08-11 17:14:36 +05:00
|
|
|
|
if (round && Math.abs(min - max) < round) {
|
2022-07-11 14:04:24 +05:00
|
|
|
|
const mid = (min + max) / 2
|
|
|
|
|
min = mid - round
|
|
|
|
|
max = mid + round
|
|
|
|
|
}
|
|
|
|
|
return { min, max }
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-04 01:34:03 +05:00
|
|
|
|
export type ExtendedChartDataset<DataType> = ChartDataset<DataType> & {
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Диапазон отображаемых значений по горизонтальной оси */
|
2022-07-11 14:04:24 +05:00
|
|
|
|
xDomain: MinMax
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Скрыть отображение шкалы графика */
|
|
|
|
|
hideLabel?: boolean
|
2022-08-22 06:32:05 +05:00
|
|
|
|
/** Показать последнее значение сверху графика */
|
|
|
|
|
showCurrentValue?: boolean
|
2022-07-11 12:54:08 +05:00
|
|
|
|
}
|
|
|
|
|
|
2022-08-04 01:34:03 +05:00
|
|
|
|
export type ExtendedChartRegistry<DataType> = ChartRegistry<DataType> & ExtendedChartDataset<DataType>
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
|
|
|
|
export type ChartGroup<DataType> = {
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Получить D3 выборку, содержащую корневой G-элемент группы */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
(): d3.Selection<SVGGElement, any, any, any>
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Уникальный ключ группы (индекс) */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
key: number
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Массив содержащихся в группе графиков */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
charts: ExtendedChartRegistry<DataType>[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const defaultOffsets: ChartOffset = {
|
|
|
|
|
top: 10,
|
|
|
|
|
bottom: 10,
|
|
|
|
|
left: 100,
|
2022-08-14 15:03:48 +05:00
|
|
|
|
right: 20,
|
2022-07-11 12:54:08 +05:00
|
|
|
|
}
|
|
|
|
|
|
2022-08-15 06:57:20 +05:00
|
|
|
|
const defaultRegulators: TelemetryRegulators = {
|
2022-08-15 11:58:32 +05:00
|
|
|
|
1: { color: '#59B359', label: 'Скорость блока' },
|
|
|
|
|
2: { color: '#FF0000', label: 'Давление' },
|
|
|
|
|
3: { color: '#0000CC', label: 'Осевая нагрузка' },
|
|
|
|
|
4: { color: '#990099', label: 'Момент на роторе' },
|
|
|
|
|
5: { color: '#007070', label: 'Расход' },
|
2022-08-15 06:57:20 +05:00
|
|
|
|
}
|
|
|
|
|
|
2022-07-11 12:54:08 +05:00
|
|
|
|
const getDefaultYAxisConfig = <DataType,>(): ChartAxis<DataType> => ({
|
|
|
|
|
type: 'time',
|
|
|
|
|
accessor: (d: any) => new Date(d.date)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const getDefaultYTicks = <DataType,>(): Required<ChartTick<DataType>> => ({
|
|
|
|
|
visible: false,
|
|
|
|
|
format: (d: d3.NumberValue, idx: number, data?: DataType) => String(d),
|
|
|
|
|
color: 'lightgray',
|
|
|
|
|
count: 10,
|
|
|
|
|
})
|
|
|
|
|
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/**
|
|
|
|
|
* @template DataType тип данных отображаемых записей
|
|
|
|
|
*/
|
2022-08-11 17:14:36 +05:00
|
|
|
|
export type D3MonitoringChartsProps<DataType extends Record<string, any>> = Omit<React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> & {
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Двумерный массив датасетов (группа-график) */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
datasetGroups: ExtendedChartDataset<DataType>[][]
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Ширина графика числом пикселей или CSS-значением (px/%/em/rem) */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
width?: string | number
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Высота графика числом пикселей или CSS-значением (px/%/em/rem) */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
height?: string | number
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Длительность анимации в миллисекундах */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
animDurationMs?: number
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Задать статус "заргужается" графику */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
loading?: boolean
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Массив отображаемых данных */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
data?: DataType[]
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Отступы графика от края SVG */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
offset?: Partial<ChartOffset>
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Цвет фона в формате CSS-значения */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
backgroundColor?: Property.Color
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Параметры общей вертикальной оси */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
yAxis?: ChartAxis<DataType>
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Параметры плагинов */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
plugins?: {
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Параметры горизонтального курсора */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
cursor?: BasePluginSettings & D3HorizontalCursorSettings<DataType>
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Параметры контекстного меню */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
menu?: BasePluginSettings & D3ContextMenuSettings
|
|
|
|
|
}
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Настройки рисок и цен деления вертикальной шкалы */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
yTicks?: ChartTick<DataType>
|
2022-07-30 22:45:08 +05:00
|
|
|
|
/** Штриховка графика */
|
|
|
|
|
dash?: string | number | number[]
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Диапозон отображаемых значений по вертикальной оси (сверху вниз) */
|
|
|
|
|
yDomain?: MinMax
|
|
|
|
|
/** Событие, вызываемое при прокрутке колёсика мышки над графиком */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
onWheel: (e: WheelEvent) => void
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Высота шкал графиков в пикселях (20 по умолчанию) */
|
|
|
|
|
axisHeight?: number
|
|
|
|
|
/** Отступ между группами графиков в пикселях (30 по умолчанию) */
|
|
|
|
|
spaceBetweenGroups?: number
|
2022-08-04 01:34:03 +05:00
|
|
|
|
/** Название графика для сохранения в базе */
|
|
|
|
|
chartName?: string
|
2022-08-11 17:14:36 +05:00
|
|
|
|
methods?: (value: {
|
|
|
|
|
setSettingsVisible: (visible: boolean) => void
|
|
|
|
|
}) => void
|
2022-07-11 12:54:08 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export type ChartSizes = ChartOffset & {
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Ширина зоны графика */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
inlineWidth: number
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Высота зоны графика */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
inlineHeight: number
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Ширина группы на графике */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
groupWidth: number
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Высота блока осей */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
axesHeight: number
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Отступ сверху до активной зоны графиков */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
chartsTop: number
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Высота активной зоны графиков */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
chartsHeight: number
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Отступ слева для `i`-ой группы */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
groupLeft: (i: number) => number
|
2022-07-25 14:30:24 +05:00
|
|
|
|
/** Отступ сверху для `i`-ой оси в группе размером `count` */
|
2022-07-11 12:54:08 +05:00
|
|
|
|
axisTop: (i: number, count: number) => number
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-25 14:30:24 +05:00
|
|
|
|
type ChartDomain = {
|
|
|
|
|
/** Шкала графика */
|
|
|
|
|
scale: d3.ScaleLinear<number, number>,
|
|
|
|
|
/** Диапазон отображаемых на графике занчений */
|
|
|
|
|
domain: Required<MinMax>,
|
|
|
|
|
}
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
|
|
|
|
const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
|
|
|
|
width: givenWidth = '100%',
|
|
|
|
|
height: givenHeight = '100%',
|
|
|
|
|
animDurationMs = 0,
|
|
|
|
|
loading = false,
|
|
|
|
|
datasetGroups,
|
|
|
|
|
data,
|
|
|
|
|
plugins,
|
|
|
|
|
offset: _offset,
|
|
|
|
|
yAxis: _yAxisConfig,
|
|
|
|
|
backgroundColor = 'transparent',
|
|
|
|
|
yDomain,
|
|
|
|
|
yTicks: _yTicks,
|
2022-07-25 14:30:24 +05:00
|
|
|
|
axisHeight = 20,
|
|
|
|
|
spaceBetweenGroups = 30,
|
2022-07-30 22:45:08 +05:00
|
|
|
|
dash,
|
2022-08-04 01:34:03 +05:00
|
|
|
|
chartName,
|
2022-08-11 17:14:36 +05:00
|
|
|
|
methods,
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
|
|
|
|
className = '',
|
|
|
|
|
...other
|
|
|
|
|
}: D3MonitoringChartsProps<DataType>) => {
|
2022-08-15 17:03:42 +05:00
|
|
|
|
const [datasets, setDatasets, resetDatasets] = useUserSettings(chartName, datasetGroups)
|
|
|
|
|
const [regulators, setRegulators, resetRegulators] = useUserSettings(`${chartName}_regulators`, defaultRegulators)
|
2022-07-11 12:54:08 +05:00
|
|
|
|
const [groups, setGroups] = useState<ChartGroup<DataType>[]>([])
|
|
|
|
|
const [svgRef, setSvgRef] = useState<SVGSVGElement | null>(null)
|
|
|
|
|
const [yAxisRef, setYAxisRef] = useState<SVGGElement | null>(null)
|
|
|
|
|
const [chartAreaRef, setChartAreaRef] = useState<SVGGElement | null>(null)
|
|
|
|
|
const [axesAreaRef, setAxesAreaRef] = useState<SVGGElement | null>(null)
|
2022-08-11 17:14:36 +05:00
|
|
|
|
const [settingsVisible, setSettingsVisible] = useState<boolean>(false)
|
2022-08-04 01:34:03 +05:00
|
|
|
|
|
2022-07-11 12:54:08 +05:00
|
|
|
|
const offset = usePartialProps(_offset, defaultOffsets)
|
|
|
|
|
const yTicks = usePartialProps<Required<ChartTick<DataType>>>(_yTicks, getDefaultYTicks)
|
|
|
|
|
const yAxisConfig = usePartialProps<ChartAxis<DataType>>(_yAxisConfig, getDefaultYAxisConfig)
|
|
|
|
|
|
|
|
|
|
const [rootRef, { width, height }] = useElementSize()
|
|
|
|
|
|
|
|
|
|
const yAxisArea = useCallback(() => d3.select(yAxisRef), [yAxisRef])
|
|
|
|
|
const axesArea = useCallback(() => d3.select(axesAreaRef), [axesAreaRef])
|
|
|
|
|
const chartArea = useCallback(() => d3.select(chartAreaRef), [chartAreaRef])
|
|
|
|
|
|
|
|
|
|
const sizes: ChartSizes = useMemo(() => {
|
|
|
|
|
const inlineWidth = Math.max(width - offset.left - offset.right, 0)
|
|
|
|
|
const inlineHeight = Math.max(height - offset.top - offset.bottom, 0)
|
|
|
|
|
const groupsCount = groups.length
|
|
|
|
|
|
2022-07-25 14:30:24 +05:00
|
|
|
|
const groupWidth = groupsCount ? (inlineWidth - spaceBetweenGroups * (groupsCount - 1)) / groupsCount : 0
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
2022-07-25 14:30:24 +05:00
|
|
|
|
const maxChartCount = Math.max(0, ...groups.map((g) => g.charts.filter((c) => !c.hideLabel).length))
|
2022-07-11 12:54:08 +05:00
|
|
|
|
const axesHeight = (axisHeight * maxChartCount)
|
|
|
|
|
|
|
|
|
|
return ({
|
|
|
|
|
...offset,
|
|
|
|
|
inlineWidth,
|
|
|
|
|
inlineHeight,
|
|
|
|
|
groupWidth,
|
|
|
|
|
axesHeight,
|
|
|
|
|
chartsTop: offset.top + axesHeight,
|
|
|
|
|
chartsHeight: inlineHeight - axesHeight,
|
2022-07-25 14:30:24 +05:00
|
|
|
|
groupLeft: (i: number) => (groupWidth + spaceBetweenGroups) * i,
|
2022-07-11 12:54:08 +05:00
|
|
|
|
axisTop: (i: number, count: number) => axisHeight * (maxChartCount - count + i + 1)
|
|
|
|
|
})
|
2022-08-10 15:22:57 +05:00
|
|
|
|
}, [groups, width, height, offset, axisHeight, spaceBetweenGroups])
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
|
|
|
|
const yAxis = useMemo(() => {
|
|
|
|
|
if (!data) return
|
|
|
|
|
|
|
|
|
|
const yAxis = d3.scaleTime()
|
|
|
|
|
.domain([yDomain?.min ?? 0, yDomain?.max ?? 0])
|
|
|
|
|
.range([0, sizes.chartsHeight])
|
|
|
|
|
|
|
|
|
|
return yAxis
|
|
|
|
|
}, [groups, data, yDomain, sizes.chartsHeight])
|
|
|
|
|
|
2022-07-25 14:30:24 +05:00
|
|
|
|
const chartDomains = useMemo(() => groups.map((group) => {
|
|
|
|
|
const out: [string | number, ChartDomain][] = group.charts.map((chart) => {
|
|
|
|
|
const mm = { ...chart.xDomain }
|
|
|
|
|
let domain: Required<MinMax> = { min: 0, max: 100 }
|
2022-08-10 15:22:57 +05:00
|
|
|
|
if (!Number.isNaN((mm.min ?? NaN) + (mm.max ?? NaN))) {
|
2022-07-25 14:30:24 +05:00
|
|
|
|
domain = mm as Required<MinMax>
|
|
|
|
|
} else if (data) {
|
|
|
|
|
const [min, max] = d3.extent(data, chart.x)
|
2022-08-11 17:14:36 +05:00
|
|
|
|
domain = calculateDomain({ min, max, ...mm })
|
2022-07-25 14:30:24 +05:00
|
|
|
|
}
|
|
|
|
|
return [chart.key, {
|
|
|
|
|
scale: d3.scaleLinear().domain([domain.min, domain.max]),
|
|
|
|
|
domain,
|
|
|
|
|
}]
|
|
|
|
|
})
|
2022-07-11 14:04:24 +05:00
|
|
|
|
|
2022-07-25 14:30:24 +05:00
|
|
|
|
out.forEach(([key], i) => {
|
|
|
|
|
const chart = group.charts.find((chart) => chart.key === key)
|
2022-08-11 17:14:36 +05:00
|
|
|
|
const bind = chart?.linkedTo
|
2022-07-25 14:30:24 +05:00
|
|
|
|
if (!bind) return
|
|
|
|
|
const bindDomain = out.find(([key]) => key === bind)
|
|
|
|
|
if (bindDomain)
|
|
|
|
|
out[i][1] = bindDomain[1]
|
2022-07-11 14:04:24 +05:00
|
|
|
|
})
|
2022-07-25 14:30:24 +05:00
|
|
|
|
|
|
|
|
|
return Object.fromEntries(out)
|
|
|
|
|
}), [groups, data])
|
2022-07-11 14:04:24 +05:00
|
|
|
|
|
2022-08-04 01:34:03 +05:00
|
|
|
|
const createAxesGroup = useCallback((i: number): ChartGroup<DataType> => Object.assign(
|
|
|
|
|
() => chartArea().select('.' + getGroupClass(i)) as d3.Selection<SVGGElement, any, any, any>,
|
|
|
|
|
{
|
|
|
|
|
key: i,
|
|
|
|
|
charts: [],
|
|
|
|
|
}
|
2022-08-11 17:14:36 +05:00
|
|
|
|
), [chartArea])
|
2022-08-04 01:34:03 +05:00
|
|
|
|
|
2022-08-15 06:57:20 +05:00
|
|
|
|
const onGroupsChange = useCallback((settings: ExtendedChartDataset<DataType>[][], regulators: TelemetryRegulators) => {
|
2022-08-04 01:34:03 +05:00
|
|
|
|
setSettingsVisible(false)
|
2022-08-15 17:03:42 +05:00
|
|
|
|
setRegulators(regulators)
|
|
|
|
|
setDatasets(settings)
|
|
|
|
|
}, [setDatasets, setRegulators])
|
2022-08-04 01:34:03 +05:00
|
|
|
|
|
2022-08-11 17:14:36 +05:00
|
|
|
|
const onGroupsReset = useCallback(() => {
|
|
|
|
|
setSettingsVisible(false)
|
2022-08-15 17:03:42 +05:00
|
|
|
|
resetRegulators()
|
|
|
|
|
resetDatasets()
|
|
|
|
|
}, [resetDatasets, resetRegulators])
|
2022-08-11 17:14:36 +05:00
|
|
|
|
|
2022-08-22 06:32:05 +05:00
|
|
|
|
useEffect(() => methods?.({ setSettingsVisible }), [methods]) /// Возвращаем в делегат доступные методы
|
2022-08-11 17:14:36 +05:00
|
|
|
|
|
2022-08-22 06:32:05 +05:00
|
|
|
|
useEffect(() => { /// Обновляем группы
|
2022-07-11 12:54:08 +05:00
|
|
|
|
if (isDev()) {
|
2022-08-04 01:34:03 +05:00
|
|
|
|
datasets.forEach((sets, i) => {
|
2022-07-11 12:54:08 +05:00
|
|
|
|
sets.forEach((set, j) => {
|
2022-08-04 01:34:03 +05:00
|
|
|
|
for (let k = j + 1; k < sets.length; k++)
|
2022-07-11 12:54:08 +05:00
|
|
|
|
if (set.key === sets[k].key)
|
|
|
|
|
console.warn(`Ключ датасета "${set.key}" неуникален (группа ${i}, индексы ${j} и ${k})!`)
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setGroups((oldGroups) => {
|
|
|
|
|
const groups: ChartGroup<DataType>[] = []
|
|
|
|
|
|
2022-08-04 01:34:03 +05:00
|
|
|
|
if (datasets.length < oldGroups.length) {
|
2022-07-11 12:54:08 +05:00
|
|
|
|
// Удаляем неактуальные группы
|
2022-08-04 01:34:03 +05:00
|
|
|
|
oldGroups.slice(datasets.length).forEach((group) => group().remove())
|
|
|
|
|
groups.push(...oldGroups.slice(0, datasets.length))
|
2022-07-11 12:54:08 +05:00
|
|
|
|
} else {
|
|
|
|
|
groups.push(...oldGroups)
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-04 01:34:03 +05:00
|
|
|
|
datasets.forEach((datasets, i) => {
|
2022-07-11 12:54:08 +05:00
|
|
|
|
let group: ChartGroup<DataType> = createAxesGroup(i)
|
|
|
|
|
|
|
|
|
|
if (group().empty())
|
|
|
|
|
chartArea().append('g')
|
|
|
|
|
.attr('class', `chart-group ${getGroupClass(i)}`)
|
|
|
|
|
|
|
|
|
|
datasets.forEach((dataset) => { // Обновляем и добавляем новые чарты
|
|
|
|
|
let chartIdx = group.charts.findIndex(({ key }) => key === dataset.key)
|
|
|
|
|
if (chartIdx < 0) {
|
|
|
|
|
chartIdx = group.charts.length
|
|
|
|
|
} else {
|
|
|
|
|
// Если типы графиков не сходятся удалить старые элементы
|
|
|
|
|
if (group.charts[chartIdx].type !== dataset.type)
|
|
|
|
|
group.charts[chartIdx]().selectAll('*').remove()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Пересоздаём график
|
|
|
|
|
const newChart: ExtendedChartRegistry<DataType> = Object.assign(
|
|
|
|
|
() => group().select('.' + getChartClass(dataset.key)) as d3.Selection<SVGGElement, DataType, any, unknown>,
|
|
|
|
|
{
|
|
|
|
|
width: 1,
|
|
|
|
|
opacity: 1,
|
|
|
|
|
label: dataset.key,
|
|
|
|
|
color: 'gray',
|
|
|
|
|
animDurationMs,
|
|
|
|
|
...dataset,
|
|
|
|
|
yAxis: dataset.yAxis ?? yAxisConfig,
|
2022-08-10 15:22:57 +05:00
|
|
|
|
y: getByAccessor(dataset.yAxis?.accessor ?? yAxisConfig.accessor),
|
2022-07-11 12:54:08 +05:00
|
|
|
|
x: getByAccessor(dataset.xAxis?.accessor),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if (newChart.type === 'line')
|
|
|
|
|
newChart.optimization = false
|
|
|
|
|
|
|
|
|
|
// Если у графика нет группы создаём её
|
|
|
|
|
if (newChart().empty())
|
|
|
|
|
group().append('g')
|
|
|
|
|
.attr('class', `chart ${getChartClass(newChart.key)}`)
|
|
|
|
|
|
|
|
|
|
group.charts[chartIdx] = newChart
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
groups[i] = group
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return groups
|
|
|
|
|
})
|
2022-08-04 01:34:03 +05:00
|
|
|
|
}, [yAxisConfig, chartArea, datasets, animDurationMs, createAxesGroup])
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
2022-08-22 06:32:05 +05:00
|
|
|
|
useEffect(() => { /// Обновляем группы и горизонтальные оси
|
2022-08-11 17:14:36 +05:00
|
|
|
|
const axesGroups = axesArea()
|
2022-07-11 12:54:08 +05:00
|
|
|
|
.selectAll('.charts-group')
|
|
|
|
|
.data(groups)
|
|
|
|
|
|
|
|
|
|
axesGroups.exit().remove()
|
|
|
|
|
axesGroups.enter()
|
|
|
|
|
.append('g')
|
|
|
|
|
.attr('class', 'charts-group')
|
|
|
|
|
|
2022-08-11 17:14:36 +05:00
|
|
|
|
const actualAxesGroups = axesArea()
|
2022-07-11 12:54:08 +05:00
|
|
|
|
.selectAll<SVGGElement | null, ChartGroup<DataType>>('.charts-group')
|
|
|
|
|
.attr('class', (g) => `charts-group ${getGroupClass(g.key)}`)
|
|
|
|
|
.attr('transform', (g) => `translate(${sizes.groupLeft(g.key)}, 0)`)
|
|
|
|
|
|
2022-07-11 14:04:24 +05:00
|
|
|
|
actualAxesGroups.each(function(group, i) {
|
2022-07-11 12:54:08 +05:00
|
|
|
|
const groupAxes = d3.select(this)
|
2022-07-25 14:30:24 +05:00
|
|
|
|
const chartsData = group.charts.filter((chart) => !chart.hideLabel)
|
2022-07-11 12:54:08 +05:00
|
|
|
|
const charts = groupAxes.selectChildren().data(chartsData)
|
|
|
|
|
|
|
|
|
|
charts.exit().remove()
|
|
|
|
|
charts.enter().append('g')
|
|
|
|
|
.attr('class', (d) => `chart ${getChartClass(d.key)}`)
|
|
|
|
|
.attr('transform', (_, j) => `translate(0, ${sizes.axisTop(j, chartsData.length)})`)
|
|
|
|
|
|
|
|
|
|
const actualCharts = groupAxes.selectChildren<SVGGElement | null, ExtendedChartRegistry<DataType>>()
|
|
|
|
|
.style('color', (d) => d.color ?? null)
|
|
|
|
|
|
|
|
|
|
actualCharts.each(function (chart, j) {
|
2022-07-11 14:04:24 +05:00
|
|
|
|
let axis = d3.axisTop(chartDomains[i][chart.key].scale.range([0, sizes.groupWidth]))
|
|
|
|
|
const domain = chartDomains[i][chart.key].domain
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
|
|
|
|
if (j === chartsData.length - 1) {
|
|
|
|
|
axis = axis
|
|
|
|
|
.ticks(5)
|
|
|
|
|
.tickSize(-sizes.chartsHeight)
|
|
|
|
|
.tickFormat((d, i) => i === 0 || i === 5 ? String(d) : '')
|
2022-07-11 14:04:24 +05:00
|
|
|
|
.tickValues(getTicks(domain, 5))
|
2022-07-11 12:54:08 +05:00
|
|
|
|
} else {
|
|
|
|
|
axis = axis.ticks(1)
|
2022-07-11 14:04:24 +05:00
|
|
|
|
.tickValues(getTicks(domain, 1))
|
2022-07-11 12:54:08 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
d3.select(this).call(axis as any)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (actualCharts.selectChild('text').empty())
|
|
|
|
|
actualCharts.append('text')
|
|
|
|
|
|
|
|
|
|
actualCharts.selectChild('text')
|
|
|
|
|
.attr('fill', 'currentColor')
|
|
|
|
|
.style('text-anchor', 'middle')
|
|
|
|
|
.style('dominant-baseline', 'middle')
|
|
|
|
|
.attr('x', sizes.groupWidth / 2)
|
|
|
|
|
.attr('y', -axisHeight / 2)
|
|
|
|
|
.text((d) => String(d.label) ?? d.key)
|
|
|
|
|
|
|
|
|
|
actualCharts.each(function (_, j) {
|
|
|
|
|
d3.select(this)
|
|
|
|
|
.selectAll('.tick line')
|
|
|
|
|
.attr('stroke', j === chartsData.length - 1 ? 'gray' : 'currentColor')
|
|
|
|
|
})
|
|
|
|
|
})
|
2022-08-11 17:14:36 +05:00
|
|
|
|
}, [groups, sizes, spaceBetweenGroups, chartDomains, axesArea])
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
|
|
|
|
useEffect(() => { // Рисуем ось Y
|
|
|
|
|
if (!yAxis) return
|
|
|
|
|
|
|
|
|
|
const getX = getByAccessor(yAxisConfig.accessor)
|
|
|
|
|
|
|
|
|
|
yAxisArea().transition()
|
|
|
|
|
.duration(animDurationMs)
|
|
|
|
|
.call(d3.axisLeft(yAxis)
|
|
|
|
|
.tickFormat((d, i) => {
|
|
|
|
|
let rowData
|
|
|
|
|
if (data)
|
|
|
|
|
rowData = data.find((row) => getX(row) === d)
|
|
|
|
|
return yTicks.format(d, i, rowData)
|
|
|
|
|
})
|
|
|
|
|
.tickSize(yTicks.visible ? -width + offset.left + offset.right : 0)
|
|
|
|
|
.ticks(yTicks.count) as any // TODO: Исправить тип
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
yAxisArea().selectAll('.tick line').attr('stroke', yTicks.color)
|
|
|
|
|
}, [yAxisArea, yAxis, animDurationMs, width, offset, yTicks])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!data || !yAxis) return
|
|
|
|
|
|
2022-07-11 14:04:24 +05:00
|
|
|
|
groups.forEach((group, i) => {
|
2022-07-11 12:54:08 +05:00
|
|
|
|
group()
|
|
|
|
|
.attr('transform', `translate(${sizes.groupLeft(group.key)}, 0)`)
|
|
|
|
|
.attr('clip-path', `url(#chart-clip)`)
|
|
|
|
|
|
|
|
|
|
group.charts.forEach((chart) => {
|
|
|
|
|
chart()
|
|
|
|
|
.attr('color', chart.color || null)
|
|
|
|
|
.attr('stroke', 'currentColor')
|
|
|
|
|
.attr('stroke-width', chart.width ?? null)
|
|
|
|
|
.attr('opacity', chart.opacity ?? null)
|
|
|
|
|
.attr('fill', 'none')
|
|
|
|
|
|
|
|
|
|
let chartData = data
|
|
|
|
|
if (!chartData) return
|
|
|
|
|
|
2022-07-11 14:04:24 +05:00
|
|
|
|
const xAxis = chartDomains[i][chart.key].scale.range([0, sizes.groupWidth])
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
|
|
|
|
switch (chart.type) {
|
|
|
|
|
case 'needle':
|
|
|
|
|
chartData = renderNeedle<DataType>(xAxis, yAxis, chart, chartData, height, offset)
|
|
|
|
|
break
|
|
|
|
|
case 'line':
|
|
|
|
|
chartData = renderLine<DataType>(xAxis, yAxis, chart, chartData)
|
|
|
|
|
break
|
|
|
|
|
case 'point':
|
|
|
|
|
chartData = renderPoint<DataType>(xAxis, yAxis, chart, chartData)
|
|
|
|
|
break
|
|
|
|
|
case 'area':
|
|
|
|
|
chartData = renderArea<DataType>(xAxis, yAxis, chart, chartData)
|
|
|
|
|
break
|
2022-07-25 14:30:24 +05:00
|
|
|
|
case 'rect_area':
|
|
|
|
|
renderRectArea<DataType>(xAxis, yAxis, chart)
|
|
|
|
|
break
|
2022-07-11 12:54:08 +05:00
|
|
|
|
default:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (chart.point)
|
2022-07-11 14:04:24 +05:00
|
|
|
|
renderPoint<DataType>(xAxis, yAxis, chart, chartData, true)
|
2022-07-30 22:45:08 +05:00
|
|
|
|
|
|
|
|
|
if (dash) chart().attr('stroke-dasharray', dash)
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
|
|
|
|
chart.afterDraw?.(chart)
|
|
|
|
|
})
|
|
|
|
|
})
|
2022-07-25 14:30:24 +05:00
|
|
|
|
}, [data, groups, height, offset, sizes, chartDomains])
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<LoaderPortal
|
|
|
|
|
show={loading}
|
|
|
|
|
style={{
|
|
|
|
|
width: givenWidth,
|
|
|
|
|
height: givenHeight,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
{...other}
|
|
|
|
|
ref={rootRef}
|
|
|
|
|
className={`asb-d3-chart ${className}`}
|
|
|
|
|
>
|
|
|
|
|
{data ? (
|
2022-08-04 01:34:03 +05:00
|
|
|
|
<D3ContextMenu
|
|
|
|
|
onSettingsOpen={() => setSettingsVisible(true)}
|
|
|
|
|
{...plugins?.menu}
|
|
|
|
|
svg={svgRef}
|
|
|
|
|
>
|
2022-07-11 12:54:08 +05:00
|
|
|
|
<svg ref={setSvgRef} width={'100%'} height={'100%'}>
|
|
|
|
|
<defs>
|
|
|
|
|
<clipPath id={`chart-clip`}>
|
|
|
|
|
{/* Сдвиг во все стороны на 1 px чтобы линии на краях было видно */}
|
|
|
|
|
<rect x={-1} y={-1} width={sizes.groupWidth + 2} height={sizes.chartsHeight + 2} />
|
|
|
|
|
</clipPath>
|
|
|
|
|
</defs>
|
|
|
|
|
<g ref={setYAxisRef} className={'axis y'} transform={`translate(${offset.left}, ${sizes.chartsTop})`} />
|
|
|
|
|
<g ref={setAxesAreaRef} className={'chart-axes'} transform={`translate(${offset.left}, ${offset.top})`}>
|
|
|
|
|
<rect width={sizes.inlineWidth} height={sizes.axesHeight} fill={backgroundColor} />
|
|
|
|
|
</g>
|
|
|
|
|
<g ref={setChartAreaRef} className={'chart-area'} transform={`translate(${offset.left}, ${sizes.chartsTop})`}>
|
|
|
|
|
<rect width={sizes.inlineWidth} height={sizes.chartsHeight} fill={backgroundColor} />
|
|
|
|
|
</g>
|
|
|
|
|
<g stroke={'black'}>
|
|
|
|
|
{d3.range(1, groups.length).map((i) => {
|
2022-07-25 14:30:24 +05:00
|
|
|
|
const x = offset.left + (sizes.groupWidth + spaceBetweenGroups) * i - spaceBetweenGroups / 2
|
2022-07-11 12:54:08 +05:00
|
|
|
|
return <line key={`${i}`} x1={x} x2={x} y1={sizes.chartsTop} y2={offset.top + sizes.inlineHeight} />
|
|
|
|
|
})}
|
|
|
|
|
</g>
|
2022-08-22 06:32:05 +05:00
|
|
|
|
<D3MonitoringLimitChart<DataType>
|
2022-08-15 06:57:20 +05:00
|
|
|
|
regulators={regulators}
|
2022-08-14 15:05:48 +05:00
|
|
|
|
data={data}
|
|
|
|
|
yAxis={yAxis}
|
|
|
|
|
width={20}
|
|
|
|
|
height={sizes.chartsHeight}
|
|
|
|
|
left={sizes.inlineWidth + sizes.left}
|
|
|
|
|
top={sizes.chartsTop}
|
2022-08-15 12:28:07 +05:00
|
|
|
|
zoneWidth={sizes.inlineWidth}
|
2022-08-14 15:05:48 +05:00
|
|
|
|
/>
|
2022-08-22 06:32:05 +05:00
|
|
|
|
<D3MonitoringCurrentValues<DataType>
|
|
|
|
|
groups={groups}
|
|
|
|
|
data={data}
|
|
|
|
|
left={offset.left}
|
|
|
|
|
sizes={sizes}
|
|
|
|
|
/>
|
2022-07-11 12:54:08 +05:00
|
|
|
|
<D3MouseZone width={width} height={height} offset={{ ...offset, top: sizes.chartsTop }}>
|
|
|
|
|
<D3HorizontalCursor
|
|
|
|
|
{...plugins?.cursor}
|
|
|
|
|
yAxis={yAxis}
|
|
|
|
|
groups={groups}
|
|
|
|
|
sizes={sizes}
|
2022-09-06 19:58:20 +05:00
|
|
|
|
spaceBetweenGroups={spaceBetweenGroups}
|
2022-07-11 12:54:08 +05:00
|
|
|
|
data={data}
|
2022-08-17 16:28:07 +05:00
|
|
|
|
height={height}
|
2022-07-11 12:54:08 +05:00
|
|
|
|
/>
|
|
|
|
|
</D3MouseZone>
|
|
|
|
|
</svg>
|
|
|
|
|
</D3ContextMenu>
|
|
|
|
|
) : (
|
|
|
|
|
<div className={'chart-empty'}>
|
|
|
|
|
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2022-08-15 06:57:20 +05:00
|
|
|
|
<D3MonitoringEditor
|
2022-08-04 01:34:03 +05:00
|
|
|
|
groups={datasets}
|
2022-08-15 06:57:20 +05:00
|
|
|
|
regulators={regulators}
|
2022-08-04 01:34:03 +05:00
|
|
|
|
visible={settingsVisible}
|
|
|
|
|
onChange={onGroupsChange}
|
|
|
|
|
onCancel={() => setSettingsVisible(false)}
|
2022-08-11 17:14:36 +05:00
|
|
|
|
onReset={onGroupsReset}
|
2022-08-04 01:34:03 +05:00
|
|
|
|
/>
|
2022-07-11 12:54:08 +05:00
|
|
|
|
</div>
|
|
|
|
|
</LoaderPortal>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-22 06:32:05 +05:00
|
|
|
|
export const D3MonitoringCharts = memo(_D3MonitoringCharts) as typeof _D3MonitoringCharts
|
2022-07-11 12:54:08 +05:00
|
|
|
|
|
|
|
|
|
export default D3MonitoringCharts
|