diff --git a/packages/chart/index.html b/packages/chart/index.html
index 4464af916e..f5e3be2c41 100644
--- a/packages/chart/index.html
+++ b/packages/chart/index.html
@@ -16,7 +16,42 @@
margin-top: 50px;
}
- #placeholder {
+ .controls {
+ max-width: 800px;
+ margin: 20px auto;
+ padding: 15px;
+ background-color: #fff;
+ border-radius: 5px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ }
+
+ .control-group {
+ margin: 10px 0;
+ }
+
+ .control-group label {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ font-size: 14px;
+ }
+
+ .control-group input[type="checkbox"] {
+ margin-right: 10px;
+ cursor: pointer;
+ width: 18px;
+ height: 18px;
+ }
+
+ .control-group .description {
+ margin-left: 28px;
+ font-size: 12px;
+ color: #666;
+ font-style: italic;
+ }
+
+ #placeholder,
+ #placeholder2 {
width: 100%;
height: 500px;
background-color: #fff;
@@ -29,9 +64,31 @@
ESM Quick Test
-
+
+
+
+
+
+ When enabled, you can use Tab to navigate through columns and Space/Enter to select them
+
+
+
+
+
+ When enabled, you can use Tab to navigate through slices and Space/Enter to select them
+
+
+
+
+
+
+
+
diff --git a/packages/chart/src/Column.css b/packages/chart/src/Column.css
index d731d289e1..587bb73322 100644
--- a/packages/chart/src/Column.css
+++ b/packages/chart/src/Column.css
@@ -9,9 +9,48 @@
.chart_Column .columnRect {
stroke: transparent;
- border-width: 1.5px;
+ border-width: 2px;
}
-.chart_Column .columnRect.selected {
- stroke: red;
+.chart_Column .dataCell.selected .columnRect {
+ stroke: #dc3545 !important;
+ stroke-width: 3px !important;
+ paint-order: fill stroke !important;
+ filter: drop-shadow(0 0 3px rgba(220, 53, 69, 0.4));
+ transition: all 0.2s ease;
+}
+
+.chart_Column .dataCell:hover .columnRect {
+ stroke: rgba(108, 117, 125, 0.6);
+ stroke-width: 2px;
+ filter: brightness(1.05);
+}
+
+.chart_Column .dataCell:focus .columnRect {
+ stroke: #007bff !important;
+ stroke-width: 3px !important;
+ paint-order: fill stroke !important;
+ filter: drop-shadow(0 0 3px rgba(0, 123, 255, 0.4));
+ transition: all 0.2s ease;
+}
+
+.chart_Column .dataCell.selected:focus .columnRect {
+ stroke: #6f42c1 !important;
+ filter: drop-shadow(0 0 3px rgba(111, 66, 193, 0.5));
+}
+
+.chart_Column .dataCell:focus-visible {
+ outline: none;
+}
+
+.chart_Column .dataCell[data-tabster-focusable] {
+ outline: none !important;
+}
+
+.chart_Column .dataCell:focus {
+ outline: none !important;
+}
+
+.chart_Column .dataCell:active {
+ outline: none !important;
}
diff --git a/packages/chart/src/Column.ts b/packages/chart/src/Column.ts
index db8d6cf35c..9f5ac11050 100644
--- a/packages/chart/src/Column.ts
+++ b/packages/chart/src/Column.ts
@@ -1,8 +1,9 @@
import { INDChart, ITooltip } from "@hpcc-js/api";
-import { InputField, Text } from "@hpcc-js/common";
+import { d3Event, InputField, Text } from "@hpcc-js/common";
import { format as d3Format } from "d3-format";
import { scaleBand as d3ScaleBand } from "d3-scale";
import { local as d3Local, select as d3Select } from "d3-selection";
+// import { createTabster, getGroupper, getTabster, type Types as TabsterTypes } from "tabster";
import { XYAxis } from "./XYAxis.ts";
import "../src/Column.css";
@@ -21,6 +22,7 @@ export class Column extends XYAxis {
private textLocal = d3Local();
private stackedTextLocal = d3Local();
private isHorizontal: boolean;
+ // protected _tabster: TabsterTypes.TabsterCore | null = null;
constructor() {
super();
@@ -34,6 +36,12 @@ export class Column extends XYAxis {
layerEnter(host: XYAxis, element, duration: number = 250) {
super.layerEnter(host, element, duration);
+
+ // this._tabster = getTabster(element.node()?.ownerDocument?.defaultView) || createTabster(element.node()?.ownerDocument?.defaultView);
+ // if (this._tabster) {
+ // getGroupper(this._tabster);
+ // }
+
const context = this;
this
.tooltipHTML(function (d) {
@@ -84,6 +92,27 @@ export class Column extends XYAxis {
this.isHorizontal = isHorizontal;
const context = this;
+ // Configure Tabster Groupper on the host's parent HTML div (not SVG element)
+ // Tabster works with HTML DOM, not SVG, so we apply to host's _parentRelativeDiv
+ const hostWidget = host as any; // Cast to access protected _parentRelativeDiv
+ if (this.tabNavigation() && hostWidget._parentRelativeDiv) {
+ hostWidget._parentRelativeDiv
+ .attr("tabindex", "0")
+ .attr("data-tabster", JSON.stringify({
+ groupper: {
+ tabbability: 2
+ }
+ }))
+ .attr("role", "group")
+ .attr("aria-label", `${this.columns()[0] || "Chart"} data`);
+ } else if (hostWidget._parentRelativeDiv) {
+ hostWidget._parentRelativeDiv
+ .attr("tabindex", null)
+ .attr("data-tabster", null)
+ .attr("role", null)
+ .attr("aria-label", null);
+ }
+
this._palette = this._palette.switch(this.paletteID());
if (this.useClonedPalette()) {
this._palette = this._palette.cloneNotExists(this.paletteID() + "_" + this.id());
@@ -110,7 +139,7 @@ export class Column extends XYAxis {
const columnScale = d3ScaleBand()
.domain(context.layerColumns(host).filter(function (_d, idx) { return idx > 0; }))
.rangeRound(isHorizontal ? [0, dataLen] : [dataLen, 0])
- .paddingInner(this.xAxisSeriesPaddingInner())
+ .paddingInner(Math.max(this.xAxisSeriesPaddingInner(), 0.05))
.paddingOuter(0)
;
let domainSums = [];
@@ -177,6 +206,15 @@ export class Column extends XYAxis {
.on("dblclick", function (d: any) {
context.dblclick(host.rowToObj(d.origRow), d.column, host._selection.selected(this));
})
+ .on("keydown", function (evt, d: any) {
+ if (context.tabNavigation()) {
+ const event = d3Event();
+ if (event.code === "Space" || event.key === "Enter") {
+ event.preventDefault();
+ host._selection.click(this);
+ }
+ }
+ })
.style("opacity", 0)
.each(function (this: SVGElement, d: any) {
const element = d3Select(this);
@@ -193,266 +231,270 @@ export class Column extends XYAxis {
.style("opacity", 1)
;
const domainLength = host.yAxisStacked() ? dataLen : columnScale.bandwidth();
- columnGEnter.merge(columnGRect as any).each(function (this: SVGElement, d: any) {
- const element = d3Select(this);
- const domainPos = host.dataPos(dataRow[0]) + (host.yAxisStacked() ? 0 : columnScale(d.column)) + offset;
- const upperValue = d.value instanceof Array ? d.value[1] : d.value;
- let valueText = d.origRow[d.idx];
- if (context.showValue()) {
- const dm = context.dataMeta();
- switch (context.showValueAsPercent()) {
- case "series":
- const seriesSum = typeof dm.sum !== "undefined" ? dm.sum : seriesSums[d.idx];
- valueText = formatPct(valueText / seriesSum);
- break;
- case "domain":
- const domainSum = typeof dm.sum !== "undefined" ? dm.sum : domainSums[dataRowIdx];
- valueText = formatPct(valueText / domainSum);
- break;
- case null:
- default:
- valueText = d3Format(context.showValueFormat())(valueText);
- break;
- }
- }
- const upperValuePos = host.valuePos(upperValue);
- const lowerValuePos = host.valuePos(d.value instanceof Array ? d.value[0] : 0);
- const valuePos = Math.min(lowerValuePos, upperValuePos);
- const valueLength = Math.abs(upperValuePos - lowerValuePos);
-
- const innerTextHeight = context.innerTextFontSize();
- const innerTextPadding = context.innerTextPadding_exists() ? context.innerTextPadding() : innerTextHeight / 2.5;
-
- const dataRect = context.intersectRectRect(
- {
- x: isHorizontal ? domainPos : valuePos,
- y: isHorizontal ? valuePos : domainPos,
- width: isHorizontal ? domainLength : valueLength,
- height: isHorizontal ? valueLength : domainLength
- },
- {
- x: 0,
- y: 0,
- width: axisSize.width,
- height: axisSize.height
+ columnGEnter.merge(columnGRect as any)
+ .attr("tabindex", context.tabNavigation() ? 0 : undefined) // Tabster Groupper manages these inner focusables
+ .attr("role", context.tabNavigation() ? "button" : undefined) // ARIA role for accessibility
+ .attr("aria-label", context.tabNavigation() ? (d: any) => `${d.column}: ${d.value instanceof Array ? d.value[1] - d.value[0] : d.value}` : undefined) // ARIA label for screen readers
+ .each(function (this: SVGElement, d: any) {
+ const element = d3Select(this);
+ const domainPos = host.dataPos(dataRow[0]) + (host.yAxisStacked() ? 0 : columnScale(d.column)) + offset;
+ const upperValue = d.value instanceof Array ? d.value[1] : d.value;
+ let valueText = d.origRow[d.idx];
+ if (context.showValue()) {
+ const dm = context.dataMeta();
+ switch (context.showValueAsPercent()) {
+ case "series":
+ const seriesSum = typeof dm.sum !== "undefined" ? dm.sum : seriesSums[d.idx];
+ valueText = formatPct(valueText / seriesSum);
+ break;
+ case "domain":
+ const domainSum = typeof dm.sum !== "undefined" ? dm.sum : domainSums[dataRowIdx];
+ valueText = formatPct(valueText / domainSum);
+ break;
+ case null:
+ default:
+ valueText = d3Format(context.showValueFormat())(valueText);
+ break;
+ }
}
- );
-
- const _rects = element.select("rect").transition().duration(duration)
- .style("fill", (d: any) => context.fillColor(d.row, d.column, d.value, d.origRow))
- ;
-
- if (isHorizontal) {
- _rects
- .attr("x", domainPos)
- .attr("y", valuePos)
- .attr("width", domainLength)
- .attr("height", valueLength)
+ const upperValuePos = host.valuePos(upperValue);
+ const lowerValuePos = host.valuePos(d.value instanceof Array ? d.value[0] : 0);
+ const valuePos = Math.min(lowerValuePos, upperValuePos);
+ const valueLength = Math.abs(upperValuePos - lowerValuePos);
+
+ const innerTextHeight = context.innerTextFontSize();
+ const innerTextPadding = context.innerTextPadding_exists() ? context.innerTextPadding() : innerTextHeight / 2.5;
+
+ const dataRect = context.intersectRectRect(
+ {
+ x: isHorizontal ? domainPos : valuePos,
+ y: isHorizontal ? valuePos : domainPos,
+ width: isHorizontal ? domainLength : valueLength,
+ height: isHorizontal ? valueLength : domainLength
+ },
+ {
+ x: 0,
+ y: 0,
+ width: axisSize.width,
+ height: axisSize.height
+ }
+ );
+
+ const _rects = element.select("rect").transition().duration(duration)
+ .style("fill", (d: any) => context.fillColor(d.row, d.column, d.value, d.origRow))
;
- } else {
- _rects
- .attr("y", domainPos)
- .attr("x", valuePos)
- .attr("height", domainLength)
- .attr("width", valueLength)
+
+ if (isHorizontal) {
+ _rects
+ .attr("x", domainPos)
+ .attr("y", valuePos)
+ .attr("width", domainLength)
+ .attr("height", valueLength)
+ ;
+ } else {
+ _rects
+ .attr("y", domainPos)
+ .attr("x", valuePos)
+ .attr("height", domainLength)
+ .attr("width", valueLength)
+ ;
+ }
+ const _texts = element.select("text").transition().duration(duration)
+ .style("font-size", innerTextHeight + "px")
+ .style("fill", (d: any) => context.textColor(d.row, d.column, d.value, d.origRow))
;
- }
- const _texts = element.select("text").transition().duration(duration)
- .style("font-size", innerTextHeight + "px")
- .style("fill", (d: any) => context.textColor(d.row, d.column, d.value, d.origRow))
- ;
- _texts.style("font-family", context.innerTextFontFamily_exists() ? context.innerTextFontFamily() : null);
+ _texts.style("font-family", context.innerTextFontFamily_exists() ? context.innerTextFontFamily() : null);
- const padding = context.innerTextPadding_exists() ? context.innerTextPadding() : 8;
+ const padding = context.innerTextPadding_exists() ? context.innerTextPadding() : 8;
- const textHeightOffset = innerTextHeight / 2.7;
+ const textHeightOffset = innerTextHeight / 2.7;
- if (isHorizontal) { // Column
- const y = dataRect.y + dataRect.height - innerTextPadding;
- _texts
- .attr("x", domainPos + (domainLength / 2))
- .attr("y", y + textHeightOffset)
- .attr("transform", `rotate(-90, ${domainPos + (domainLength / 2)}, ${y})`)
- ;
- } else { // Bar
- _texts
- .attr("x", dataRect.x + padding)
- .attr("y", domainPos + (domainLength / 2) + textHeightOffset)
- ;
- }
- _texts
- .attr("height", domainLength)
- .attr("width", valueLength)
- ;
- if (context.showInnerText()) {
+ if (isHorizontal) { // Column
+ const y = dataRect.y + dataRect.height - innerTextPadding;
+ _texts
+ .attr("x", domainPos + (domainLength / 2))
+ .attr("y", y + textHeightOffset)
+ .attr("transform", `rotate(-90, ${domainPos + (domainLength / 2)}, ${y})`)
+ ;
+ } else { // Bar
+ _texts
+ .attr("x", dataRect.x + padding)
+ .attr("y", domainPos + (domainLength / 2) + textHeightOffset)
+ ;
+ }
_texts
- .text((d: any) => {
- const innerText = context.innerText(d.origRow, d.origRow[columnLength], d.idx);
- if (innerText) {
- const clippedValueLength = isHorizontal ? dataRect.height : dataRect.width;
- const innerTextObj = context.calcInnerText(clippedValueLength, innerText, valueText);
- d.innerTextObj = innerTextObj;
-
- return innerTextObj.text;
- }
- return "";
- })
+ .attr("height", domainLength)
+ .attr("width", valueLength)
;
- }
- const dataText = element.selectAll(".dataText").data(context.showValue() ? [`${upperValue}`] : []);
- const dataTextEnter = dataText.enter().append("g")
- .attr("class", "dataText")
- .each(function (this: SVGElement, d) {
- context.textLocal.set(this, new Text().target(this).colorStroke_default("transparent"));
- });
- dataTextEnter.merge(dataText as any)
- .each(function (this: SVGElement) {
- const pos = { x: 0, y: 0 };
- const valueFontFamily = context.valueFontFamily();
- const valueFontSize = context.valueFontSize();
- const textSize = context.textSize(valueText, valueFontFamily, valueFontSize);
-
- const isPositive = parseFloat(valueText) >= 0;
-
- let valueAnchor = context.valueAnchor() ? context.valueAnchor() : isHorizontal ? "middle" : "start";
-
- const leftSpace = dataRect.x;
- const rightSpace = axisSize.width - (dataRect.x + dataRect.width);
- const topSpace = dataRect.y;
- const bottomSpace = axisSize.height - (dataRect.y + dataRect.height);
-
- let noRoomInside;
- let isOutside;
- let noRoomOnExpectedSide;
-
- if (d.innerTextObj) {
- const { padding, valueTextWidth } = d.innerTextObj;
- isOutside = false;
- if (isHorizontal) { // Column
- valueAnchor = "middle";
- pos.x = domainPos + (domainLength / 2);
-
- if (d.innerTextObj.category === 4) {
- isOutside = true;
- pos.y = valuePos - padding - (valueFontSize / 2);
- } else {
- pos.y = valuePos + padding + (valueFontSize / 2);
+ if (context.showInnerText()) {
+ _texts
+ .text((d: any) => {
+ const innerText = context.innerText(d.origRow, d.origRow[columnLength], d.idx);
+ if (innerText) {
+ const clippedValueLength = isHorizontal ? dataRect.height : dataRect.width;
+ const innerTextObj = context.calcInnerText(clippedValueLength, innerText, valueText);
+ d.innerTextObj = innerTextObj;
+
+ return innerTextObj.text;
}
- } else { // Bar
- valueAnchor = "start";
- if (d.innerTextObj.category === 4) {
- isOutside = true;
- pos.x = (valueLength + valuePos) + padding;
- } else {
- pos.x = (valueLength + valuePos) - valueTextWidth - padding;
+ return "";
+ })
+ ;
+ }
+ const dataText = element.selectAll(".dataText").data(context.showValue() ? [`${upperValue}`] : []);
+ const dataTextEnter = dataText.enter().append("g")
+ .attr("class", "dataText")
+ .each(function (this: SVGElement, d) {
+ context.textLocal.set(this, new Text().target(this).colorStroke_default("transparent"));
+ });
+ dataTextEnter.merge(dataText as any)
+ .each(function (this: SVGElement) {
+ const pos = { x: 0, y: 0 };
+ const valueFontFamily = context.valueFontFamily();
+ const valueFontSize = context.valueFontSize();
+ const textSize = context.textSize(valueText, valueFontFamily, valueFontSize);
+
+ const isPositive = parseFloat(valueText) >= 0;
+
+ let valueAnchor = context.valueAnchor() ? context.valueAnchor() : isHorizontal ? "middle" : "start";
+
+ const leftSpace = dataRect.x;
+ const rightSpace = axisSize.width - (dataRect.x + dataRect.width);
+ const topSpace = dataRect.y;
+ const bottomSpace = axisSize.height - (dataRect.y + dataRect.height);
+
+ let noRoomInside;
+ let isOutside;
+ let noRoomOnExpectedSide;
+
+ if (d.innerTextObj) {
+ const { padding, valueTextWidth } = d.innerTextObj;
+ isOutside = false;
+ if (isHorizontal) { // Column
+ valueAnchor = "middle";
+ pos.x = domainPos + (domainLength / 2);
+
+ if (d.innerTextObj.category === 4) {
+ isOutside = true;
+ pos.y = valuePos - padding - (valueFontSize / 2);
+ } else {
+ pos.y = valuePos + padding + (valueFontSize / 2);
+ }
+ } else { // Bar
+ valueAnchor = "start";
+ if (d.innerTextObj.category === 4) {
+ isOutside = true;
+ pos.x = (valueLength + valuePos) + padding;
+ } else {
+ pos.x = (valueLength + valuePos) - valueTextWidth - padding;
+ }
+ pos.y = domainPos + (domainLength / 2);
}
- pos.y = domainPos + (domainLength / 2);
- }
- } else {
- /*
- IF this.valueCentered() and NO ROOM INSIDE
- ...then ASSUME THERES ROOM OUTSIDE
- IF NO ROOM OUTSIDE ON EXPECTED SIDE
- ...then ASSUME THERES ROOM ON THE OPPOSITE SIDE
- */
- if (isHorizontal) { // Column
- noRoomInside = dataRect.height < textSize.height;
- isOutside = !context.valueCentered() || noRoomInside;
-
- pos.x = dataRect.x + (dataRect.width / 2);
-
- if (isOutside) {
- if (isPositive) {
- noRoomOnExpectedSide = topSpace < textSize.height + padding;
- if (noRoomOnExpectedSide) {
- if (!noRoomInside) {
- isOutside = false;
- pos.y = dataRect.y + (dataRect.height / 2);
+ } else {
+ /*
+ IF this.valueCentered() and NO ROOM INSIDE
+ ...then ASSUME THERES ROOM OUTSIDE
+ IF NO ROOM OUTSIDE ON EXPECTED SIDE
+ ...then ASSUME THERES ROOM ON THE OPPOSITE SIDE
+ */
+ if (isHorizontal) { // Column
+ noRoomInside = dataRect.height < textSize.height;
+ isOutside = !context.valueCentered() || noRoomInside;
+
+ pos.x = dataRect.x + (dataRect.width / 2);
+
+ if (isOutside) {
+ if (isPositive) {
+ noRoomOnExpectedSide = topSpace < textSize.height + padding;
+ if (noRoomOnExpectedSide) {
+ if (!noRoomInside) {
+ isOutside = false;
+ pos.y = dataRect.y + (dataRect.height / 2);
+ } else {
+ pos.y = dataRect.y + dataRect.height + textSize.height;
+ }
} else {
- pos.y = dataRect.y + dataRect.height + textSize.height;
+ pos.y = dataRect.y - (textSize.height / 2) - padding;
}
} else {
- pos.y = dataRect.y - (textSize.height / 2) - padding;
- }
- } else {
- noRoomOnExpectedSide = bottomSpace < textSize.height;
- if (noRoomOnExpectedSide) {
- if (!noRoomInside) {
- isOutside = false;
- pos.y = dataRect.y + (dataRect.height / 2);
+ noRoomOnExpectedSide = bottomSpace < textSize.height;
+ if (noRoomOnExpectedSide) {
+ if (!noRoomInside) {
+ isOutside = false;
+ pos.y = dataRect.y + (dataRect.height / 2);
+ } else {
+ pos.y = dataRect.y - (textSize.height / 2) - padding;
+ }
} else {
- pos.y = dataRect.y - (textSize.height / 2) - padding;
+ pos.y = dataRect.y + textSize.height + padding;
}
- } else {
- pos.y = dataRect.y + textSize.height + padding;
}
+ } else {
+ pos.y = dataRect.y + (dataRect.height / 2);
}
- } else {
+ } else { // Bar
+ noRoomInside = dataRect.width < textSize.width;
+ isOutside = !context.valueCentered() || noRoomInside;
+
pos.y = dataRect.y + (dataRect.height / 2);
- }
- } else { // Bar
- noRoomInside = dataRect.width < textSize.width;
- isOutside = !context.valueCentered() || noRoomInside;
-
- pos.y = dataRect.y + (dataRect.height / 2);
-
- if (isOutside) {
- if (isPositive) {
- noRoomOnExpectedSide = rightSpace < textSize.width + padding;
- if (noRoomOnExpectedSide) {
- if (context.showInnerText() || !noRoomInside) {
- isOutside = false;
- pos.x = dataRect.x + (dataRect.width / 2);
+
+ if (isOutside) {
+ if (isPositive) {
+ noRoomOnExpectedSide = rightSpace < textSize.width + padding;
+ if (noRoomOnExpectedSide) {
+ if (context.showInnerText() || !noRoomInside) {
+ isOutside = false;
+ pos.x = dataRect.x + (dataRect.width / 2);
+ } else {
+ pos.x = dataRect.x - (textSize.width - padding);
+ }
} else {
- pos.x = dataRect.x - (textSize.width - padding);
+ pos.x = dataRect.x + dataRect.width + (textSize.width / 2) + padding;
}
} else {
- pos.x = dataRect.x + dataRect.width + (textSize.width / 2) + padding;
- }
- } else {
- noRoomOnExpectedSide = leftSpace < textSize.width;
- if (noRoomOnExpectedSide) {
- if (context.showInnerText() || !noRoomInside) {
- isOutside = false;
- pos.x = dataRect.x + (dataRect.width / 2);
+ noRoomOnExpectedSide = leftSpace < textSize.width;
+ if (noRoomOnExpectedSide) {
+ if (context.showInnerText() || !noRoomInside) {
+ isOutside = false;
+ pos.x = dataRect.x + (dataRect.width / 2);
+ } else {
+ pos.x = dataRect.x + dataRect.width + (textSize.width - padding);
+ }
} else {
- pos.x = dataRect.x + dataRect.width + (textSize.width - padding);
+ pos.x = dataRect.x - (textSize.width - padding);
}
- } else {
- pos.x = dataRect.x - (textSize.width - padding);
}
+ } else {
+ pos.x = dataRect.x + (dataRect.width / 2);
}
- } else {
- pos.x = dataRect.x + (dataRect.width / 2);
}
}
- }
- const textColor = isOutside ? null : context.textColor(d.row, d.column, d.value, d.origRow);
-
- // Prevent overlapping labels on stacked columns
- const columns = context.columns();
- const hideValue = (context.yAxisStacked() && noRoomInside) ||
- (isOutside && context.yAxisStacked() && columns.indexOf(d.column) !== columns.length - 1);
- context.textLocal.get(this)
- .pos(pos)
- .anchor(valueAnchor)
- .fontFamily(valueFontFamily)
- .fontSize(valueFontSize)
- .text(`${valueText}`)
- .colorFill(textColor)
- .visible(context.showValue() && !hideValue)
- .render()
- ;
-
- });
- dataText.exit()
- .each(function (this: SVGElement, d) {
- context.textLocal.get(this).target(null);
- })
- .remove()
- ;
- });
+ const textColor = isOutside ? null : context.textColor(d.row, d.column, d.value, d.origRow);
+
+ // Prevent overlapping labels on stacked columns
+ const columns = context.columns();
+ const hideValue = (context.yAxisStacked() && noRoomInside) ||
+ (isOutside && context.yAxisStacked() && columns.indexOf(d.column) !== columns.length - 1);
+ context.textLocal.get(this)
+ .pos(pos)
+ .anchor(valueAnchor)
+ .fontFamily(valueFontFamily)
+ .fontSize(valueFontSize)
+ .text(`${valueText}`)
+ .colorFill(textColor)
+ .visible(context.showValue() && !hideValue)
+ .render()
+ ;
+
+ });
+ dataText.exit()
+ .each(function (this: SVGElement, d) {
+ context.textLocal.get(this).target(null);
+ })
+ .remove()
+ ;
+ });
columnGRect.exit().transition().duration(duration)
.style("opacity", 0)
.remove()
@@ -517,7 +559,6 @@ export class Column extends XYAxis {
}
calcInnerText(offset, innerText, valueText) {
-
const fontFamily = this.innerTextFontFamily_exists() ? this.innerTextFontFamily() : "Verdana";
const fontSize = this.innerTextFontSize();
const valueFontFamily = this.valueFontFamily_exists() ? this.valueFontFamily() : "Verdana";
@@ -627,6 +668,10 @@ export interface Column {
tooltipFormat(_): string;
tooltipStyle(): "default" | "none" | "series-table";
tooltipStyle(_: "default" | "none" | "series-table"): this;
+
+ // Tab Navigation
+ tabNavigation(): boolean;
+ tabNavigation(_: boolean): this;
}
Column.prototype.publish("valueFontFamily", null, "string", "Font family of value text", null, { optional: true });
@@ -644,8 +689,9 @@ Column.prototype.publish("showValueAsPercentFormat", ".0%", "string", "D3 Format
Column.prototype.publish("showDomainTotal", false, "boolean", "Show Total Value for Stacked Columns", null);
Column.prototype.publish("valueCentered", false, "boolean", "Show Value in center of column");
Column.prototype.publish("valueAnchor", "middle", "set", "text-anchor for shown value text", ["start", "middle", "end"]);
-Column.prototype.publish("xAxisSeriesPaddingInner", 0, "number", "Determines the ratio of the range that is reserved for blank space between band (0->1)");
+Column.prototype.publish("xAxisSeriesPaddingInner", 0.0, "number", "Determines the ratio of the range that is reserved for blank space between band (0->1)");
Column.prototype.publish("tooltipInnerTextEllipsedOnly", false, "boolean", "Show tooltip only when inner text is truncated with an ellipsis");
+Column.prototype.publish("tabNavigation", false, "boolean", "Enable or disable tab navigation");
/*
const origUseClonedPalette = Column.prototype.useClonedPalette;
diff --git a/packages/chart/src/Pie.css b/packages/chart/src/Pie.css
index 3be1ede838..1a2caec935 100644
--- a/packages/chart/src/Pie.css
+++ b/packages/chart/src/Pie.css
@@ -7,13 +7,16 @@
}
.chart_Pie .arc path {
- stroke: white;
- stroke-width: 0.75px;
+ stroke: transparent;
+ stroke-width: 2px;
}
.chart_Pie .arc.selected path {
- stroke: red;
- stroke-width: 1.5px;
+ stroke: #dc3545 !important;
+ stroke-width: 3px !important;
+ paint-order: fill stroke !important;
+ filter: drop-shadow(0 0 3px rgba(220, 53, 69, 0.4));
+ transition: all 0.2s ease;
}
.chart_Pie polyline {
@@ -21,4 +24,40 @@
stroke: black;
stroke-width: 2px;
fill: none;
-}
\ No newline at end of file
+}
+
+.chart_Pie .arc:hover path {
+ stroke: rgba(108, 117, 125, 0.6);
+ stroke-width: 2px;
+ filter: brightness(1.05);
+}
+
+.chart_Pie .arc:focus path {
+ stroke: #007bff !important;
+ stroke-width: 3px !important;
+ paint-order: fill stroke !important;
+ filter: drop-shadow(0 0 4px rgba(0, 123, 255, 0.4));
+ transition: all 0.2s ease;
+}
+
+.chart_Pie .arc.selected:focus path {
+ stroke: #6f42c1 !important;
+ filter: drop-shadow(0 0 5px rgba(111, 66, 193, 0.5));
+}
+
+.chart_Pie .arc:focus-visible {
+ outline: none;
+}
+
+.chart_Pie .arc[data-tabster-focusable] {
+ outline: none !important;
+}
+
+.chart_Pie .arc:focus {
+ outline: none !important;
+}
+
+.chart_Pie .arc:active {
+ outline: none !important;
+}
+
diff --git a/packages/chart/src/Pie.ts b/packages/chart/src/Pie.ts
index 65bb825613..ea2ba1a92f 100644
--- a/packages/chart/src/Pie.ts
+++ b/packages/chart/src/Pie.ts
@@ -1,10 +1,11 @@
import { I2DChart, ITooltip } from "@hpcc-js/api";
-import { InputField, SVGWidget, Utility } from "@hpcc-js/common";
+import { d3Event, InputField, SVGWidget, Utility } from "@hpcc-js/common";
import { degreesToRadians, normalizeRadians } from "@hpcc-js/util";
import { format as d3Format } from "d3-format";
import { interpolate as d3Interpolate } from "d3-interpolate";
import { select as d3Select } from "d3-selection";
import { arc as d3Arc, pie as d3Pie } from "d3-shape";
+// import { createTabster, getGroupper, getTabster, getTabsterAttribute, Types, GroupperTabbabilities, setTabsterAttribute } from "tabster";
import "../src/Pie.css";
@@ -33,6 +34,9 @@ export class Pie extends SVGWidget {
private _maxLabelBottom = 0;
private _seriesValueFormatter;
private _seriesPercentageFormatter;
+ // protected _tabster: Types.TabsterCore | null = null;
+ // protected _groupper: Types.GroupperAPI | null = null;
+
constructor() {
super();
I2DChart.call(this);
@@ -140,12 +144,23 @@ export class Pie extends SVGWidget {
_slices;
_labels;
- enter(_domNode, element) {
- super.enter(_domNode, element);
- this._selection.widgetElement(element);
+
+ enter(domNode, element) {
+ super.enter(domNode, element);
+ this._selection
+ .widgetElement(element)
+ .skipBringToTop(true)
+ ;
+
this._slices = element.append("g");
this._labels = element.append("g");
+
+ // Initialize Tabster and Groupper
+ // this._tabster = getTabster(domNode.ownerDocument?.defaultView) || createTabster(domNode.ownerDocument?.defaultView);
+ // this._groupper = getGroupper(this._tabster);
+
const context = this;
+
this
.tooltipHTML(function (d) {
switch (context.tooltipStyle()) {
@@ -170,6 +185,11 @@ export class Pie extends SVGWidget {
update(_domNode, element) {
super.update(_domNode, element);
const context = this;
+ // element
+ // .attr("tabindex", this.tabNavigation() ? "0" : null)
+ // ;
+ // setTabsterAttribute(this._parentRelativeDiv.node(), this.tabNavigation() ? { groupper: { tabbability: 2 }, focusable: {} } : {}, true);
+
this.updateD3Pie();
this._palette = this._palette.switch(this.paletteID());
this._seriesValueFormatter = d3Format(this.seriesValueFormat() as string);
@@ -179,13 +199,19 @@ export class Pie extends SVGWidget {
}
this._smallValueLabelHeight = this.calcSmallValueLabelHeight();
this._totalValue = this.calcTotalValue();
- const innerRadius = this.calcInnerRadius();
const outerRadius = this.calcOuterRadius();
+ const innerRadius = Math.max(this.calcInnerRadius(), Math.min(outerRadius / 30, 6));
const labelRadius = outerRadius + 12;
+ const paddingValue = this.slicePadding();
+
+ // Use a small pad angle for visual separation
+ const padAngleRadians = paddingValue > 0 ? Math.min(paddingValue, 0.05) : 0;
+
this.d3Arc
.innerRadius(innerRadius)
.padRadius(outerRadius)
.outerRadius(outerRadius)
+ .padAngle(padAngleRadians)
;
this._quadIdxArr = [[], [], [], []];
@@ -213,6 +239,13 @@ export class Pie extends SVGWidget {
.on("dblclick", function (d) {
context.dblclick(context.rowToObj(d.data), context.columns()[1], context._selection.selected(this));
})
+ .on("keydown", function (evt, d) {
+ const event = d3Event();
+ if (context.tabNavigation() && (event.code === "Space" || event.key === "Enter")) {
+ event.preventDefault();
+ context._selection.click(this);
+ }
+ })
.each(function (d, i) {
d3Select(this).append("path")
.on("mouseout.tooltip", context.tooltip.hide)
@@ -223,6 +256,9 @@ export class Pie extends SVGWidget {
})
.merge(arc).transition()
.attr("opacity", 1)
+ .attr("tabindex", context.tabNavigation() ? "0" : undefined) // Tabster Groupper manages these inner focusables
+ .attr("role", context.tabNavigation() ? "button" : undefined) // ARIA role for accessibility
+ .attr("aria-label", context.tabNavigation() ? (d: any) => `${d.data[0]}: ${d.data[1]}` : undefined) // ARIA label for screen readers
.each(function (d, i) {
const quad = context.getQuadrant(midAngle(d));
context._quadIdxArr[quad].push(i);
@@ -429,7 +465,7 @@ export class Pie extends SVGWidget {
}
this.d3Pie
- .padAngle(0.0025)
+ .padAngle(0) // No geometric padding - CSS-only solution
.startAngle(startAngle)
.endAngle(2 * Math.PI + startAngle)
.value(function (d) {
@@ -456,6 +492,8 @@ export interface Pie {
startAngle(_: number): this;
labelHeight(): number;
labelHeight(_: number): this;
+ slicePadding(): number;
+ slicePadding(_: number): this;
seriesPercentageFormat(): string;
seriesPercentageFormat(_: string): this;
showLabels(): boolean;
@@ -463,10 +501,14 @@ export interface Pie {
sortDataByValue(): "none" | "ascending" | "descending";
sortDataByValue(_: "none" | "ascending" | "descending"): this;
- paletteID(_?: string): string | Pie;
- useClonedPalette(_?: boolean): boolean | Pie;
- outerText(_?: boolean): boolean | Pie;
+ paletteID(): string;
+ paletteID(_: string): this;
+ useClonedPalette(): boolean;
+ useClonedPalette(_: boolean): this;
+ outerText(): boolean;
+ outerText(_: boolean): this;
innerRadius(): number;
+ innerRadius(_: number): this;
innerRadius_exists(): boolean;
// I2DChart
@@ -492,6 +534,10 @@ export interface Pie {
// SimpleSelectionMixin
_selection: Utility.SimpleSelection;
+
+ // Tab Navigation
+ tabNavigation(): boolean;
+ tabNavigation(_: boolean): this;
}
Pie.prototype.publish("showLabels", true, "boolean", "If true, wedge labels will display");
Pie.prototype.publish("showSeriesValue", false, "boolean", "Append data series value next to label", null, { disable: w => !w.showLabels() });
@@ -504,4 +550,6 @@ Pie.prototype.publish("innerRadius", 0, "number", "Sets inner pie hole radius as
Pie.prototype.publish("minOuterRadius", 20, "number", "Minimum outer radius (pixels)");
Pie.prototype.publish("startAngle", 0, "number", "Starting angle of the first (and largest) wedge (degrees)");
Pie.prototype.publish("labelHeight", 12, "number", "Font size of labels (pixels)", null, { disable: w => !w.showLabels() });
+Pie.prototype.publish("slicePadding", 0.01, "number", "Padding between pie slices (converted to pixels)", null, { tags: ["Basic"], range: { min: 0, step: 0.01, max: 0.2 } });
Pie.prototype.publish("sortDataByValue", "descending", "set", "Sort data by value", ["none", "ascending", "descending"]);
+Pie.prototype.publish("tabNavigation", false, "boolean", "Enable or disable tab navigation");
diff --git a/packages/dataflow/package.json b/packages/dataflow/package.json
index bdbea31113..f30d156ba3 100644
--- a/packages/dataflow/package.json
+++ b/packages/dataflow/package.json
@@ -53,5 +53,5 @@
"bugs": {
"url": "https://github.com/hpcc-systems/Visualization/issues"
},
- "homepage": "https://github.com/hpcc-systems/Visualization/tree/trunk/packages/dataflow"
+ "homepage": "https://github.com/hpcc-systems/Visualization/tree/main/packages/dataflow"
}
\ No newline at end of file
diff --git a/packages/markdown-it-plugins/package.json b/packages/markdown-it-plugins/package.json
index 23819950f7..2c34e4a906 100644
--- a/packages/markdown-it-plugins/package.json
+++ b/packages/markdown-it-plugins/package.json
@@ -79,5 +79,5 @@
"bugs": {
"url": "https://github.com/hpcc-systems/Visualization/issues"
},
- "homepage": "https://github.com/hpcc-systems/Visualization/tree/trunk/packages/markdonw-it-plugins"
+ "homepage": "https://github.com/hpcc-systems/Visualization/tree/main/packages/markdonw-it-plugins"
}
\ No newline at end of file
diff --git a/packages/observablehq-compiler/bin/ojscc.mjs b/packages/observablehq-compiler/bin/ojscc.mjs
index 5df998c436..83fbe5c7e2 100755
--- a/packages/observablehq-compiler/bin/ojscc.mjs
+++ b/packages/observablehq-compiler/bin/ojscc.mjs
@@ -64,5 +64,5 @@ yargs
)
.help("h")
.alias("h", "help")
- .epilog("https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observablehq-compiler")
+ .epilog("https://github.com/hpcc-systems/Visualization/tree/main/packages/observablehq-compiler")
.parse();
diff --git a/packages/observablehq-compiler/package.json b/packages/observablehq-compiler/package.json
index 7f3efd2b72..05e6baede5 100644
--- a/packages/observablehq-compiler/package.json
+++ b/packages/observablehq-compiler/package.json
@@ -85,5 +85,5 @@
"bugs": {
"url": "https://github.com/hpcc-systems/Visualization/issues"
},
- "homepage": "https://github.com/hpcc-systems/Visualization/tree/trunk/packages/observablehq-compiler"
+ "homepage": "https://github.com/hpcc-systems/Visualization/tree/main/packages/observablehq-compiler"
}
\ No newline at end of file
diff --git a/packages/util/package.json b/packages/util/package.json
index 747479e8ed..fc0ee05871 100644
--- a/packages/util/package.json
+++ b/packages/util/package.json
@@ -53,5 +53,5 @@
"bugs": {
"url": "https://github.com/hpcc-systems/Visualization/issues"
},
- "homepage": "https://github.com/hpcc-systems/Visualization/tree/trunk/packages/util"
+ "homepage": "https://github.com/hpcc-systems/Visualization/tree/main/packages/util"
}
\ No newline at end of file