kepler.gl
Version:
kepler.gl is a webgl based application to visualize large scale location data in the browser
179 lines (159 loc) • 5.47 kB
JavaScript
// Copyright (c) 2020 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
import document from 'global/document';
import {parseFieldValue} from 'utils/data-utils';
const MIN_GHOST_CELL_SIZE = 200;
/**
* Measure rows and column content to determin min width for each column
* @param {*} param0
*/
export function renderedSize({
text: {rows, column},
type = 'string',
colIdx,
numRowsToCalculate = 10,
fontSize = 12,
font = 'Lato',
cellPadding = 30,
maxCellSize = 400,
maxHeaderSize = 150,
minCellSize = 45,
optionsButton = 30
}) {
if (!document) {
return {
row: 0,
header: 0
};
}
const textCanvas = document.createElement('canvas');
document.body.appendChild(textCanvas);
const context = textCanvas.getContext('2d');
context.font = [fontSize, font].join('px ');
let rowsToSample = [...Array(numRowsToCalculate)].map(() =>
Math.floor(Math.random() * (rows.length - 1 - 0 + 1))
);
// IF we have less than 10 rows, lets measure all of them
if (rows.length < numRowsToCalculate) {
rowsToSample = Array.from(Array(rows.length).keys());
}
const rowWidth = Math.max(
...rowsToSample.map(
rowIdx =>
Math.ceil(context.measureText(parseFieldValue(rows[rowIdx][colIdx], type)).width) +
cellPadding
)
);
// header cell only has left padding
const headerWidth =
Math.ceil(context.measureText(column).width) + cellPadding / 2 + optionsButton;
const minRowWidth = minCellSize + cellPadding;
const minHeaderWidth = minCellSize + cellPadding / 2 + optionsButton;
const clampedRowWidth = clamp(minRowWidth, maxCellSize, rowWidth);
const clampedHeaderWidth = clamp(minHeaderWidth, maxHeaderSize, headerWidth);
// cleanup
textCanvas.parentElement.removeChild(textCanvas);
return {
row: clampedRowWidth,
header: clampedHeaderWidth
};
}
function clamp(min, max, value) {
return Math.max(Math.min(max, value), min);
}
function getColumnOrder(pinnedColumns = [], unpinnedColumns = []) {
return [...pinnedColumns, ...unpinnedColumns];
}
function getMinCellSize(cellSizeCache) {
return Object.keys(cellSizeCache).reduce(
(accu, col) => ({
...accu,
[col]: cellSizeCache[col].row
}),
{}
);
}
function getSizeSum(sizeCache, key) {
return Object.keys(sizeCache).reduce(
(acc, val) => acc + (key ? sizeCache[val][key] : sizeCache[val]),
0
);
}
/**
* Expand cell to fit both row and header, if there is still room left,
* expand last cell to fit the entire width of the container
* @param {object} cellSizeCache
* @param {string[]} columnOrder
* @param {number} containerWidth
* @param {number} roomToFill
*/
function expandCellSize(cellSizeCache, columnOrder, containerWidth, roomToFill) {
let remaining = roomToFill;
const expandedCellSize = columnOrder.reduce((accu, col) => {
let size = cellSizeCache[col].row;
if (cellSizeCache[col].row < cellSizeCache[col].header && remaining > 0) {
// if we are cutting off the header, expand to fit it
size =
cellSizeCache[col].header - cellSizeCache[col].row < remaining
? cellSizeCache[col].header
: cellSizeCache[col].row + remaining;
remaining -= size - cellSizeCache[col].row;
}
return {
...accu,
[col]: size
};
}, {});
let ghost = null;
if (remaining > 0 && remaining < MIN_GHOST_CELL_SIZE) {
// expand last cell
const lastCell = columnOrder[columnOrder.length - 1];
expandedCellSize[lastCell] += remaining;
} else if (remaining >= MIN_GHOST_CELL_SIZE) {
// if too much left add a ghost cell
ghost = remaining;
}
return {
cellSizeCache: expandedCellSize,
ghost
};
}
/**
* Adjust cell size based on container width
* @param {number} containerWidth
* @param {Object} cellSizeCache
* @param {string[]} pinnedColumns
* @param {string[]} unpinnedColumns
*/
export function adjustCellsToContainer(
containerWidth,
cellSizeCache,
pinnedColumns,
unpinnedColumns
) {
const minRowSum = getSizeSum(cellSizeCache, 'row');
if (minRowSum >= containerWidth) {
// we apply the min Width to all cells
return {cellSizeCache: getMinCellSize(cellSizeCache)};
}
// if we have some room to expand
const columnOrder = getColumnOrder(pinnedColumns, unpinnedColumns);
return expandCellSize(cellSizeCache, columnOrder, containerWidth, containerWidth - minRowSum);
}