Поверхность оптимизации
SurfaceWidget показывает результаты перебора параметров как трёхмерный ландшафт: одна метрика над двумя дискретными осями, где значение метрики задаёт и высоту, и цвет. Контрол принимает те же данные, что и тепловая карта оптимизации, поэтому один набор результатов можно показать плоской картой, поверхностью или обеими сразу.

Создание и обновление
import {
HeatDirections,
SurfaceWidget,
type SurfaceData,
type TradingHost,
} from '@stocksharp/trading-controls';
declare const host: TradingHost;
const surface = SurfaceWidget.create(
document.querySelector<HTMLElement>('#surface')!,
{},
{ host },
);
const sweep: SurfaceData = {
xLabel: 'Fast',
yLabel: 'Slow',
metricLabel: 'Net profit',
betterWhen: HeatDirections.Higher,
cells: [
{ x: '10', y: '50', value: 1_250 },
{ x: '10', y: '80', value: -320 },
{ x: '20', y: '50', value: 2_480 },
{ x: '20', y: '80', value: 640 },
],
};
surface.update(sweep);
Единственная зависимость — host; интерфейс SurfaceDeps других полей не содержит. Второй аргумент create — сохранённое состояние экземпляра: поверхность его не читает и ничего в него не пишет.
update заменяет весь набор целиком. Поверхность — это один прогон перебора, поэтому частичного обновления у неё нет: половина одного прогона поверх половины другого дала бы ландшафт из двух разных сводок.
Статическое свойство SurfaceWidget.TYPE равно ControlTypes.OptimizationSurface — идентификатору optimizationSurface.
Данные
Ячейка HeatCell — это пара значений осей и измеренная метрика: { x, y, value }. Значения осей строковые, потому что ось дискретна: 10, 00:05:00 и True — равноправные позиции на ней. Числовые значения упорядочиваются как числа, остальные — как текст, поэтому перебор 5, 8, 12, 40 остаётся последовательностью.
Поле betterWhen обязательно и принимает HeatDirections.Higher или HeatDirections.Lower. Без него ландшафт просадок поднял бы худший угол в вершину.
Несколько прогонов на одной паре сворачиваются в среднее — это одна ячейка. Пара, которую перебор не проходил, остаётся дырой: грань рисуется, только когда известны все четыре её угла, а пропуск не интерполируется — отсутствующий результат не равен нулевому. Если ячеек нет вовсе или хотя бы на одной оси меньше двух различных значений, вместо ландшафта показывается заглушка с текстом по ключу NoOptimizationResults.
Подписи xLabel, yLabel и metricLabel идут в подписи осей; metricLabel дополнительно выводится в заголовке панели.
Отображение
Высота грани — это положение значения относительно опорной точки шкалы, свёрнутое в диапазон от пола до верха: опорная точка приходится на середину высоты, поэтому пол означает не «худший результат», а нижний край шкалы. Цвет берётся из host.presentation.canvasPalette(): up для значений лучше опорной точки, down — хуже, насыщенность растёт с удалением от неё. Грани заливаются от дальних к ближним, поэтому ближний гребень перекрывает то, что за ним, и обводятся цветом grid, чтобы сетка читалась там, где две соседние грани почти одного оттенка.
Проекция ортографическая: поверхность читают, сравнивая высоты по всему полю, а перспектива укоротила бы дальнюю сторону гребня относительно ближней.
Под ландшафтом рисуются два ребра пола и вертикальная ось шкалы. На параметрических осях выводится до восьми меток: пока значения помещаются — все, дальше каждое второе, каждое третье и так далее, причём первое и последнее подписываются всегда. На вертикальной оси пять меток, подписанных значениями метрики. Подписи переносятся на те рёбра пола, которые ближе к наблюдателю, — при повороте это пересчитывается, чтобы числа не оказались поверх сетки.
Обзор и жесты
Все жесты приходят через pointer-события, поэтому мышь, перо и палец идут одним путём:
- перетаскивание одним указателем поворачивает поверхность: по горизонтали меняется
yaw, по вертикали —pitch; - два указателя меняют масштаб расстоянием между ними; поворот при этом остаётся за одним указателем;
- колесо тоже меняет масштаб. Дельта приводится к «щелчкам» независимо от того, в пикселях, строках или страницах её сообщил браузер, и ограничивается двумя щелчками на событие, чтобы мышь и трекпад давали сопоставимый шаг.
Жест над холстом контрол забирает себе, иначе перетаскивание на телефоне и колесо на настольном браузере прокручивали бы страницу вместо ландшафта.
Наклон и масштаб ограничены: pitch — от MIN_PITCH (0,12) до MAX_PITCH (1,45), масштаб — от 0,4 до 4. При наклоне в ноль каждая грань выродилась бы в линию, а при прямом угле поверхность стала бы плоской картой, то есть другим контролом. Поворот yaw не ограничен, а замыкается: развернуть ландшафт кругом, чтобы посмотреть на обратный склон гребня, — осмысленный жест. Начальный обзор — DEFAULT_VIEW; кнопка в заголовке панели (ResetView) возвращает к нему.
Когда указатель не поворачивает поверхность, контрол ищет ближайшую измеренную точку в радиусе 22 CSS-пикселей. Найденная точка обводится кольцом цветом up, а в полосе над холстом появляется строка: значение по оси xLabel, значение по оси yLabel и метрика. Полоса лежит поверх холста, а не в заголовке панели: показание относится к точке под указателем и должно быть рядом с ней. Пара, которую перебор не проходил, указателю не предлагается; при равном расстоянии выбирается точка, которая ближе к наблюдателю.
Что делает контрол и что остаётся хосту
Контрол сам обрабатывает жесты, следит за размером холста через ResizeObserver и перерисовывает ландшафт под текущий размер и плотность пикселей экрана, обслуживает кнопку сброса обзора и кнопку закрытия панели, вызывающую host.close(), а также регистрируется через host.register и снимается с регистрации в dispose. Цвета и шрифт холста приходят из host.presentation.canvasPalette(). Весь видимый текст запрашивается через host.t: OptimizationSurface, ResetView, ClosePanel, OptimizationSurfaceChart, NoOptimizationResults.
Данные подаёт хост: контрол не запускает оптимизацию, не подписывается на её ход и не знает, чем получены результаты, — что передано в update, то и нарисовано. Клика по грани нет: грань соответствует ячейке, а не отдельному прогону, поэтому открывать по ней нечего — нажатие поворачивает ландшафт.
Собственных настроек в host.preferences контрол не хранит, ключей у него нет, host.persistState не вызывает. Текущий обзор доступен методом view() — если его нужно восстанавливать между сеансами, хост сохраняет и возвращает эти значения сам.
Публичные методы
update(data)— показать набор результатов целиком.view()— вернуть копию текущего обзора (yaw,pitch,zoom).resetView()— вернуть обзор кDEFAULT_VIEW.dispose()— отключить наблюдатель размера, вызватьhost.unregisterи убрать корневой элемент.
Вспомогательные функции
Геометрия вынесена из контрола в отдельный модуль и экспортируется пакетом — на ней можно построить свою отрисовку:
surfaceLayout(input)— разложить ячейки в грани, оси и вершины для заданных размеров и обзора;null, когда рисовать нечего.project(nx, ny, nz, view, box)— спроецировать точку единичного куба на холст.dragView(view, dx, dy)— обзор после перетаскивания на столько пикселей.zoomView(view, factor)— обзор после изменения масштаба.clampView(view)— обзор, приведённый к допустимым пределам.DEFAULT_VIEW,MIN_PITCH,MAX_PITCH— начальный обзор и границы наклона.