UNPKG

@adaptabletools/adaptable

Version:

Powerful AG Grid extension which provides advanced, cutting-edge functionality to meet all DataGrid requirements

298 lines (297 loc) 14 kB
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; } };