From eb11480e2c8a1651c6449decc46b3cdf7311f68f Mon Sep 17 00:00:00 2001 From: Robert Monfera Date: Tue, 27 Apr 2021 15:44:46 +0200 Subject: [PATCH] feat: simple screenspace constraint solver (#1141) --- .../layout/viewmodel/fill_text_layout.ts | 2 +- src/common/color_calcs.test.ts | 2 +- src/common/text_utils.ts | 2 +- .../monotonic_hill_climb.ts} | 0 .../screenspace_marker_scale_compressor.ts | 65 +++++++++++++++++++ 5 files changed, 68 insertions(+), 3 deletions(-) rename src/{common/optimize.ts => solvers/monotonic_hill_climb.ts} (100%) create mode 100644 src/solvers/screenspace_marker_scale_compressor.ts diff --git a/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts b/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts index 4e6a227031..b943550d14 100644 --- a/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts +++ b/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts @@ -30,8 +30,8 @@ import { wrapToTau, } from '../../../../common/geometry'; import { logarithm } from '../../../../common/math'; -import { integerSnap, monotonicHillClimb } from '../../../../common/optimize'; import { Box, Font, PartialFont, TextMeasure, VerticalAlignments } from '../../../../common/text_utils'; +import { integerSnap, monotonicHillClimb } from '../../../../solvers/monotonic_hill_climb'; import { ValueFormatter } from '../../../../utils/common'; import { Layer } from '../../specs'; import { Config, Padding } from '../types/config_types'; diff --git a/src/common/color_calcs.test.ts b/src/common/color_calcs.test.ts index 48e3f2e5fc..c9c27cb436 100644 --- a/src/common/color_calcs.test.ts +++ b/src/common/color_calcs.test.ts @@ -17,8 +17,8 @@ * under the License. */ +import { integerSnap, monotonicHillClimb } from '../solvers/monotonic_hill_climb'; import { makeHighContrastColor, combineColors } from './color_calcs'; -import { integerSnap, monotonicHillClimb } from './optimize'; describe('calcs', () => { describe('test makeHighContrastColor', () => { diff --git a/src/common/text_utils.ts b/src/common/text_utils.ts index e8381ec5b0..1cf4800727 100644 --- a/src/common/text_utils.ts +++ b/src/common/text_utils.ts @@ -20,9 +20,9 @@ import { $Values as Values } from 'utility-types'; import { ArrayEntry } from '../chart_types/partition_chart/layout/utils/group_by_rollup'; +import { integerSnap, monotonicHillClimb } from '../solvers/monotonic_hill_climb'; import { Datum } from '../utils/common'; import { Pixels } from './geometry'; -import { integerSnap, monotonicHillClimb } from './optimize'; /** @public */ export const FONT_VARIANTS = Object.freeze(['normal', 'small-caps'] as const); diff --git a/src/common/optimize.ts b/src/solvers/monotonic_hill_climb.ts similarity index 100% rename from src/common/optimize.ts rename to src/solvers/monotonic_hill_climb.ts diff --git a/src/solvers/screenspace_marker_scale_compressor.ts b/src/solvers/screenspace_marker_scale_compressor.ts new file mode 100644 index 0000000000..8c207416a3 --- /dev/null +++ b/src/solvers/screenspace_marker_scale_compressor.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Cartesian, Pixels, Ratio } from '../common/geometry'; + +/** @internal */ +export type ArrayIndex = number; + +/** @internal */ +export interface ScaleCompression { + bounds: ArrayIndex[]; + scaleMultiplier: Ratio; +} + +/** + * A set of Cartesian positioned items with screenspace size (eg. axis tick labels, or scatterplot points) are represented as: + * - a column vector of Cartesian positions in the domain (can be any unit) + * - a column vector of screenspace size (eg. widths in pixels) which has the same number of elements + * The available room in the same screenspace units (practically, pixels) is supplied. + * + * Returns the scale multiplier, as well as the index of the elements determining (compressing) the scale, if solvable. + * If not solvable, it returns a non-finite number in `scaleMultiplier` and no indices in `bounds`. + * @internal + */ +export const screenspaceMarkerScaleCompressor = ( + domainPositions: Cartesian[], + itemWidths: Pixels[], + outerWidth: Pixels, +): ScaleCompression => { + const result: ScaleCompression = { bounds: [], scaleMultiplier: Infinity }; + const itemCount = Math.min(domainPositions.length, itemWidths.length); + for (let left = 0; left < itemCount; left++) { + for (let right = 0; right < itemCount; right++) { + if (domainPositions[left] > domainPositions[right]) continue; // must adhere to left <= right + + const range = outerWidth - itemWidths[left] / 2 - itemWidths[right] / 2; // negative if not enough room + const domain = domainPositions[right] - domainPositions[left]; // always non-negative and finite + const scaleMultiplier = range / domain; // may not be finite, and that's OK + + if (scaleMultiplier < result.scaleMultiplier || Number.isNaN(scaleMultiplier)) { + result.bounds[0] = left; + result.bounds[1] = right; + result.scaleMultiplier = scaleMultiplier; // will persist a Number.finite() value for solvable pairs + } + } + } + + return result; +};