From 354d6945d75c8a75dee0315e3720d321c3fca550 Mon Sep 17 00:00:00 2001 From: goodmice Date: Thu, 4 Aug 2022 01:33:43 +0500 Subject: [PATCH 1/9] =?UTF-8?q?*=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BF=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=82?= =?UTF-8?q?=D0=B5=D0=BA=D1=81=D1=82=D0=B0=20=D0=B2=20=D0=B1=D1=83=D1=84?= =?UTF-8?q?=D0=B5=D1=80=20*=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=20=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=D0=B0=20=D1=86=D0=B2=D0=B5?= =?UTF-8?q?=D1=82=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ColorPicker.tsx | 129 ++++++++++++++++++++++++ src/components/factory.ts | 9 ++ src/styles/components/color_picker.less | 21 ++++ 3 files changed, 159 insertions(+) create mode 100644 src/components/ColorPicker.tsx create mode 100644 src/styles/components/color_picker.less diff --git a/src/components/ColorPicker.tsx b/src/components/ColorPicker.tsx new file mode 100644 index 0000000..8e4cbef --- /dev/null +++ b/src/components/ColorPicker.tsx @@ -0,0 +1,129 @@ +import { memo, useCallback, useEffect, useMemo, useState } from 'react' +import { Input, Popover, Slider } from 'antd' +import { CopyOutlined } from '@ant-design/icons' + +import { copyToClipboard } from './factory' + +import '@styles/components/color_picker.less' + +export class Color { + public r: number + public g: number + public b: number + public a: number = 1 + + public constructor(color: Color | string) + public constructor(r: number, g: number, b: number, a?: number) + + constructor(...args: any[]) { + let out + if (args[0] instanceof Color) { + out = args[0] + } else if (typeof args[0] === 'string') { + out = Color.parseToObject(args[0]) + } else if (typeof args[0] === 'number') { + out = { r: args[0], g: args[1], b: args[2], a: args[3] ?? 1 } + } else throw new Error('Некорректные аргументы') + this.r = out.r + this.g = out.g + this.b = out.b + this.a = out.a + } + + public static parse(str: string): Color { + const out = Color.parseToObject(str) + return new Color(out.r, out.g, out.b, out.a) + } + + private static parseToObject(str: string) { + let rgb: number[] = [] + let a: number = 1 + if (str.startsWith('rgb')) { + const parts = str.replaceAll(/\s/g, '').match(/rgba?\((\d+),(\d+),(\d)+(?:,([\d.]+))?\)/) + if (parts) { + rgb = parts.slice(1, 4).map((v) => Math.min(0, Math.max(parseInt(v), 255))) + if (parts[4]) a = parseFloat(`0${parts[4]}`) + } + } else if (str.startsWith('#')) { + const parts = str.slice(1) + let rgba: string[] | null = parts.length > 5 ? parts.match(/.{1,2}/g) : [...parts] + if (rgba) { + rgb = rgba.slice(0, 3).map((v) => parseInt(v, 16)) + if (rgba[3]) a = parseInt(rgba[3], 16) / 255 + } + } + if (rgb.length < 3) + throw new Error('Некорректная строка') + return { r: rgb[0], g: rgb[1], b: rgb[2], a } + } + + public toString = () => this.toHexString() + public toCssString = () => `rgba(${this.r},${this.g},${this.b},${this.a})` + public toHexString() { + const a = Math.floor(this.a * 255) + let out = '#' + [this.r, this.g, this.b].map((v) => v.toString(16).padStart(2, '0')).join('') + if (a < 255) out += a.toString(16).padStart(2, '0') + return out + } +} + +export type ColorPickerProps = { + value?: string | Color + onChange?: (value: Color) => void + size?: number | string + id?: string +} + +const makeChangeColor = (set: React.Dispatch>, accessor: 'r' | 'g' | 'b' | 'a') => (value: number) => set((prev: Color) => { + const out = new Color(prev) + out[accessor] = value + return out +}) + +export const ColorPicker = memo(({ value = '#AA33BB', onChange, size, ...other }) => { + const [color, setColor] = useState(new Color(255, 255, 255)) + + useEffect(() => setColor(new Color(value)), [value]) + + const divStyle = useMemo(() => ({ + width: size, + height: size, + backgroundColor: color.toCssString(), + }), [size, color]) + + const changeR = useMemo(() => makeChangeColor(setColor, 'r'), []) + const changeG = useMemo(() => makeChangeColor(setColor, 'g'), []) + const changeB = useMemo(() => makeChangeColor(setColor, 'b'), []) + const changeA = useMemo(() => makeChangeColor(setColor, 'a'), []) + + const onClose = useCallback((visible: boolean) => { + if (!visible) + onChange?.(color) + }, [color, onChange]) + + const onCopyClick = useCallback(() => copyToClipboard(color.toHexString()), [color]) + + return ( + +
+ + + + +
+ + )} /> + + )} + > +
+ + ) +}) + +export default ColorPicker diff --git a/src/components/factory.ts b/src/components/factory.ts index 3d721da..9fe9c64 100755 --- a/src/components/factory.ts +++ b/src/components/factory.ts @@ -35,6 +35,15 @@ export const notify = (body?: ReactNode, notifyType: NotifyType = 'info', other? }) } +export const copyToClipboard = (value: string, successText?: string, errorText?: string) => { + try { + navigator.clipboard.writeText(value) + notify(successText ?? 'Текст успешно скопирован в буфер обмена', 'info') + } catch (ex) { + notify(errorText ?? 'Не удалось скопировать текст в буфер обмена', 'error') + } +} + type asyncFunction = (...args: any) => Promise const parseApiEror = (err: unknown, actionName?: string) => { diff --git a/src/styles/components/color_picker.less b/src/styles/components/color_picker.less new file mode 100644 index 0000000..aba100d --- /dev/null +++ b/src/styles/components/color_picker.less @@ -0,0 +1,21 @@ +.asb-color-picker-content { + display: flex; + flex-direction: column; + align-items: stretch; + width: 150px; + height: 150px; + + & > .asb-color-picker-sliders { + flex-grow: 1; + display: flex; + align-items: stretch; + justify-content: space-between; + padding-bottom: 20px; + } +} + +.asb-color-picker-preview { + border: 1px solid black; + width: 20px; + height: 20px; +} \ No newline at end of file From cefcf9a75e4896f61ff165220cdbbb871947f8a7 Mon Sep 17 00:00:00 2001 From: goodmice Date: Thu, 4 Aug 2022 01:34:03 +0500 Subject: [PATCH 2/9] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BE=D0=BA=D0=BD=D0=BE=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B8=20=D0=B3=D1=80=D1=83=D0=BF?= =?UTF-8?q?=D0=BF=D1=8B=20=D0=B8=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/d3/D3MonitoringChartEditor.tsx | 106 +++++++++++++ src/components/d3/D3MonitoringCharts.tsx | 89 ++++++++--- .../d3/D3MonitoringGroupsEditor.tsx | 141 ++++++++++++++++++ src/components/d3/plugins/D3ContextMenu.tsx | 12 +- 4 files changed, 326 insertions(+), 22 deletions(-) create mode 100644 src/components/d3/D3MonitoringChartEditor.tsx create mode 100644 src/components/d3/D3MonitoringGroupsEditor.tsx diff --git a/src/components/d3/D3MonitoringChartEditor.tsx b/src/components/d3/D3MonitoringChartEditor.tsx new file mode 100644 index 0000000..3bf55d1 --- /dev/null +++ b/src/components/d3/D3MonitoringChartEditor.tsx @@ -0,0 +1,106 @@ +import { Button, Form, FormItemProps, Input, InputNumber, Select, Space, Tooltip, Typography } from 'antd' +import { CSSProperties, memo, useCallback, useEffect, useMemo, useState } from 'react' + +import { ColorPicker, Color } from '../ColorPicker' +import { ExtendedChartDataset } from './D3MonitoringCharts' +import { MinMax } from './types' + +const { Item: RawItem } = Form + +const Item = ({ style, ...other }: FormItemProps) => style={{ margin: 0, marginBottom: 5, ...style }} {...other} /> + +const lineTypes = [ + { value: 'line', label: 'Линия' }, + { value: 'rect_area', label: 'Прямоугольная зона' }, + { value: 'point', label: 'Точки' }, + { value: 'area', label: 'Зона' }, + { value: 'needle', label: 'Иглы' }, +] + +export type D3MonitoringChartEditorProps = Omit, HTMLDivElement>, 'onChange'> & { + chart?: ExtendedChartDataset | null + onChange: (value: ExtendedChartDataset) => boolean +} + +const _D3MonitoringChartEditor = ({ + chart: value, + onChange, + + style, + ...other +}: D3MonitoringChartEditorProps) => { + const [domain, setDomain] = useState({}) + const [color, setColor] = useState() + + const [form] = Form.useForm() + + const onDomainChange = useCallback((mm: MinMax) => { + setDomain((prev) => ({ + min: ('min' in mm) ? mm.min : prev?.min, + max: ('max' in mm) ? mm.max : prev?.max, + })) + }, []) + + const onColorChange = useCallback((color: Color) => setColor((prev) => color ?? prev), []) + + useEffect(() => { + if (value?.type) { + form.setFieldsValue(value) + } else { + form.resetFields() + } + setColor(value?.color ? new Color(String(value.color)) : undefined) + setDomain(value?.xDomain ?? {}) + }, [value, form]) + + const onSave = useCallback(() => { + if (!value) return + const values = form.getFieldsValue() + const newValue = { + ...value, + color, + xDomain: domain, + ...values, + } + onChange(newValue) + }, [form, domain, color, value]) + + const divStyle: CSSProperties = useMemo(() => ({ + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + alignItems: 'center', + ...style, + }), [style]) + + return ( +
+
+ + + + + + + + onDomainChange({ min })} placeholder={'Мин'} /> + onDomainChange({ max })} placeholder={'Макс'} /> + + + + + +
+ ) +} + +export const D3MonitoringChartEditor = memo(_D3MonitoringChartEditor) as typeof _D3MonitoringChartEditor + +export default D3MonitoringChartEditor diff --git a/src/components/d3/D3MonitoringCharts.tsx b/src/components/d3/D3MonitoringCharts.tsx index 2ff5abf..9e7ce25 100644 --- a/src/components/d3/D3MonitoringCharts.tsx +++ b/src/components/d3/D3MonitoringCharts.tsx @@ -25,6 +25,9 @@ import { import D3MouseZone from './D3MouseZone' import { getByAccessor, getChartClass, getGroupClass, getTicks } from './functions' import { renderArea, renderLine, renderNeedle, renderPoint, renderRectArea } from './renders' +import D3MonitoringGroupsEditor from './D3MonitoringGroupsEditor' +import { UserSettingsService } from '@asb/services/api' +import { invokeWebApiWrapperAsync } from '../factory' const roundTo = (v: number, to: number = 50) => { if (to == 0) return v @@ -43,14 +46,14 @@ const calculateDomain = (mm: MinMax, round: number = 100): Required => { return { min, max } } -type ExtendedChartDataset = ChartDataset & { +export type ExtendedChartDataset = ChartDataset & { /** Диапазон отображаемых значений по горизонтальной оси */ xDomain: MinMax /** Скрыть отображение шкалы графика */ hideLabel?: boolean } -type ExtendedChartRegistry = ChartRegistry & ExtendedChartDataset +export type ExtendedChartRegistry = ChartRegistry & ExtendedChartDataset export type ChartGroup = { /** Получить D3 выборку, содержащую корневой G-элемент группы */ @@ -121,6 +124,8 @@ export type D3MonitoringChartsProps> = Reac axisHeight?: number /** Отступ между группами графиков в пикселях (30 по умолчанию) */ spaceBetweenGroups?: number + /** Название графика для сохранения в базе */ + chartName?: string } export type ChartSizes = ChartOffset & { @@ -165,15 +170,46 @@ const _D3MonitoringCharts = >({ axisHeight = 20, spaceBetweenGroups = 30, dash, + chartName, className = '', ...other }: D3MonitoringChartsProps) => { + const [datasets, setDatasets] = useState[][]>([]) const [groups, setGroups] = useState[]>([]) const [svgRef, setSvgRef] = useState(null) const [yAxisRef, setYAxisRef] = useState(null) const [chartAreaRef, setChartAreaRef] = useState(null) const [axesAreaRef, setAxesAreaRef] = useState(null) + const [settingsVisible, setSettingsVisible] = useState(true) + + useEffect(() => { + if (!chartName) { + setDatasets(datasetGroups) + return + } + let datasets: ExtendedChartDataset[][] = [] + let needInsert = false + invokeWebApiWrapperAsync( + async () => { + const sets = await UserSettingsService.get(chartName) + needInsert = !sets + datasets = sets ?? datasetGroups + }, + undefined, + 'Не удалось загрузить настройки графиков' + ) + setDatasets(datasets) + if (needInsert) { + invokeWebApiWrapperAsync( + async () => { + await UserSettingsService.insert(chartName, datasets) + }, + undefined, + 'Не удалось сохранить настройки графиков' + ) + } + }, [datasetGroups, chartName]) const offset = usePartialProps(_offset, defaultOffsets) const yTicks = usePartialProps>>(_yTicks, getDefaultYTicks) @@ -218,14 +254,6 @@ const _D3MonitoringCharts = >({ return yAxis }, [groups, data, yDomain, sizes.chartsHeight]) - const createAxesGroup = useCallback((i: number): ChartGroup => Object.assign( - () => chartArea().select('.' + getGroupClass(i)) as d3.Selection, - { - key: i, - charts: [], - } - ), [chartArea, axesArea]) - const chartDomains = useMemo(() => groups.map((group) => { const out: [string | number, ChartDomain][] = group.charts.map((chart) => { const mm = { ...chart.xDomain } @@ -254,14 +282,26 @@ const _D3MonitoringCharts = >({ return Object.fromEntries(out) }), [groups, data]) + const createAxesGroup = useCallback((i: number): ChartGroup => Object.assign( + () => chartArea().select('.' + getGroupClass(i)) as d3.Selection, + { + key: i, + charts: [], + } + ), [chartArea, axesArea]) + + const onGroupsChange = useCallback((sets: ExtendedChartDataset[][]) => { + setDatasets(sets) + setSettingsVisible(false) + }, []) + useEffect(() => { if (isDev()) { - datasetGroups.forEach((sets, i) => { + datasets.forEach((sets, i) => { sets.forEach((set, j) => { - for (let k = j + 1; k < sets.length; k++) { + for (let k = j + 1; k < sets.length; k++) if (set.key === sets[k].key) console.warn(`Ключ датасета "${set.key}" неуникален (группа ${i}, индексы ${j} и ${k})!`) - } }) }) } @@ -269,15 +309,15 @@ const _D3MonitoringCharts = >({ setGroups((oldGroups) => { const groups: ChartGroup[] = [] - if (datasetGroups.length < oldGroups.length) { + if (datasets.length < oldGroups.length) { // Удаляем неактуальные группы - oldGroups.slice(datasetGroups.length).forEach((group) => group().remove()) - groups.push(...oldGroups.slice(0, datasetGroups.length)) + oldGroups.slice(datasets.length).forEach((group) => group().remove()) + groups.push(...oldGroups.slice(0, datasets.length)) } else { groups.push(...oldGroups) } - datasetGroups.forEach((datasets, i) => { + datasets.forEach((datasets, i) => { let group: ChartGroup = createAxesGroup(i) if (group().empty()) @@ -326,7 +366,7 @@ const _D3MonitoringCharts = >({ return groups }) - }, [yAxisConfig, chartArea, datasetGroups, animDurationMs, createAxesGroup]) + }, [yAxisConfig, chartArea, datasets, animDurationMs, createAxesGroup]) useEffect(() => { const axesGroups = d3.select(axesAreaRef) @@ -479,7 +519,11 @@ const _D3MonitoringCharts = >({ className={`asb-d3-chart ${className}`} > {data ? ( - + setSettingsVisible(true)} + {...plugins?.menu} + svg={svgRef} + > @@ -516,6 +560,13 @@ const _D3MonitoringCharts = >({
)} + setSettingsVisible(false)} + /> ) diff --git a/src/components/d3/D3MonitoringGroupsEditor.tsx b/src/components/d3/D3MonitoringGroupsEditor.tsx new file mode 100644 index 0000000..d08ed14 --- /dev/null +++ b/src/components/d3/D3MonitoringGroupsEditor.tsx @@ -0,0 +1,141 @@ +import { Key, memo, useCallback, useEffect, useMemo, useState } from 'react' +import { Divider, Modal, Tooltip, Tree } from 'antd' + +import { getChartIcon } from '@utils' + +import { ChartGroup, ExtendedChartDataset } from './D3MonitoringCharts' +import D3MonitoringChartEditor from './D3MonitoringChartEditor' + +export type D3MonitoringGroupsEditorProps = { + visible?: boolean + groups: ExtendedChartDataset[][] + onChange: (value: ExtendedChartDataset[][]) => void + onCancel: () => void + name?: string +} + +const moveToPos = (arr: T[], pos: number, newPos: number): T[] => { + if (pos === newPos) return arr + if (newPos === -1) return [arr[pos], ...arr.slice(0, pos), ...arr.slice(pos + 1)] + if (newPos === arr.length) return [...arr.slice(0, pos), ...arr.slice(pos + 1), arr[pos]] + const newArray = [] + for (let i = 0; i < arr.length; i++) { + if (i == newPos) newArray.push(arr[pos]) + if (i !== pos) newArray.push(arr[i]) + } + return newArray +} + +const getChartLabel = (chart: ExtendedChartDataset) => ( + + {getChartIcon(chart)} {chart.label} + +) + +const _D3MonitoringGroupsEditor = ({ + visible, + groups: oldGroups, + onChange, + onCancel, + name, +}: D3MonitoringGroupsEditorProps) => { + const [groups, setGroups] = useState[][]>([]) + const [expand, setExpand] = useState([]) + const [selected, setSelected] = useState([]) + + useEffect(() => setGroups(oldGroups), [oldGroups]) + + const onModalOk = useCallback(() => onChange(groups), [groups]) + + const onDrop = useCallback((info: any) => { + const { dragNode, dropPosition, dropToGap } = info + + const nodes = dragNode.pos.split('-') + const groupPos = Number(nodes[1]) + if (!Number.isFinite(groupPos)) return + setGroups((prev) => { + if (dropToGap) { + if (nodes.length < 3) + return moveToPos(prev, groupPos, dropPosition) + } else { + if (nodes.length < 3) return prev + const chartPos = Number(nodes[2]) + if (Number.isFinite(chartPos)) { + prev[groupPos] = moveToPos(prev[groupPos], chartPos, dropPosition) + return [ ...prev ] + } + } + return prev + }) + }, []) + + const treeItems = useMemo(() => groups.map((group, i) => ({ + key: `0-${i}`, + title: `Группа #${i} (${group.length})`, + selectable: false, + children: group.map((chart, j) => ({ + key: `0-${i}-${j}`, + title: getChartLabel(chart), + selectable: true, + })) + })), [groups]) + + const selectedIdx = useMemo(() => { + if (!selected) return null + const parts = String(selected[0]).split('-') + const group = Number(parts[1]) + const chart = Number(parts[2]) + if (!Number.isFinite(group + chart)) return null + return { group, chart } + }, [selected]) + + const selectedChart = useMemo(() => { + if (!selectedIdx) return null + + return groups[selectedIdx.group][selectedIdx.chart] + }, [groups, selectedIdx]) + + const onChartChange = useCallback((chart: ExtendedChartDataset) => { + if (!selectedIdx) return false + setGroups((prev) => { + const groups = [ ...prev ] + groups[selectedIdx.group][selectedIdx.chart] = chart + return groups + }) + return true + }, [selectedIdx]) + + return ( + +
+
+ setExpand(keys)} + expandedKeys={expand} + selectedKeys={selected} + treeData={treeItems} + onDrop={onDrop} + onSelect={setSelected} + height={250} + /> +
+ + chart={selectedChart} style={{ flexGrow: 1 }} onChange={onChartChange} /> +
+
+ ) +} + +export const D3MonitoringGroupsEditor = memo(_D3MonitoringGroupsEditor) as typeof _D3MonitoringGroupsEditor + +export default D3MonitoringGroupsEditor diff --git a/src/components/d3/plugins/D3ContextMenu.tsx b/src/components/d3/plugins/D3ContextMenu.tsx index 9c78d0d..3bbc553 100644 --- a/src/components/d3/plugins/D3ContextMenu.tsx +++ b/src/components/d3/plugins/D3ContextMenu.tsx @@ -8,7 +8,7 @@ import { BasePluginSettings } from './base' export type D3ContextMenuSettings = { /** Метод или объект отрисовки пунктов выпадающего меню */ - overlay?: FunctionalValue<(svg: SVGSVGElement | null) => ReactElement | null> + overlay?: FunctionalValue<(svg: SVGSVGElement | null, onUpdate?: () => void, onSettingsOpen?: () => void) => ReactElement | null> /** Название графика для загрузки */ downloadFilename?: string /** Событие, вызываемое при нажатий кнопки "Обновить" */ @@ -23,6 +23,8 @@ export type D3ContextMenuProps = BasePluginSettings & D3ContextMenuSettings & { children: any /** SVG-элемент */ svg: SVGSVGElement | null + /** Событие, вызываемое при нажатий кнопки "Настройки" */ + onSettingsOpen?: () => void } export const D3ContextMenu = memo(({ @@ -30,6 +32,7 @@ export const D3ContextMenu = memo(({ downloadFilename = 'chart', additionalMenuItems, onUpdate, + onSettingsOpen, trigger = ['contextMenu'], enabled = true, children, @@ -43,6 +46,9 @@ export const D3ContextMenu = memo(({ if (onUpdate) menuItems.push({ key: 'refresh', label: 'Обновить', onClick: onUpdate }) + if (onSettingsOpen) + menuItems.push({ key: 'settings', label: 'Настройки', onClick: onSettingsOpen }) + if (svg) menuItems.push({ key: 'download', label: ( Сохранить @@ -52,11 +58,11 @@ export const D3ContextMenu = memo(({ menuItems.push(...additionalMenuItems) return menuItems - }, [svg, downloadFilename, onUpdate, additionalMenuItems]) + }, [svg, downloadFilename, onUpdate, onSettingsOpen, additionalMenuItems]) return ( )} + overlay={overlay(svg, onUpdate, onSettingsOpen) || ( )} disabled={!enabled} trigger={trigger} > From 310253133dd82f4124249a51ca42646237a6fb00 Mon Sep 17 00:00:00 2001 From: goodmice Date: Mon, 8 Aug 2022 11:14:17 +0500 Subject: [PATCH 3/9] =?UTF-8?q?=D0=AD=D0=BB=D0=B5=D0=BC=D0=B5=D0=BD=D1=82?= =?UTF-8?q?=D1=8B=20=D1=83=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B0=D1=80=D1=85=D0=B8=D0=B2=D0=BE=D0=BC=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BD=D0=B5=D1=81=D0=B5=D0=BD=D1=8B=20=D0=B2=20LoaderPor?= =?UTF-8?q?tal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Telemetry/Archive/index.jsx | 42 +++++++++++++-------------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/src/pages/Telemetry/Archive/index.jsx b/src/pages/Telemetry/Archive/index.jsx index 4f0d2d3..e01beb3 100755 --- a/src/pages/Telemetry/Archive/index.jsx +++ b/src/pages/Telemetry/Archive/index.jsx @@ -222,7 +222,7 @@ const Archive = memo(() => { const chartData = useMemo(() => cutData(dataSaub, domain.min, domain.max), [dataSaub, domain]) return ( - <> +
Начальная дата:  @@ -243,27 +243,25 @@ const Archive = memo(() => {
- - formatDate(d) - }} - plugins={{ - menu: { enabled: false }, - cursor: { - width: 200, - render: cursorRender, - } - }} - height={'76vh'} - onWheel={onGraphWheel} - /> - - + formatDate(d) + }} + plugins={{ + menu: { enabled: false }, + cursor: { + width: 200, + render: cursorRender, + } + }} + height={'76vh'} + onWheel={onGraphWheel} + /> +
) }) From 5511c064106cc6a0cf01205adf8a2e307f0045a9 Mon Sep 17 00:00:00 2001 From: goodmice Date: Mon, 8 Aug 2022 12:35:08 +0500 Subject: [PATCH 4/9] =?UTF-8?q?*=20=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=20=D0=B2=D1=8B=D0=B1?= =?UTF-8?q?=D0=BE=D1=80=D0=B0=20=D1=86=D0=B2=D0=B5=D1=82=D0=B0=20*=20?= =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B7=D0=BE?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BD=D0=B0=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=D0=B5=20=D0=B8=20=D0=B2=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D1=86=D0=B5=20*=20=D0=94=D0=BE=D0=BF=D0=BE=D0=BB=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D1=81=D1=82=D0=B8=D0=BB=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/WellOperations/Tvd/TLChart.jsx | 10 ++-- src/pages/WellOperations/Tvd/TLPie.jsx | 62 ++++++++++++++++++++---- src/pages/WellOperations/Tvd/index.jsx | 39 ++++++--------- src/styles/tvd.less | 27 +++++++++++ 4 files changed, 101 insertions(+), 37 deletions(-) diff --git a/src/pages/WellOperations/Tvd/TLChart.jsx b/src/pages/WellOperations/Tvd/TLChart.jsx index b2ed74b..7050c87 100644 --- a/src/pages/WellOperations/Tvd/TLChart.jsx +++ b/src/pages/WellOperations/Tvd/TLChart.jsx @@ -8,8 +8,11 @@ import { useIdWell } from '@asb/context' import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { DetectedOperationService } from '@api' +import { unique } from '@utils/filters' import { formatDate } from '@utils' +import { makeGetColor } from '.' + const defaultOffset = { left: 40, right: 20, top: 20, bottom: 20 } const zeroDate = moment('2000-01-01 00:00:00') @@ -39,12 +42,13 @@ export const TLChart = memo(({ backgroundColor = '#0000', barHeight = 15, offset = defaultOffset, - color, }) => { const [isLoading, setIsLoading] = useState(false) const [svgRef, setSvgRef] = useState() const [data, setData] = useState() + const getColor = useMemo(() => makeGetColor(data?.map((row) => row.idCategory).filter(unique)), [data]) + const [rootRef, { width, height }] = useElementSize() const idWell = useIdWell() @@ -119,8 +123,8 @@ export const TLChart = memo(({ .attr('y', (d) => yAxis(moment(d.startTime).startOf('day')) - barHeight / 2) .attr('width', (d) => xAxis(d.endTime) - xAxis(d.startTime)) .attr('height', barHeight) - .attr('fill', (d) => color ? color(d.idCategory) : '#0008') - }, [svgRef, xAxis, yAxis, data, color]) + .attr('fill', (d) => getColor(d.idCategory)) + }, [svgRef, xAxis, yAxis, data, getColor]) return (
diff --git a/src/pages/WellOperations/Tvd/TLPie.jsx b/src/pages/WellOperations/Tvd/TLPie.jsx index 1305ca5..828c856 100644 --- a/src/pages/WellOperations/Tvd/TLPie.jsx +++ b/src/pages/WellOperations/Tvd/TLPie.jsx @@ -1,4 +1,4 @@ -import { memo, useEffect, useMemo, useState } from 'react' +import { memo, useCallback, useEffect, useMemo, useState } from 'react' import { useElementSize } from 'usehooks-ts' import { Empty } from 'antd' import * as d3 from 'd3' @@ -8,6 +8,11 @@ import { makeColumn, makeNumericColumn, makeTextColumn, Table } from '@component import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { DetectedOperationService } from '@api' +import { unique } from '@utils/filters' + +import { makeGetColor } from '.' + +import '@styles/tvd.less' const tableColumns = [ makeColumn('Цвет', 'color', { width: 50, render: (d) => ( @@ -19,10 +24,11 @@ const tableColumns = [ makeNumericColumn('Процент, %', 'percent', undefined, undefined, (d) => d ? d.toFixed(2) : '---', 100) ] -export const TLPie = memo(({ color }) => { +export const TLPie = memo(() => { const [isLoading, setIsLoading] = useState(false) const [svgRef, setSvgRef] = useState() const [stats, setStats] = useState([]) + const [selected, setSelected] = useState([]) const [rootRef, { width, height }] = useElementSize() @@ -30,20 +36,50 @@ export const TLPie = memo(({ color }) => { const pie = useMemo(() => d3.pie().value((d) => d.minutesTotal), []) + const getColor = useMemo(() => makeGetColor(stats?.map((row) => row.idCategory).filter(unique)), [stats]) + const tableData = useMemo(() => { if (!stats) return null const totalTime = stats.reduce((out, stat) => out + stat.minutesTotal, 0) return stats.map((stat) => ({ ...stat, - color: color(stat.idCategory), + color: getColor(stat.idCategory), percent: stat.minutesTotal / totalTime * 100, })) - }, [stats, color]) + }, [stats, getColor]) const data = useMemo(() => tableData ? pie(tableData) : null, [tableData]) const radius = useMemo(() => Math.min(width, height) / 2, [width, height]) + const onRow = useCallback((record) => { + const out = { + onMouseEnter: () => { + d3.selectAll('.tl-pie-part') + .filter((d) => d.data.idCategory === record.idCategory) + .attr('transform', 'scale(1.05)') + }, + onMouseLeave: () => { + d3.selectAll('.tl-pie-part') + .filter((d) => d.data.idCategory === record.idCategory) + .attr('transform', 'scale(1)') + } + } + if (record.idCategory === selected) + out.style = { background: '#FAFAFA' } + return out + }, [selected]) + + const onPieOver = useCallback(function (e, d) { + setSelected(d.data.idCategory) + d3.select(this).attr('transform', 'scale(1.05)') + }, []) + + const onPieOut = useCallback(function (e, d) { + setSelected(null) + d3.select(this).attr('transform', 'scale(1)') + }, []) + useEffect(() => { invokeWebApiWrapperAsync( async () => { @@ -66,9 +102,13 @@ export const TLPie = memo(({ color }) => { const newSlices = slices.enter().append('path') slices.merge(newSlices) + .attr('class', 'tl-pie-part') .attr('d', d3.arc().innerRadius(radius * 0.4).outerRadius(radius * 0.8)) - .attr('fill', (d) => color ? color(d.data.idCategory) : '#0008') - }, [svgRef, data, color, radius]) + .attr('fill', (d) => d.data.color) + .attr('data-id', (d) => d.idCategory) + .on('mouseover', onPieOver) + .on('mouseout', onPieOut) + }, [svgRef, data, radius, onPieOver, onPieOut]) useEffect(() => { if (!data) return @@ -81,7 +121,8 @@ export const TLPie = memo(({ color }) => { .data(data, (d) => d.data.category) lines.exit().remove() - const newLines = lines.enter().append('polyline') + const newLines = lines.enter() + .append('polyline') const abovePi = (d) => (d.startAngle + d.endAngle) / 2 < Math.PI @@ -118,8 +159,8 @@ export const TLPie = memo(({ color }) => {
{data ? ( -
-
+
+
@@ -134,10 +175,11 @@ export const TLPie = memo(({ color }) => { dataSource={tableData} scroll={{ y: '20vh', x: true }} pagination={false} + onRow={onRow} />
) : ( -
+
)} diff --git a/src/pages/WellOperations/Tvd/index.jsx b/src/pages/WellOperations/Tvd/index.jsx index 8b495c9..550de4c 100644 --- a/src/pages/WellOperations/Tvd/index.jsx +++ b/src/pages/WellOperations/Tvd/index.jsx @@ -9,7 +9,6 @@ import { D3Chart } from '@components/d3' import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { formatDate, fractionalSum, wrapPrivateComponent, getOperations } from '@utils' -import { DetectedOperationService } from '@api' import TLPie from './TLPie' import TLChart from './TLChart' @@ -21,12 +20,19 @@ import AdditionalTables from './AdditionalTables' import '@styles/index.css' import '@styles/tvd.less' -const colorArray = [ - '#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#3498db', - '#2980b9', '#9b59b6', '#8e44ad', '#34495e', '#2c3e50', - '#f1c40f', '#f39c12', '#e67e22', '#d35400', '#e74c3c', - '#c0392b', '#ecf0f1', '#bdc3c7', '#95a5a6', '#7f8c8d', -] +export const makeGetColor = (types) => (type) => { + if (!type) return '#0008' + const raw = [ + '#1abc9c', '#16a085', '#2ecc71', '#27ae60', '#3498db', + '#2980b9', '#9b59b6', '#8e44ad', '#34495e', '#2c3e50', + '#f1c40f', '#f39c12', '#e67e22', '#d35400', '#e74c3c', + '#c0392b', '#ecf0f1', '#bdc3c7', '#95a5a6', '#7f8c8d', + ] + + if (!types || types.length <= 0) return raw[type] + const i = types.indexOf(type) + return i < 0 ? raw[type] : raw[i] +} const Item = ({ label, children, ...other }) => (
{label}: {children}
) @@ -149,7 +155,6 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => { const [isLoading, setIsLoading] = useState(false) const [pointsEnabled, setPointsEnabled] = useState(true) const [selectedTab, setSelectedTab] = useState('Скрыть') - const [color, setColor] = useState() const idWellContext = useIdWell() const idWell = useMemo(() => wellId ?? idWellContext, [wellId, idWellContext]) @@ -189,20 +194,6 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => { ) }, [idWell]) - useEffect(() => { - invokeWebApiWrapperAsync( - async () => { - const cats = await DetectedOperationService.getCategories() - const color = d3.scaleOrdinal() - .domain(cats.map((cat) => cat.id)) - .range(colorArray) - setColor(() => color) - }, - undefined, - 'Не удалось получить список типов операций' - ) - }, []) - return (
@@ -250,8 +241,8 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => { />
{selectedTab === 'НПВ' && } - {selectedTab === 'ЕСО' && } - {selectedTab === 'Статистика' && } + {selectedTab === 'ЕСО' && } + {selectedTab === 'Статистика' && }
diff --git a/src/styles/tvd.less b/src/styles/tvd.less index 08137fc..4f61f63 100755 --- a/src/styles/tvd.less +++ b/src/styles/tvd.less @@ -78,3 +78,30 @@ } } } + +.empty-wrapper { + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.tl-pie { + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: space-between; + height: 100%; + + & .tl-pie-chart { + flex-grow: 1; + + & .lines { + pointer-events: none; + } + } +} + +.tl-pie-part { + transition: transform .1s ease-in-out; +} From fc6646eb80b898c95c819f3e628de0d13578e70d Mon Sep 17 00:00:00 2001 From: goodmice Date: Tue, 9 Aug 2022 11:54:06 +0500 Subject: [PATCH 5/9] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20tooltip=20=D0=B4=D0=BB=D1=8F=20=D0=B3=D1=80=D0=B0?= =?UTF-8?q?=D1=84=D0=B8=D0=BA=D0=B0=20=D0=BE=D0=BF=D0=B5=D1=80=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=B2=D1=80=D0=B5=D0=BC=D1=8F-=D0=B4=D0=B5?= =?UTF-8?q?=D0=BD=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Telemetry/Operations/OperationsChart.jsx | 8 ++-- src/pages/Telemetry/Operations/index.jsx | 6 ++- src/pages/WellOperations/Tvd/TLChart.jsx | 46 +++++++++++++++++-- src/styles/d3.less | 20 +++++++- src/styles/tvd.less | 11 +++++ 5 files changed, 82 insertions(+), 9 deletions(-) diff --git a/src/pages/Telemetry/Operations/OperationsChart.jsx b/src/pages/Telemetry/Operations/OperationsChart.jsx index a0e7e82..3a20be2 100644 --- a/src/pages/Telemetry/Operations/OperationsChart.jsx +++ b/src/pages/Telemetry/Operations/OperationsChart.jsx @@ -8,7 +8,7 @@ import '@styles/detected_operations.less' const displayNumber = makeDisplayValue({ fixed: 2 }) -const makeTooltipRender = (category) => ([{ data: [data] }]) => ( +export const makeTooltipRender = (category) => ([{ data: [data] }]) => (
{data.operationCategory?.name} {formatDate(data.dateStart, undefined, 'DD.MM.YYYY')} @@ -21,12 +21,12 @@ const makeTooltipRender = (category) => ([{ data: [data] }]) => ( {displayNumber(data.depthEnd)} м - {category?.name ?? 'Ключ'}: + {category?.keyValueName ?? 'Ключ'}: {displayNumber(data.value)} - {category?.unit ?? '----'} + {category?.keyValueUnits ?? '----'} Цель: {displayNumber(data.operationValue?.targetValue)} - {category?.unit ?? '----'} + {category?.keyValueUnits ?? '----'}
) diff --git a/src/pages/Telemetry/Operations/index.jsx b/src/pages/Telemetry/Operations/index.jsx index 8306be8..b04f835 100644 --- a/src/pages/Telemetry/Operations/index.jsx +++ b/src/pages/Telemetry/Operations/index.jsx @@ -70,7 +70,11 @@ const Operations = memo(() => { invokeWebApiWrapperAsync( async () => { const categories = arrayOrDefault(await DetectedOperationService.getCategories()) - setCategories(categories.map(({ id, name }) => ({ value: id, label: name }))) + setCategories(categories.map((category) => ({ + ...category, + value: category.id, + label: category.name, + }))) }, setIsLoading, 'Не удалось загрзуить категории операций' diff --git a/src/pages/WellOperations/Tvd/TLChart.jsx b/src/pages/WellOperations/Tvd/TLChart.jsx index 7050c87..84b431d 100644 --- a/src/pages/WellOperations/Tvd/TLChart.jsx +++ b/src/pages/WellOperations/Tvd/TLChart.jsx @@ -11,8 +11,13 @@ import { DetectedOperationService } from '@api' import { unique } from '@utils/filters' import { formatDate } from '@utils' +import { makeTooltipRender } from '@pages/Telemetry/Operations/OperationsChart' import { makeGetColor } from '.' +import '@styles/d3.less' +import '@styles/tvd.less' +import '@styles/detected_operations.less' + const defaultOffset = { left: 40, right: 20, top: 20, bottom: 20 } const zeroDate = moment('2000-01-01 00:00:00') @@ -42,10 +47,13 @@ export const TLChart = memo(({ backgroundColor = '#0000', barHeight = 15, offset = defaultOffset, + tooltipSize = { width: 200, height: 220 }, }) => { const [isLoading, setIsLoading] = useState(false) const [svgRef, setSvgRef] = useState() const [data, setData] = useState() + const [selected, setSelected] = useState() + const [tooltipPos, setTooltipPos] = useState({ x: 0, y: 0 }) const getColor = useMemo(() => makeGetColor(data?.map((row) => row.idCategory).filter(unique)), [data]) @@ -124,22 +132,54 @@ export const TLChart = memo(({ .attr('width', (d) => xAxis(d.endTime) - xAxis(d.startTime)) .attr('height', barHeight) .attr('fill', (d) => getColor(d.idCategory)) + .on('mouseover', (e, data) => { + d3.select(svgRef).select('.chart-area') + .selectAll('rect') + .filter((d) => d.idCategory === data.idCategory) + .attr('stroke-width', '2px') + const rect = e.target.getBoundingClientRect() + setTooltipPos({ x: rect.x, y: rect.y }) + setSelected(data) + }) + .on('mouseout', (e, data) => { + d3.select(svgRef).select('.chart-area') + .selectAll('rect') + .filter((d) => d.idCategory === data.idCategory) + .attr('stroke-width', '0') + setSelected(null) + }) }, [svgRef, xAxis, yAxis, data, getColor]) + const tooltipStyle = useMemo(() => { + return { + ...tooltipSize, + left: tooltipPos.x - tooltipSize.width - 8, + top: tooltipPos.y - tooltipSize.height / 2 + barHeight / 2, + } + }, [tooltipPos, tooltipSize, barHeight]) + return (
- + + {selected && ( +
+
+ {makeTooltipRender(selected.operationCategory)([{ data: [selected] }])} +
+
+ )} {data ? ( - + - + ) : ( diff --git a/src/styles/d3.less b/src/styles/d3.less index 5701d12..be70bbe 100644 --- a/src/styles/d3.less +++ b/src/styles/d3.less @@ -41,7 +41,7 @@ &.bottom { - margin-top: 0; + margin-bottom: @arrow-size; &::after { border-top-color: @bg-color; @@ -50,6 +50,24 @@ margin-left: -@arrow-size; } } + + &.left { + margin-left: @arrow-size; + &::after { + border-right-color: @bg-color; + top: 50%; + right: 100%; + } + } + + &.right { + margin-right: @arrow-size; + &::after { + border-left-color: @bg-color; + top: 50%; + left: 100%; + } + } & .tooltip-content { overflow: hidden; diff --git a/src/styles/tvd.less b/src/styles/tvd.less index 4f61f63..65beb38 100755 --- a/src/styles/tvd.less +++ b/src/styles/tvd.less @@ -86,6 +86,17 @@ justify-content: center; } +.tl-op-tooltip { + position: absolute; + pointer-events: none; +} + +.tl-op-chart { + & .chart-area > rect { + transition: stroke-width .25s ease-in-out; + } +} + .tl-pie { display: flex; flex-direction: column; From 15333510fdac913f191ab98cfa8f387f8e0a7d3e Mon Sep 17 00:00:00 2001 From: goodmice Date: Wed, 10 Aug 2022 14:39:03 +0500 Subject: [PATCH 6/9] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D1=8B=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD=D0=B8=D1=87=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=BB=D1=8F=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D1=89=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82=D1=83=D0=BB?= =?UTF-8?q?=D1=82=D0=B8=D0=BF=D0=B0=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=95=D0=A1=D0=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/icons/WellIcon.tsx | 8 ++++---- src/pages/WellOperations/Tvd/TLChart.jsx | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/icons/WellIcon.tsx b/src/components/icons/WellIcon.tsx index f0b7ddf..e0f8ed1 100755 --- a/src/components/icons/WellIcon.tsx +++ b/src/components/icons/WellIcon.tsx @@ -31,7 +31,7 @@ const defaultProps: WellIconProps = { } export const WellIcon = React.memo(({ width, height, state, online, colors, ...other } : WellIconProps = defaultProps) => { - colors = {...defaultColors, ...colors} + colors = { ...defaultColors, ...colors } return ( {online && ( // Полоски, показывающие наличие свежей телеметрии - - - + + + )} diff --git a/src/pages/WellOperations/Tvd/TLChart.jsx b/src/pages/WellOperations/Tvd/TLChart.jsx index 84b431d..2d5feca 100644 --- a/src/pages/WellOperations/Tvd/TLChart.jsx +++ b/src/pages/WellOperations/Tvd/TLChart.jsx @@ -137,8 +137,7 @@ export const TLChart = memo(({ .selectAll('rect') .filter((d) => d.idCategory === data.idCategory) .attr('stroke-width', '2px') - const rect = e.target.getBoundingClientRect() - setTooltipPos({ x: rect.x, y: rect.y }) + setTooltipPos(e.target.getBoundingClientRect()) setSelected(data) }) .on('mouseout', (e, data) => { @@ -153,8 +152,8 @@ export const TLChart = memo(({ const tooltipStyle = useMemo(() => { return { ...tooltipSize, - left: tooltipPos.x - tooltipSize.width - 8, - top: tooltipPos.y - tooltipSize.height / 2 + barHeight / 2, + left: Math.min(tooltipPos.x, screen.width) - tooltipSize.width - 8, + top: Math.min(tooltipPos.y - tooltipSize.height / 2 + barHeight / 2, screen.height - tooltipSize.height - 160), } }, [tooltipPos, tooltipSize, barHeight]) From a8613e6b759d31eabf2bf59216e96ed64300e2a0 Mon Sep 17 00:00:00 2001 From: goodmice Date: Wed, 10 Aug 2022 15:22:57 +0500 Subject: [PATCH 7/9] =?UTF-8?q?=D0=A1=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D1=8F=20=D0=B8?= =?UTF-8?q?=20=D1=81=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F?= =?UTF-8?q?=20=D0=BD=D0=B0=D1=81=D1=82=D1=80=D0=BE=D0=B5=D0=BA=20=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D1=84=D0=B8=D0=BA=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/d3/D3MonitoringChartEditor.tsx | 109 ++++++++---------- src/components/d3/D3MonitoringCharts.tsx | 51 ++++---- .../d3/D3MonitoringGroupsEditor.tsx | 81 +++++++------ .../d3/plugins/D3HorizontalCursor.tsx | 4 +- src/components/d3/plugins/D3Tooltip.tsx | 2 +- src/pages/Telemetry/TelemetryView/index.jsx | 7 +- 6 files changed, 128 insertions(+), 126 deletions(-) diff --git a/src/components/d3/D3MonitoringChartEditor.tsx b/src/components/d3/D3MonitoringChartEditor.tsx index 3bf55d1..02bad6e 100644 --- a/src/components/d3/D3MonitoringChartEditor.tsx +++ b/src/components/d3/D3MonitoringChartEditor.tsx @@ -1,4 +1,4 @@ -import { Button, Form, FormItemProps, Input, InputNumber, Select, Space, Tooltip, Typography } from 'antd' +import { Button, Form, FormItemProps, Input, InputNumber, Select, Tooltip } from 'antd' import { CSSProperties, memo, useCallback, useEffect, useMemo, useState } from 'react' import { ColorPicker, Color } from '../ColorPicker' @@ -17,87 +17,68 @@ const lineTypes = [ { value: 'needle', label: 'Иглы' }, ] -export type D3MonitoringChartEditorProps = Omit, HTMLDivElement>, 'onChange'> & { - chart?: ExtendedChartDataset | null +export type D3MonitoringChartEditorProps = { + chart: ExtendedChartDataset onChange: (value: ExtendedChartDataset) => boolean } const _D3MonitoringChartEditor = ({ chart: value, onChange, - - style, - ...other }: D3MonitoringChartEditorProps) => { - const [domain, setDomain] = useState({}) - const [color, setColor] = useState() - const [form] = Form.useForm() - const onDomainChange = useCallback((mm: MinMax) => { - setDomain((prev) => ({ - min: ('min' in mm) ? mm.min : prev?.min, - max: ('max' in mm) ? mm.max : prev?.max, - })) - }, []) - - const onColorChange = useCallback((color: Color) => setColor((prev) => color ?? prev), []) - - useEffect(() => { - if (value?.type) { - form.setFieldsValue(value) - } else { - form.resetFields() - } - setColor(value?.color ? new Color(String(value.color)) : undefined) - setDomain(value?.xDomain ?? {}) - }, [value, form]) - - const onSave = useCallback(() => { - if (!value) return + const onSave = useCallback((props: Partial>) => { const values = form.getFieldsValue() const newValue = { ...value, - color, - xDomain: domain, ...values, + ...props } onChange(newValue) - }, [form, domain, color, value]) + }, [value]) - const divStyle: CSSProperties = useMemo(() => ({ - display: 'flex', - flexDirection: 'column', - justifyContent: 'space-between', - alignItems: 'center', - ...style, - }), [style]) + const onDomainChange = useCallback((mm: MinMax) => { + onSave({ xDomain: { + min: ('min' in mm) ? mm.min : value.xDomain?.min, + max: ('max' in mm) ? mm.max : value.xDomain?.max, + }}) + }, [value]) + + const onColorChange = useCallback((color: Color) => { + onSave({ color: color.toHexString() }) + }, [value]) + + useEffect(() => { + if (value.type) + form.setFieldsValue(value) + else + form.resetFields() + }, [value, form]) return ( -
-
- - - - - - - - onDomainChange({ min })} placeholder={'Мин'} /> - onDomainChange({ max })} placeholder={'Макс'} /> - - - - - -
+
+ + onSave({ label: e.target.value })} /> + onSave({ shortLabel: e.target.value })} /> + + + + + onDomainChange({ min })} placeholder={'Мин'} /> + onDomainChange({ max })} placeholder={'Макс'} /> + + + + + ) } diff --git a/src/components/d3/D3MonitoringCharts.tsx b/src/components/d3/D3MonitoringCharts.tsx index 9e7ce25..379078a 100644 --- a/src/components/d3/D3MonitoringCharts.tsx +++ b/src/components/d3/D3MonitoringCharts.tsx @@ -184,31 +184,27 @@ const _D3MonitoringCharts = >({ const [settingsVisible, setSettingsVisible] = useState(true) useEffect(() => { - if (!chartName) { - setDatasets(datasetGroups) - return - } - let datasets: ExtendedChartDataset[][] = [] - let needInsert = false invokeWebApiWrapperAsync( async () => { - const sets = await UserSettingsService.get(chartName) - needInsert = !sets - datasets = sets ?? datasetGroups + let sets = chartName ? await UserSettingsService.get(chartName) : null + if (typeof sets === 'string') + sets = JSON.parse(sets) + if (Array.isArray(sets)) { + setDatasets(sets) + } else if (Array.isArray(datasetGroups)) { + setDatasets(datasetGroups) + if (chartName) { + invokeWebApiWrapperAsync( + async () => await UserSettingsService.insert(chartName, datasetGroups), + undefined, + 'Не удалось сохранить настройки графиков' + ) + } + } }, undefined, 'Не удалось загрузить настройки графиков' ) - setDatasets(datasets) - if (needInsert) { - invokeWebApiWrapperAsync( - async () => { - await UserSettingsService.insert(chartName, datasets) - }, - undefined, - 'Не удалось сохранить настройки графиков' - ) - } }, [datasetGroups, chartName]) const offset = usePartialProps(_offset, defaultOffsets) @@ -242,7 +238,7 @@ const _D3MonitoringCharts = >({ groupLeft: (i: number) => (groupWidth + spaceBetweenGroups) * i, axisTop: (i: number, count: number) => axisHeight * (maxChartCount - count + i + 1) }) - }, [groups, height, offset]) + }, [groups, width, height, offset, axisHeight, spaceBetweenGroups]) const yAxis = useMemo(() => { if (!data) return @@ -258,7 +254,7 @@ const _D3MonitoringCharts = >({ const out: [string | number, ChartDomain][] = group.charts.map((chart) => { const mm = { ...chart.xDomain } let domain: Required = { min: 0, max: 100 } - if (mm.min && mm.max) { + if (!Number.isNaN((mm.min ?? NaN) + (mm.max ?? NaN))) { domain = mm as Required } else if (data) { const [min, max] = d3.extent(data, chart.x) @@ -291,9 +287,18 @@ const _D3MonitoringCharts = >({ ), [chartArea, axesArea]) const onGroupsChange = useCallback((sets: ExtendedChartDataset[][]) => { + if (chartName) { + invokeWebApiWrapperAsync( + async () => { + await UserSettingsService.update(chartName, sets) + }, + undefined, + 'Не удалось сохранить параметры графиков' + ) + } setDatasets(sets) setSettingsVisible(false) - }, []) + }, [chartName]) useEffect(() => { if (isDev()) { @@ -345,7 +350,7 @@ const _D3MonitoringCharts = >({ animDurationMs, ...dataset, yAxis: dataset.yAxis ?? yAxisConfig, - y: getByAccessor(dataset.yAxis.accessor ?? yAxisConfig.accessor), + y: getByAccessor(dataset.yAxis?.accessor ?? yAxisConfig.accessor), x: getByAccessor(dataset.xAxis?.accessor), } ) diff --git a/src/components/d3/D3MonitoringGroupsEditor.tsx b/src/components/d3/D3MonitoringGroupsEditor.tsx index d08ed14..c392b58 100644 --- a/src/components/d3/D3MonitoringGroupsEditor.tsx +++ b/src/components/d3/D3MonitoringGroupsEditor.tsx @@ -1,10 +1,11 @@ -import { Key, memo, useCallback, useEffect, useMemo, useState } from 'react' -import { Divider, Modal, Tooltip, Tree } from 'antd' +import { CSSProperties, Key, memo, useCallback, useEffect, useMemo, useState } from 'react' +import { Divider, Empty, Modal, Tooltip, Tree } from 'antd' import { getChartIcon } from '@utils' -import { ChartGroup, ExtendedChartDataset } from './D3MonitoringCharts' +import { ExtendedChartDataset } from './D3MonitoringCharts' import D3MonitoringChartEditor from './D3MonitoringChartEditor' +import { notify } from '../factory' export type D3MonitoringGroupsEditorProps = { visible?: boolean @@ -14,30 +15,30 @@ export type D3MonitoringGroupsEditorProps = { name?: string } -const moveToPos = (arr: T[], pos: number, newPos: number): T[] => { - if (pos === newPos) return arr - if (newPos === -1) return [arr[pos], ...arr.slice(0, pos), ...arr.slice(pos + 1)] - if (newPos === arr.length) return [...arr.slice(0, pos), ...arr.slice(pos + 1), arr[pos]] - const newArray = [] - for (let i = 0; i < arr.length; i++) { - if (i == newPos) newArray.push(arr[pos]) - if (i !== pos) newArray.push(arr[i]) - } - return newArray -} - const getChartLabel = (chart: ExtendedChartDataset) => ( {getChartIcon(chart)} {chart.label} -) +) + +const divStyle: CSSProperties = { + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + alignItems: 'center', + flexGrow: 1, +} + +const getNodePos = (node: any): { group: number, chart?: number } => { + const out = node.pos.split('-').map(Number) + return { group: out[1], chart: out[2] } +} const _D3MonitoringGroupsEditor = ({ visible, groups: oldGroups, onChange, onCancel, - name, }: D3MonitoringGroupsEditorProps) => { const [groups, setGroups] = useState[][]>([]) const [expand, setExpand] = useState([]) @@ -48,22 +49,30 @@ const _D3MonitoringGroupsEditor = ({ const onModalOk = useCallback(() => onChange(groups), [groups]) const onDrop = useCallback((info: any) => { - const { dragNode, dropPosition, dropToGap } = info + const { dragNode, dropPosition, node } = info - const nodes = dragNode.pos.split('-') - const groupPos = Number(nodes[1]) + const targetNodes = getNodePos(node) + const dragNodes = getNodePos(dragNode) + const groupPos = dragNodes.group if (!Number.isFinite(groupPos)) return setGroups((prev) => { - if (dropToGap) { - if (nodes.length < 3) - return moveToPos(prev, groupPos, dropPosition) - } else { - if (nodes.length < 3) return prev - const chartPos = Number(nodes[2]) - if (Number.isFinite(chartPos)) { - prev[groupPos] = moveToPos(prev[groupPos], chartPos, dropPosition) - return [ ...prev ] + if (typeof dragNodes.chart === 'undefined') { + const groups = [ ...prev ] + const movedGroups = groups.splice(dragNodes.group, 1) + groups.splice(Math.max(dropPosition - 1, 0), 0, ...movedGroups) + return groups + } else if (Number.isFinite(dragNodes.chart)) { + if (groupPos !== targetNodes.group) { + const dragKey = prev[dragNodes.group][dragNodes.chart].key + if (prev[targetNodes.group].find((chart) => chart.key === dragKey)) { + notify('График с данным ключом уже существует в этой группе. Перемещение невозможно', 'warning') + return prev + } } + const groups = [ ...prev ] + const charts = groups[groupPos].splice(dragNodes.chart, 1) + groups[targetNodes.group].splice(Math.max(dropPosition - 1, 0), 0, ...charts) + return groups } return prev }) @@ -108,7 +117,7 @@ const _D3MonitoringGroupsEditor = ({ return ( ({ title={'Настройка групп графиков'} >
-
+
setExpand(keys)} expandedKeys={expand} @@ -130,7 +139,13 @@ const _D3MonitoringGroupsEditor = ({ />
- chart={selectedChart} style={{ flexGrow: 1 }} onChange={onChartChange} /> +
+ {selectedChart ? ( + chart={selectedChart} onChange={onChartChange} /> + ) : ( + + )} +
) diff --git a/src/components/d3/plugins/D3HorizontalCursor.tsx b/src/components/d3/plugins/D3HorizontalCursor.tsx index 2a3f212..3b99157 100644 --- a/src/components/d3/plugins/D3HorizontalCursor.tsx +++ b/src/components/d3/plugins/D3HorizontalCursor.tsx @@ -1,4 +1,4 @@ -import { CSSProperties, ReactNode, SVGProps, useEffect, useMemo, useRef, useState } from 'react' +import { CSSProperties, ReactNode, SVGProps, useCallback, useEffect, useMemo, useRef, useState } from 'react' import * as d3 from 'd3' import { useD3MouseZone } from '@components/d3/D3MouseZone' @@ -104,7 +104,7 @@ const _D3HorizontalCursor = ({ if (!unsubscribe() && isDev()) console.warn('Не удалось отвязать эвент') } - }, [subscribe]) + }, [subscribe, fixed, mouseState.visible]) useEffect(() => { if (!zone || !getXLine) return diff --git a/src/components/d3/plugins/D3Tooltip.tsx b/src/components/d3/plugins/D3Tooltip.tsx index 8622d59..86e9f9c 100644 --- a/src/components/d3/plugins/D3Tooltip.tsx +++ b/src/components/d3/plugins/D3Tooltip.tsx @@ -110,7 +110,7 @@ function _D3Tooltip>({ if (!unsubscribe() && isDev()) console.warn('Не удалось отвязать эвент') } - }, [visible]) + }, [subscribe, visible]) useEffect(() => { if (!tooltipRef.current || !zoneRect || fixed) return diff --git a/src/pages/Telemetry/TelemetryView/index.jsx b/src/pages/Telemetry/TelemetryView/index.jsx index 7d5791c..8110c32 100755 --- a/src/pages/Telemetry/TelemetryView/index.jsx +++ b/src/pages/Telemetry/TelemetryView/index.jsx @@ -49,7 +49,6 @@ const makeDataset = (label, shortLabel, color, key, unit, other) => ({ label, shortLabel, color, - yAxis, xAxis: { type: 'linear', accessor: key, @@ -171,7 +170,7 @@ const TelemetryView = memo(() => { useEffect(() => { const subscribtion = saubSubject$.pipe( buffer(saubSubject$.pipe(throttleTime(700))) - ).subscribe((data) => handleDataSaub(data.flat())) + ).subscribe((data) => handleDataSaub(data.flat().filter(Boolean))) return () => subscribtion.unsubscribe() }, [saubSubject$]) @@ -179,7 +178,7 @@ const TelemetryView = memo(() => { useEffect(() => { const subscribtion = spinSubject$.pipe( buffer(spinSubject$.pipe(throttleTime(700))) - ).subscribe((data) => handleDataSpin(data.flat())) + ).subscribe((data) => handleDataSpin(data.flat().filter(Boolean))) return () => subscribtion.unsubscribe() }, [spinSubject$]) @@ -294,9 +293,11 @@ const TelemetryView = memo(() => { formatDate(d, 'YYYY-MM-DD') From 934eab2f3c5ff7afe02a54319298cdf308be3674 Mon Sep 17 00:00:00 2001 From: goodmice Date: Thu, 11 Aug 2022 17:14:36 +0500 Subject: [PATCH 8/9] =?UTF-8?q?*=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20=D1=80=D0=B5=D0=B4=D0=B0=D0=BA=D1=82=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=B2=D1=8F=D0=B7=D0=BA=D0=B8=20*=20=D0=98=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=81=D0=BE=D1=85=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/d3/D3MonitoringChartEditor.tsx | 33 +++++- src/components/d3/D3MonitoringCharts.tsx | 105 +++++++++++------- .../d3/D3MonitoringGroupsEditor.tsx | 60 ++++++---- src/components/d3/types.ts | 2 +- src/pages/Telemetry/Archive/index.jsx | 3 +- src/pages/Telemetry/TelemetryView/index.jsx | 9 +- 6 files changed, 137 insertions(+), 75 deletions(-) diff --git a/src/components/d3/D3MonitoringChartEditor.tsx b/src/components/d3/D3MonitoringChartEditor.tsx index 02bad6e..462e9ff 100644 --- a/src/components/d3/D3MonitoringChartEditor.tsx +++ b/src/components/d3/D3MonitoringChartEditor.tsx @@ -1,5 +1,5 @@ import { Button, Form, FormItemProps, Input, InputNumber, Select, Tooltip } from 'antd' -import { CSSProperties, memo, useCallback, useEffect, useMemo, useState } from 'react' +import { memo, useCallback, useEffect, useMemo } from 'react' import { ColorPicker, Color } from '../ColorPicker' import { ExtendedChartDataset } from './D3MonitoringCharts' @@ -18,11 +18,13 @@ const lineTypes = [ ] export type D3MonitoringChartEditorProps = { + group: ExtendedChartDataset[] chart: ExtendedChartDataset onChange: (value: ExtendedChartDataset) => boolean } const _D3MonitoringChartEditor = ({ + group, chart: value, onChange, }: D3MonitoringChartEditorProps) => { @@ -30,6 +32,7 @@ const _D3MonitoringChartEditor = ({ const onSave = useCallback((props: Partial>) => { const values = form.getFieldsValue() + if (!values['label']) return const newValue = { ...value, ...values, @@ -51,16 +54,34 @@ const _D3MonitoringChartEditor = ({ useEffect(() => { if (value.type) - form.setFieldsValue(value) + form.setFieldsValue({ + linkedTo: null, + label: null, + shortLabel: null, + ...value, + }) else form.resetFields() }, [value, form]) + const options = useMemo(() => group.filter((chart) => chart.key !== value.key).map((chart) => ({ + value: chart.key, + label: chart.label, + })), [group, value]) + return (
- + + onSave({ label: e.target.value })} /> @@ -69,10 +90,10 @@ const _D3MonitoringChartEditor = ({ - onDomainChange({ min })} placeholder={'Мин'} /> - onDomainChange({ max })} placeholder={'Макс'} /> + onDomainChange({ min })} placeholder={'Мин'} /> + onDomainChange({ max })} placeholder={'Макс'} /> diff --git a/src/components/d3/D3MonitoringCharts.tsx b/src/components/d3/D3MonitoringCharts.tsx index 379078a..86ee9c0 100644 --- a/src/components/d3/D3MonitoringCharts.tsx +++ b/src/components/d3/D3MonitoringCharts.tsx @@ -5,7 +5,9 @@ import { Empty } from 'antd' import * as d3 from 'd3' import LoaderPortal from '@components/LoaderPortal' +import { invokeWebApiWrapperAsync } from '@components/factory' import { isDev, usePartialProps } from '@utils' +import { UserSettingsService } from '@api' import { ChartAxis, @@ -23,22 +25,25 @@ import { 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' -import D3MonitoringGroupsEditor from './D3MonitoringGroupsEditor' -import { UserSettingsService } from '@asb/services/api' -import { invokeWebApiWrapperAsync } from '../factory' const roundTo = (v: number, to: number = 50) => { - if (to == 0) return v - if (v < 0) return Math.round(v / to) * to - return Math.ceil(v / to) * to + if (v === 0) return v + return (v > 0 ? Math.ceil : Math.round)(v / to) * to } -const calculateDomain = (mm: MinMax, round: number = 100): Required => { +const calculateDomain = (mm: MinMax): Required => { + let round = Math.abs((mm.max ?? 0) - (mm.min ?? 0)) + if (round < 10) round = 10 + else if (round < 100) round = roundTo(round, 10) + else if (round < 1000) round = roundTo(round, 100) + else if (round < 10000) round = roundTo(round, 1000) + else round = 0 let min = roundTo(mm.min ?? 0, round) let max = roundTo(mm.max ?? round, round) - if (min - max < round) { + if (round && Math.abs(min - max) < round) { const mid = (min + max) / 2 min = mid - round max = mid + round @@ -86,7 +91,7 @@ const getDefaultYTicks = (): Required> => ({ /** * @template DataType тип данных отображаемых записей */ -export type D3MonitoringChartsProps> = React.DetailedHTMLProps, HTMLDivElement> & { +export type D3MonitoringChartsProps> = Omit, HTMLDivElement>, 'ref'> & { /** Двумерный массив датасетов (группа-график) */ datasetGroups: ExtendedChartDataset[][] /** Ширина графика числом пикселей или CSS-значением (px/%/em/rem) */ @@ -126,6 +131,9 @@ export type D3MonitoringChartsProps> = Reac spaceBetweenGroups?: number /** Название графика для сохранения в базе */ chartName?: string + methods?: (value: { + setSettingsVisible: (visible: boolean) => void + }) => void } export type ChartSizes = ChartOffset & { @@ -171,6 +179,7 @@ const _D3MonitoringCharts = >({ spaceBetweenGroups = 30, dash, chartName, + methods, className = '', ...other @@ -181,32 +190,8 @@ const _D3MonitoringCharts = >({ const [yAxisRef, setYAxisRef] = useState(null) const [chartAreaRef, setChartAreaRef] = useState(null) const [axesAreaRef, setAxesAreaRef] = useState(null) - const [settingsVisible, setSettingsVisible] = useState(true) + const [settingsVisible, setSettingsVisible] = useState(false) - useEffect(() => { - invokeWebApiWrapperAsync( - async () => { - let sets = chartName ? await UserSettingsService.get(chartName) : null - if (typeof sets === 'string') - sets = JSON.parse(sets) - if (Array.isArray(sets)) { - setDatasets(sets) - } else if (Array.isArray(datasetGroups)) { - setDatasets(datasetGroups) - if (chartName) { - invokeWebApiWrapperAsync( - async () => await UserSettingsService.insert(chartName, datasetGroups), - undefined, - 'Не удалось сохранить настройки графиков' - ) - } - } - }, - undefined, - 'Не удалось загрузить настройки графиков' - ) - }, [datasetGroups, chartName]) - const offset = usePartialProps(_offset, defaultOffsets) const yTicks = usePartialProps>>(_yTicks, getDefaultYTicks) const yAxisConfig = usePartialProps>(_yAxisConfig, getDefaultYAxisConfig) @@ -258,7 +243,7 @@ const _D3MonitoringCharts = >({ domain = mm as Required } else if (data) { const [min, max] = d3.extent(data, chart.x) - domain = calculateDomain({ min, max, ...mm }, 100) + domain = calculateDomain({ min, max, ...mm }) } return [chart.key, { scale: d3.scaleLinear().domain([domain.min, domain.max]), @@ -268,7 +253,7 @@ const _D3MonitoringCharts = >({ out.forEach(([key], i) => { const chart = group.charts.find((chart) => chart.key === key) - const bind = chart?.bindDomainFrom + const bind = chart?.linkedTo if (!bind) return const bindDomain = out.find(([key]) => key === bind) if (bindDomain) @@ -284,7 +269,7 @@ const _D3MonitoringCharts = >({ key: i, charts: [], } - ), [chartArea, axesArea]) + ), [chartArea]) const onGroupsChange = useCallback((sets: ExtendedChartDataset[][]) => { if (chartName) { @@ -300,6 +285,44 @@ const _D3MonitoringCharts = >({ setSettingsVisible(false) }, [chartName]) + const onGroupsReset = useCallback(() => { + setSettingsVisible(false) + setDatasets(datasetGroups) + if (chartName) { + invokeWebApiWrapperAsync( + async () => await UserSettingsService.delete(chartName), + undefined, + 'Не удалось удалить настройки графиков' + ) + } + }, [datasetGroups, chartName]) + + useEffect(() => methods?.({ setSettingsVisible }), [methods]) + + useEffect(() => { + invokeWebApiWrapperAsync( + async () => { + let sets = chartName ? await UserSettingsService.get(chartName) : null + if (typeof sets === 'string') + sets = JSON.parse(sets) + if (Array.isArray(sets)) { + setDatasets(sets) + } else if (Array.isArray(datasetGroups)) { + setDatasets(datasetGroups) + if (chartName) { + invokeWebApiWrapperAsync( + async () => await UserSettingsService.insert(chartName, datasetGroups), + undefined, + 'Не удалось сохранить настройки графиков' + ) + } + } + }, + undefined, + 'Не удалось загрузить настройки графиков' + ) + }, [datasetGroups, chartName]) + useEffect(() => { if (isDev()) { datasets.forEach((sets, i) => { @@ -374,7 +397,7 @@ const _D3MonitoringCharts = >({ }, [yAxisConfig, chartArea, datasets, animDurationMs, createAxesGroup]) useEffect(() => { - const axesGroups = d3.select(axesAreaRef) + const axesGroups = axesArea() .selectAll('.charts-group') .data(groups) @@ -383,7 +406,7 @@ const _D3MonitoringCharts = >({ .append('g') .attr('class', 'charts-group') - const actualAxesGroups = d3.select(axesAreaRef) + const actualAxesGroups = axesArea() .selectAll>('.charts-group') .attr('class', (g) => `charts-group ${getGroupClass(g.key)}`) .attr('transform', (g) => `translate(${sizes.groupLeft(g.key)}, 0)`) @@ -436,7 +459,7 @@ const _D3MonitoringCharts = >({ .attr('stroke', j === chartsData.length - 1 ? 'gray' : 'currentColor') }) }) - }, [groups, sizes, spaceBetweenGroups, chartDomains]) + }, [groups, sizes, spaceBetweenGroups, chartDomains, axesArea]) useEffect(() => { // Рисуем ось Y if (!yAxis) return @@ -566,11 +589,11 @@ const _D3MonitoringCharts = >({
)} setSettingsVisible(false)} + onReset={onGroupsReset} />
diff --git a/src/components/d3/D3MonitoringGroupsEditor.tsx b/src/components/d3/D3MonitoringGroupsEditor.tsx index c392b58..25f9f13 100644 --- a/src/components/d3/D3MonitoringGroupsEditor.tsx +++ b/src/components/d3/D3MonitoringGroupsEditor.tsx @@ -1,5 +1,7 @@ import { CSSProperties, Key, memo, useCallback, useEffect, useMemo, useState } from 'react' -import { Divider, Empty, Modal, Tooltip, Tree } from 'antd' +import { Button, Divider, Empty, Modal, Popconfirm, Tooltip, Tree } from 'antd' +import { UndoOutlined } from '@ant-design/icons' +import { EventDataNode } from 'antd/lib/tree' import { getChartIcon } from '@utils' @@ -12,7 +14,7 @@ export type D3MonitoringGroupsEditorProps = { groups: ExtendedChartDataset[][] onChange: (value: ExtendedChartDataset[][]) => void onCancel: () => void - name?: string + onReset: () => void } const getChartLabel = (chart: ExtendedChartDataset) => ( @@ -29,7 +31,7 @@ const divStyle: CSSProperties = { flexGrow: 1, } -const getNodePos = (node: any): { group: number, chart?: number } => { +const getNodePos = (node: EventDataNode): { group: number, chart?: number } => { const out = node.pos.split('-').map(Number) return { group: out[1], chart: out[2] } } @@ -39,6 +41,7 @@ const _D3MonitoringGroupsEditor = ({ groups: oldGroups, onChange, onCancel, + onReset, }: D3MonitoringGroupsEditorProps) => { const [groups, setGroups] = useState[][]>([]) const [expand, setExpand] = useState([]) @@ -48,30 +51,37 @@ const _D3MonitoringGroupsEditor = ({ const onModalOk = useCallback(() => onChange(groups), [groups]) - const onDrop = useCallback((info: any) => { + const onDrop = useCallback((info: { + node: EventDataNode + dragNode: EventDataNode + dropPosition: number + }) => { const { dragNode, dropPosition, node } = info const targetNodes = getNodePos(node) const dragNodes = getNodePos(dragNode) const groupPos = dragNodes.group - if (!Number.isFinite(groupPos)) return + if (!Number.isFinite(groupPos + dropPosition)) return setGroups((prev) => { - if (typeof dragNodes.chart === 'undefined') { + const chartPos = dragNodes.chart + if (typeof chartPos === 'undefined') { const groups = [ ...prev ] - const movedGroups = groups.splice(dragNodes.group, 1) + const movedGroups = groups.splice(groupPos, 1) groups.splice(Math.max(dropPosition - 1, 0), 0, ...movedGroups) return groups - } else if (Number.isFinite(dragNodes.chart)) { - if (groupPos !== targetNodes.group) { - const dragKey = prev[dragNodes.group][dragNodes.chart].key - if (prev[targetNodes.group].find((chart) => chart.key === dragKey)) { + } else if (Number.isFinite(chartPos)) { + const targetGroup = targetNodes.group + const dragKey = prev[groupPos][chartPos].key + if (groupPos !== targetGroup) { + if (prev[targetGroup].find((chart) => chart.key === dragKey)) { notify('График с данным ключом уже существует в этой группе. Перемещение невозможно', 'warning') return prev } } const groups = [ ...prev ] - const charts = groups[groupPos].splice(dragNodes.chart, 1) - groups[targetNodes.group].splice(Math.max(dropPosition - 1, 0), 0, ...charts) + const charts = groups[groupPos].filter((chart) => chart.key === dragKey || chart.linkedTo === dragKey) + groups[groupPos] = groups[groupPos].filter((chart) => chart.key !== dragKey && chart.linkedTo !== dragKey) + groups[targetGroup].splice(Math.max(dropPosition - 1, 0), 0, ...charts) return groups } return prev @@ -98,11 +108,8 @@ const _D3MonitoringGroupsEditor = ({ return { group, chart } }, [selected]) - const selectedChart = useMemo(() => { - if (!selectedIdx) return null - - return groups[selectedIdx.group][selectedIdx.chart] - }, [groups, selectedIdx]) + const selectedGroup = useMemo(() => selectedIdx ? groups[selectedIdx.group] : null, [groups, selectedIdx]) + const selectedChart = useMemo(() => selectedIdx ? groups[selectedIdx.group][selectedIdx.chart] : null, [groups, selectedIdx]) const onChartChange = useCallback((chart: ExtendedChartDataset) => { if (!selectedIdx) return false @@ -119,10 +126,17 @@ const _D3MonitoringGroupsEditor = ({ centered width={800} visible={visible} - onOk={onModalOk} - onCancel={onCancel} - okText={'Сохранить изменения'} title={'Настройка групп графиков'} + onCancel={onCancel} + footer={( + <> + + + + + + + )} >
@@ -140,8 +154,8 @@ const _D3MonitoringGroupsEditor = ({
- {selectedChart ? ( - chart={selectedChart} onChange={onChartChange} /> + {selectedGroup && selectedChart ? ( + group={selectedGroup} chart={selectedChart} onChange={onChartChange} /> ) : ( )} diff --git a/src/components/d3/types.ts b/src/components/d3/types.ts index 6094803..5f7ae80 100644 --- a/src/components/d3/types.ts +++ b/src/components/d3/types.ts @@ -62,7 +62,7 @@ export type BaseChartDataset = { /** Параметры штриховки графика */ dash?: string | number | [string | number, string | number] /** Привязка домена к домену другого графика */ - bindDomainFrom?: string | number + linkedTo?: string | number } export type LineChartDataset = { diff --git a/src/pages/Telemetry/Archive/index.jsx b/src/pages/Telemetry/Archive/index.jsx index e01beb3..03b20b9 100755 --- a/src/pages/Telemetry/Archive/index.jsx +++ b/src/pages/Telemetry/Archive/index.jsx @@ -14,7 +14,7 @@ import { PeriodPicker, defaultPeriod } from '@components/selectors/PeriodPicker' import { formatDate, range, wrapPrivateComponent } from '@utils' import { TelemetryDataSaubService } from '@api' -import { makeChartGroups, normalizeData } from '../TelemetryView' +import { makeChartGroups, normalizeData, yAxis } from '../TelemetryView' import cursorRender from '../TelemetryView/cursorRender' const DATA_COUNT = 2048 // Колличество точек на подгрузку графика @@ -247,6 +247,7 @@ const Archive = memo(() => { datasetGroups={chartGroups} data={chartData} yDomain={domain} + yAxis={yAxis} yTicks={{ visible: true, format: (d) => formatDate(d) diff --git a/src/pages/Telemetry/TelemetryView/index.jsx b/src/pages/Telemetry/TelemetryView/index.jsx index 8110c32..dca17a5 100755 --- a/src/pages/Telemetry/TelemetryView/index.jsx +++ b/src/pages/Telemetry/TelemetryView/index.jsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback, memo, useMemo } from 'react' import { BehaviorSubject, buffer, throttleTime } from 'rxjs' -import { Select } from 'antd' +import { Button, Select } from 'antd' import { useIdWell } from '@asb/context' import { makeDateSorter } from '@components/Table' @@ -36,7 +36,7 @@ import '@styles/message.css' const { Option } = Select -const yAxis = { +export const yAxis = { type: 'time', accessor: (d) => new Date(d.date), format: (d) => formatDate(d, undefined, 'YYYY-MM-DD HH:mm:ss'), @@ -71,7 +71,7 @@ export const makeChartGroups = (flowChart) => { maxXAccessor: 'depthEnd', minYAccessor: accessor + 'Min', maxYAccessor: accessor + 'Max', - bindDomainFrom: accessor, + linkedTo: accessor, }) return [ @@ -148,6 +148,7 @@ const TelemetryView = memo(() => { const [flowChartData, setFlowChartData] = useState([]) const [rop, setRop] = useState(null) const [domain, setDomain] = useState({}) + const [chartMethods, setChartMethods] = useState() const idWell = useIdWell() @@ -271,6 +272,7 @@ const TelemetryView = memo(() => { Интервал: 
+
Статус: