import {select} from 'd3-selection';
import {pluck, utils} from '../core/utils';
import {d3compat} from '../core/config';
import {constants} from '../core/constants';
/**
* htmlLegend is a attachable widget that can be added to other dc charts to render horizontal/vertical legend
* labels.
* @example
* chart.legend(HtmlLegend().container(legendContainerElement).horizontal(false))
* @returns {HtmlLegend}
*/
export class HtmlLegend {
constructor () {
this._htmlLegendDivCssClass = 'dc-html-legend';
this._legendItemCssClassHorizontal = 'dc-legend-item-horizontal';
this._legendItemCssClassVertical = 'dc-legend-item-vertical';
this._parent = undefined;
this._container = undefined;
this._legendText = pluck('name');
this._maxItems = undefined;
this._horizontal = false;
this._legendItemClass = undefined;
this._highlightSelected = false;
this._keyboardAccessible = false;
}
parent (p) {
if (!arguments.length) {
return this._parent;
}
this._parent = p;
return this;
}
render () {
this._defaultLegendItemCssClass = this._horizontal ? this._legendItemCssClassHorizontal : this._legendItemCssClassVertical;
this._container.select(`div.${this._htmlLegendDivCssClass}`).remove();
const container = this._container.append('div').attr('class', this._htmlLegendDivCssClass);
container.attr('style', `max-width:${this._container.nodes()[0].style.width}`);
let legendables = this._parent.legendables();
const filters = this._parent.filters();
if (this._maxItems !== undefined) {
legendables = legendables.slice(0, this._maxItems);
}
const legendItemClassName = this._legendItemClass ? this._legendItemClass : this._defaultLegendItemCssClass;
const itemEnter = container.selectAll(`div.${legendItemClassName}`)
.data(legendables).enter()
.append('div')
.classed(legendItemClassName, true)
.on('mouseover', d3compat.eventHandler(d => this._parent.legendHighlight(d)))
.on('mouseout', d3compat.eventHandler(d => this._parent.legendReset(d)))
.on('click', d3compat.eventHandler(d => this._parent.legendToggle(d)));
if (this._highlightSelected) {
itemEnter.classed(constants.SELECTED_CLASS, d => filters.indexOf(d.name) !== -1);
}
itemEnter.append('span')
.attr('class', 'dc-legend-item-color')
.style('background-color', pluck('color'));
itemEnter.append('span')
.attr('class', 'dc-legend-item-label')
.classed('dc-tabbable', this._keyboardAccessible)
.attr('title', this._legendText)
.text(this._legendText);
if (this._keyboardAccessible) {
this._makeLegendKeyboardAccessible();
}
}
/**
* Set the container selector for the legend widget. Required.
* @param {String} [container]
* @return {String|HtmlLegend}
*/
container (container) {
if (!arguments.length) {
return this._container;
}
this._container = select(container);
return this;
}
/**
* This can be optionally used to override class for legenditem and just use this class style.
* This is helpful for overriding the style of a particular chart rather than overriding
* the style for all charts.
*
* Setting this will disable the highlighting of selected items also.
* @param {String} [legendItemClass]
* @return {String|HtmlLegend}
*/
legendItemClass (legendItemClass) {
if (!arguments.length) {
return this._legendItemClass;
}
this._legendItemClass = legendItemClass;
return this;
}
/**
* This can be optionally used to enable highlighting legends for the selections/filters for the
* chart.
* @param {String} [highlightSelected]
* @return {String|HtmlLegend}
*/
highlightSelected (highlightSelected) {
if (!arguments.length) {
return this._highlightSelected;
}
this._highlightSelected = highlightSelected;
return this;
}
/**
* Display the legend horizontally instead of vertically
* @param {String} [horizontal]
* @return {String|HtmlLegend}
*/
horizontal (horizontal) {
if (!arguments.length) {
return this._horizontal;
}
this._horizontal = horizontal;
return this;
}
/**
* Set or get the legend text function. The legend widget uses this function to render the legend
* text for each item. If no function is specified the legend widget will display the names
* associated with each group.
* @param {Function} [legendText]
* @returns {Function|HtmlLegend}
* @example
* // default legendText
* legend.legendText(pluck('name'))
*
* // create numbered legend items
* chart.legend(new HtmlLegend().legendText(function(d, i) { return i + '. ' + d.name; }))
*
* // create legend displaying group counts
* chart.legend(new HtmlLegend().legendText(function(d) { return d.name + ': ' d.data; }))
*/
legendText (legendText) {
if (!arguments.length) {
return this._legendText;
}
this._legendText = legendText;
return this;
}
/**
* Maximum number of legend items to display
* @param {Number} [maxItems]
* @return {HtmlLegend}
*/
maxItems (maxItems) {
if (!arguments.length) {
return this._maxItems;
}
this._maxItems = utils.isNumber(maxItems) ? maxItems : undefined;
return this;
}
/**
* If set, individual legend items will be focusable from keyboard and on pressing Enter or Space
* will behave as if clicked on.
*
* If `svgDescription` on the parent chart has not been explicitly set, will also set the default
* SVG description text to the class constructor name, like BarChart or HeatMap, and make the entire
* SVG focusable.
* @param {Boolean} [keyboardAccessible=false]
* @returns {Boolean|HtmlLegend}
*/
keyboardAccessible (keyboardAccessible) {
if (!arguments.length) {
return this._keyboardAccessible;
}
this._keyboardAccessible = keyboardAccessible;
return this;
}
_makeLegendKeyboardAccessible () {
if (!this._parent._svgDescription) {
this._parent.svg().append('desc')
.attr('id', `desc-id-${this._parent.__dcFlag__}`)
.html(`${this._parent.svgDescription()}`);
this._parent.svg()
.attr('tabindex', '0')
.attr('role', 'img')
.attr('aria-labelledby', `desc-id-${this._parent.__dcFlag__}`);
}
const tabElements = this.container()
.selectAll('.dc-legend-item-label.dc-tabbable')
.attr('tabindex', 0);
tabElements
.on('keydown', d3compat.eventHandler((d, event) => {
// trigger only if d is an object
if (event.keyCode === 13 && typeof d === 'object') {
d.chart.legendToggle(d)
}
// special case for space key press - prevent scrolling
if (event.keyCode === 32 && typeof d === 'object') {
d.chart.legendToggle(d)
event.preventDefault();
}
}))
.on('focus', d3compat.eventHandler(d => {
this._parent.legendHighlight(d);
}))
.on('blur', d3compat.eventHandler(d => {
this._parent.legendReset(d);
}));
}
}
export const htmlLegend = () => new HtmlLegend();