diff --git a/src/components/d3/D3Chart.tsx b/src/components/d3/D3Chart.tsx index 4f4dbc8..31dc4a5 100644 --- a/src/components/d3/D3Chart.tsx +++ b/src/components/d3/D3Chart.tsx @@ -50,7 +50,7 @@ const createAxis = (config: ChartAxis) => { export type D3ChartProps = React.DetailedHTMLProps, HTMLDivElement> & { xAxis: ChartAxis datasets: ChartDataset[] - data?: DataType[] + data?: DataType[] | Record domain?: Partial width?: number | string height?: number | string @@ -58,7 +58,7 @@ export type D3ChartProps = React.DetailedHTMLProps animDurationMs?: number backgroundColor?: Property.Color - ticks?: ChartTicks + ticks?: ChartTicks plugins?: { menu?: BasePluginSettings & D3ContextMenuSettings tooltip?: BasePluginSettings & D3TooltipSettings @@ -104,11 +104,26 @@ export const D3Chart = memo>(({ if (!data) return const xAxis = createAxis(xAxisConfig) - - const [minX, maxX] = d3.extent(data, getX) - xAxis.domain([domain?.x?.min ?? minX, domain?.x?.max ?? maxX]) xAxis.range([0, width - offset.left - offset.right]) + if (domain?.x?.min && domain?.x?.max) { + xAxis.domain([domain.x.min, domain.x.max]) + return xAxis + } + + if (Array.isArray(data)) { + const [minX, maxX] = d3.extent(data, getX) + xAxis.domain([domain?.x?.min ?? minX, domain?.x?.max ?? maxX]) + } else { + let [minX, maxX] = [Infinity, -Infinity] + for (const key in data) { + const [min, max] = d3.extent(data[key], getX) + if (min < minX) minX = min + if (max > maxX) maxX = max + } + xAxis.domain([domain?.x?.min ?? minX, domain?.x?.max ?? maxX]) + } + return xAxis }, [xAxisConfig, getX, data, domain, width, offset]) @@ -127,11 +142,21 @@ export const D3Chart = memo>(({ let minY = Infinity let maxY = -Infinity - charts.forEach(({ y }) => { - const [min, max] = d3.extent(data, y) - if (min && min < minY) minY = min - if (max && max > maxY) maxY = max - }) + if (Array.isArray(data)) { + charts.forEach(({ y }) => { + const [min, max] = d3.extent(data, y) + if (min && min < minY) minY = min + if (max && max > maxY) maxY = max + }) + } else { + for (const key in data) { + const chart = charts.find((chart) => chart.key === key) + if (!chart) continue + const [min, max] = d3.extent(data[key], chart.y) + if (min && min < minY) minY = min + if (max && max > maxY) maxY = max + } + } yAxis.domain([ domain?.y?.min ?? minY, @@ -149,7 +174,7 @@ export const D3Chart = memo>(({ .duration(animDurationMs) .call(d3.axisBottom(xAxis) .tickSize((ticks?.x?.visible ?? false) ? -height + offset.bottom : 0) - .tickFormat((d) => formatDate(d, undefined, 'YYYY-MM-DD') || 'NaN') + .tickFormat((d) => ticks?.x?.format?.(d) ?? String(d)) .ticks(ticks?.x?.count ?? 10) as any // TODO: Исправить тип ) @@ -223,7 +248,8 @@ export const D3Chart = memo>(({ .attr('opacity', chart.opacity ?? 1) .attr('fill', 'none') - let d = data + let d = Array.isArray(data) ? data : data[String(chart.key).split(':')[0]] + if (!d) return let elms switch (chart.type) { @@ -294,15 +320,13 @@ export const D3Chart = memo>(({ .attr('stroke', chart.point?.strokeColor ?? null) .attr('fill', chart.point?.fillColor ?? null) - elms = chart().selectAll() - break } default: break } - chart.afterDraw?.(elms) + chart.afterDraw?.(chart) }) }, [charts, data, xAxis, yAxis, height]) @@ -311,15 +335,17 @@ export const D3Chart = memo>(({ }, [redrawCharts]) return ( - +
{data ? ( diff --git a/src/components/d3/types.ts b/src/components/d3/types.ts index 9423456..6070bce 100644 --- a/src/components/d3/types.ts +++ b/src/components/d3/types.ts @@ -60,9 +60,13 @@ export type ChartOffset = { right: number } -export type ChartTicks = { +export type ChartTicks = { color?: Property.Color - x?: { visible?: boolean, count?: number } + x?: { + visible?: boolean, + count?: number, + format?: (d: d3.NumberValue) => string, + } y?: { visible?: boolean, count?: number } } diff --git a/src/pages/Telemetry/Operations/OperationsChart.jsx b/src/pages/Telemetry/Operations/OperationsChart.jsx index 55e9535..aac8172 100644 --- a/src/pages/Telemetry/Operations/OperationsChart.jsx +++ b/src/pages/Telemetry/Operations/OperationsChart.jsx @@ -1,6 +1,7 @@ import { memo, useCallback, useMemo, useState } from 'react' import { D3Chart } from '@components/d3' +import { formatDate } from '@utils' import '@styles/detected_operations.less' @@ -45,6 +46,12 @@ const xAxis = { accessor: (row) => new Date(row.dateStart), } +const ticks = { + color: '#F9F2ED', + x: { visible: true, format: (d) => formatDate(d, undefined, 'YYYY-MM-DD') }, + y: { visible: true }, +} + export const OperationsChart = memo(({ data, yDomain, height }) => { const [isChartLoading, setIsChartLoading] = useState(false) @@ -58,6 +65,21 @@ export const OperationsChart = memo(({ data, yDomain, height }) => { setTimeout(() => setIsChartLoading(false), 2000) }, []) + const plugins = useMemo(() => ({ + tooltip: { + enabled: true, + type: 'nearest', + limit: 10, + }, + cursor: { + enabled: true, + }, + menu: { + enabled: false, + onUpdate: onChartUpdate, + } + }), [onChartUpdate]) + return ( { data={data} loading={isChartLoading} height={height} - plugins={{ - tooltip: { - enabled: true, - type: 'nearest', - limit: 10, - }, - cursor: { - enabled: true, - }, - menu: { - enabled: false, - onUpdate: onChartUpdate, - } - }} - ticks={{ color: '#F9F2ED', y: { visible: true }, x: { visible: true } }} + plugins={plugins} + ticks={ticks} /> ) }) diff --git a/src/pages/WellOperations/Tvd/index.jsx b/src/pages/WellOperations/Tvd/index.jsx index 2e4fbe0..ac28332 100755 --- a/src/pages/WellOperations/Tvd/index.jsx +++ b/src/pages/WellOperations/Tvd/index.jsx @@ -1,26 +1,14 @@ import { useNavigate } from 'react-router-dom' -import { memo, useState, useRef, useEffect, useCallback, useMemo } from 'react' +import { memo, useState, useEffect, useCallback, useMemo } from 'react' import { DoubleLeftOutlined, DoubleRightOutlined } from '@ant-design/icons' import { Switch, Button } from 'antd' -import { - Chart, - TimeScale, - LinearScale, - Legend, - LineController, - PointElement, - LineElement, - Tooltip -} from 'chart.js' -import 'chartjs-adapter-moment' -import zoomPlugin from 'chartjs-plugin-zoom' -import ChartDataLabels from 'chartjs-plugin-datalabels' - import { useIdWell } from '@asb/context' +import { D3Chart } from '@components/d3' import LoaderPortal from '@components/LoaderPortal' import { invokeWebApiWrapperAsync } from '@components/factory' import { formatDate, fractionalSum, wrapPrivateComponent, getOperations } from '@utils' +import { unique } from '@utils/filters' import NptTable from './NptTable' import NetGraphExport from './NetGraphExport' @@ -28,97 +16,96 @@ import AdditionalTables from './AdditionalTables' import '@styles/index.css' import '@styles/tvd.less' -import { unique } from '@asb/utils/filters' -Chart.register( - TimeScale, - LinearScale, - LineController, - LineElement, - PointElement, - Legend, - ChartDataLabels, - zoomPlugin, - Tooltip, -) - -const numericRender = (value) => Number.isFinite(value) ? (+value).toFixed(2) : '-' - -const scaleTypes = { - day: { - min: 0, +const datasets = [{ + key: 'fact', + label: 'Факт', + type: 'line', + color: '#0A0', + width: 3, + yAxis: { type: 'linear', - display: true, - title: { display: false, text: '' }, - ticks: { stepSize: 1 } + accessor: (row) => row.depth, + unit: 'м', }, +}, { + key: 'plan', + label: 'План', + type: 'line', + color: '#F00', + width: 3, + yAxis: { + type: 'linear', + accessor: (row) => row.depth, + unit: 'м', + }, +}, { + key: 'predict', + label: 'Прогноз', + type: 'line', + color: 'purple', + width: 1, + afterDraw: (d) => d().selectAll('path').attr('stroke-dasharray', [7, 3]), + yAxis: { + type: 'linear', + accessor: 'depth', + unit: 'м', + }, +}, { + key: 'withoutNpt', + label: '', + type: 'line', + color: '#00F', + width: 3, + yAxis: { + type: 'linear', + accessor: 'depth', + unit: 'м', + }, +}] + +const xAxis = { date: { - display: true, - title: { display: true }, type: 'time', - time: { unit: 'day', displayFormats: { day: 'DD.MM.YYYY' } }, - grid: { drawTicks: true }, - ticks: { - stepSize: 3, - major: { enabled: true }, - z: 1, - display: true, - textStrokeColor: '#fff', - textStrokeWidth: 2, - color: '#000', - } + accessor: (row) => new Date(row.dateStart), + }, + day: { + type: 'linear', + accessor: 'day', + unit: 'день', } } -const defaultOptions = { - responsive: true, - maintainAspectRatio: false, - aspectRatio: false, - interaction: { intersect: false, mode: 'point' }, - scales: { - x: scaleTypes.day, - y: { - type: 'linear', - position: 'top', - reverse: true, - display: true, - title: { display: false, text: '' } - } - }, - parsing: { xAxisKey: 'day', yAxisKey: 'depth' }, - elements: { point: { radius: 1.7 } }, - plugins: { - legend: { display: true }, - datalabels: { display: false }, - tooltip: { - enabled: true, - position: 'nearest', - callbacks: { - title: (items) => [ - `Дата: ${formatDate(items[0].raw?.date) ?? '-'}`, - `День с начала бурения: ${parseInt(items[0].raw?.day)}`, - ], - afterTitle: (items) => `Глубина: ${numericRender(items[0].raw?.depth)}`, - label: (item) => [ - item.raw.wellSectionTypeName + ': ' + item.raw.categoryName, - `Длительность (ч): ${item.raw.nptHours}` - ], - }, +const ticks = { + day: { + x: { + visible: true, + format: (d) => d, }, + y: { visible: true }, }, + date: { + x: { + visible: true, + format: (d) => formatDate(d, undefined, 'YYYY-MM-DD'), + }, + y: { visible: true }, + } } -const makeDataset = (data, label, color, borderWidth = 1.5, borderDash) => ({ - label, - data, - backgroundColor: color, - borderColor: color, - borderWidth, - borderDash, -}) +const domain = { + date: { + y: { min: 4500, max: 0 }, + }, + day: { + x: { min: 0 }, + y: { min: 4500, max: 0 }, + } +} + +const numericRender = (value) => Number.isFinite(value) ? (+value).toFixed(2) : '-' const Tvd = memo(({ idWell: wellId, title, ...other }) => { - const [chart, setChart] = useState() const [xLabel, setXLabel] = useState('day') const [operations, setOperations] = useState({}) const [tableVisible, setTableVisible] = useState(false) @@ -127,7 +114,6 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => { const idWellContext = useIdWell() const idWell = useMemo(() => wellId ?? idWellContext, [wellId, idWellContext]) - const chartRef = useRef(null) const navigate = useNavigate() const onPointClick = useCallback((e) => { @@ -141,15 +127,11 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => { const ids = points.map((p) => p.raw.id).filter(Boolean).filter(unique).join(',') navigate(`/well/${idWell}/operations/${datasetName}/?selectedId=${ids}`) }, [idWell, navigate]) - - useEffect(() => { - invokeWebApiWrapperAsync( - async () => setOperations(await getOperations(idWell)), - setIsLoading, - `Не удалось загрузить операции по скважине "${idWell}"`, - 'Получение списка опервций по скважине' - ) - }, [idWell]) + + const toogleTable = useCallback(() => { + setOperations(pre => ({ ...pre })) + setTableVisible(v => !v) + }, []) const chartData = useMemo(() => { const withoutNpt = [] @@ -163,46 +145,17 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => { }) }) - return { datasets: [ - makeDataset(operations?.fact, 'Факт', '#0A0', 3), - makeDataset(operations?.predict, 'Прогноз', 'purple', 1, [7, 3]), - makeDataset(operations?.plan, 'План', '#F00', 3), - makeDataset(withoutNpt, 'Факт без НПВ', '#00F', 3) - ]} + return { ...operations, withoutNpt } }, [operations]) useEffect(() => { - if (!chartRef.current) return - const options = {} - Object.assign(options, defaultOptions) - - const newChart = new Chart(chartRef.current, { - type: 'line', - options: options, - plugins: [ChartDataLabels], - data: { datasets: [] }, - }) - setChart(newChart) - - return () => newChart?.destroy() - }, [chartRef]) - - useEffect(() => { - if (!chart) return - chart.data = chartData - chart.options.onClick = onPointClick - chart.options.scales.x = scaleTypes[xLabel] - chart.options.parsing.xAxisKey = xLabel - chart.update() - // Обнуление ширины необходимо для уменьшения размена при resize после появления элементов - chart.canvas.parentNode.style.width = '0' - chart.resize() - }, [chart, chartData, xLabel, onPointClick]) - - const toogleTable = useCallback(() => { - setOperations(pre => ({ ...pre })) - setTableVisible(v => !v) - }, []) + invokeWebApiWrapperAsync( + async () => setOperations(await getOperations(idWell)), + setIsLoading, + `Не удалось загрузить операции по скважине "${idWell}"`, + 'Получение списка опервций по скважине' + ) + }, [idWell]) return (
@@ -224,7 +177,19 @@ const Tvd = memo(({ idWell: wellId, title, ...other }) => {
- +
{tableVisible && }
diff --git a/src/styles/d3.less b/src/styles/d3.less index 504109a..dcef26e 100644 --- a/src/styles/d3.less +++ b/src/styles/d3.less @@ -1,4 +1,6 @@ .asb-d3-chart { + width: 100%; + height: 100%; & .tooltip { @color: white; diff --git a/src/styles/tvd.less b/src/styles/tvd.less index a1d51b0..2020071 100755 --- a/src/styles/tvd.less +++ b/src/styles/tvd.less @@ -22,7 +22,7 @@ position: relative; flex: 1; - > div { + > .tvd-tr-table, > .tvd-bl-table { position: absolute; //pointer-events: none; transition: opacity .25s ease-out;