@adaptabletools/adaptable
Version:
Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
306 lines (305 loc) • 14.7 kB
JavaScript
import Helper from '../../Utilities/Helpers/Helper';
import { clamp } from '../../Utilities/Extensions/NumberExtensions';
import { buildBarStyleCellTextLabels, hasBarStyleCellTextConfigured, mountBarStyleCellText, } from '../../Utilities/Helpers/StyledColumns/BarStylesHelper';
import { shouldRenderStyledColumnOnRow } from '../../Utilities/Helpers/StyledColumns/StyledColumnHelper';
const SVG_NS = 'http://www.w3.org/2000/svg';
const DEFAULT_TRACK_HEIGHT = 4;
const DEFAULT_BAND_HEIGHT = 14;
const DEFAULT_VALUE_MARKER_SIZE = 10;
const DEFAULT_LINE_MARKER_THICKNESS = 2;
const DEFAULT_TRACK_COLOR = 'var(--ab-color-foreground, #888)';
const DEFAULT_VALUE_MARKER_COLOR = 'var(--ab-color-accent, #07c)';
const DEFAULT_REFERENCE_MARKER_COLOR = 'var(--ab-color-foreground, #111)';
const DEFAULT_BACK_COLOR = 'transparent';
const resolveBoundValue = (bound, abColumn, rowNode, api) => {
if (bound == undefined) {
return undefined;
}
if (typeof bound === 'number') {
return bound;
}
const internalApi = api.styledColumnApi.internalApi;
if (bound === 'Col-Min') {
return internalApi.getMinValueForNumericColumn(abColumn);
}
if (bound === 'Col-Max') {
return internalApi.getMaxValueForNumericColumn(abColumn);
}
if (bound === 'Col-Avg') {
return internalApi.getAvgValueForNumericColumn(abColumn);
}
if (bound === 'Col-Median') {
return internalApi.getMedianValueForNumericColumn(abColumn);
}
if (typeof bound === 'string') {
if (!api.columnApi.isColumnInGrid(bound)) {
return undefined;
}
const raw = api.gridApi.getRawValueFromRowNode(rowNode, bound);
const num = typeof raw === 'number' ? raw : Number(raw);
return isNaN(num) ? undefined : num;
}
return undefined;
};
const normaliseMarker = (override, defaults) => {
const shape = override?.Shape ?? defaults.Shape;
return {
Shape: shape,
Color: override?.Color ?? defaults.Color,
Size: override?.Size ?? (shape === 'Line' ? DEFAULT_LINE_MARKER_THICKNESS : defaults.Size),
};
};
const toFraction = (value, min, max) => {
if (max === min) {
return 0;
}
return clamp((value - min) / (max - min), 0, 1);
};
export const getRangeBarRendererForColumn = (styledColumn, abColumn, api) => {
if (!styledColumn.RangeBarStyle) {
return;
}
const rangeStyle = styledColumn.RangeBarStyle;
return class RangeBarRenderer {
eGui;
init(params) {
if (!shouldRenderStyledColumnOnRow(styledColumn, params.node, api)) {
if (params.value != undefined) {
this.eGui = document.createElement('div');
this.eGui.append(String(params.value));
}
return;
}
const rawCellValue = params.value;
if (Helper.objectNotExists(rawCellValue)) {
this.eGui = document.createElement('div');
return;
}
const numericValue = typeof rawCellValue === 'number' ? rawCellValue : Number(rawCellValue);
if (isNaN(numericValue)) {
this.eGui = document.createElement('div');
this.eGui.append(params.formatValue?.(rawCellValue) ?? String(rawCellValue));
return;
}
const min = resolveBoundValue(rangeStyle.Min, abColumn, params.node, api);
const max = resolveBoundValue(rangeStyle.Max, abColumn, params.node, api);
if (min == undefined || max == undefined || max <= min) {
this.eGui = document.createElement('div');
this.eGui.append(params.formatValue?.(rawCellValue) ?? String(rawCellValue));
return;
}
const referenceProperties = rangeStyle.Reference;
const referenceValue = referenceProperties?.Value != undefined
? resolveBoundValue(referenceProperties.Value, abColumn, params.node, api)
: undefined;
const valueMarker = normaliseMarker(rangeStyle.Marker, {
Shape: 'Diamond',
Color: DEFAULT_VALUE_MARKER_COLOR,
Size: DEFAULT_VALUE_MARKER_SIZE,
});
const referenceMarker = normaliseMarker(referenceProperties?.Marker, {
Shape: 'Line',
Color: DEFAULT_REFERENCE_MARKER_COLOR,
Size: DEFAULT_LINE_MARKER_THICKNESS,
});
const outOfRangeProperties = rangeStyle.OutOfRange;
const outOfRangeMode = outOfRangeProperties?.Mode ?? 'Clamp';
const outOfRangeColor = outOfRangeProperties?.Color;
const isOutOfRange = numericValue < min || numericValue > max;
const hideValueMarker = isOutOfRange && outOfRangeMode === 'Hide';
const isVertical = rangeStyle.Orientation === 'Vertical';
const trackProperties = rangeStyle.Track;
const trackThickness = trackProperties?.Height ?? DEFAULT_TRACK_HEIGHT;
const bandThickness = Math.max(trackThickness + 6, DEFAULT_BAND_HEIGHT);
const cellTextProperties = rangeStyle.CellTextProperties;
const hasCellText = hasBarStyleCellTextConfigured(cellTextProperties);
const valueAxisCoord = (fraction) => isVertical ? (1 - fraction) * 100 : fraction * 100;
this.eGui = document.createElement('div');
this.eGui.className = 'ab-RangeBar__wrapper';
this.eGui.style.display = 'flex';
this.eGui.style.flexDirection = 'column';
this.eGui.style.alignItems = isVertical ? 'center' : 'stretch';
this.eGui.style.justifyContent = 'center';
this.eGui.style.position = 'relative';
this.eGui.style.height = '100%';
const svg = document.createElementNS(SVG_NS, 'svg');
svg.setAttribute('class', 'ab-RangeBar__svg');
svg.setAttribute('preserveAspectRatio', 'none');
svg.setAttribute('shape-rendering', 'crispEdges');
if (isVertical) {
svg.setAttribute('width', String(bandThickness));
svg.setAttribute('height', '100%');
svg.setAttribute('viewBox', `0 0 ${bandThickness} 100`);
}
else {
svg.setAttribute('width', '100%');
svg.setAttribute('height', String(bandThickness));
svg.setAttribute('viewBox', `0 0 100 ${bandThickness}`);
}
const setRectAxes = (rect, primaryStart, primaryLen, crossStart, crossLen) => {
if (isVertical) {
rect.setAttribute('x', String(crossStart));
rect.setAttribute('y', String(primaryStart));
rect.setAttribute('width', String(crossLen));
rect.setAttribute('height', String(primaryLen));
}
else {
rect.setAttribute('x', String(primaryStart));
rect.setAttribute('y', String(crossStart));
rect.setAttribute('width', String(primaryLen));
rect.setAttribute('height', String(crossLen));
}
};
const ranges = rangeStyle.CellRanges ?? [];
if (ranges.length === 0) {
const back = document.createElementNS(SVG_NS, 'rect');
setRectAxes(back, 0, 100, 0, bandThickness);
back.setAttribute('fill', rangeStyle.BackColor ?? DEFAULT_BACK_COLOR);
svg.appendChild(back);
}
else {
for (const range of ranges) {
const bandMin = api.styledColumnApi.internalApi.getCellColorRangeMinValue(range, abColumn, rangeStyle.RangeValueType);
const bandMax = api.styledColumnApi.internalApi.getCellColorRangeMaxValue(range, abColumn, rangeStyle.RangeValueType);
if (bandMin == undefined || bandMax == undefined) {
continue;
}
const a = valueAxisCoord(toFraction(bandMin, min, max));
const b = valueAxisCoord(toFraction(bandMax, min, max));
const bandRect = document.createElementNS(SVG_NS, 'rect');
setRectAxes(bandRect, Math.min(a, b), Math.abs(b - a), 0, bandThickness);
bandRect.setAttribute('fill', range.Color);
svg.appendChild(bandRect);
}
}
const trackCrossOffset = (bandThickness - trackThickness) / 2;
const track = document.createElementNS(SVG_NS, 'rect');
setRectAxes(track, 0, 100, trackCrossOffset, trackThickness);
track.setAttribute('fill', trackProperties?.Color ?? DEFAULT_TRACK_COLOR);
track.setAttribute('class', 'ab-RangeBar__track');
if (trackProperties?.Color == undefined) {
track.setAttribute('opacity', ranges.length === 0 ? '0.8' : '0.35');
}
svg.appendChild(track);
if (referenceValue != undefined && !isNaN(referenceValue)) {
const refCoord = valueAxisCoord(toFraction(referenceValue, min, max));
const refEl = createMarkerElement(referenceMarker, refCoord, bandThickness, isVertical);
if (refEl) {
refEl.setAttribute('class', 'ab-RangeBar__referenceMarker');
svg.appendChild(refEl);
}
}
if (!hideValueMarker) {
let valueFraction;
if (outOfRangeMode === 'Overflow') {
valueFraction = max === min ? 0 : (numericValue - min) / (max - min);
}
else {
valueFraction = toFraction(numericValue, min, max);
}
const valueCoord = valueAxisCoord(valueFraction);
const effectiveMarker = isOutOfRange && outOfRangeMode === 'Clamp' && outOfRangeColor
? { ...valueMarker, Color: outOfRangeColor }
: valueMarker;
const markerEl = createMarkerElement(effectiveMarker, valueCoord, bandThickness, isVertical);
if (markerEl) {
markerEl.setAttribute('class', 'ab-RangeBar__valueMarker');
svg.appendChild(markerEl);
}
}
const svgContainer = document.createElement('div');
svgContainer.style.position = 'relative';
svgContainer.style.display = 'flex';
svgContainer.style.alignItems = 'center';
svgContainer.style.justifyContent = 'center';
if (isVertical) {
svgContainer.style.flex = '1 1 auto';
svgContainer.style.minHeight = '0';
}
else {
svgContainer.style.flex = '0 0 auto';
}
svgContainer.appendChild(svg);
this.eGui.appendChild(svgContainer);
if (hasCellText) {
const pct = clamp((numericValue - min) / (max - min), 0, 1) * 100;
const formattedCellValue = String(params.formatValue?.(numericValue) ?? numericValue);
const labels = buildBarStyleCellTextLabels(cellTextProperties, formattedCellValue, `${pct.toFixed(0)}%`);
mountBarStyleCellText({
wrapperEl: this.eGui,
mergedOverlayEl: svgContainer,
textClassName: 'ab-RangeBar__text',
cellTextProperties,
labels,
});
}
}
getGui() {
return this.eGui;
}
refresh(_params) {
return false;
}
};
};
const createMarkerElement = (marker, valueCoord, crossLen, isVertical) => {
const xy = (primary, cross) => isVertical ? [cross, primary] : [primary, cross];
switch (marker.Shape) {
case 'Line': {
const crossA = crossLen * 0.1;
const crossB = crossLen * 0.9;
const [x1, y1] = xy(valueCoord, crossA);
const [x2, y2] = xy(valueCoord, crossB);
const line = document.createElementNS(SVG_NS, 'line');
line.setAttribute('x1', String(x1));
line.setAttribute('x2', String(x2));
line.setAttribute('y1', String(y1));
line.setAttribute('y2', String(y2));
line.setAttribute('stroke', marker.Color);
line.setAttribute('stroke-width', String(marker.Size));
line.setAttribute('vector-effect', 'non-scaling-stroke');
return line;
}
case 'Dot': {
const [cx, cy] = xy(valueCoord, crossLen / 2);
const circle = document.createElementNS(SVG_NS, 'circle');
circle.setAttribute('cx', String(cx));
circle.setAttribute('cy', String(cy));
circle.setAttribute('r', String(marker.Size / 2));
circle.setAttribute('fill', marker.Color);
circle.setAttribute('shape-rendering', 'geometricPrecision');
return circle;
}
case 'Diamond': {
const half = marker.Size / 2;
const crossMid = crossLen / 2;
const ptList = [
xy(valueCoord, crossMid - half),
xy(valueCoord + half, crossMid),
xy(valueCoord, crossMid + half),
xy(valueCoord - half, crossMid),
];
const polygon = document.createElementNS(SVG_NS, 'polygon');
polygon.setAttribute('points', ptList.map((p) => p.join(',')).join(' '));
polygon.setAttribute('fill', marker.Color);
polygon.setAttribute('shape-rendering', 'geometricPrecision');
return polygon;
}
case 'Triangle': {
const half = marker.Size / 2;
const crossMid = crossLen / 2;
const ptList = [
xy(valueCoord - half, crossMid + half),
xy(valueCoord + half, crossMid + half),
xy(valueCoord, crossMid - half),
];
const polygon = document.createElementNS(SVG_NS, 'polygon');
polygon.setAttribute('points', ptList.map((p) => p.join(',')).join(' '));
polygon.setAttribute('fill', marker.Color);
polygon.setAttribute('shape-rendering', 'geometricPrecision');
return polygon;
}
default:
return undefined;
}
};