UNPKG

@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
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; } };