UNPKG

@antv/g2

Version:

the Grammar of Graphics in Javascript

304 lines 12.6 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.LegendFilter = exports.attributesOf = exports.dataOf = exports.legendClearSetState = exports.legendsContinuousOf = exports.legendsOf = exports.itemsOf = exports.labelOf = exports.markerOf = exports.LEGEND_LABEL_CLASS_NAME = exports.LEGEND_MAKER_CLASS_NAME = exports.LEGEND_ITEMS_CLASS_NAME = exports.CONTINUOUS_LEGEND_CLASS_NAME = exports.CATEGORY_LEGEND_CLASS_NAME = void 0; const util_1 = require("@antv/util"); const helper_1 = require("../utils/helper"); const utils_1 = require("./utils"); exports.CATEGORY_LEGEND_CLASS_NAME = 'legend-category'; exports.CONTINUOUS_LEGEND_CLASS_NAME = 'legend-continuous'; exports.LEGEND_ITEMS_CLASS_NAME = 'items-item'; exports.LEGEND_MAKER_CLASS_NAME = 'legend-category-item-marker'; exports.LEGEND_LABEL_CLASS_NAME = 'legend-category-item-label'; function markerOf(item) { return item.getElementsByClassName(exports.LEGEND_MAKER_CLASS_NAME)[0]; } exports.markerOf = markerOf; function labelOf(item) { return item.getElementsByClassName(exports.LEGEND_LABEL_CLASS_NAME)[0]; } exports.labelOf = labelOf; function itemsOf(root) { return root.getElementsByClassName(exports.LEGEND_ITEMS_CLASS_NAME); } exports.itemsOf = itemsOf; function legendsOf(root) { return root.getElementsByClassName(exports.CATEGORY_LEGEND_CLASS_NAME); } exports.legendsOf = legendsOf; function legendsContinuousOf(root) { return root.getElementsByClassName(exports.CONTINUOUS_LEGEND_CLASS_NAME); } exports.legendsContinuousOf = legendsContinuousOf; function legendClearSetState(root, setState) { const legends = [...legendsOf(root), ...legendsContinuousOf(root)]; legends.forEach((legend) => { setState(legend, (v) => v); }); } exports.legendClearSetState = legendClearSetState; function dataOf(root) { // legend -> layout -> container let parent = root.parentNode; while (parent && !parent.__data__) { parent = parent.parentNode; } return parent.__data__; } exports.dataOf = dataOf; function attributesOf(root) { let child = root; while (child && !child.attr('class').startsWith('legend')) { child = child.children[0]; } return child.attributes; } exports.attributesOf = attributesOf; function legendFilterOrdinal(root, { legends, // given the root of chart returns legends to be manipulated marker: markerOf, // given the legend returns the marker label: labelOf, // given the legend returns the label datum, // given the legend returns the value filter, // invoke when dispatch filter event, emitter, channel, state = {}, // state options }) { // Index handler by item. const itemClick = new Map(); const itemPointerenter = new Map(); const itemPointerout = new Map(); const { unselected = { markerStroke: '#aaa', markerFill: '#aaa', labelFill: '#aaa', }, } = state; const markerStyle = { unselected: (0, helper_1.subObject)(unselected, 'marker') }; const labelStyle = { unselected: (0, helper_1.subObject)(unselected, 'label') }; const { setState: setM, removeState: removeM } = (0, utils_1.useState)(markerStyle, undefined); const { setState: setL, removeState: removeL } = (0, utils_1.useState)(labelStyle, undefined); const items = Array.from(legends(root)); let selectedValues = items.map(datum); const updateLegendState = () => { for (const item of items) { const value = datum(item); const marker = markerOf(item); const label = labelOf(item); if (!selectedValues.includes(value)) { setM(marker, 'unselected'); setL(label, 'unselected'); } else { removeM(marker, 'unselected'); removeL(label, 'unselected'); } } }; for (const item of items) { // Defined handlers. const pointerenter = () => { (0, utils_1.setCursor)(root, 'pointer'); }; const pointerout = () => { (0, utils_1.restoreCursor)(root); }; const click = (event) => __awaiter(this, void 0, void 0, function* () { const value = datum(item); const index = selectedValues.indexOf(value); if (index === -1) selectedValues.push(value); else selectedValues.splice(index, 1); if (selectedValues.length === 0) selectedValues.push(...items.map(datum)); yield filter(selectedValues); updateLegendState(); const { nativeEvent = true } = event; if (!nativeEvent) return; if (selectedValues.length === items.length) { emitter.emit('legend:reset', { nativeEvent }); } else { // Emit events. emitter.emit('legend:filter', Object.assign(Object.assign({}, event), { nativeEvent, data: { channel, values: selectedValues, } })); } }); // Bind and store handlers. item.addEventListener('click', click); item.addEventListener('pointerenter', pointerenter); item.addEventListener('pointerout', pointerout); itemClick.set(item, click); itemPointerenter.set(item, pointerenter); itemPointerout.set(item, pointerout); } const onFilter = (event) => __awaiter(this, void 0, void 0, function* () { const { nativeEvent } = event; if (nativeEvent) return; const { data } = event; const { channel: specifiedChannel, values } = data; if (specifiedChannel !== channel) return; selectedValues = values; yield filter(selectedValues); updateLegendState(); }); const onEnd = (event) => __awaiter(this, void 0, void 0, function* () { const { nativeEvent } = event; if (nativeEvent) return; selectedValues = items.map(datum); yield filter(selectedValues); updateLegendState(); }); emitter.on('legend:filter', onFilter); emitter.on('legend:reset', onEnd); return () => { for (const item of items) { item.removeEventListener('click', itemClick.get(item)); item.removeEventListener('pointerenter', itemPointerenter.get(item)); item.removeEventListener('pointerout', itemPointerout.get(item)); emitter.off('legend:filter', onFilter); emitter.off('legend:reset', onEnd); } }; } function legendFilterContinuous(_, { legend, filter, emitter, channel }) { const onValueChange = ({ detail: { value } }) => { filter(value); emitter.emit({ nativeEvent: true, data: { channel, values: value, }, }); }; legend.addEventListener('valuechange', onValueChange); return () => { legend.removeEventListener('valuechange', onValueChange); }; } function filterView(context, // View instance, { legend, // Legend instance. channel, // Filter Channel. value, // Filtered Values. ordinal, // Data type of the legend. channels, // Channels for this legend. allChannels, // Channels for all legends. facet = false, // For facet. }) { return __awaiter(this, void 0, void 0, function* () { const { view, update, setState } = context; setState(legend, (viewOptions) => { const { marks } = viewOptions; // Add filter transform for every marks, // which will skip for mark without color channel. const newMarks = marks.map((mark) => { if (mark.type === 'legends') return mark; // Inset after aggregate transform, such as group, and bin. const { transform = [] } = mark; const index = transform.findIndex(({ type }) => type.startsWith('group') || type.startsWith('bin')); const newTransform = [...transform]; newTransform.splice(index + 1, 0, { type: 'filter', [channel]: { value, ordinal }, }); // Set domain of scale to preserve encoding. const newScale = Object.fromEntries(channels.map((channel) => [ channel, { domain: view.scale[channel].getOptions().domain }, ])); return (0, util_1.deepMix)({}, mark, Object.assign(Object.assign({ transform: newTransform, scale: newScale }, (!ordinal && { animate: false })), { legend: facet ? false : Object.fromEntries(allChannels.map((d) => [d, { preserve: true }])) })); }); return Object.assign(Object.assign({}, viewOptions), { marks: newMarks }); }); yield update(); }); } function filterFacets(facets, options) { for (const facet of facets) { filterView(facet, Object.assign(Object.assign({}, options), { facet: true })); } } function LegendFilter() { return (context, contexts, emitter) => { const { container } = context; const facets = contexts.filter((d) => d !== context); const isFacet = facets.length > 0; const channelsOf = (legend) => { return dataOf(legend).scales.map((d) => d.name); }; const legends = [ ...legendsOf(container), ...legendsContinuousOf(container), ]; const allChannels = legends.flatMap(channelsOf); const filter = isFacet ? (0, util_1.throttle)(filterFacets, 50, { trailing: true }) : (0, util_1.throttle)(filterView, 50, { trailing: true }); const removes = legends.map((legend) => { const { name: channel, domain } = dataOf(legend).scales[0]; const channels = channelsOf(legend); const common = { legend, channel, channels, allChannels, }; if (legend.className === exports.CATEGORY_LEGEND_CLASS_NAME) { return legendFilterOrdinal(container, { legends: itemsOf, marker: markerOf, label: labelOf, datum: (d) => { const { __data__: datum } = d; const { index } = datum; return domain[index]; }, filter: (value) => { const options = Object.assign(Object.assign({}, common), { value, ordinal: true }); if (isFacet) filter(facets, options); else filter(context, options); }, state: legend.attributes.state, channel, emitter, }); } else { return legendFilterContinuous(container, { legend, filter: (value) => { const options = Object.assign(Object.assign({}, common), { value, ordinal: false }); if (isFacet) filter(facets, options); else filter(context, options); }, emitter, channel, }); } }); return () => { removes.forEach((remove) => remove()); }; }; } exports.LegendFilter = LegendFilter; //# sourceMappingURL=legendFilter.js.map