hyper-tuner-cloud/src/components/Dialog/Table.tsx

278 lines
7.5 KiB
TypeScript

/* eslint-disable react/no-array-index-key */
import {
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import {
Button,
InputNumber,
Modal,
Popover,
Space,
} from 'antd';
import {
PlusCircleOutlined,
MinusCircleOutlined,
EditOutlined,
} from '@ant-design/icons';
import TableDragSelect from 'react-table-drag-select';
import {
isDecrement,
isEscape,
isIncrement,
isReplace,
} from '../../utils/keyboard/shortcuts';
type AxisType = 'x' | 'y';
type CellsType = boolean[][];
type DataType = number[][];
type OnChangeType = (data: DataType) => void;
enum Operations {
INC,
DEC,
REPLACE,
}
type HslType = [number, number, number];
const Table = ({
name,
xLabel,
yLabel,
xData,
yData,
disabled,
onChange,
xMin,
xMax,
yMin,
yMax,
xUnits,
yUnits,
}: {
name: string,
xLabel: string,
yLabel: string,
xData: number[],
yData: number[],
disabled: boolean,
onChange?: OnChangeType,
xMin: number,
xMax: number,
yMin: number,
yMax: number,
xUnits: string,
yUnits: string,
}) => {
const titleProps = { disabled: true };
const [isModalVisible, setIsModalVisible] = useState(false);
const [modalValue, setModalValue] = useState<number | undefined>();
const [data, _setData] = useState<DataType>([yData, xData]);
// data starts from `1` index, 0 is title / name
const rowsCount = data[1].length + 1;
const generateCells = () => [
Array(rowsCount).fill(false),
Array(rowsCount).fill(false),
];
const [cells, _setCells] = useState<CellsType>(generateCells());
const cellsRef = useRef(cells);
const dataRef = useRef(data);
const modalInputRef = useRef<HTMLInputElement | null>(null);
const setCells = (currentCells: CellsType) => {
cellsRef.current = currentCells;
_setCells(currentCells);
};
const setData = (currentData: DataType) => {
dataRef.current = currentData;
_setData(currentData);
if (onChange) {
onChange(currentData);
}
};
const modifyData = useCallback((operation: Operations, currentCells: CellsType, currentData: DataType, value = 0): DataType => {
const newData = [...currentData.map((row) => [...row])];
// rowIndex: [0 => Y, 1 => X]
const isY = (row: number) => row === 0;
const isX = (row: number) => row === 1;
const isNotGreater = (row: number, val: number) => (isY(row) && val < yMax) || (isX(row) && val < xMax);
const isNotLess = (row: number, val: number) => (isY(row) && val > yMin) || (isX(row) && val > xMin);
currentCells.forEach((_, rowIndex) => {
currentCells[rowIndex].forEach((selected, valueIndex) => {
if (!selected) {
return;
}
const current = newData[rowIndex][valueIndex - 1];
switch (operation) {
case Operations.INC:
if (isNotGreater(rowIndex, current)) {
newData[rowIndex][valueIndex - 1] += 1;
}
break;
case Operations.DEC:
if (isNotLess(rowIndex, current)) {
newData[rowIndex][valueIndex - 1] -= 1;
}
break;
case Operations.REPLACE:
if (isX(rowIndex) && value > xMax) {
newData[rowIndex][valueIndex - 1] = xMax;
break;
}
if (isX(rowIndex) && value < xMin) {
newData[rowIndex][valueIndex - 1] = xMin;
break;
}
if (isY(rowIndex) && value < yMin) {
newData[rowIndex][valueIndex - 1] = yMin;
break;
}
if (isY(rowIndex) && value > yMax) {
newData[rowIndex][valueIndex - 1] = yMax;
break;
}
newData[rowIndex][valueIndex - 1] = value;
break;
default:
break;
}
});
});
return [...newData];
}, [xMax, xMin, yMax, yMin]);
const oneModalOk = () => {
setData(modifyData(Operations.REPLACE, cellsRef.current, dataRef.current, modalValue));
setIsModalVisible(false);
setModalValue(undefined);
};
const onModalCancel = () => {
setIsModalVisible(false);
setModalValue(undefined);
};
const resetCells = () => setCells(generateCells());
const increment = () => setData(modifyData(Operations.INC, cellsRef.current, dataRef.current));
const decrement = () => setData(modifyData(Operations.DEC, cellsRef.current, dataRef.current));
const replace = () => {
// don't show modal when no cell is selected
if (cellsRef.current.flat().find((val) => val === true)) {
setModalValue(undefined);
setIsModalVisible(true);
setInterval(() => modalInputRef.current?.focus(), 1);
}
};
useEffect(() => {
const keyboardListener = (e: KeyboardEvent) => {
if (isIncrement(e)) {
increment();
}
if (isDecrement(e)) {
decrement();
}
if (isReplace(e)) {
e.preventDefault();
replace();
}
if (isEscape(e)) {
resetCells();
}
};
document.addEventListener('keydown', keyboardListener);
return () => {
document.removeEventListener('keydown', keyboardListener);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const colorHsl = (min: number, max: number, value: number): HslType => {
const saturation = 60;
const lightness = 40;
const coldDeg = 220;
const hotDeg = 0;
const remap = (x: number, inMin: number, inMax: number, outMin: number, outMax: number) => (x - inMin) * (outMax - outMin) / (inMax - inMin) + outMin;
let hue = remap(value, min, max, coldDeg, hotDeg);
// fallback to cold temp
if (Number.isNaN(hue)) {
hue = coldDeg;
}
return [hue, saturation, lightness];
};
const renderRow = (axis: AxisType, input: number[]) => input
.map((value, index) => {
const [hue, sat, light] = colorHsl(Math.min(...input), Math.max(...input), value);
return (
<td
className="value"
key={`${axis}-${index}-${value}-${hue}${sat}${light}`}
style={{ backgroundColor: `hsl(${hue}, ${sat}%, ${light}%)` }}
>
{`${value}`}
</td>
);
});
return (
<>
<div className="table table-2d">
<Popover
visible={cells.flat().find((val) => val === true) === true}
content={
<Space>
<Button onClick={decrement} icon={<MinusCircleOutlined />} />
<Button onClick={increment} icon={<PlusCircleOutlined />} />
<Button onClick={replace} icon={<EditOutlined />} />
</Space>
}
>
<TableDragSelect
key={name}
value={cells}
onChange={setCells}
>
<tr>
<td {...titleProps} className="title-curve" key={yLabel}>{`${yLabel} (${yUnits})`}</td>
{renderRow('y', data[0])}
</tr>
<tr>
<td {...titleProps} className="title-curve" key={xLabel}>{`${xLabel} (${xUnits})`}</td>
{renderRow('x', data[1])}
</tr>
</TableDragSelect>
</Popover>
</div>
<Modal
title="Set cell values"
visible={isModalVisible}
onOk={oneModalOk}
onCancel={onModalCancel}
centered
forceRender
>
<InputNumber
ref={modalInputRef}
value={modalValue}
onChange={(val) => setModalValue(Number(val))}
onPressEnter={oneModalOk}
style={{ width: '20%' }}
/>
</Modal>
</>
);
};
export default Table;