Visualization — constructing visual representations of
abstract data to
amplify cognition.
Берём абстрактные величины и бережно переносим их в визуальные величины, которые понимает машина.
Есть две формулы...
(x - b) / k
k * x + b
Шкала — функция, которая принимает абстрактное значение и возвращает визуальное.
function scaleLinear(domain, range) {
return x => interpolate(range, normalize(domain, x));
}
// [N, N] -> [0, 1]
function normalize(range, x) {
let k = range[1] - range[0];
let b = range[0];
return (x - b) / k;
}
// [0, 1] -> [N, N]
function interpolate(range, t) {
let k = range[1] - range[0];
let b = range[0];
return b + k * t;
}
function scale([d0, d1], [r0, r1], x) {
return r0 + (r1 - r0) * (x - d0) / (d1 - d0);
}
let datapoints = [0.7300683106640126, -0.4606999589484973,
0.0810442791570592, 1.264658107394293, -0.3999839805247,
0.73774975545273, 1.181487753486226, -0.444039268800015,
0.738077186476263, 0.0268356600561, -0.4351210367944046,
0.2615460040301957, -0.560046949247555, 1.1022347693372,
-2.7468864731377, -0.647799719212206, 1.550835553230532,
1.312599295229566, -0.6733235254171, -0.502827667037121,
-0.373466838991289, 0.0364248070202155, ...];
let domainX = [0, datapoints.length - 1];
let scaleX = scaleLinear(domainX, [0, width]);
let domainY = [Math.min(...datapoints), Math.max(...datapoints)];
let scaleY = scaleLinear(domainY, [height, 0])
let getX = (d, i) => scaleX(i);
let getY = (d) => scaleY(d);
return <path d={line(datapoints, getX, getY)} />
function line(data, x, y) {
return data.reduce((curve, d, i) => {
let prefix = i === 0 ? 'M' : 'L';
let point = prefix + x(d, i) + ',' + y(d, i);
return curve + point;
}, '');
}
M0,149.6283711248115L0.80080080800807,158.4719886183878L1.60161601606015,160.7229895999148L2.40402402402402,136.7026041695307L3.20320203203203,172.5591461581857L4.00404004004004,123.4183330530276L4.80804804804805,135.0474115675488L5.65605605605605,162.1072718749057L6.40606406406406,166.7711007103693L7.207272072072007,145.88920391649837...
Не видно же ничего!
Масштабируем величины данных,
а не
машин
Возможность пользователя взаимодействовать с данными, дает свободу исследования и поиска новых ответов 🙏
let domainX = [0, datapoints.length - 1];
let scaleX = scaleLinear(domainX, [0, width]);
// Участок данных на который мы смотрим
let [window, setWindow] = useState(domainX);
let rescaledX = scaleLinear(window, [0, width]);
// Линия рисуется относительно текущего участка
let getX = (d, i) => rescaledX(i);
render = (Data) => UI;
update = (State, Action) => NewState;
Actions = [Update Thing, Add Thing, Delete Thing, ...];
State = Actions.reduce(update, InitialState);
State.subscribe(render);
The holy grail
of state reduction 👏
let transform = { k: 1, x: 0 };
function handleEvent(event) {
let action = { /* необходимые данные от event */ };
// вычисляем новое состояние масштабирования
transform = reduce(transform, action);
// вычисляем новый участок
setWindow([
scaleX.inverse((range[0] - transform.x) / transform.k),
scaleX.inverse((range[1] - transform.x) / transform.k)
])
}
Динамически изменяя window
все зависимые от
него детали интерфейса высчитывают свое
новое состояние.
Они не хранят его отдельно потому нет проблемы с синхронизацией отдельных элементов.
function handleWheelEvent(event) {
let action = {
type: 'wheel',
deltaY: event.deltaY,
pointerX: event.layerX,
};
transform = reduce(transform, action);
// ...
}
function reduce(transform, action) {
// только код обработки type == 'wheel'
let deltaK = 2 ** (-action.deltaY / 500);
let deltaX = (action.pointerX - transform.x) / transform.k;
// scaleExtent = [1, 32]
let k = constrain(scaleExtent, transform.k * deltaK);
// translateExtent = [0, width]
let x = constrainX(translateExtent,
action.pointerX - deltaX * k, k);
return { k, x };
}
Что я сейчас вообще читаю? 😨
Использовать D3 и другие — нормально, но не помешает понимать математику под капотом
Интерактивные визуализации делают ваше приложение эффективным для пользователя