Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/graphieros/tiny-spark. An elegant, reactive and responsive sparkline chart solution without dependency. tiny-spark.graphieros.com
chart sparkline universal
Something went wrong. Try again.
26 kB · 699 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699import { CHART_TYPE, TINY_SPARK, POINT, XMLNS, DATA_ATTRIBUTE, ANIMATION_DURATION, TooltipState, ELEMENT_DATASET } from "../types"import { animateAreaProgressively, animatePath, createIndividualAreaWithCuts, createSmoothAreaSegments, createSmoothPath, createSmoothPathWithCuts, createStraightPath, createStraightPathWithCuts, SVG } from "./svg";
export function getCharts() { const charts = document.querySelectorAll('.tiny-spark') return charts}
export function hasDataset(element: TINY_SPARK, name: string) { const attrs = Object.keys(element.dataset) return attrs.includes(name)}
export function getDatasetValue(element: TINY_SPARK, name: string, fallback: number | string) { if (!hasDataset(element, name)) return fallback; return element.dataset[name]}
export function getElementColors(element: TINY_SPARK) { if (!element) return { color: '#1A1A1A', backgroundColor: '#FFFFFF' }; const computedStyle = window.getComputedStyle(element); const color = computedStyle.getPropertyValue('color') || '#1A1A1A'; const backgroundColor = computedStyle.getPropertyValue('background-color'); const background = computedStyle.getPropertyValue('background'); const bg = backgroundColor || background || '#FFFFFF'; return { color, backgroundColor: bg };}
export function createUid() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' .replace(/[xy]/g, function (c) { const r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); });}
export function parseDataset(chart: TINY_SPARK) { const dataSetStr = chart.getAttribute(ELEMENT_DATASET.SET); if (!dataSetStr) return []; const str = dataSetStr .replace(/,(?=,)/g, ',null') .replace(/\[,/g, '[null,') .replace(/,\]/g, ',null]'); try { const parsed = JSON.parse(str); if (Array.isArray(parsed) && parsed.every(item => typeof item === 'number' || [null, undefined].includes(item))) { return parsed; } console.warn('data-set is not an array of numbers.'); return []; } catch (error) { console.error('Error parsing data-set:', error); return []; }}
export function setGradientOffset(i: number, n: number): string { if (n <= 1) return '0%'; return `${(i * 100) / (n - 1)}%`;}
function getDates(chart: TINY_SPARK) { const dates = chart.getAttribute(ELEMENT_DATASET.DATES); if (!dates) return [] try { const parsed = JSON.parse(dates.replaceAll(`'`, '"')); if (Array.isArray(parsed) && parsed.every(item => typeof item === 'string')) { return parsed; } console.warn('data-dates is not an array of strings'); return []; } catch (error) { console.error('Error parsing data-dates', error); return []; }}
function getTemperatureColors(chart: TINY_SPARK) { const colors = chart.getAttribute(ELEMENT_DATASET.TEMPERATURE_COLORS); if (!colors) return []; try { const parsed = JSON.parse(colors.replaceAll(`'`, '"')); if (Array.isArray(parsed) && parsed.every(item => typeof item === 'string')) { return parsed; } console.warn('data-temperature-colors is not an array of strings'); return []; } catch (error) { console.error('Error parsing data-temperature-colors', error); return []; }}
export function minMax(dataset: number[]) { return { min: Math.min(...dataset), max: Math.max(...dataset) }}
export function nextTick() { return new Promise(resolve => setTimeout(resolve, 0));}
export function localeNum(chart: TINY_SPARK, num: number) { const locale = String(getDatasetValue(chart, DATA_ATTRIBUTE.NUMBER_LOCALE, navigator.language || 'en-US')); const rounding = Number(String(getDatasetValue(chart, DATA_ATTRIBUTE.NUMBER_ROUNDING, 0))); return num.toLocaleString(locale, { useGrouping: true, minimumFractionDigits: rounding, maximumFractionDigits: rounding });}
export function domPlot(svg: SVGSVGElement, svgX: number, svgY: number) { if (!svg.createSVGPoint || !svg.getScreenCTM) { console.error("Your browser does not support SVG coordinate transformation."); return { x: 0, y: 0 }; } const screenCTM = svg.getScreenCTM(); if (!screenCTM) { console.error("Cannot obtain the screen CTM."); return { x: 0, y: 0 }; }
const point = svg.createSVGPoint(); point.x = svgX; point.y = svgY;
const domPoint = point.matrixTransform(screenCTM);
return { x: domPoint.x, y: domPoint.y };}
const tooltipStates: Record<string, TooltipState> = {}
export function tooltip( svg: SVGSVGElement, chart: TINY_SPARK, point: POINT, id: string, show: boolean) { let state = tooltipStates[id];
if (!show) { if (state) { cancelAnimationFrame(state.frameId!); state.frameId = null; state.tool.style.opacity = '0'; } return }
const isBar = chart.dataset.type === CHART_TYPE.BAR; const { x, y } = domPlot( svg, point.x, isBar && !point.isPositive ? point.bar.y : point.y );
if (!state) { const tool = document.createElement('div'); tool.classList.add('tiny-spark-tooltip'); tool.setAttribute('id', `tooltip_${id}`); tool.setAttribute('role', 'tooltip'); tool.setAttribute('aria-live', 'polite'); tool.style.position = 'fixed'; tool.style.pointerEvents = 'none'; tool.style.opacity = '0'; tool.style.willChange = 'top, left'; document.body.appendChild(tool);
state = tooltipStates[id] = { targetX: 0, targetY: 0, displayX: 0, displayY: 0, frameId: null, tool, width: 0, height: 0, hasSnapped: false } }
state.tool.setAttribute('aria-hidden', 'false'); state.tool.innerHTML = ` <div class="tiny-spark-tooltip-content"> ${point.d ? `${point.d}: ` : ''}${ [null, undefined].includes(point.v as any) ? '-' : localeNum(chart, Number(point.v)) } </div> `;
const { width: w, height: h } = state.tool.getBoundingClientRect(); state.width = w; state.height = h;
const radius = Number(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_RADIUS, 3)); state.targetX = x - state.width / 2; state.targetY = y - state.height - radius * 1.5;
if (!state.hasSnapped) { state.displayX = state.targetX; state.displayY = state.targetY; state.tool.style.left = `${state.displayX}px`; state.tool.style.top = `${state.displayY}px`; state.tool.style.opacity = '1'; state.hasSnapped = true; return; }
const SMOOTHING = Number(getDatasetValue(chart, DATA_ATTRIBUTE.TOOLTIP_SMOOTHING, 1)) / 10;
function animate() { state.displayX += (state.targetX - state.displayX) * SMOOTHING; state.displayY += (state.targetY - state.displayY) * SMOOTHING;
state.tool.style.left = `${Math.round(state.displayX)}px`; state.tool.style.top = `${Math.round(state.displayY)}px`; state.tool.style.opacity = '1';
state.frameId = requestAnimationFrame(animate); } if (state.frameId == null) animate();}
function makeGradient({ id, colorStart, colorEnd, opacityStart, opacityEnd }: { id: string, colorStart: string, colorEnd: string; opacityStart: number; opacityEnd: number;}) { const linearGradient = document.createElementNS(XMLNS, 'linearGradient'); linearGradient.setAttribute('id', id); linearGradient.setAttribute('x1', '0'); linearGradient.setAttribute('x2', '0'); linearGradient.setAttribute('y1', '0'); linearGradient.setAttribute('y2', '1'); const stopStart = document.createElementNS(XMLNS, 'stop'); stopStart.setAttribute('offset', '0%'); stopStart.setAttribute('stop-color', colorStart); stopStart.setAttribute('stop-opacity', String(opacityStart)); const stopEnd = document.createElementNS(XMLNS, 'stop'); stopEnd.setAttribute('offset', '100%'); stopEnd.setAttribute('stop-color', colorEnd); stopEnd.setAttribute('stop-opacity', String(opacityEnd)); [stopStart, stopEnd].forEach(stop => { linearGradient.appendChild(stop); }); return linearGradient;}
/////////////////////////////////////////////////////
function hasAllSameValues(array: number[]) { if (array.length <= 1) return true; const firstValue = array[0]; for (let index = 1; index < array.length; index += 1) { if (array[index] !== firstValue) { return false; } } return true;}
function clear(chart: TINY_SPARK) { chart.innerHTML = ''}
export function createChart(chart: TINY_SPARK, firstTime: boolean) { const isBar = chart.dataset.type && chart.dataset.type === 'bar';
let animate = firstTime; clear(chart); const { svg, svgId, width, height, viewBox } = SVG(chart); const { color, backgroundColor } = getElementColors(chart);
const padding = { T: 12, R: 12, B: 12, L: 12 }; const lastValueId = createUid(); const showLastValue = String(getDatasetValue(chart, DATA_ATTRIBUTE.SHOW_LAST_VALUE, 'false')) === 'true';
// DRAWING AREA const area = { left: padding.L, top: padding.T, width: width - padding.L - padding.R, height: height - padding.T - padding.B, bottom: height - padding.B, right: width - padding.R };
const dataset = parseDataset(chart); const { min: MIN } = minMax(dataset); const positiveDataset = dataset.map(d => { return [null, undefined].includes(d) ? d : d + (MIN < 0 ? Math.abs(MIN) : 0) }); const { max } = minMax(positiveDataset); let slot = area.width / (dataset.length - 1) === Infinity ? area.width : area.width / (dataset.length - 1);
if (isBar) { const [v_0, v_1, v_2, v_3] = viewBox.split(' '); // Add padding to leave space for bars svg.setAttribute('viewBox', `${Number(v_0) - slot / 2} ${v_1} ${Number(v_2) + slot} ${v_3}`) }
const allNeg = !dataset.some(d => d >= 0); const dates = getDates(chart);
// DATAPOINTS const allPoints = positiveDataset.map((d, i) => { const uniqueCase = { w: positiveDataset.length === 1 ? slot / 2 : 0, h: positiveDataset.length === 1 ? area.height / 2 : 0 }
const x = area.left + ((slot * i)) + uniqueCase.w; const y = ((1 - ((d || 0) / max)) * area.height) + uniqueCase.h + padding.T; const y_0 = ((1 - ((MIN < 0 ? Math.abs(MIN) : 0) / max)) * area.height) + padding.T + uniqueCase.h;
const isPositive = dataset[i] >= 0
return { y: allNeg && dataset.length === 1 ? area.top + area.height / 2 : y, x, v: dataset[i], d: dates[i] || null, isPositive, bar: { x: x - slot / 2, y: dataset.length === 1 ? area.top : isPositive ? y : allNeg ? area.top : y_0, h: dataset.length === 1 ? area.height : isPositive ? y_0 - y : allNeg && dataset.length === 0 ? area.height : isNaN(y - y_0) ? 0 : y - y_0, w: slot } } });
function makePathArea(): SVGPathElement { const _pathArea = document.createElementNS(XMLNS, 'path'); _pathArea.classList.add('tiny-spark-line-area'); return _pathArea; }
function createAreaGradient(id: string): SVGDefsElement { const defs = document.createElementNS(XMLNS, 'defs'); defs.appendChild(makeGradient({ id, colorStart: String(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_FROM, '#FFFFFF')), colorEnd: String(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_TO, '#FFFFFF')), opacityStart: Math.max(0, Math.min(1, Number(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_FROM_OPACITY, 1)))), opacityEnd: Math.max(0, Math.min(1, Number(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_TO_OPACITY, 1)))) })); return defs; }
function createBarGradients(id: string): SVGDefsElement { const defs = document.createElementNS(XMLNS, 'defs'); defs.appendChild(makeGradient({ id: `pos_${id}`, colorStart: String(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_FROM, '#FFFFFF')), colorEnd: String(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_TO, '#FFFFFF')), opacityStart: Math.max(0, Math.min(1, Number(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_FROM_OPACITY, 1)))), opacityEnd: Math.max(0, Math.min(1, Number(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_TO_OPACITY, 1)))) })); defs.appendChild(makeGradient({ id: `neg_${id}`, colorStart: String(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_TO, '#FFFFFF')), colorEnd: String(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_FROM, '#FFFFFF')), opacityStart: Math.max(0, Math.min(1, Number(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_TO_OPACITY, 1)))), opacityEnd: Math.max(0, Math.min(1, Number(getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_FROM_OPACITY, 1)))), })); return defs; }
const points = [...allPoints].map((p) => [null, undefined, Infinity, -Infinity, NaN, 'NaN'].includes(p.v) ? {...p, v: null } : p); const animation = chart.getAttribute('data-animation'); const path = document.createElementNS(XMLNS, 'path'); path.classList.add('tiny-spark-line-path'); const pathArea = makePathArea(); const pathAreas: SVGPathElement[] = [];
const hasGradient = !!getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_FROM, '') && !!getDatasetValue(chart, DATA_ATTRIBUTE.GRADIENT_TO, ''); const gradientId = createUid();
// PATH & AREA if (!isBar) { if (!chart.dataset.curve || chart.dataset.curve === 'true') { if (!chart.dataset.cutNull || chart.dataset.cutNull === 'false') { path.setAttribute('d', `M ${createSmoothPath(points)}`); } else { path.setAttribute('d', `M ${createSmoothPathWithCuts(points)}`); } } else { if (!chart.dataset.cutNull || chart.dataset.cutNull === 'false') { path.setAttribute('d', `M ${createStraightPath(points)}`); } else { path.setAttribute('d', `M ${createStraightPathWithCuts(points)}`); } } path.setAttribute('fill', 'none');
const temperatureColors = getTemperatureColors(chart);
if (temperatureColors.length && !hasAllSameValues(points.map(p => p.y))) { const temperatureId = createUid(); const temperatureDefs = document.createElementNS(XMLNS, 'defs'); const temperatureLinearGradient = document.createElementNS(XMLNS, 'linearGradient'); temperatureLinearGradient.setAttribute('gradientTransform', 'rotate(90)'); temperatureLinearGradient.setAttribute('id', temperatureId); temperatureColors.forEach((c, ci) => { const stop = document.createElementNS(XMLNS, 'stop'); stop.setAttribute('offset', setGradientOffset(ci, temperatureColors.length)); stop.setAttribute('stop-color', c); temperatureLinearGradient.appendChild(stop); }); temperatureDefs.appendChild(temperatureLinearGradient); svg.prepend(temperatureDefs); path.setAttribute('stroke', `url(#${temperatureId})`); } else { path.setAttribute('stroke', String(getDatasetValue(chart, DATA_ATTRIBUTE.LINE_COLOR, color))); }
path.setAttribute('stroke-width', String(getDatasetValue(chart, DATA_ATTRIBUTE.LINE_THICKNESS, 2))); path.setAttribute('stroke-linecap', 'round'); if (animation === 'true' && animate) { path.style.opacity = '0' pathArea.style.opacity = '0' }
if (hasGradient) { svg.appendChild(createAreaGradient(gradientId)); } // AREAS if (allPoints.length) { if (!chart.dataset.curve || chart.dataset.curve === 'true') { if (!chart.dataset.cutNull || chart.dataset.cutNull === 'false') { pathArea.setAttribute('d', `M ${points[0].x},${area.bottom} ${createSmoothPath(points)} L ${points.at(-1)!.x},${area.bottom} Z`); } else { const segments = createSmoothAreaSegments(points, area.bottom, true, true); segments.forEach(seg => { const pth = makePathArea() pth.setAttribute('d', seg); if (hasGradient) { pth.setAttribute('fill', `url(#${gradientId})`); } else { pth.setAttribute('fill', String(getDatasetValue(chart, DATA_ATTRIBUTE.AREA_COLOR, 'transparent'))); } pathAreas.push(pth); }); } } else { if (!chart.dataset.cutNull || chart.dataset.cutNull === 'false') { pathArea.setAttribute('d', `M ${points[0].x},${area.bottom} ${createStraightPath(points)} L ${points.at(-1)!.x},${area.bottom} Z`); } else { const segments = createIndividualAreaWithCuts(points, area.bottom).split(';'); segments.forEach(seg => { const pth = makePathArea() pth.setAttribute('d', `M ${seg} Z`); if (hasGradient) { pth.setAttribute('fill', `url(#${gradientId})`); } else { pth.setAttribute('fill', String(getDatasetValue(chart, DATA_ATTRIBUTE.AREA_COLOR, 'transparent'))); } pathAreas.push(pth); }); } } } if (hasGradient) { pathArea.setAttribute('fill', `url(#${gradientId})`); } else { pathArea.setAttribute('fill', String(getDatasetValue(chart, DATA_ATTRIBUTE.AREA_COLOR, 'transparent'))); } if (allPoints.length > 1) { if (pathAreas.length) { pathAreas.forEach(p => { svg.appendChild(p) }) } else { svg.appendChild(pathArea); } svg.appendChild(path); } }
const indicators: SVGLineElement[] = [];
// VERTICAL INDICATOR allPoints.forEach((_, i) => { const indicator = document.createElementNS(XMLNS, 'line'); indicator.classList.add('tiny-spark-indicator'); indicator.setAttribute('id', `indicator_${svgId}_${i}`); indicator.setAttribute('x1', String(area.left + (allPoints.length === 1 ? area.width / 2 : (i * slot)))); indicator.setAttribute('x2', String(area.left + (allPoints.length === 1 ? area.width / 2 : (i * slot)))); indicator.setAttribute('y1', String(area.top)); indicator.setAttribute('y2', String(area.bottom)); indicator.setAttribute('stroke', String(getDatasetValue(chart, DATA_ATTRIBUTE.INDICATOR_COLOR, '#1A1A1A'))); indicator.setAttribute('stroke-width', String(getDatasetValue(chart, DATA_ATTRIBUTE.INDICATOR_WIDTH, '1'))); indicator.setAttribute('stroke-linecap', 'round'); indicator.style.pointerEvents = 'none'; indicator.style.opacity = '0'; indicators.push(indicator); svg.appendChild(indicator); })
let plots: SVGCircleElement[] = []; let bars: SVGRectElement[] = [];
// PLOTS const hasPlotRadius = Number(String(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_RADIUS, 0))) > 0; const isInPlotViewRange = !String(getDatasetValue(chart, DATA_ATTRIBUTE.HIDE_PLOTS_ABOVE, '')) || allPoints.length <= (Number(String(getDatasetValue(chart, DATA_ATTRIBUTE.HIDE_PLOTS_ABOVE, 0)))); const canShowPlots = hasPlotRadius && isInPlotViewRange;
const plotGroup = document.createElementNS(XMLNS, 'g');
// BARS if (isBar) { if (hasGradient) { svg.appendChild(createBarGradients(gradientId)); } allPoints.forEach(({ bar, v }, i) => { if (![null, undefined].includes(v)) { const rect = document.createElementNS(XMLNS, 'rect'); rect.classList.add('tiny-spark-datapoint-bar'); rect.setAttribute('x', String(bar.x)); rect.setAttribute('y', String(bar.y)); rect.setAttribute('width', String(bar.w)); rect.setAttribute('height', String(bar.h)); if (hasGradient) { if (v >= 0) { rect.setAttribute('fill', `url(#pos_${gradientId})`) } else { rect.setAttribute('fill', `url(#neg_${gradientId})`) } } else { rect.setAttribute('fill', String(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_COLOR, String(getDatasetValue(chart, 'lineColor', color))))) } rect.style.opacity = allPoints.length === 1 ? '1' : '0'; rect.style.transition = `opacity ${i * ((ANIMATION_DURATION * 2) / allPoints.length)}ms ease-in`; bars.push(rect); plotGroup.appendChild(rect); } }); svg.appendChild(plotGroup); }
if (hasPlotRadius && !isBar) { allPoints.forEach(({ x, y, v }, i) => { if (![null, undefined].includes(v)) { const circle = document.createElementNS(XMLNS, 'circle'); circle.classList.add('tiny-spark-datapoint-circle'); circle.classList.add(`circle-${svgId}`); circle.setAttribute('id', `circle_${svgId}_${i}`); circle.setAttribute('cx', String(x || 0)); circle.setAttribute('cy', String(y || 0)); circle.setAttribute('r', String(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_RADIUS, 3))); circle.setAttribute('fill', String(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_COLOR, String(getDatasetValue(chart, 'lineColor', color))))); circle.setAttribute('stroke', backgroundColor); circle.style.opacity = allPoints.length === 1 ? '1' : '0'; circle.style.transition = `opacity ${i * ((ANIMATION_DURATION * 2) / allPoints.length)}ms ease-in`; circle.style.pointerEvents = 'none'; plots.push(circle); if (canShowPlots) { plotGroup.appendChild(circle); } } }); svg.appendChild(plotGroup); }
// LAST VALUE let lastValueText: SVGTextElement | null = null
if (showLastValue && allPoints.length && allPoints.at(-1)) { const fontSize = Number(getDatasetValue(chart, DATA_ATTRIBUTE.LAST_VALUE_FONT_SIZE, 12))
lastValueText = document.createElementNS(XMLNS, 'text'); lastValueText.classList.add('tiny-spark-last-value'); lastValueText.setAttribute('id', lastValueId);
if (isBar) { lastValueText.setAttribute('x', String(allPoints.at(-1)!.x + Number(getDatasetValue(chart, DATA_ATTRIBUTE.LINE_THICKNESS, 2)))); lastValueText.setAttribute('y', allPoints.at(-1)?.isPositive ? String(allPoints.at(-1)!.y - fontSize / 3) : String(allPoints.at(-1)!.bar.y + allPoints.at(-1)!.bar.h + fontSize)); lastValueText.setAttribute('text-anchor', 'middle'); } else { lastValueText.setAttribute('x', String(allPoints.at(-1)!.x + 6 + Number(getDatasetValue(chart, DATA_ATTRIBUTE.LINE_THICKNESS, 2)))); lastValueText.setAttribute('y', String(allPoints.at(-1)!.y + fontSize / 3)); lastValueText.setAttribute('text-anchor', 'start'); }
lastValueText.setAttribute('font-size', String(fontSize) + 'px'); lastValueText.setAttribute('fill', String(getDatasetValue(chart, DATA_ATTRIBUTE.LAST_VALUE_COLOR, String(getDatasetValue(chart, DATA_ATTRIBUTE.INDICATOR_COLOR, '#1A1A1A'))))); lastValueText.innerHTML = localeNum(chart, Number(allPoints.at(-1)!.v)); lastValueText.style.opacity = allPoints.length === 1 ? '1' : '0'; svg.appendChild(lastValueText); }
const trapGroup = document.createElementNS(XMLNS, 'g');
// TOOLTIP TRAPS allPoints.forEach((point, i) => { const thatPlot = plots[i]; const trap = document.createElementNS(XMLNS, 'rect'); trap.classList.add('tiny-spark-tooltip-trap'); trap.setAttribute('x', `${allPoints.length === 1 ? 0 : area.left + i * slot - slot / 2}`); trap.setAttribute('y', `${area.top}`); trap.setAttribute('height', `${area.height}`); trap.setAttribute('width', `${slot}`); trap.setAttribute('fill', 'transparent'); trap.setAttribute('aria-describedby', `tooltip_${svgId}`) trap.addEventListener('mouseenter', () => { tooltip(svg, chart, point, svgId, true); if (canShowPlots) { const circle = document.getElementById(`circle_${svgId}_${i}`); circle?.setAttribute('r', String(Number(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_RADIUS, 3)) * 1.5)) } else { svg.appendChild(thatPlot); } indicators[i].style.opacity = '1'; if (showLastValue && lastValueText) { if(i === allPoints.length - 1) { lastValueText.style.opacity = '0'; } else { lastValueText.style.opacity = '1'; } } }); trap.addEventListener('mouseout', () => { tooltip(svg, chart, point, svgId, false); if (canShowPlots) { const circle = document.getElementById(`circle_${svgId}_${i}`); circle?.setAttribute('r', String(Number(getDatasetValue(chart, DATA_ATTRIBUTE.PLOT_RADIUS, 3)))) } else { thatPlot.remove(); } indicators.forEach(indicator => indicator.style.opacity = '0'); if (showLastValue && lastValueText) { lastValueText.style.opacity = '1'; } });
trapGroup.appendChild(trap); });
svg.appendChild(trapGroup);
if (animation === 'true' && animate) { nextTick().then(() => { plots.forEach(circle => { circle.style.opacity = '1' }) bars.forEach(bar => { bar.style.opacity = '1' }) animatePath(path, ANIMATION_DURATION, () => { if (lastValueText) { lastValueText.style.opacity = '1'; } }); animateAreaProgressively(svg as unknown as TINY_SPARK, pathArea); }) } else { plots.forEach(circle => { circle.style.opacity = '1' }); bars.forEach(bar => { bar.style.opacity = '1' }); if (lastValueText) { lastValueText.style.opacity = '1'; } }
chart.appendChild(svg);
chart.addEventListener('mouseleave', () => { const st = tooltipStates[svgId]; if (st) { cancelAnimationFrame(st.frameId!); st.frameId = null; st.tool.style.opacity = '0'; st.hasSnapped = false; } });}