forked from ddrilling/asb_cloud_front
* Типы графиков перенесены в отдельный файл
* Закончена работа с тултипами (кроме nearest)
This commit is contained in:
parent
5109d73013
commit
571d8de440
@ -1,7 +1,7 @@
|
||||
import { memo, ReactNode, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useElementSize } from 'usehooks-ts'
|
||||
import { Empty } from 'antd'
|
||||
import { Property } from 'csstype'
|
||||
import { Empty } from 'antd'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import LoaderPortal from '@components/LoaderPortal'
|
||||
@ -15,103 +15,11 @@ import {
|
||||
D3Cursor,
|
||||
D3CursorSettings,
|
||||
D3Tooltip,
|
||||
D3TooltipSettings
|
||||
D3TooltipSettings,
|
||||
} from './plugins'
|
||||
|
||||
import '@styles/d3.less'
|
||||
|
||||
type DefaultDataType = Record<string, any>
|
||||
|
||||
export type ChartAxis<DataType> = {
|
||||
type: 'linear' | 'time',
|
||||
accessor: keyof DataType | ((d: DataType) => any)
|
||||
}
|
||||
|
||||
export type BaseChartDataset<DataType> = {
|
||||
key: string | number
|
||||
label?: ReactNode
|
||||
yAxis: ChartAxis<DataType>
|
||||
color?: Property.Color
|
||||
opacity?: number
|
||||
width?: Property.StrokeWidth
|
||||
tooltip?: D3TooltipSettings<DataType>
|
||||
animDurationMs?: number
|
||||
afterDraw?: (d: any) => void
|
||||
}
|
||||
|
||||
export type LineChartDataset<DataType> = {
|
||||
type: 'line'
|
||||
point?: {
|
||||
radius?: number
|
||||
color?: Property.Color
|
||||
}
|
||||
nullValues?: 'skip' | 'gap' | 'none'
|
||||
optimization?: boolean
|
||||
}
|
||||
|
||||
export type AreaChartDataset<DataType> = {
|
||||
type: 'area'
|
||||
fillColor?: Property.Color
|
||||
point?: {
|
||||
radius?: number
|
||||
color?: Property.Color
|
||||
}
|
||||
}
|
||||
|
||||
export type NeedleChartDataset<DataType> = {
|
||||
type: 'needle'
|
||||
}
|
||||
|
||||
export type ChartDataset<DataType> = BaseChartDataset<DataType> & (
|
||||
AreaChartDataset<DataType> |
|
||||
LineChartDataset<DataType> |
|
||||
NeedleChartDataset<DataType>
|
||||
)
|
||||
|
||||
export type ChartDomain = {
|
||||
x: { min?: number, max?: number }
|
||||
y: { min?: number, max?: number }
|
||||
}
|
||||
|
||||
export type ChartOffset = {
|
||||
top: number
|
||||
bottom: number
|
||||
left: number
|
||||
right: number
|
||||
}
|
||||
|
||||
export type ChartTicks = {
|
||||
color?: Property.Color
|
||||
x?: { visible?: boolean, count?: number }
|
||||
y?: { visible?: boolean, count?: number }
|
||||
}
|
||||
|
||||
export type D3ChartProps<DataType = DefaultDataType> = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
xAxis: ChartAxis<DataType>
|
||||
datasets: ChartDataset<DataType>[]
|
||||
data?: DataType[]
|
||||
domain?: Partial<ChartDomain>
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
loading?: boolean
|
||||
offset?: Partial<ChartOffset>
|
||||
mode: 'horizontal' | 'vertical'
|
||||
animDurationMs?: number
|
||||
backgroundColor?: Property.Color
|
||||
ticks?: ChartTicks
|
||||
plugins?: {
|
||||
menu?: BasePluginSettings & D3ContextMenuSettings
|
||||
tooltip?: BasePluginSettings & D3TooltipSettings<DataType>
|
||||
cursor?: BasePluginSettings & D3CursorSettings
|
||||
}
|
||||
}
|
||||
|
||||
type Selection = d3.Selection<any, any, null, undefined>
|
||||
|
||||
type ChartRegistry<DataType = DefaultDataType> = ChartDataset<DataType> & {
|
||||
(): Selection
|
||||
y: (value: any) => number
|
||||
}
|
||||
import { ChartAxis, ChartDataset, ChartDomain, ChartOffset, ChartRegistry, ChartTicks, DefaultDataType } from './types'
|
||||
|
||||
const defaultOffsets: ChartOffset = {
|
||||
top: 10,
|
||||
@ -125,10 +33,6 @@ const defaultXAxisConfig: ChartAxis<DefaultDataType> = {
|
||||
accessor: (d: any) => new Date(d.date)
|
||||
}
|
||||
|
||||
const defaultCursorPlugin: BasePluginSettings & D3CursorSettings = {
|
||||
enabled: true,
|
||||
}
|
||||
|
||||
const getGroupClass = (key: string | number) => `chart-id-${key}`
|
||||
|
||||
const getByAccessor = <T extends Record<string, any>>(accessor: string | ((d: T) => any)) => {
|
||||
@ -143,7 +47,26 @@ const createAxis = <DataType,>(config: ChartAxis<DataType>) => {
|
||||
return d3.scaleLinear()
|
||||
}
|
||||
|
||||
export const D3Chart = memo<D3ChartProps>(({
|
||||
export type D3ChartProps<DataType = DefaultDataType> = React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
xAxis: ChartAxis<DataType>
|
||||
datasets: ChartDataset<DataType>[]
|
||||
data?: DataType[]
|
||||
domain?: Partial<ChartDomain>
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
loading?: boolean
|
||||
offset?: Partial<ChartOffset>
|
||||
animDurationMs?: number
|
||||
backgroundColor?: Property.Color
|
||||
ticks?: ChartTicks
|
||||
plugins?: {
|
||||
menu?: BasePluginSettings & D3ContextMenuSettings
|
||||
tooltip?: BasePluginSettings & D3TooltipSettings<DataType>
|
||||
cursor?: BasePluginSettings & D3CursorSettings
|
||||
}
|
||||
}
|
||||
|
||||
export const D3Chart = memo<D3ChartProps<DefaultDataType>>(({
|
||||
className = '',
|
||||
xAxis: _xAxisConfig,
|
||||
datasets,
|
||||
@ -153,7 +76,6 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
height: givenHeight = '100%',
|
||||
loading,
|
||||
offset: _offset,
|
||||
mode = 'horizontal',
|
||||
animDurationMs = 200,
|
||||
backgroundColor = 'transparent',
|
||||
ticks,
|
||||
@ -174,7 +96,7 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
|
||||
const getX = useMemo(() => getByAccessor(xAxisConfig.accessor), [xAxisConfig.accessor])
|
||||
|
||||
const [charts, setCharts] = useState<ChartRegistry[]>([])
|
||||
const [charts, setCharts] = useState<ChartRegistry<DefaultDataType>[]>([])
|
||||
|
||||
const [rootRef, { width, height }] = useElementSize()
|
||||
|
||||
@ -272,7 +194,9 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
() => chartArea().select('.' + getGroupClass(dataset.key)),
|
||||
{
|
||||
...dataset,
|
||||
y: getByAccessor(dataset.yAxis.accessor)
|
||||
xAxis: dataset.xAxis ?? xAxisConfig,
|
||||
y: getByAccessor(dataset.yAxis.accessor),
|
||||
x: getByAccessor(dataset.xAxis?.accessor ?? xAxisConfig.accessor),
|
||||
}
|
||||
)
|
||||
|
||||
@ -293,7 +217,8 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
|
||||
charts.forEach((chart) => {
|
||||
chart()
|
||||
.attr('stroke', String(chart.color))
|
||||
.attr('color', chart.color ?? null)
|
||||
.attr('stroke', 'currentColor')
|
||||
.attr('stroke-width', chart.width ?? 1)
|
||||
.attr('opacity', chart.opacity ?? 1)
|
||||
.attr('fill', 'none')
|
||||
@ -305,7 +230,7 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
case 'needle':
|
||||
elms = chart()
|
||||
.selectAll('line')
|
||||
.data(data)
|
||||
.data(d)
|
||||
|
||||
elms.exit().remove()
|
||||
elms.enter().append('line')
|
||||
@ -314,15 +239,15 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
.selectAll('line')
|
||||
.transition()
|
||||
.duration(chart.animDurationMs ?? animDurationMs)
|
||||
.attr('x1', (d: any) => xAxis(getX(d)))
|
||||
.attr('x2', (d: any) => xAxis(getX(d)))
|
||||
.attr('x1', (d: any) => xAxis(chart.x(d)))
|
||||
.attr('x2', (d: any) => xAxis(chart.x(d)))
|
||||
.attr('y1', height - offset.bottom - offset.top)
|
||||
.attr('y2', (d: any) => yAxis(chart.y(d)))
|
||||
|
||||
break
|
||||
case 'line': {
|
||||
let line = d3.line()
|
||||
.x(d => xAxis(getX(d)))
|
||||
.x(d => xAxis(chart.x(d)))
|
||||
.y(d => yAxis(chart.y(d)))
|
||||
|
||||
switch (chart.nullValues || 'skip') {
|
||||
@ -341,14 +266,36 @@ export const D3Chart = memo<D3ChartProps>(({
|
||||
d = optimize(d)
|
||||
}
|
||||
|
||||
|
||||
if (chart().selectAll('path').empty())
|
||||
chart().append('path')
|
||||
|
||||
elms = chart().selectAll('path')
|
||||
chart().selectAll('path')
|
||||
.transition()
|
||||
.duration(chart.animDurationMs ?? animDurationMs)
|
||||
.attr('d', line(d as any))
|
||||
|
||||
const radius = chart.point?.radius ?? 3
|
||||
elms = chart()
|
||||
.selectAll('circle')
|
||||
.data(d)
|
||||
|
||||
elms.exit().remove()
|
||||
elms.enter().append('circle')
|
||||
|
||||
elms = chart()
|
||||
.selectAll('circle')
|
||||
.transition()
|
||||
.duration(chart.animDurationMs ?? animDurationMs)
|
||||
.attr('cx', (d: any) => xAxis(chart.x(d)))
|
||||
.attr('cy', (d: any) => yAxis(chart.y(d)))
|
||||
.attr('r', radius)
|
||||
.attr('stroke-width', chart.point?.strokeWidth ?? null)
|
||||
.attr('stroke', chart.point?.strokeColor ?? null)
|
||||
.attr('fill', chart.point?.fillColor ?? null)
|
||||
|
||||
elms = chart().selectAll()
|
||||
|
||||
break
|
||||
}
|
||||
default:
|
||||
|
@ -1,2 +1,4 @@
|
||||
export * from './D3Chart'
|
||||
export type { D3ChartProps, ChartAxis, ChartDataset, ChartDomain, ChartOffset } from './D3Chart'
|
||||
export type { D3ChartProps } from './D3Chart'
|
||||
|
||||
export * from './types'
|
||||
|
@ -1,22 +1,34 @@
|
||||
import { CSSProperties, ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { CSSProperties, ReactNode, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { BarChartOutlined, LineChartOutlined } from '@ant-design/icons'
|
||||
import * as d3 from 'd3'
|
||||
|
||||
import { isDev } from '@utils'
|
||||
import { formatDate, isDev } from '@utils'
|
||||
|
||||
import { D3MouseState, useD3MouseZone } from '../D3MouseZone'
|
||||
import { ChartRegistry, DefaultDataType } from '../types'
|
||||
import { wrapPlugin } from './base'
|
||||
|
||||
import '@styles/d3.less'
|
||||
|
||||
type D3TooltipPosition = 'bottom' | 'top' | 'left' | 'right' | 'none'
|
||||
type D3TooltipTouchType = 'x' | 'y' | 'all'
|
||||
|
||||
export type D3RenderData<DataType> = {
|
||||
chart: ChartRegistry<DataType>
|
||||
data: DataType[]
|
||||
selection: d3.Selection<any, DataType, any, any>
|
||||
}[]
|
||||
|
||||
export type D3RenderFunction<DataType> = (data: D3RenderData<DataType>, mouseState: D3MouseState) => ReactNode
|
||||
|
||||
export type BaseTooltip<DataType> = {
|
||||
render?: (data: DataType, target: d3.Selection<any, any, null, undefined>, mouseState: D3MouseState) => ReactNode
|
||||
render?: D3RenderFunction<DataType>
|
||||
width?: number | string
|
||||
height?: number | string
|
||||
style?: CSSProperties
|
||||
position?: D3TooltipPosition
|
||||
className?: string
|
||||
touchType?: D3TooltipTouchType
|
||||
}
|
||||
|
||||
export type AccurateTooltip = { type: 'accurate' }
|
||||
@ -27,49 +39,129 @@ export type NearestTooltip = {
|
||||
|
||||
export type D3TooltipSettings<DataType> = BaseTooltip<DataType> & (AccurateTooltip | NearestTooltip)
|
||||
|
||||
export type D3TooltipProps<DataType = Record<string, any>> = Partial<D3TooltipSettings<DataType>> & {
|
||||
charts: any[],
|
||||
}
|
||||
|
||||
const defaultRender = <DataType,>(data: DataType, target: any, mouseState: D3MouseState) => (
|
||||
const defaultRender: D3RenderFunction<Record<string, any>> = (data, mouseState) => (
|
||||
<>
|
||||
X: {mouseState.x} Y: {mouseState.y}
|
||||
<br/>
|
||||
Data: {JSON.stringify(data)}
|
||||
{data.length > 0 ? data.map(({ chart, data }) => {
|
||||
let Icon
|
||||
switch (chart.type) {
|
||||
case 'needle': Icon = BarChartOutlined; break
|
||||
case 'line': Icon = LineChartOutlined; break
|
||||
// case 'area': Icon = AreaChartOutlined; break
|
||||
// case 'dot': Icon = DotChartOutLined; break
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={'tooltip-group'} key={chart.key}>
|
||||
<div className={'group-label'}>
|
||||
<Icon style={{ color: chart.color }} />
|
||||
<span>{chart.label}:</span>
|
||||
</div>
|
||||
{data.map((d, i) => (
|
||||
<span key={`${i}`}>
|
||||
{formatDate(chart.x(d))} {chart.xAxis.unit} :: {chart.y(d).toFixed(2)} {chart.yAxis.unit}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}) : (
|
||||
<span>Данных нет</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
export type D3TooltipProps<DataType = DefaultDataType> = Partial<D3TooltipSettings<DataType>> & {
|
||||
charts: ChartRegistry<DataType>[],
|
||||
}
|
||||
|
||||
const getDistance = (x1: number, y1: number, x2: number, y2: number) =>
|
||||
Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2))
|
||||
|
||||
const makeIsCircleTouched = (x: number, y: number, limit: number) => function (this: d3.BaseType, d: any, i: number) {
|
||||
const elm = d3.select(this)
|
||||
const cx = +elm.attr('cx')
|
||||
const cy = +elm.attr('cy')
|
||||
const r = +elm.attr('r')
|
||||
|
||||
if (Number.isNaN(cx + cy + r)) return false
|
||||
const distance = getDistance(x, y, cx, cy)
|
||||
return (distance - r) <= (limit || 0)
|
||||
}
|
||||
|
||||
const makeIsLineTouched = (x: number, y: number, limit: number) => function (this: d3.BaseType, d: any, i: number) {
|
||||
const elm = d3.select(this)
|
||||
const dx = +elm.attr('x1')
|
||||
const y1 = +elm.attr('y1')
|
||||
const y2 = +elm.attr('y2')
|
||||
|
||||
if (Number.isNaN(dx + y1 + y2)) return false
|
||||
|
||||
const ymin = Math.min(y1, y2)
|
||||
const ymax = Math.max(y1, y2)
|
||||
|
||||
const pd = getDistance(x, y, dx, ymin) // Расстояние до верхней точки
|
||||
const distance = (ymin <= y && y <= ymax) ? Math.abs(x - dx) : pd
|
||||
|
||||
return distance <= (limit || 0)
|
||||
}
|
||||
|
||||
const getTouchedElements = <DataType,>(
|
||||
chart: ChartRegistry<DataType>,
|
||||
x: number,
|
||||
y: number,
|
||||
limit: number = 0,
|
||||
touchType: D3TooltipTouchType = 'all'
|
||||
): d3.Selection<any, DataType, any, any> => {
|
||||
let nodes: d3.Selection<any, any, any, any>
|
||||
switch (chart.type) {
|
||||
case 'line':
|
||||
nodes = chart().selectAll('circle')
|
||||
if (touchType === 'all')
|
||||
nodes = nodes.filter(makeIsCircleTouched(x, y, limit))
|
||||
break
|
||||
case 'needle':
|
||||
nodes = chart().selectAll('line')
|
||||
if (touchType === 'all')
|
||||
nodes = nodes.filter(makeIsLineTouched(x, y, limit))
|
||||
break
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
export const D3Tooltip = wrapPlugin<D3TooltipProps>(function D3Tooltip({
|
||||
type = 'accurate',
|
||||
width = 200,
|
||||
height = 100,
|
||||
height = 120,
|
||||
render = defaultRender,
|
||||
charts,
|
||||
position: _position = 'bottom',
|
||||
className,
|
||||
style: _style = {},
|
||||
touchType = 'all',
|
||||
...other
|
||||
}) {
|
||||
const { mouseState, zoneRect, subscribe } = useD3MouseZone()
|
||||
const [tooltipBody, setTooltipBody] = useState<any>()
|
||||
const [style, setStyle] = useState<CSSProperties>(_style)
|
||||
const [position, setPosition] = useState<D3TooltipPosition>(_position ?? 'bottom')
|
||||
const [visible, setVisible] = useState(false)
|
||||
const [fixed, setFixed] = useState(false)
|
||||
|
||||
const tooltipRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const onMiddleClick = useCallback((e: Event) => {
|
||||
if ((e as MouseEvent).button === 1 && visible)
|
||||
setFixed((prev) => !prev)
|
||||
}, [visible])
|
||||
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscribe('auxclick', (e) => {
|
||||
if ((e as MouseEvent).button === 1)
|
||||
setFixed((prev) => !prev)
|
||||
})
|
||||
const unsubscribe = subscribe('auxclick', onMiddleClick)
|
||||
|
||||
return () => {
|
||||
if (unsubscribe)
|
||||
if (!unsubscribe() && isDev())
|
||||
console.warn('Не удалось отвязать эвент')
|
||||
}
|
||||
}, [])
|
||||
}, [onMiddleClick])
|
||||
|
||||
useEffect(() => {
|
||||
if (!tooltipRef.current || !zoneRect || fixed) return
|
||||
@ -80,13 +172,12 @@ export const D3Tooltip = wrapPlugin<D3TooltipProps>(function D3Tooltip({
|
||||
...prev,
|
||||
left: -rect.width,
|
||||
top: -rect.height,
|
||||
opacity: 0,
|
||||
}))
|
||||
return
|
||||
}
|
||||
|
||||
const offsetX = -rect.width / 2 // По центру
|
||||
const offsetY = 30 // Чуть выше курсора
|
||||
const offsetY = 15 // Чуть выше курсора
|
||||
|
||||
const left = Math.max(0, Math.min(zoneRect.width - rect.width, mouseState.x + offsetX))
|
||||
const top = mouseState.y - offsetY - rect.height
|
||||
@ -95,14 +186,31 @@ export const D3Tooltip = wrapPlugin<D3TooltipProps>(function D3Tooltip({
|
||||
...prev,
|
||||
left,
|
||||
top,
|
||||
opacity: 1
|
||||
}))
|
||||
}, [tooltipRef.current, mouseState, zoneRect, fixed])
|
||||
|
||||
useEffect(() => {
|
||||
if (fixed) return
|
||||
setTooltipBody(render({}, d3.select('.nothing'), mouseState))
|
||||
}, [mouseState, charts, fixed])
|
||||
if (!mouseState.visible)
|
||||
return setVisible(false)
|
||||
|
||||
const data: D3RenderData<DefaultDataType> = []
|
||||
charts.forEach((chart) => {
|
||||
const touched = getTouchedElements(chart, mouseState.x, mouseState.y, 2, touchType)
|
||||
|
||||
if (touched.empty()) return
|
||||
|
||||
data.push({
|
||||
chart,
|
||||
data: touched.data(),
|
||||
selection: touched,
|
||||
})
|
||||
})
|
||||
|
||||
setVisible(data.length > 0)
|
||||
if (data.length > 0)
|
||||
setTooltipBody(render(data, mouseState))
|
||||
}, [charts, touchType, mouseState, fixed])
|
||||
|
||||
return (
|
||||
<foreignObject
|
||||
@ -110,8 +218,9 @@ export const D3Tooltip = wrapPlugin<D3TooltipProps>(function D3Tooltip({
|
||||
y={style.top}
|
||||
width={width}
|
||||
height={height}
|
||||
opacity={style.opacity}
|
||||
opacity={visible ? 1 : 0}
|
||||
pointerEvents={fixed ? 'all' : 'none'}
|
||||
style={{ transition: 'opacity .1s ease-out' }}
|
||||
>
|
||||
<div
|
||||
{...other}
|
||||
|
73
src/components/d3/types.ts
Normal file
73
src/components/d3/types.ts
Normal file
@ -0,0 +1,73 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Property } from 'csstype'
|
||||
|
||||
import {
|
||||
D3TooltipSettings
|
||||
} from './plugins'
|
||||
|
||||
export type DefaultDataType = Record<string, any>
|
||||
type Selection = d3.Selection<any, any, null, undefined>
|
||||
|
||||
export type ChartAxis<DataType> = {
|
||||
type: 'linear' | 'time',
|
||||
accessor: keyof DataType | ((d: DataType) => any)
|
||||
unit?: ReactNode
|
||||
}
|
||||
|
||||
export type BaseChartDataset<DataType> = {
|
||||
key: string | number
|
||||
label?: ReactNode
|
||||
yAxis: ChartAxis<DataType>
|
||||
xAxis: ChartAxis<DataType>
|
||||
color?: Property.Color
|
||||
opacity?: number
|
||||
width?: Property.StrokeWidth
|
||||
tooltip?: D3TooltipSettings<DataType>
|
||||
animDurationMs?: number
|
||||
afterDraw?: (d: any) => void
|
||||
}
|
||||
|
||||
export type LineChartDataset = {
|
||||
type: 'line'
|
||||
point?: {
|
||||
radius?: number
|
||||
strokeColor?: Property.Stroke
|
||||
strokeWidth?: Property.StrokeWidth
|
||||
fillColor?: Property.Fill
|
||||
}
|
||||
nullValues?: 'skip' | 'gap' | 'none'
|
||||
optimization?: boolean
|
||||
}
|
||||
|
||||
export type NeedleChartDataset = {
|
||||
type: 'needle'
|
||||
}
|
||||
|
||||
export type ChartDataset<DataType> = BaseChartDataset<DataType> & (
|
||||
LineChartDataset |
|
||||
NeedleChartDataset
|
||||
)
|
||||
|
||||
export type ChartDomain = {
|
||||
x: { min?: number, max?: number }
|
||||
y: { min?: number, max?: number }
|
||||
}
|
||||
|
||||
export type ChartOffset = {
|
||||
top: number
|
||||
bottom: number
|
||||
left: number
|
||||
right: number
|
||||
}
|
||||
|
||||
export type ChartTicks = {
|
||||
color?: Property.Color
|
||||
x?: { visible?: boolean, count?: number }
|
||||
y?: { visible?: boolean, count?: number }
|
||||
}
|
||||
|
||||
export type ChartRegistry<DataType = DefaultDataType> = ChartDataset<DataType> & {
|
||||
(): Selection
|
||||
y: (value: any) => number
|
||||
x: (value: any) => number
|
||||
}
|
@ -7,18 +7,8 @@ import '@styles/detected_operations.less'
|
||||
// Палитра: https://colorhunt.co/palette/f9f2ed3ab0ffffb562f87474
|
||||
|
||||
const chartDatasets = [{
|
||||
key: 'normLine',
|
||||
type: 'area',
|
||||
width: 2,
|
||||
color: '#FFB562',
|
||||
opacity: 0.3,
|
||||
yAxis: {
|
||||
type: 'linear',
|
||||
accessor: (row) => row.operationValue?.standardValue,
|
||||
},
|
||||
fillColor: '#FFB562'
|
||||
}, {
|
||||
key: 'normBars',
|
||||
label: 'Нормативные значения',
|
||||
type: 'needle',
|
||||
width: 2,
|
||||
color: '#FFB562',
|
||||
@ -29,6 +19,7 @@ const chartDatasets = [{
|
||||
},
|
||||
}, {
|
||||
key: 'bars',
|
||||
label: 'Действительные значения',
|
||||
type: 'needle',
|
||||
width: 2,
|
||||
color: '#3AB0FF',
|
||||
@ -39,6 +30,7 @@ const chartDatasets = [{
|
||||
},
|
||||
}, {
|
||||
key: 'target',
|
||||
label: 'Целевые значения',
|
||||
type: 'line',
|
||||
color: '#F87474',
|
||||
yAxis: {
|
||||
@ -50,7 +42,7 @@ const chartDatasets = [{
|
||||
|
||||
const xAxis = {
|
||||
type: 'time',
|
||||
accessor: (row) => new Date(row.dateStart)
|
||||
accessor: (row) => new Date(row.dateStart),
|
||||
}
|
||||
|
||||
export const OperationsChart = memo(({ data, yDomain, height }) => {
|
||||
@ -77,12 +69,14 @@ export const OperationsChart = memo(({ data, yDomain, height }) => {
|
||||
plugins={{
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
type: 'nearest',
|
||||
limit: 10,
|
||||
},
|
||||
cursor: {
|
||||
enabled: true,
|
||||
},
|
||||
menu: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
onUpdate: onChartUpdate,
|
||||
}
|
||||
}}
|
||||
|
@ -8,6 +8,7 @@
|
||||
width: 100%;
|
||||
height: 100% - @arrow-size;
|
||||
|
||||
font-size: 13px;
|
||||
color: @color;
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
@ -31,6 +32,24 @@
|
||||
left: 50%;
|
||||
margin-left: -@arrow-size;
|
||||
}
|
||||
|
||||
& .tooltip-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
align-items: flex-start;
|
||||
|
||||
& .group-label {
|
||||
width: 200%;
|
||||
overflow: hidden;
|
||||
|
||||
& span {
|
||||
font-weight: 600;
|
||||
margin-left: 5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .chart-empty {
|
||||
|
Loading…
Reference in New Issue
Block a user