diff --git a/src/components/d3/index.ts b/src/components/d3/index.ts index 8fd3688..4811ec3 100644 --- a/src/components/d3/index.ts +++ b/src/components/d3/index.ts @@ -1,6 +1,4 @@ export * from './D3Chart' export type { D3ChartProps } from './D3Chart' -export * from './D3MonitoringCharts' - export * from './types' diff --git a/src/components/d3/plugins/D3HorizontalCursor.tsx b/src/components/d3/monitoring/D3HorizontalCursor.tsx similarity index 95% rename from src/components/d3/plugins/D3HorizontalCursor.tsx rename to src/components/d3/monitoring/D3HorizontalCursor.tsx index 3b99157..a8a1d12 100644 --- a/src/components/d3/plugins/D3HorizontalCursor.tsx +++ b/src/components/d3/monitoring/D3HorizontalCursor.tsx @@ -1,12 +1,12 @@ -import { CSSProperties, ReactNode, SVGProps, useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { CSSProperties, ReactNode, SVGProps, useEffect, useMemo, useRef, useState } from 'react' import * as d3 from 'd3' +import { wrapPlugin } from '@components/d3/plugins/base' import { useD3MouseZone } from '@components/d3/D3MouseZone' -import { ChartGroup, ChartSizes } from '@components/d3/D3MonitoringCharts' +import { D3TooltipPosition } from '@components/d3/plugins/D3Tooltip' import { getChartIcon, isDev, usePartialProps } from '@utils' -import { wrapPlugin } from './base' -import { D3TooltipPosition } from './D3Tooltip' +import { ChartGroup, ChartSizes } from './D3MonitoringCharts' import '@styles/d3.less' diff --git a/src/components/d3/D3MonitoringChartEditor.tsx b/src/components/d3/monitoring/D3MonitoringChartEditor.tsx similarity index 97% rename from src/components/d3/D3MonitoringChartEditor.tsx rename to src/components/d3/monitoring/D3MonitoringChartEditor.tsx index 462e9ff..86ffdeb 100644 --- a/src/components/d3/D3MonitoringChartEditor.tsx +++ b/src/components/d3/monitoring/D3MonitoringChartEditor.tsx @@ -1,9 +1,10 @@ import { Button, Form, FormItemProps, Input, InputNumber, Select, Tooltip } from 'antd' import { memo, useCallback, useEffect, useMemo } from 'react' -import { ColorPicker, Color } from '../ColorPicker' +import { MinMax } from '@components/d3/types' +import { ColorPicker, Color } from '@components/ColorPicker' + import { ExtendedChartDataset } from './D3MonitoringCharts' -import { MinMax } from './types' const { Item: RawItem } = Form diff --git a/src/components/d3/D3MonitoringCharts.tsx b/src/components/d3/monitoring/D3MonitoringCharts.tsx similarity index 91% rename from src/components/d3/D3MonitoringCharts.tsx rename to src/components/d3/monitoring/D3MonitoringCharts.tsx index 86ee9c0..ae37671 100644 --- a/src/components/d3/D3MonitoringCharts.tsx +++ b/src/components/d3/monitoring/D3MonitoringCharts.tsx @@ -16,18 +16,19 @@ import { ChartRegistry, ChartTick, MinMax -} from './types' +} from '@components/d3/types' import { BasePluginSettings, D3ContextMenu, D3ContextMenuSettings, - D3HorizontalCursor, - D3HorizontalCursorSettings -} from './plugins' -import D3MouseZone from './D3MouseZone' -import D3MonitoringGroupsEditor from './D3MonitoringGroupsEditor' -import { getByAccessor, getChartClass, getGroupClass, getTicks } from './functions' -import { renderArea, renderLine, renderNeedle, renderPoint, renderRectArea } from './renders' +} from '@components/d3/plugins' +import D3MouseZone from '@components/d3/D3MouseZone' +import { getByAccessor, getChartClass, getGroupClass, getTicks } from '@components/d3/functions' +import { renderArea, renderLine, renderNeedle, renderPoint, renderRectArea } from '@components/d3/renders' + +import D3MonitoringEditor from './D3MonitoringEditor' +import { D3HorizontalCursor, D3HorizontalCursorSettings } from './D3HorizontalCursor' +import D3MonitoringLimitChart, { TelemetryRegulators } from './D3MonitoringLimitChart' const roundTo = (v: number, to: number = 50) => { if (v === 0) return v @@ -73,7 +74,15 @@ const defaultOffsets: ChartOffset = { top: 10, bottom: 10, left: 100, - right: 10, + right: 20, +} + +const defaultRegulators: TelemetryRegulators = { + 1: { color: '#59B359', label: 'Скорость блока' }, + 2: { color: '#FF0000', label: 'Давление' }, + 3: { color: '#0000CC', label: 'Осевая нагрузка' }, + 4: { color: '#990099', label: 'Момент на роторе' }, + 5: { color: '#007070', label: 'Расход' }, } const getDefaultYAxisConfig = (): ChartAxis => ({ @@ -191,6 +200,7 @@ const _D3MonitoringCharts = >({ const [chartAreaRef, setChartAreaRef] = useState(null) const [axesAreaRef, setAxesAreaRef] = useState(null) const [settingsVisible, setSettingsVisible] = useState(false) + const [regulators, setRegulators] = useState(defaultRegulators) const offset = usePartialProps(_offset, defaultOffsets) const yTicks = usePartialProps>>(_yTicks, getDefaultYTicks) @@ -271,23 +281,28 @@ const _D3MonitoringCharts = >({ } ), [chartArea]) - const onGroupsChange = useCallback((sets: ExtendedChartDataset[][]) => { + const onGroupsChange = useCallback((settings: ExtendedChartDataset[][], regulators: TelemetryRegulators) => { if (chartName) { invokeWebApiWrapperAsync( async () => { - await UserSettingsService.update(chartName, sets) + await UserSettingsService.update(chartName, { + settings, + regulators, + }) }, undefined, 'Не удалось сохранить параметры графиков' ) } - setDatasets(sets) + setDatasets(settings) + setRegulators(regulators) setSettingsVisible(false) }, [chartName]) const onGroupsReset = useCallback(() => { setSettingsVisible(false) setDatasets(datasetGroups) + setRegulators(defaultRegulators) if (chartName) { invokeWebApiWrapperAsync( async () => await UserSettingsService.delete(chartName), @@ -305,13 +320,20 @@ const _D3MonitoringCharts = >({ let sets = chartName ? await UserSettingsService.get(chartName) : null if (typeof sets === 'string') sets = JSON.parse(sets) - if (Array.isArray(sets)) { - setDatasets(sets) + if (sets && Array.isArray(sets.settings)) { + if (sets.regulators) + setRegulators(sets.regulators) + if (Array.isArray(sets.settings)) { + setDatasets(sets.settings) + } } else if (Array.isArray(datasetGroups)) { setDatasets(datasetGroups) if (chartName) { invokeWebApiWrapperAsync( - async () => await UserSettingsService.insert(chartName, datasetGroups), + async () => await UserSettingsService.insert(chartName, { + settings: datasetGroups, + regulators: defaultRegulators, + }), undefined, 'Не удалось сохранить настройки графиков' ) @@ -572,6 +594,16 @@ const _D3MonitoringCharts = >({ return })} + >({ )} - setSettingsVisible(false)} diff --git a/src/components/d3/D3MonitoringGroupsEditor.tsx b/src/components/d3/monitoring/D3MonitoringEditor.tsx similarity index 82% rename from src/components/d3/D3MonitoringGroupsEditor.tsx rename to src/components/d3/monitoring/D3MonitoringEditor.tsx index 25f9f13..bf904db 100644 --- a/src/components/d3/D3MonitoringGroupsEditor.tsx +++ b/src/components/d3/monitoring/D3MonitoringEditor.tsx @@ -3,16 +3,19 @@ import { Button, Divider, Empty, Modal, Popconfirm, Tooltip, Tree } from 'antd' import { UndoOutlined } from '@ant-design/icons' import { EventDataNode } from 'antd/lib/tree' +import { notify } from '@components/factory' import { getChartIcon } from '@utils' import { ExtendedChartDataset } from './D3MonitoringCharts' +import { TelemetryRegulators } from './D3MonitoringLimitChart' import D3MonitoringChartEditor from './D3MonitoringChartEditor' -import { notify } from '../factory' +import D3MonitoringLimitEditor from './D3MonitoringLimitEditor' export type D3MonitoringGroupsEditorProps = { visible?: boolean groups: ExtendedChartDataset[][] - onChange: (value: ExtendedChartDataset[][]) => void + regulators: TelemetryRegulators + onChange: (value: ExtendedChartDataset[][], regs: TelemetryRegulators) => void onCancel: () => void onReset: () => void } @@ -36,9 +39,12 @@ const getNodePos = (node: EventDataNode): { group: number, chart?: number } => { return { group: out[1], chart: out[2] } } -const _D3MonitoringGroupsEditor = ({ +type EditingMode = null | 'limit' | 'chart' + +const _D3MonitoringEditor = ({ visible, groups: oldGroups, + regulators: oldRegulators, onChange, onCancel, onReset, @@ -46,10 +52,13 @@ const _D3MonitoringGroupsEditor = ({ const [groups, setGroups] = useState[][]>([]) const [expand, setExpand] = useState([]) const [selected, setSelected] = useState([]) + const [mode, setMode] = useState(null) + const [regulators, setRegulators] = useState({}) useEffect(() => setGroups(oldGroups), [oldGroups]) + useEffect(() => setRegulators(oldRegulators), [oldRegulators]) - const onModalOk = useCallback(() => onChange(groups), [groups]) + const onModalOk = useCallback(() => onChange(groups, regulators), [groups, regulators]) const onDrop = useCallback((info: { node: EventDataNode @@ -148,23 +157,29 @@ const _D3MonitoringGroupsEditor = ({ selectedKeys={selected} treeData={treeItems} onDrop={onDrop} - onSelect={setSelected} + onSelect={(value) => { + setSelected(value) + setMode('chart') + }} height={250} /> +
- {selectedGroup && selectedChart ? ( + {mode === 'chart' && selectedGroup && selectedChart ? ( group={selectedGroup} chart={selectedChart} onChange={onChartChange} /> + ) : (mode === 'limit' ? ( + value={regulators} onChange={setRegulators} /> ) : ( - )} + ))}
) } -export const D3MonitoringGroupsEditor = memo(_D3MonitoringGroupsEditor) as typeof _D3MonitoringGroupsEditor +export const D3MonitoringEditor = memo(_D3MonitoringEditor) as typeof _D3MonitoringEditor -export default D3MonitoringGroupsEditor +export default D3MonitoringEditor diff --git a/src/components/d3/monitoring/D3MonitoringLimitChart.tsx b/src/components/d3/monitoring/D3MonitoringLimitChart.tsx new file mode 100644 index 0000000..97dc80c --- /dev/null +++ b/src/components/d3/monitoring/D3MonitoringLimitChart.tsx @@ -0,0 +1,181 @@ +import { CSSProperties, memo, useEffect, useMemo, useState } from 'react' +import * as d3 from 'd3' + +import { Grid, GridItem } from '@components/Grid' +import { formatDate, makeDisplayValue } from '@utils' +import { TelemetryDataSaubDto } from '@api' + +type LimitChartData = { + id: number + dateStart: Date + dateEnd: Date + depthStart: number | null + depthEnd: number | null +} + +type LimitChartDataRaw = { + id?: number + dateStart?: string + dateEnd?: string + depthStart?: number | null + depthEnd?: number | null +} + +export type TelemetryRegulators = Record + +const getLast = (out: LimitChartDataRaw[]) => out.at(-1) as LimitChartDataRaw +function isDataCorrect(value: LimitChartDataRaw): value is Required { + return typeof value.id !== 'undefined' +} + +const calcualteData = (data: DataType[]) => { + const out = data.filter((row) => row.dateTime).reduce((out, row) => { + const last = getLast(out) + if (last.id === row.idFeedRegulator) { + if (!row.idFeedRegulator) return out + last.dateEnd = row.dateTime + last.depthEnd = row.wellDepth + } else { + let n: LimitChartDataRaw = {} + if (row.idFeedRegulator) { + n = { + id: row.idFeedRegulator, + dateStart: row.dateTime, + dateEnd: row.dateTime, + depthStart: row.wellDepth, + depthEnd: row.wellDepth, + } + } + out.push(n) + } + return out + }, [{}] as LimitChartDataRaw[]) + + return out.filter(isDataCorrect).map((row) => ({ + id: row.id, + dateStart: new Date(row.dateStart), + dateEnd: new Date(row.dateEnd), + depthStart: row.depthStart, + depthEnd: row.depthEnd, + })) +} + +export type D3MonitoringLimitChartProps = { + yAxis?: d3.ScaleTime + regulators: TelemetryRegulators + data: DataType[] + width: number + height: number + left: number + top: number + zoneWidth?: number +} + +const tooltipWidth = 270 +const tooltipHeight = 120 + +const displayValue = makeDisplayValue() + +const _D3MonitoringLimitChart = ({ + yAxis, + data: chartData, + width, + height, + left, + top, + regulators, + zoneWidth = 0, +}: D3MonitoringLimitChartProps) => { + const [ref, setRef] = useState(null) + const [selected, setSelected] = useState() + + const data = useMemo(() => calcualteData(chartData), [chartData]) + + useEffect(() => { + if (!ref || !yAxis) return + const elms = d3.select(ref).select('.bars').selectAll('rect').data(data) + + elms.exit().remove() + const newElms = elms.enter().append('rect') + + elms.merge(newElms) + .attr('width', width) + .attr('height', (d) => Math.max(yAxis(d.dateEnd) - yAxis(d.dateStart), 1)) + .attr('y', (d) => yAxis(d.dateStart)) + .attr('fill', (d) => regulators[d.id].color) + .on('mouseover', (_, d) => { + const y = yAxis(d.dateStart) - tooltipHeight + setSelected({ ...d, y, x: -tooltipWidth - 10, visible: true }) + }) + .on('mouseout', () => setSelected((pre) => pre ? ({ ...pre, visible: false }) : undefined)) + }, [yAxis, data, ref, width]) + + const zoneY1 = useMemo(() => yAxis && selected ? yAxis(selected.dateStart) : 0, [yAxis, selected]) + const zoneY2 = useMemo(() => yAxis && selected ? yAxis(selected.dateEnd) : 0, [yAxis, selected]) + + const opacityStyle: CSSProperties = useMemo(() => ({ + transition: 'opacity .1s ease-in-out', + opacity: selected?.visible ? 1 : 0, + }), [selected]) + + const tooltipStyle: CSSProperties = useMemo(() => ({ + ...opacityStyle, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + }), [opacityStyle]) + + return ( + + + + {selected && ( + + + + + + )} + + + {selected && ( + +
+ Ограничивающий параметр + {regulators[selected.id].label} + + Начало: + {formatDate(selected.dateStart)} + {displayValue(selected.depthStart)} + м. + Конец: + {formatDate(selected.dateEnd)} + {displayValue(selected.depthEnd)} + м. + +
+
+ )} +
+ ) +} + +export const D3MonitoringLimitChart = memo(_D3MonitoringLimitChart) as typeof _D3MonitoringLimitChart + +export default D3MonitoringLimitChart diff --git a/src/components/d3/monitoring/D3MonitoringLimitEditor.tsx b/src/components/d3/monitoring/D3MonitoringLimitEditor.tsx new file mode 100644 index 0000000..fbb64fb --- /dev/null +++ b/src/components/d3/monitoring/D3MonitoringLimitEditor.tsx @@ -0,0 +1,18 @@ +import { memo } from 'react' +import { TelemetryRegulators } from './D3MonitoringLimitChart' + +export type D3MonitoringLimitEditorProps = { + value: TelemetryRegulators + onChange: (value: TelemetryRegulators) => void +} + +const _D3MonitoringLimitEditor = ({ value, onChange }: D3MonitoringLimitEditorProps) => { + + return ( + <> + ) +} + +export const D3MonitoringLimitEditor = memo(_D3MonitoringLimitEditor) as typeof _D3MonitoringLimitEditor + +export default D3MonitoringLimitEditor diff --git a/src/components/d3/monitoring/index.ts b/src/components/d3/monitoring/index.ts new file mode 100644 index 0000000..59688d8 --- /dev/null +++ b/src/components/d3/monitoring/index.ts @@ -0,0 +1 @@ +export * from './D3MonitoringCharts' diff --git a/src/components/d3/plugins/index.ts b/src/components/d3/plugins/index.ts index c2fa895..40c5d84 100644 --- a/src/components/d3/plugins/index.ts +++ b/src/components/d3/plugins/index.ts @@ -1,6 +1,5 @@ export * from './base' export * from './D3ContextMenu' export * from './D3Cursor' -export * from './D3HorizontalCursor' export * from './D3Legend' export * from './D3Tooltip' diff --git a/src/pages/Telemetry/Archive/index.jsx b/src/pages/Telemetry/Archive/index.jsx index 03b20b9..cb57c64 100755 --- a/src/pages/Telemetry/Archive/index.jsx +++ b/src/pages/Telemetry/Archive/index.jsx @@ -5,7 +5,7 @@ import { Select } from 'antd' import { useIdWell } from '@asb/context' import { Flex } from '@components/Grid' -import { D3MonitoringCharts } from '@components/d3' +import { D3MonitoringCharts } from '@components/d3/monitoring' import { CopyUrlButton } from '@components/CopyUrl' import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' diff --git a/src/pages/Telemetry/TelemetryView/index.jsx b/src/pages/Telemetry/TelemetryView/index.jsx index dca17a5..6e69057 100755 --- a/src/pages/Telemetry/TelemetryView/index.jsx +++ b/src/pages/Telemetry/TelemetryView/index.jsx @@ -4,7 +4,7 @@ import { Button, Select } from 'antd' import { useIdWell } from '@asb/context' import { makeDateSorter } from '@components/Table' -import { D3MonitoringCharts } from '@components/d3' +import { D3MonitoringCharts } from '@components/d3/monitoring' import LoaderPortal from '@components/LoaderPortal' import { Grid, GridItem, Flex } from '@components/Grid' import { invokeWebApiWrapperAsync } from '@components/factory' diff --git a/src/styles/d3.less b/src/styles/d3.less index be70bbe..6ec1bb0 100644 --- a/src/styles/d3.less +++ b/src/styles/d3.less @@ -8,7 +8,7 @@ @arrow-size: 8px; width: 100%; - height: 100% - @arrow-size; + height: 100%; font-size: 13px; color: @color; @@ -30,6 +30,7 @@ &.top { margin-top: @arrow-size; + height: 100% - @arrow-size; &::after { border-bottom-color: @bg-color; @@ -42,6 +43,7 @@ &.bottom { margin-bottom: @arrow-size; + height: 100% - @arrow-size; &::after { border-top-color: @bg-color; @@ -53,8 +55,11 @@ &.left { margin-left: @arrow-size; + width: 100% - @arrow-size; + &::after { border-right-color: @bg-color; + margin-top: -@arrow-size; top: 50%; right: 100%; } @@ -62,8 +67,11 @@ &.right { margin-right: @arrow-size; + width: 100% - @arrow-size; + &::after { border-left-color: @bg-color; + margin-top: -@arrow-size; top: 50%; left: 100%; } diff --git a/src/utils/functions/numbers.tsx b/src/utils/functions/numbers.tsx index c60a46e..6e6ca26 100644 --- a/src/utils/functions/numbers.tsx +++ b/src/utils/functions/numbers.tsx @@ -35,7 +35,11 @@ export const makeDisplayValue = ({ def = '----', inf = (v) => `${v < 0 ? '-' : ''}\u221E`, fixed = 2 -}: DisplayValueOptions) => (v: unknown): ReactNode => { +}: DisplayValueOptions = { + def: '----', + inf: (v) => `${v < 0 ? '-' : ''}\u221E`, + fixed: 2 +}) => (v: unknown): ReactNode => { if (typeof v === 'undefined' || v === null || String(v) === 'NaN') return def let f = Number(v) if (typeof v === 'string') {