@adaptabletools/adaptable
Version:
Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements
298 lines (297 loc) • 14 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_BAR_HEIGHT = 8;
const DEFAULT_BAND_HEIGHT = 14;
const DEFAULT_MARKER_LINE_THICKNESS = 2;
const DEFAULT_MARKER_SHAPE_SIZE = 8;
const DEFAULT_BAR_COLOR = 'var(--ab-color-accent, #07c)';
const DEFAULT_MARKER_COLOR = 'var(--ab-color-foreground, #111)';
const DEFAULT_BACK_COLOR = 'transparent';
const isTargetDefinition = (t) => !!t && typeof t === 'object' && 'Value' in t;
const resolveTargetValue = (target, styledColumn, abColumn, rowNode, api) => {
const internalApi = api.styledColumnApi.internalApi;
const value = isTargetDefinition(target) ? target.Value : target;
if (typeof value === 'number') {
return value;
}
if (value === 'Col-Avg') {
return internalApi.getAvgValueForNumericColumn(abColumn);
}
if (value === 'Col-Median') {
return internalApi.getMedianValueForNumericColumn(abColumn);
}
if (typeof value === 'string') {
if (!api.columnApi.isColumnInGrid(value)) {
return undefined;
}
const raw = api.gridApi.getRawValueFromRowNode(rowNode, value);
return typeof raw === 'number' ? raw : Number(raw);
}
return undefined;
};
const normaliseMarker = (override, base) => {
const shape = override?.Shape ?? base?.Shape ?? 'Line';
return {
Shape: shape,
Color: override?.Color ?? base?.Color ?? DEFAULT_MARKER_COLOR,
Size: override?.Size ??
base?.Size ??
(shape === 'Line' ? DEFAULT_MARKER_LINE_THICKNESS : DEFAULT_MARKER_SHAPE_SIZE),
};
};
const resolveTargets = (bulletStyle, styledColumn, abColumn, rowNode, api) => {
const targetSpec = bulletStyle.TargetProperties?.Target;
if (targetSpec == undefined) {
return [];
}
const targets = Array.isArray(targetSpec) ? targetSpec : [targetSpec];
const resolved = [];
for (const target of targets) {
const value = resolveTargetValue(target, styledColumn, abColumn, rowNode, api);
if (value == undefined || isNaN(value)) {
continue;
}
const marker = normaliseMarker(isTargetDefinition(target) ? target.Marker : undefined, bulletStyle.TargetProperties?.Marker);
resolved.push({
value,
marker,
label: isTargetDefinition(target) ? target.Label : undefined,
});
}
return resolved;
};
const resolveOrigin = (bulletStyle, cellValue, min, max) => {
const origin = bulletStyle.Origin ?? 'Auto';
if (typeof origin === 'number') {
return origin;
}
if (origin === 'Zero') {
return 0;
}
if (min < 0 || max < 0 || cellValue < 0) {
return 0;
}
return min;
};
const toFraction = (value, min, max) => {
if (max === min) {
return 0;
}
return clamp((value - min) / (max - min), 0, 1);
};
export const getBulletChartRendererForColumn = (styledColumn, abColumn, api) => {
if (!styledColumn.BulletChartStyle) {
return;
}
const bulletStyle = styledColumn.BulletChartStyle;
return class BulletChartRenderer {
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 = api.styledColumnApi.internalApi.getNumericStyleMinValue(styledColumn, abColumn, params.node, rawCellValue);
const max = api.styledColumnApi.internalApi.getNumericStyleMaxValue(styledColumn, abColumn, params.node, rawCellValue);
if (min == undefined || max == undefined || max <= min) {
this.eGui = document.createElement('div');
this.eGui.append(params.formatValue?.(rawCellValue) ?? String(rawCellValue));
return;
}
const cellValue = numericValue;
const targets = resolveTargets(bulletStyle, styledColumn, abColumn, params.node, api);
const origin = resolveOrigin(bulletStyle, numericValue, min, max);
const isVertical = bulletStyle.Orientation === 'Vertical';
const barThickness = bulletStyle.Bar?.Height ?? DEFAULT_BAR_HEIGHT;
const bandThickness = Math.max(barThickness + 6, DEFAULT_BAND_HEIGHT);
const cellTextProperties = bulletStyle.CellTextProperties;
const hasCellText = hasBarStyleCellTextConfigured(cellTextProperties);
const valueAxisCoord = (fraction) => isVertical ? (1 - fraction) * 100 : fraction * 100;
this.eGui = document.createElement('div');
this.eGui.className = 'ab-BulletChart__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-BulletChart__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 = bulletStyle.CellRanges ?? [];
if (ranges.length === 0) {
const back = document.createElementNS(SVG_NS, 'rect');
setRectAxes(back, 0, 100, 0, bandThickness);
back.setAttribute('fill', bulletStyle.BackColor ?? DEFAULT_BACK_COLOR);
svg.appendChild(back);
}
else {
for (const range of ranges) {
const bandMin = api.styledColumnApi.internalApi.getCellColorRangeMinValue(range, abColumn, bulletStyle.RangeValueType);
const bandMax = api.styledColumnApi.internalApi.getCellColorRangeMaxValue(range, abColumn, bulletStyle.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 valueFraction = toFraction(numericValue, min, max);
const originFraction = toFraction(origin, min, max);
const valueCoord = valueAxisCoord(valueFraction);
const originCoord = valueAxisCoord(originFraction);
const barCrossOffset = (bandThickness - barThickness) / 2;
const bar = document.createElementNS(SVG_NS, 'rect');
setRectAxes(bar, Math.min(valueCoord, originCoord), Math.abs(valueCoord - originCoord), barCrossOffset, barThickness);
bar.setAttribute('fill', bulletStyle.Bar?.Color ?? DEFAULT_BAR_COLOR);
bar.setAttribute('class', 'ab-BulletChart__bar');
svg.appendChild(bar);
for (const target of targets) {
const tCoord = valueAxisCoord(toFraction(target.value, min, max));
const marker = createMarkerElement(target.marker, tCoord, bandThickness, isVertical);
if (marker) {
marker.setAttribute('class', 'ab-BulletChart__marker');
svg.appendChild(marker);
}
}
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 formattedCellValue = String(params.formatValue?.(cellValue) ?? cellValue);
const labels = buildBarStyleCellTextLabels(cellTextProperties, formattedCellValue, `${(valueFraction * 100).toFixed(0)}%`);
mountBarStyleCellText({
wrapperEl: this.eGui,
mergedOverlayEl: svgContainer,
textClassName: 'ab-BulletChart__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;
}
};