forked from ddrilling/asb_cloud_front
Типы компонентов D3 задокументированы
This commit is contained in:
parent
ebec1957e7
commit
121ec4f50b
@ -57,21 +57,37 @@ const createAxis = <DataType,>(config: ChartAxis<DataType>) => {
|
||||
}
|
||||
|
||||
export type D3ChartProps<DataType> = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
/** Параметры общей горизонтальной оси */
|
||||
xAxis: ChartAxis<DataType>
|
||||
/** Параметры графиков */
|
||||
datasets: ChartDataset<DataType>[]
|
||||
/** Массив отображаемых данных или объект с парами ключ графика-данные */
|
||||
data?: DataType[] | Record<string, DataType[]>
|
||||
/** Диапозон отображаемых значений */
|
||||
domain?: Partial<ChartDomain>
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
/** Ширина графика числом пикселей или CSS-значением (px/%/em/rem) */
|
||||
width?: string | number
|
||||
/** Высота графика числом пикселей или CSS-значением (px/%/em/rem) */
|
||||
height?: string | number
|
||||
/** Задать статус "заргужается" графику */
|
||||
loading?: boolean
|
||||
/** Отступы графика от края SVG */
|
||||
offset?: Partial<ChartOffset>
|
||||
/** Длительность анимации в миллисекундах */
|
||||
animDurationMs?: number
|
||||
/** Цвет фона в формате CSS-значения */
|
||||
backgroundColor?: Property.Color
|
||||
/** Настройки рисок и цен деления графика */
|
||||
ticks?: ChartTicks<DataType>
|
||||
/** Параметры плагинов */
|
||||
plugins?: {
|
||||
/** Параметры контекстного меню */
|
||||
menu?: BasePluginSettings & D3ContextMenuSettings
|
||||
/** Параметры всплывающей подсказки */
|
||||
tooltip?: BasePluginSettings & D3TooltipSettings<DataType>
|
||||
/** Параметры курсора */
|
||||
cursor?: BasePluginSettings & D3CursorSettings
|
||||
/** Параметры блока легенды */
|
||||
legend?: BasePluginSettings & D3LegendSettings
|
||||
}
|
||||
}
|
||||
|
@ -20,12 +20,11 @@ import {
|
||||
D3ContextMenu,
|
||||
D3ContextMenuSettings,
|
||||
D3HorizontalCursor,
|
||||
D3HorizontalCursorSettings,
|
||||
D3TooltipSettings
|
||||
D3HorizontalCursorSettings
|
||||
} from './plugins'
|
||||
import D3MouseZone from './D3MouseZone'
|
||||
import { getByAccessor, getChartClass, getGroupClass, getTicks } from './functions'
|
||||
import { renderArea, renderLine, renderNeedle, renderPoint } from './renders'
|
||||
import { renderArea, renderLine, renderNeedle, renderPoint, renderRectArea } from './renders'
|
||||
|
||||
const roundTo = (v: number, to: number = 50) => {
|
||||
if (to == 0) return v
|
||||
@ -44,22 +43,21 @@ const calculateDomain = (mm: MinMax, round: number = 100): Required<MinMax> => {
|
||||
return { min, max }
|
||||
}
|
||||
|
||||
type AxisScale = d3.ScaleTime<number, number, never> | d3.ScaleLinear<number, number, never>
|
||||
|
||||
type ExtendedChartDataset<DataType> = ChartDataset<DataType> & {
|
||||
/** Диапазон отображаемых значений по горизонтальной оси */
|
||||
xDomain: MinMax
|
||||
hideXAxis?: boolean
|
||||
/** Скрыть отображение шкалы графика */
|
||||
hideLabel?: boolean
|
||||
}
|
||||
|
||||
type ExtendedChartRegistry<DataType> = ExtendedChartDataset<DataType> & {
|
||||
(): d3.Selection<SVGGElement, DataType, any, any>
|
||||
y: (value: any) => number
|
||||
x: (value: any) => number
|
||||
}
|
||||
type ExtendedChartRegistry<DataType> = ChartRegistry<DataType> & ExtendedChartDataset<DataType>
|
||||
|
||||
export type ChartGroup<DataType> = {
|
||||
/** Получить D3 выборку, содержащую корневой G-элемент группы */
|
||||
(): d3.Selection<SVGGElement, any, any, any>
|
||||
/** Уникальный ключ группы (индекс) */
|
||||
key: number
|
||||
/** Массив содержащихся в группе графиков */
|
||||
charts: ExtendedChartRegistry<DataType>[]
|
||||
}
|
||||
|
||||
@ -82,51 +80,72 @@ const getDefaultYTicks = <DataType,>(): Required<ChartTick<DataType>> => ({
|
||||
count: 10,
|
||||
})
|
||||
|
||||
const findChartsByKey = <DataType,>(groups: ChartGroup<DataType>[], key: string) => {
|
||||
const out: ChartRegistry<DataType>[] = []
|
||||
groups.forEach((group) => {
|
||||
const res = group.charts.find((chart) => chart.key === key)
|
||||
if (res) out.push(res)
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
export type D3MonitoringChartsProps<DataType> = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
/**
|
||||
* @template DataType тип данных отображаемых записей
|
||||
*/
|
||||
export type D3MonitoringChartsProps<DataType extends Record<string, any>> = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
/** Двумерный массив датасетов (группа-график) */
|
||||
datasetGroups: ExtendedChartDataset<DataType>[][]
|
||||
/** Ширина графика числом пикселей или CSS-значением (px/%/em/rem) */
|
||||
width?: string | number
|
||||
/** Высота графика числом пикселей или CSS-значением (px/%/em/rem) */
|
||||
height?: string | number
|
||||
/** Длительность анимации в миллисекундах */
|
||||
animDurationMs?: number
|
||||
/** Задать статус "заргужается" графику */
|
||||
loading?: boolean
|
||||
/** Массив отображаемых данных */
|
||||
data?: DataType[]
|
||||
/** Отступы графика от края SVG */
|
||||
offset?: Partial<ChartOffset>
|
||||
/** Цвет фона в формате CSS-значения */
|
||||
backgroundColor?: Property.Color
|
||||
/** Параметры общей вертикальной оси */
|
||||
yAxis?: ChartAxis<DataType>
|
||||
/** Параметры плагинов */
|
||||
plugins?: {
|
||||
/** Параметры горизонтального курсора */
|
||||
cursor?: BasePluginSettings & D3HorizontalCursorSettings<DataType>
|
||||
/** Параметры контекстного меню */
|
||||
menu?: BasePluginSettings & D3ContextMenuSettings
|
||||
tooltip?: BasePluginSettings & D3TooltipSettings<DataType>
|
||||
}
|
||||
/** Настройки рисок и цен деления вертикальной шкалы */
|
||||
yTicks?: ChartTick<DataType>
|
||||
yDomain?: {
|
||||
min?: number
|
||||
max?: number
|
||||
}
|
||||
/** Диапозон отображаемых значений по вертикальной оси (сверху вниз) */
|
||||
yDomain?: MinMax
|
||||
/** Событие, вызываемое при прокрутке колёсика мышки над графиком */
|
||||
onWheel: (e: WheelEvent) => void
|
||||
/** Высота шкал графиков в пикселях (20 по умолчанию) */
|
||||
axisHeight?: number
|
||||
/** Отступ между группами графиков в пикселях (30 по умолчанию) */
|
||||
spaceBetweenGroups?: number
|
||||
}
|
||||
|
||||
export type ChartSizes = ChartOffset & {
|
||||
/** Ширина зоны графика */
|
||||
inlineWidth: number
|
||||
/** Высота зоны графика */
|
||||
inlineHeight: number
|
||||
/** Ширина группы на графике */
|
||||
groupWidth: number
|
||||
/** Высота блока осей */
|
||||
axesHeight: number
|
||||
/** Отступ сверху до активной зоны графиков */
|
||||
chartsTop: number
|
||||
/** Высота активной зоны графиков */
|
||||
chartsHeight: number
|
||||
/** Отступ слева для `i`-ой группы */
|
||||
groupLeft: (i: number) => number
|
||||
/** Отступ сверху для `i`-ой оси в группе размером `count` */
|
||||
axisTop: (i: number, count: number) => number
|
||||
}
|
||||
|
||||
const axisHeight = 20
|
||||
const space = 30
|
||||
type ChartDomain = {
|
||||
/** Шкала графика */
|
||||
scale: d3.ScaleLinear<number, number>,
|
||||
/** Диапазон отображаемых на графике занчений */
|
||||
domain: Required<MinMax>,
|
||||
}
|
||||
|
||||
const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
width: givenWidth = '100%',
|
||||
@ -141,12 +160,13 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
backgroundColor = 'transparent',
|
||||
yDomain,
|
||||
yTicks: _yTicks,
|
||||
axisHeight = 20,
|
||||
spaceBetweenGroups = 30,
|
||||
|
||||
className = '',
|
||||
...other
|
||||
}: D3MonitoringChartsProps<DataType>) => {
|
||||
const [groups, setGroups] = useState<ChartGroup<DataType>[]>([])
|
||||
const [groupScales, setGroupScales] = useState<Record<string, AxisScale>[]>([])
|
||||
const [svgRef, setSvgRef] = useState<SVGSVGElement | null>(null)
|
||||
const [yAxisRef, setYAxisRef] = useState<SVGGElement | null>(null)
|
||||
const [chartAreaRef, setChartAreaRef] = useState<SVGGElement | null>(null)
|
||||
@ -167,10 +187,9 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
const inlineHeight = Math.max(height - offset.top - offset.bottom, 0)
|
||||
const groupsCount = groups.length
|
||||
|
||||
const groupWidth = groupsCount ? (inlineWidth - space * (groupsCount - 1)) / groupsCount : 0
|
||||
const groupWidth = groupsCount ? (inlineWidth - spaceBetweenGroups * (groupsCount - 1)) / groupsCount : 0
|
||||
|
||||
let maxChartCount = Math.max(...groups.map((group) => group.charts.length))
|
||||
if (!Number.isFinite(maxChartCount)) maxChartCount = 0
|
||||
const maxChartCount = Math.max(0, ...groups.map((g) => g.charts.filter((c) => !c.hideLabel).length))
|
||||
const axesHeight = (axisHeight * maxChartCount)
|
||||
|
||||
return ({
|
||||
@ -181,7 +200,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
axesHeight,
|
||||
chartsTop: offset.top + axesHeight,
|
||||
chartsHeight: inlineHeight - axesHeight,
|
||||
groupLeft: (i: number) => (groupWidth + space) * i,
|
||||
groupLeft: (i: number) => (groupWidth + spaceBetweenGroups) * i,
|
||||
axisTop: (i: number, count: number) => axisHeight * (maxChartCount - count + i + 1)
|
||||
})
|
||||
}, [groups, height, offset])
|
||||
@ -204,12 +223,8 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
}
|
||||
), [chartArea, axesArea])
|
||||
|
||||
const chartDomains: Record<string, {
|
||||
scale: d3.ScaleLinear<number, number>,
|
||||
domain: Required<MinMax>,
|
||||
}>[] = useMemo(() => {
|
||||
return groups.map((group) => {
|
||||
const out = group.charts.map((chart) => {
|
||||
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 }
|
||||
if (mm.min && mm.max) {
|
||||
@ -224,9 +239,17 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
}]
|
||||
})
|
||||
|
||||
return Object.fromEntries(out)
|
||||
out.forEach(([key], i) => {
|
||||
const chart = group.charts.find((chart) => chart.key === key)
|
||||
const bind = chart?.bindDomainFrom
|
||||
if (!bind) return
|
||||
const bindDomain = out.find(([key]) => key === bind)
|
||||
if (bindDomain)
|
||||
out[i][1] = bindDomain[1]
|
||||
})
|
||||
}, [groups, data])
|
||||
|
||||
return Object.fromEntries(out)
|
||||
}), [groups, data])
|
||||
|
||||
useEffect(() => {
|
||||
if (isDev()) {
|
||||
@ -319,7 +342,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
|
||||
actualAxesGroups.each(function(group, i) {
|
||||
const groupAxes = d3.select(this)
|
||||
const chartsData = group.charts.filter((chart) => !chart.hideXAxis)
|
||||
const chartsData = group.charts.filter((chart) => !chart.hideLabel)
|
||||
const charts = groupAxes.selectChildren().data(chartsData)
|
||||
|
||||
charts.exit().remove()
|
||||
@ -365,7 +388,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
.attr('stroke', j === chartsData.length - 1 ? 'gray' : 'currentColor')
|
||||
})
|
||||
})
|
||||
}, [groups, groupScales, sizes, space, chartDomains])
|
||||
}, [groups, sizes, spaceBetweenGroups, chartDomains])
|
||||
|
||||
useEffect(() => { // Рисуем ось Y
|
||||
if (!yAxis) return
|
||||
@ -422,6 +445,9 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
case 'area':
|
||||
chartData = renderArea<DataType>(xAxis, yAxis, chart, chartData)
|
||||
break
|
||||
case 'rect_area':
|
||||
renderRectArea<DataType>(xAxis, yAxis, chart)
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
@ -432,7 +458,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
chart.afterDraw?.(chart)
|
||||
})
|
||||
})
|
||||
}, [data, groups, groupScales, height, offset, sizes, chartDomains])
|
||||
}, [data, groups, height, offset, sizes, chartDomains])
|
||||
|
||||
return (
|
||||
<LoaderPortal
|
||||
@ -465,7 +491,7 @@ const _D3MonitoringCharts = <DataType extends Record<string, unknown>>({
|
||||
</g>
|
||||
<g stroke={'black'}>
|
||||
{d3.range(1, groups.length).map((i) => {
|
||||
const x = offset.left + (sizes.groupWidth + space) * i - space / 2
|
||||
const x = offset.left + (sizes.groupWidth + spaceBetweenGroups) * i - spaceBetweenGroups / 2
|
||||
return <line key={`${i}`} x1={x} x2={x} y1={sizes.chartsTop} y2={offset.top + sizes.inlineHeight} />
|
||||
})}
|
||||
</g>
|
||||
|
@ -1,27 +1,39 @@
|
||||
import { createContext, memo, ReactNode, useCallback, useContext, useEffect, useRef, useState } from 'react'
|
||||
import { createContext, memo, ReactNode, useCallback, useContext, useEffect, useState } from 'react'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import '@styles/d3.less'
|
||||
import { ChartOffset } from './types'
|
||||
|
||||
export type D3MouseState = {
|
||||
/** Позиция мыши по оси X */
|
||||
x: number
|
||||
/** Позиция мыши по оси Y */
|
||||
y: number
|
||||
/** Находится ли мышь над активной зоной графика */
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
type SubscribeFunction = (name: keyof SVGElementEventMap, handler: EventListenerOrEventListenerObject) => null | (() => boolean)
|
||||
|
||||
export type D3MouseZoneContext = {
|
||||
/** Состояние мыши */
|
||||
mouseState: D3MouseState,
|
||||
zone: (() => d3.Selection<any, any, null, undefined>) | null
|
||||
/** Получение D3 выборки, содержащей RECT-элемент обрабатывающий события мыши */
|
||||
zone: (() => d3.Selection<SVGRectElement, any, null, undefined>) | null
|
||||
/** Параметры позиционирования и размера зоны обработки событий */
|
||||
zoneRect: DOMRect | null
|
||||
/** Подписка на событие */
|
||||
subscribe: SubscribeFunction
|
||||
}
|
||||
|
||||
export type D3MouseZoneProps = {
|
||||
/** Ширина активной зоны в пикселях */
|
||||
width: number
|
||||
/** Высота активной зоны в пикселях */
|
||||
height: number
|
||||
offset: Record<string, number>
|
||||
/** Отступы от края svg */
|
||||
offset: ChartOffset
|
||||
/** Контекстные плагины */
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
@ -41,31 +53,31 @@ export const D3MouseZoneContext = createContext<D3MouseZoneContext>(defaultMouse
|
||||
export const useD3MouseZone = () => useContext(D3MouseZoneContext)
|
||||
|
||||
export const D3MouseZone = memo<D3MouseZoneProps>(({ width, height, offset, children }) => {
|
||||
const rectRef = useRef<SVGRectElement>(null)
|
||||
const [rectRef, setRectRef] = useState<SVGRectElement | null>(null)
|
||||
|
||||
const [state, setState] = useState<D3MouseState>({ x: 0, y: 0, visible: false })
|
||||
const [childContext, setChildContext] = useState<D3MouseZoneContext>(defaultMouseZoneContext)
|
||||
|
||||
const subscribeEvent: SubscribeFunction = useCallback((name, handler) => {
|
||||
if (!rectRef.current) return null
|
||||
rectRef.current.addEventListener(name, handler)
|
||||
if (!rectRef) return null
|
||||
rectRef.addEventListener(name, handler)
|
||||
return () => {
|
||||
if (!rectRef.current) return false
|
||||
rectRef.current.removeEventListener(name, handler)
|
||||
if (!rectRef) return false
|
||||
rectRef.removeEventListener(name, handler)
|
||||
return true
|
||||
}
|
||||
}, [rectRef.current])
|
||||
}, [rectRef])
|
||||
|
||||
const updateContext = useCallback(() => {
|
||||
const zone = rectRef.current ? (() => d3.select(rectRef.current)) : null
|
||||
const zone = rectRef ? (() => d3.select(rectRef)) : null
|
||||
|
||||
setChildContext({
|
||||
mouseState: state,
|
||||
zone,
|
||||
zoneRect: rectRef.current?.getBoundingClientRect() || null,
|
||||
zoneRect: rectRef?.getBoundingClientRect() || null,
|
||||
subscribe: subscribeEvent,
|
||||
})
|
||||
}, [rectRef.current, state, subscribeEvent])
|
||||
}, [rectRef, state, subscribeEvent])
|
||||
|
||||
const onMouse = useCallback((e: any) => {
|
||||
const rect = e.target.getBoundingClientRect()
|
||||
@ -91,7 +103,7 @@ export const D3MouseZone = memo<D3MouseZoneProps>(({ width, height, offset, chil
|
||||
return (
|
||||
<g className={'asb-d3-mouse-zone'} transform={`translate(${offset.left}, ${offset.top})`}>
|
||||
<rect
|
||||
ref={rectRef}
|
||||
ref={setRectRef}
|
||||
pointerEvents={'all'}
|
||||
className={'event-zone'}
|
||||
width={Math.max(width - offset.left - offset.right, 0)}
|
||||
|
@ -7,15 +7,21 @@ import { FunctionalValue, svgToDataURL, useFunctionalValue } from '@utils'
|
||||
import { BasePluginSettings } from './base'
|
||||
|
||||
export type D3ContextMenuSettings = {
|
||||
/** Метод или объект отрисовки пунктов выпадающего меню */
|
||||
overlay?: FunctionalValue<ReactElement | null, [SVGSVGElement | null]>
|
||||
/** Название графика для загрузки */
|
||||
downloadFilename?: string
|
||||
/** Событие, вызываемое при нажатий кнопки "Обновить" */
|
||||
onUpdate?: () => void
|
||||
/** Дополнительные пункты меню */
|
||||
additionalMenuItems?: ItemType[]
|
||||
/** Условия вызова контекстного меню */
|
||||
trigger?: ('click' | 'hover' | 'contextMenu')[]
|
||||
}
|
||||
|
||||
export type D3ContextMenuProps = BasePluginSettings & D3ContextMenuSettings & {
|
||||
children: any
|
||||
/** SVG-элемент */
|
||||
svg: SVGSVGElement | null
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import { wrapPlugin } from './base'
|
||||
import '@styles/d3.less'
|
||||
|
||||
export type D3CursorSettings = {
|
||||
/** Параметры стиля линии */
|
||||
lineStyle?: SVGProps<SVGGElement>
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { AreaChartOutlined, BarChartOutlined, BorderOuterOutlined, DotChartOutlined, LineChartOutlined } from '@ant-design/icons'
|
||||
import { CSSProperties, ReactNode, SVGProps, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import { useD3MouseZone } from '@components/d3/D3MouseZone'
|
||||
import { ChartGroup, ChartSizes } from '@components/d3/D3MonitoringCharts'
|
||||
import { isDev, usePartialProps } from '@utils'
|
||||
import { getChartIcon, isDev, usePartialProps } from '@utils'
|
||||
|
||||
import { wrapPlugin } from './base'
|
||||
import { D3TooltipPosition } from './D3Tooltip'
|
||||
@ -40,22 +39,13 @@ const offsetY = 5
|
||||
const makeDefaultRender = <DataType,>(): D3GroupRenderFunction<DataType> => (group, data) => (
|
||||
<>
|
||||
{data.length > 0 ? group.charts.map((chart) => {
|
||||
let Icon
|
||||
switch (chart.type) {
|
||||
case 'needle': Icon = BarChartOutlined; break
|
||||
case 'line': Icon = LineChartOutlined; break
|
||||
case 'point': Icon = DotChartOutlined; break
|
||||
case 'area': Icon = AreaChartOutlined; break
|
||||
case 'rect_area': Icon = BorderOuterOutlined; break
|
||||
}
|
||||
|
||||
const xFormat = (d: number | Date) => chart.xAxis.format?.(d) ?? `${(+d).toFixed(2)} ${chart.xAxis.unit ?? ''}`
|
||||
const yFormat = (d: number) => chart.yAxis.format?.(d) ?? `${d?.toFixed(2)} ${chart.yAxis.unit ?? ''}`
|
||||
|
||||
return (
|
||||
<div className={'tooltip-group'} key={chart.key}>
|
||||
<div className={'group-label'}>
|
||||
<Icon style={{ color: chart.color }} />
|
||||
{getChartIcon(chart)}
|
||||
<span>{chart.shortLabel || chart.label}:</span>
|
||||
</div>
|
||||
{data.map((d, i) => (
|
||||
@ -89,10 +79,12 @@ const _D3HorizontalCursor = <DataType,>({
|
||||
const zoneRef = useRef(null)
|
||||
|
||||
const { mouseState, zoneRect, subscribe } = useD3MouseZone()
|
||||
|
||||
const [position, setPosition] = useState<D3TooltipPosition>(_position ?? 'bottom')
|
||||
const [tooltipBodies, setTooltipBodies] = useState<ReactNode[]>([])
|
||||
const [tooltipY, setTooltipY] = useState(0)
|
||||
const [fixed, setFixed] = useState(false)
|
||||
const [lineY, setLineY] = useState(0)
|
||||
|
||||
const lineStyle = usePartialProps(_lineStyle, defaultLineStyle)
|
||||
|
||||
@ -157,8 +149,6 @@ const _D3HorizontalCursor = <DataType,>({
|
||||
setTooltipY(top)
|
||||
}, [sizes.chartsHeight, height, mouseState, fixed])
|
||||
|
||||
const [lineY, setLineY] = useState<number>(0)
|
||||
|
||||
useEffect(() => {
|
||||
if (fixed || !mouseState.visible) return
|
||||
setLineY(mouseState.y)
|
||||
|
@ -11,21 +11,29 @@ import { wrapPlugin } from './base'
|
||||
export type LegendPosition = 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center'
|
||||
|
||||
export type D3LegendSettings = {
|
||||
/** Высота блока легенды графика в пикселях */
|
||||
width?: number
|
||||
/** Ширина блока легенды графика в пикселях */
|
||||
height?: number
|
||||
/** Отступ блока в зависимости от положения */
|
||||
offset?: {
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
/** Положение блока легенды на SVG */
|
||||
position?: LegendPosition
|
||||
/** Цвет текста в блоке легенды */
|
||||
color?: Property.Color
|
||||
/** Цвет фона блока легенды */
|
||||
backgroundColor?: Property.Color
|
||||
/** Тип позиционирования записей в блоке легенды */
|
||||
type?: 'horizontal' | 'vertical'
|
||||
}
|
||||
|
||||
const defaultOffset = { x: 10, y: 10 }
|
||||
|
||||
export type D3LegendProps<DataType> = D3LegendSettings & {
|
||||
/** Массив графиков */
|
||||
charts: ChartRegistry<DataType>[]
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { AreaChartOutlined, BarChartOutlined, BorderOuterOutlined, DotChartOutlined, LineChartOutlined } from '@ant-design/icons'
|
||||
import { CSSProperties, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import { isDev } from '@utils'
|
||||
|
||||
import { D3MouseState, useD3MouseZone } from '../D3MouseZone'
|
||||
import { ChartRegistry } from '../types'
|
||||
import { ChartRegistry } from '@components/d3/types'
|
||||
import { D3MouseState, useD3MouseZone } from '@components/d3/D3MouseZone'
|
||||
import { getTouchedElements, wrapPlugin } from './base'
|
||||
|
||||
import '@styles/d3.less'
|
||||
@ -13,20 +13,29 @@ import '@styles/d3.less'
|
||||
export type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
|
||||
|
||||
export type D3RenderData<DataType> = {
|
||||
/** Параметры графика */
|
||||
chart: ChartRegistry<DataType>
|
||||
/** Данные графика */
|
||||
data: DataType[]
|
||||
/** D3 выборка элементов графика */
|
||||
selection?: d3.Selection<any, DataType, any, any>
|
||||
}
|
||||
|
||||
export type D3RenderFunction<DataType> = (data: D3RenderData<DataType>[], mouseState: D3MouseState) => ReactNode
|
||||
|
||||
export type D3TooltipSettings<DataType> = {
|
||||
/** Функция отрисоки тултипа */
|
||||
render?: D3RenderFunction<DataType>
|
||||
/** Ширина тултипа */
|
||||
width?: number | string
|
||||
/** Высота тултипа */
|
||||
height?: number | string
|
||||
/** CSS-стиль тултипа */
|
||||
style?: CSSProperties
|
||||
/** Положение тултипа */
|
||||
position?: D3TooltipPosition
|
||||
className?: string
|
||||
/** Допуск к точке до срабатывания тултипа */
|
||||
limit?: number
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ export const renderArea = <DataType extends Record<string, unknown>>(
|
||||
): DataType[] => {
|
||||
if (chart.type !== 'area') return data
|
||||
|
||||
let area = d3.area()
|
||||
let area = d3.area<DataType>()
|
||||
|
||||
if ('y0' in chart) {
|
||||
area = area.y0(yAxis(chart.y0 ?? 0))
|
||||
|
@ -11,7 +11,7 @@ export const renderLine = <DataType extends Record<string, unknown>>(
|
||||
): DataType[] => {
|
||||
if (chart.type !== 'line') return data
|
||||
|
||||
let line = d3.line()
|
||||
let line = d3.line<DataType>()
|
||||
.x(d => xAxis(chart.x(d)))
|
||||
.y(d => yAxis(chart.y(d)))
|
||||
|
||||
@ -38,7 +38,7 @@ export const renderLine = <DataType extends Record<string, unknown>>(
|
||||
chart().selectAll('path')
|
||||
.transition()
|
||||
.duration(chart.animDurationMs ?? 0)
|
||||
.attr('d', line(data as any))
|
||||
.attr('d', line(data))
|
||||
.attr('stroke-dasharray', String(chart.dash ?? ''))
|
||||
|
||||
return data
|
||||
|
@ -20,13 +20,13 @@ export const renderNeedle = <DataType extends Record<string, unknown>>(
|
||||
currentNeedles.enter().append('line')
|
||||
|
||||
chart()
|
||||
.selectAll('line')
|
||||
.selectAll<SVGLineElement, DataType>('line')
|
||||
.transition()
|
||||
.duration(chart.animDurationMs ?? 0)
|
||||
.attr('x1', (d: any) => xAxis(chart.x(d)))
|
||||
.attr('x2', (d: any) => xAxis(chart.x(d)))
|
||||
.attr('x1', (d) => xAxis(chart.x(d)))
|
||||
.attr('x2', (d) => xAxis(chart.x(d)))
|
||||
.attr('y1', height - offset.bottom - offset.top)
|
||||
.attr('y2', (d: any) => yAxis(chart.y(d)))
|
||||
.attr('y2', (d) => yAxis(chart.y(d)))
|
||||
.attr('stroke-dasharray', String(chart.dash ?? ''))
|
||||
|
||||
return data
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { getByAccessor } from '../functions'
|
||||
import { ChartRegistry } from '../types'
|
||||
import { getByAccessor } from '@components/d3/functions'
|
||||
import { ChartRegistry } from '@components/d3/types'
|
||||
|
||||
export const renderRectArea = <DataType extends Record<string, any>>(
|
||||
xAxis: (value: d3.NumberValue) => number,
|
||||
|
@ -1,70 +1,102 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Property } from 'csstype'
|
||||
|
||||
import {
|
||||
D3TooltipSettings
|
||||
} from './plugins'
|
||||
import { D3TooltipSettings } from './plugins'
|
||||
|
||||
export type AxisAccessor<DataType extends Record<string, any>> = keyof DataType | ((d: DataType) => any)
|
||||
|
||||
export type ChartAxis<DataType> = {
|
||||
/** Тип шкалы */
|
||||
type: 'linear' | 'time',
|
||||
/** Ключ записи или метод по которому будет извлекаться значение оси из массива данных */
|
||||
accessor: AxisAccessor<DataType>
|
||||
/** Единица измерения, отображаемая рядом со значением */
|
||||
unit?: ReactNode
|
||||
/** Метод форматирования значения оси */
|
||||
format?: (v: d3.NumberValue) => ReactNode
|
||||
}
|
||||
|
||||
export type PointChartDataset = {
|
||||
type: 'point'
|
||||
/** Радиус точек */
|
||||
radius?: number
|
||||
/** Форма точек */
|
||||
shape?: 'circle' | 'line'
|
||||
/** Цвет обводки точек */
|
||||
strokeColor?: Property.Color
|
||||
/** Толщина обводки */
|
||||
strokeWidth?: number | string
|
||||
/** Прозрачность обводки */
|
||||
strokeOpacity?: number
|
||||
/** Цвет заполнения */
|
||||
fillColor?: Property.Color
|
||||
/** Прозрачность заполнения */
|
||||
fillOpacity?: number
|
||||
}
|
||||
|
||||
export type BaseChartDataset<DataType> = {
|
||||
/** Уникальный ключ графика */
|
||||
key: string | number
|
||||
/** Параметры вертикальной оси */
|
||||
yAxis: ChartAxis<DataType>
|
||||
/** Параметры горизонтальной оси */
|
||||
xAxis: ChartAxis<DataType>
|
||||
|
||||
/** Название графика */
|
||||
label?: ReactNode
|
||||
/** Короткое название графика */
|
||||
shortLabel?: ReactNode
|
||||
/** Цвет графика */
|
||||
color?: Property.Color
|
||||
/** Прозрачность */
|
||||
opacity?: number
|
||||
/** Ширина */
|
||||
width?: number | string
|
||||
tooltip?: D3TooltipSettings<DataType>
|
||||
/** Длительность анимаций для графики */
|
||||
animDurationMs?: number
|
||||
/** Парамаетры точек графика */
|
||||
point?: Omit<PointChartDataset, 'type'>
|
||||
afterDraw?: (d: any) => void
|
||||
/** Событие, вызываемое после отрисовки графика */
|
||||
afterDraw?: (d: ChartRegistry<DataType>) => void
|
||||
/** Параметры штриховки графика */
|
||||
dash?: string | number | [string | number, string | number]
|
||||
}
|
||||
|
||||
export type AreaChartDataset = {
|
||||
type: 'area'
|
||||
x0?: number
|
||||
y0?: number
|
||||
areaColor?: Property.Color
|
||||
nullValues?: 'skip' | 'gap' | 'none'
|
||||
optimization?: boolean
|
||||
}
|
||||
|
||||
export type RectArea<DataType extends Record<string, number>> = {
|
||||
type: 'rect_area'
|
||||
minXAccessor?: AxisAccessor<DataType>
|
||||
maxXAccessor?: AxisAccessor<DataType>
|
||||
minYAccessor?: AxisAccessor<DataType>
|
||||
maxYAccessor?: AxisAccessor<DataType>
|
||||
data?: DataType[]
|
||||
/** Привязка домена к домену другого графика */
|
||||
bindDomainFrom?: string | number
|
||||
}
|
||||
|
||||
export type LineChartDataset = {
|
||||
type: 'line'
|
||||
/** Обработка NULL значений */
|
||||
nullValues?: 'skip' | 'gap' | 'none'
|
||||
/** Оптимизация одинаковых точек */
|
||||
optimization?: boolean
|
||||
}
|
||||
|
||||
export type AreaChartDataset = Omit<LineChartDataset, 'type'> & {
|
||||
type: 'area'
|
||||
/** Константная граница по горизонтальной оси (если не задана не отображается) */
|
||||
x0?: number
|
||||
/** Константная граница по вертикальной оси (если не задана не отображается) */
|
||||
y0?: number
|
||||
/** Цвет заполнения */
|
||||
areaColor?: Property.Color
|
||||
}
|
||||
|
||||
export type RectArea<DataType extends Record<string, number>> = {
|
||||
type: 'rect_area'
|
||||
/** Акцессор минимального значения по горизонтальной оси */
|
||||
minXAccessor?: AxisAccessor<DataType>
|
||||
/** Акцессор максимального значения по горизонтальной оси */
|
||||
maxXAccessor?: AxisAccessor<DataType>
|
||||
/** Акцессор минимального значения по вертикальной оси */
|
||||
minYAccessor?: AxisAccessor<DataType>
|
||||
/** Акцессор максимального значения по вертикальной оси */
|
||||
maxYAccessor?: AxisAccessor<DataType>
|
||||
/** Дополнительные данные */
|
||||
data?: DataType[]
|
||||
}
|
||||
|
||||
export type NeedleChartDataset = {
|
||||
type: 'needle'
|
||||
}
|
||||
@ -77,35 +109,56 @@ export type ChartDataset<DataType> = BaseChartDataset<DataType> & (
|
||||
RectArea<Record<string, any>>
|
||||
)
|
||||
|
||||
export type MinMax = { min?: number, max?: number }
|
||||
export type MinMax = {
|
||||
/** Минимальное значение */
|
||||
min?: number
|
||||
/** Максимальное значение */
|
||||
max?: number
|
||||
}
|
||||
|
||||
export type ChartDomain = {
|
||||
/** Отображаемый диапозон по горизонтальной оси */
|
||||
x?: MinMax
|
||||
/** Отображаемый диапозон по вертикальной оси */
|
||||
y?: MinMax
|
||||
}
|
||||
|
||||
export type ChartOffset = {
|
||||
/** Отступ сверху */
|
||||
top: number
|
||||
/** Отступ снизу */
|
||||
bottom: number
|
||||
/** Отступ слева */
|
||||
left: number
|
||||
/** Отступ справа */
|
||||
right: number
|
||||
}
|
||||
|
||||
export type ChartTick<DataType> = {
|
||||
/** Отображать ли риски на шкале */
|
||||
visible?: boolean,
|
||||
/** Колличество рисок */
|
||||
count?: number | d3.TimeInterval,
|
||||
/** Формат значений на рисках */
|
||||
format?: (d: d3.NumberValue, idx: number, data?: DataType) => string,
|
||||
/** Цвет шкалы */
|
||||
color?: Property.Color
|
||||
}
|
||||
|
||||
export type ChartTicks<DataType> = {
|
||||
/** Цвет шкал графика */
|
||||
color?: Property.Color
|
||||
/** Параметры шкалы горизонтальной оси */
|
||||
x?: ChartTick<DataType>
|
||||
/** Параметры шкалы вертикальной оси */
|
||||
y?: ChartTick<DataType>
|
||||
}
|
||||
|
||||
export type ChartRegistry<DataType> = ChartDataset<DataType> & {
|
||||
/** Получить D3 выборку, содержащую корневой G-элемент графика */
|
||||
(): d3.Selection<SVGGElement, DataType, any, any>
|
||||
y: (value: any) => number
|
||||
x: (value: any) => number
|
||||
/** Получить значение по вертикальной оси из предоставленой записи */
|
||||
y: (value: DataType) => number
|
||||
/** Получить значение по горизонтальной оси из предоставленой записи */
|
||||
x: (value: DataType) => number
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker'
|
||||
import { formatDate, range, wrapPrivateComponent } from '@utils'
|
||||
import { TelemetryDataSaubService } from '@api'
|
||||
|
||||
import { chartGroups, normalizeData } from '../TelemetryView'
|
||||
import { makeChartGroups, normalizeData } from '../TelemetryView'
|
||||
import cursorRender from '../TelemetryView/cursorRender'
|
||||
|
||||
const DATA_COUNT = 2048 // Колличество точек на подгрузку графика
|
||||
@ -101,6 +101,8 @@ export const cutData = (data, beginDate, endDate) => {
|
||||
return data
|
||||
}
|
||||
|
||||
const chartGroups = makeChartGroups([])
|
||||
|
||||
const Archive = memo(() => {
|
||||
const [dataSaub, setDataSaub] = useState([])
|
||||
const [dateLimit, setDateLimit] = useState({ from: 0, to: new Date() })
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Select, DatePicker, Input, Tooltip } from 'antd'
|
||||
import { useState, useEffect, memo, useCallback, useMemo } from 'react'
|
||||
import { Select, DatePicker, Input, Tooltip } from 'antd'
|
||||
import { LinkOutlined } from '@ant-design/icons'
|
||||
import { Link } from 'react-router-dom'
|
||||
import moment from 'moment'
|
||||
|
||||
@ -10,7 +11,6 @@ import { makeColumn, makeDateColumn, makeNumericColumn, makeNumericSorter, makeT
|
||||
import { wrapPrivateComponent } from '@utils'
|
||||
import { MessageService } from '@api'
|
||||
|
||||
import {LinkOutlined} from '@ant-design/icons'
|
||||
import '@styles/message.css'
|
||||
|
||||
const pageSize = 26
|
||||
|
@ -58,37 +58,59 @@ const makeDataset = (label, shortLabel, color, key, unit, other) => ({
|
||||
...other,
|
||||
})
|
||||
|
||||
export const chartGroups = [
|
||||
export const makeChartGroups = (flowChart) => {
|
||||
const makeAreaOptions = (accessor) => ({
|
||||
type: 'rect_area',
|
||||
data: flowChart,
|
||||
hideLabel: true,
|
||||
yAxis: {
|
||||
type: 'linear',
|
||||
accessor: 'depth',
|
||||
},
|
||||
minXAccessor: 'depthStart',
|
||||
maxXAccessor: 'depthEnd',
|
||||
minYAccessor: accessor + 'Min',
|
||||
maxYAccessor: accessor + 'Max',
|
||||
bindDomainFrom: accessor,
|
||||
})
|
||||
console.log(flowChart)
|
||||
return [
|
||||
[
|
||||
makeDataset("Высота блока", "Высота ТБ","#303030", "blockPosition", "м"),
|
||||
makeDataset("Глубина скважины", "Глубина скв","#7789A1", "wellDepth", "м", { dash }),
|
||||
makeDataset("Расход", "Расход","#007070", "flow", "л/с"),
|
||||
makeDataset("Положение долота", "Долото","#B39D59", "bitPosition", "м"),
|
||||
makeDataset('Высота блока', 'Высота ТБ','#303030', 'blockPosition', 'м'),
|
||||
makeDataset('Глубина скважины', 'Глубина скв','#7789A1', 'wellDepth', 'м', { dash }),
|
||||
makeDataset('Расход', 'Расход','#007070', 'flow', 'л/с'),
|
||||
makeDataset('Положение долота', 'Долото','#B39D59', 'bitPosition', 'м'),
|
||||
makeDataset('Расход', 'Расход','#007070', 'flowMM', 'л/с', makeAreaOptions('flow')),
|
||||
], [
|
||||
makeDataset("Скорость блока", "Скорость ТБ","#59B359", "blockSpeed", "м/ч"),
|
||||
makeDataset("Скорость заданная", "Скор зад-я","#95B359", "blockSpeedSp", "м/ч", { dash }),
|
||||
makeDataset('Скорость блока', 'Скорость ТБ','#59B359', 'blockSpeed', 'м/ч'),
|
||||
makeDataset('Скорость заданная', 'Скор зад-я','#95B359', 'blockSpeedSp', 'м/ч', { dash }),
|
||||
], [
|
||||
makeDataset("Давление", "Давл","#FF0000", "pressure", "атм"),
|
||||
makeDataset("Давление заданное", "Давл зад-е","#CC0000", "pressureSp", "атм"),
|
||||
makeDataset("Давление ХХ", "Давл ХХ","#CC4429", "pressureIdle", "атм", { dash }),
|
||||
makeDataset("Перепад давления максимальный", "ΔР макс","#B34A36", "pressureDeltaLimitMax", "атм", { dash }),
|
||||
makeDataset('Давление', 'Давл','#FF0000', 'pressure', 'атм'),
|
||||
makeDataset('Давление заданное', 'Давл зад-е','#CC0000', 'pressureSp', 'атм'),
|
||||
makeDataset('Давление ХХ', 'Давл ХХ','#CC4429', 'pressureIdle', 'атм', { dash }),
|
||||
makeDataset('Перепад давления максимальный', 'ΔР макс','#B34A36', 'pressureDeltaLimitMax', 'атм', { dash }),
|
||||
makeDataset('Давление', 'Давл','#FF0000', 'pressureMM', 'атм', makeAreaOptions('pressure')),
|
||||
], [
|
||||
makeDataset("Осевая нагрузка", "Нагр","#0000CC", "axialLoad", "т"),
|
||||
makeDataset("Осевая нагрузка заданная", "Нагр зад-я","#3D6DCC", "axialLoadSp", "т", { dash }),
|
||||
makeDataset("Осевая нагрузка максимальная", "Нагр макс","#3D3DCC", "axialLoadLimitMax", "т", { dash }),
|
||||
makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoad', 'т'),
|
||||
makeDataset('Осевая нагрузка заданная', 'Нагр зад-я','#3D6DCC', 'axialLoadSp', 'т', { dash }),
|
||||
makeDataset('Осевая нагрузка максимальная', 'Нагр макс','#3D3DCC', 'axialLoadLimitMax', 'т', { dash }),
|
||||
makeDataset('Осевая нагрузка', 'Нагр','#0000CC', 'axialLoadMM', 'т', makeAreaOptions('axialLoad')),
|
||||
], [
|
||||
makeDataset("Вес на крюке", "Вес на крюке","#00B3B3", "hookWeight", "т"),
|
||||
makeDataset("Вес инструмента ХХ", "Вес инст ХХ","#29CCB1", "hookWeightIdle", "т", { dash }),
|
||||
makeDataset("Вес инструмента минимальный", "Вес инст мин","#47A1B3", "hookWeightLimitMin", "т", { dash }),
|
||||
makeDataset("Вес инструмента максимальный", "Вес инст мах","#2D7280", "hookWeightLimitMax", "т", { dash }),
|
||||
makeDataset("Обороты ротора", "Об ротора","#11B32F", "rotorSpeed", "об/мин"),
|
||||
makeDataset('Вес на крюке', 'Вес на крюке','#00B3B3', 'hookWeight', 'т'),
|
||||
makeDataset('Вес инструмента ХХ', 'Вес инст ХХ','#29CCB1', 'hookWeightIdle', 'т', { dash }),
|
||||
makeDataset('Вес инструмента минимальный', 'Вес инст мин','#47A1B3', 'hookWeightLimitMin', 'т', { dash }),
|
||||
makeDataset('Вес инструмента максимальный', 'Вес инст мах','#2D7280', 'hookWeightLimitMax', 'т', { dash }),
|
||||
makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeed', 'об/мин'),
|
||||
makeDataset('Обороты ротора', 'Об ротора','#11B32F', 'rotorSpeedMM', 'об/мин', makeAreaOptions('rotorSpeed')),
|
||||
], [
|
||||
makeDataset("Момент на роторе", "Момент","#990099", "rotorTorque", "кН·м"),
|
||||
makeDataset("План. Момент на роторе", "Момент зад-й","#9629CC", "rotorTorqueSp", "кН·м", { dash }),
|
||||
makeDataset("Момент на роторе х.х.", "Момень ХХ","#CC2996", "rotorTorqueIdle", "кН·м", { dash }),
|
||||
makeDataset("Момент максимальный", "Момент макс","#FF00FF", "rotorTorqueLimitMax", "кН·м", { dash }),
|
||||
makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorque', 'кН·м'),
|
||||
makeDataset('План. Момент на роторе', 'Момент зад-й','#9629CC', 'rotorTorqueSp', 'кН·м', { dash }),
|
||||
makeDataset('Момент на роторе х.х.', 'Момень ХХ','#CC2996', 'rotorTorqueIdle', 'кН·м', { dash }),
|
||||
makeDataset('Момент максимальный', 'Момент макс','#FF00FF', 'rotorTorqueLimitMax', 'кН·м', { dash }),
|
||||
makeDataset('Момент на роторе', 'Момент','#990099', 'rotorTorqueMM', 'кН·м', makeAreaOptions('rotorTorque')),
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
const getLast = (data) =>
|
||||
Array.isArray(data) ? data.at(-1) : data
|
||||
@ -214,6 +236,8 @@ const TelemetryView = memo(() => {
|
||||
return dataSaub.slice(i, j)
|
||||
}, [dataSaub, domain])
|
||||
|
||||
const chartGroups = useMemo(() => makeChartGroups(flowChartData), [flowChartData])
|
||||
|
||||
return (
|
||||
<LoaderPortal show={showLoader}>
|
||||
<Grid style={{ gridTemplateColumns: 'auto repeat(6, 1fr)' }}>
|
||||
|
@ -23,7 +23,7 @@ export const getDistance = (x1: number, y1: number, x2: number, y2: number, type
|
||||
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))
|
||||
}
|
||||
|
||||
export const getChartIcon = <DataType,>(chart: ChartDataset<DataType>, options: Omit<AntdIconProps, 'ref'>) => {
|
||||
export const getChartIcon = <DataType,>(chart: ChartDataset<DataType>, options?: Omit<AntdIconProps, 'ref'>) => {
|
||||
let Icon
|
||||
switch (chart.type) {
|
||||
case 'needle': Icon = BarChartOutlined; break
|
||||
|
Loading…
Reference in New Issue
Block a user