Типы компонентов D3 задокументированы

This commit is contained in:
goodmice 2022-07-25 14:30:24 +05:00
parent ebec1957e7
commit 121ec4f50b
17 changed files with 307 additions and 160 deletions

View File

@ -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
}
}

View File

@ -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>

View File

@ -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)}

View File

@ -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
}

View File

@ -9,6 +9,7 @@ import { wrapPlugin } from './base'
import '@styles/d3.less'
export type D3CursorSettings = {
/** Параметры стиля линии */
lineStyle?: SVGProps<SVGGElement>
}

View File

@ -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)

View File

@ -11,25 +11,33 @@ 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>[]
}
const _D3Legend = <DataType, >({
const _D3Legend = <DataType,>({
charts,
width,
height,

View File

@ -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
}

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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
}

View File

@ -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() })

View File

@ -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

View File

@ -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)' }}>

View File

@ -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