@antv/g2
Version:
the Grammar of Graphics in Javascript
213 lines • 10.3 kB
JavaScript
;
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.DrillDown = DrillDown;
const util_1 = require("@antv/util");
const selection_1 = require("../utils/selection");
const runtime_1 = require("../runtime");
const partition_1 = require("../mark/partition");
// Get partition element.
const getElementsPartition = (plot) => {
return plot.querySelectorAll('.element').filter((item) => {
const itemStyle = item.style || {};
return itemStyle[partition_1.PARTITION_TYPE_FIELD] === partition_1.PARTITION_TYPE;
});
};
function selectPlotArea(root) {
return (0, selection_1.select)(root).select(`.${runtime_1.PLOT_CLASS_NAME}`).node();
}
// Default breadCrumb config.
const DEFAULT_BREADCRUMB = {
rootText: 'root',
style: {
fill: 'rgba(0, 0, 0, 0.6)',
fontSize: 11,
},
y: 4,
active: {
fill: 'rgba(0, 0, 0, 0.4)',
},
};
/**
* DrillDown interaction for partition visualization.
* Based on the proven sunburst drilldown implementation.
*/
function DrillDown(drillDownOptions = {}) {
const { breadCrumb: textConfig = {} } = drillDownOptions;
const breadCrumb = (0, util_1.deepMix)({}, DEFAULT_BREADCRUMB, textConfig);
return (context) => {
const { update, setState, container, view, options: viewOptions } = context;
const document = container.ownerDocument;
const plotArea = selectPlotArea(container);
const partitionMark = viewOptions.marks.find(({ id }) => id === partition_1.PARTITION_TYPE);
if (!partitionMark)
return;
const { state } = partitionMark;
// Create breadCrumbTextsGroup to save textSeparator and drillTexts.
const textGroup = document.createElement('g');
textGroup.style.transform = `translate(${breadCrumb.x || 0}px, ${breadCrumb.y || 0}px)`;
textGroup.setAttribute('x', 'drilldown-breadcrumb');
plotArea.appendChild(textGroup);
// Modify data and scale according to the path and level of current click to achieve drilling down, drilling up and initialization effects.
const drillDownClick = (path) => __awaiter(this, void 0, void 0, function* () {
// Clear text.
textGroup.removeChildren();
// More path creation text.
if (path && path.length > 0) {
// Create root text.
const rootText = document.createElement('text', {
style: Object.assign({ text: breadCrumb.rootText }, breadCrumb.style),
});
textGroup.appendChild(rootText);
let name = '';
const pathArray = path;
let y = breadCrumb.style.y;
let x = rootText.getBBox().width;
const maxWidth = plotArea.getBBox().width;
// Create path: 'type1 / type2 / type3' -> '/ type1 / type2 / type3'.
const drillTexts = pathArray.map((text, index) => {
const textSeparator = document.createElement('text', {
style: Object.assign(Object.assign({ x, text: ' / ' }, breadCrumb.style), { y }),
});
textGroup.appendChild(textSeparator);
x += textSeparator.getBBox().width;
name = `${name}${text} / `;
const drillText = document.createElement('text', {
name: name.replace(/\s\/\s$/, ''),
style: Object.assign(Object.assign({ text,
x }, breadCrumb.style), { y }),
});
textGroup.appendChild(drillText);
x += drillText.getBBox().width;
/**
* Page width exceeds maximum, line feed.
* | ----maxWidth---- |
* | / type1 / type2 / type3 |
* ->
* | ----maxWidth---- |
* | / type1 / type2 |
* | / type3 |
*/
if (x > maxWidth) {
y = textGroup.getBBox().height;
x = 0;
textSeparator.attr({
x,
y,
});
x += textSeparator.getBBox().width;
drillText.attr({
x,
y,
});
x += drillText.getBBox().width;
}
return drillText;
});
// Add active state and drilldown interaction.
const textStack = [rootText, ...drillTexts];
textStack.forEach((item, index) => {
// Skip the last drillText
if (index === drillTexts.length)
return;
const originalAttrs = Object.assign({}, item.attributes);
item.attr('cursor', 'pointer');
item.addEventListener('mouseenter', () => {
item.attr(breadCrumb.active);
});
item.addEventListener('mouseleave', () => {
item.attr(originalAttrs);
});
item.addEventListener('click', () => {
drillDownClick(path.slice(0, index));
});
});
}
// Update marks.
setState('drillDown', (viewOptions) => {
const { marks } = viewOptions;
// Add filter transform for every mark,
// which will skip for mark without color channel.
const newMarks = marks.map((mark) => {
if (mark.id !== partition_1.PARTITION_TYPE && mark.type !== 'rect')
return mark;
// Inset after aggregate transform, such as group and bin.
const { data } = mark;
const newData = data.filter((item) => {
var _a;
const itemPath = (_a = item.path) !== null && _a !== void 0 ? _a : [];
if (path.length === 0)
return true;
if (!Array.isArray(itemPath) || itemPath.length < path.length)
return false;
for (let i = 0; i < path.length; i++) {
if (itemPath[i] !== path[i])
return false;
}
return true;
});
// DrillDown by filtering the data and scale.
return (0, util_1.deepMix)({}, mark, {
data: newData,
});
});
return Object.assign(Object.assign({}, viewOptions), { marks: newMarks });
});
yield update();
});
const createDrillClick = (e) => {
var _a, _b;
const item = e.target;
// Get properties directly from DOM element attributes instead of using get function.
const markType = item.markType;
const itemStyle = item.style || {};
const partitionType = itemStyle[partition_1.PARTITION_TYPE_FIELD];
const childNodeCount = itemStyle[partition_1.CHILD_NODE_COUNT];
const itemData = item.__data__;
if (markType !== 'rect' ||
partitionType !== partition_1.PARTITION_TYPE ||
!childNodeCount) {
return;
}
const path = (_b = (_a = itemData === null || itemData === void 0 ? void 0 : itemData.data) === null || _a === void 0 ? void 0 : _a.path) !== null && _b !== void 0 ? _b : [];
drillDownClick(path);
};
// Add click drill interaction.
plotArea.addEventListener('click', createDrillClick);
// Change attribute keys.
const changeStyleKey = (0, util_1.keys)(Object.assign(Object.assign({}, state.active), state.inactive));
const createActive = () => {
const elements = getElementsPartition(plotArea);
elements.forEach((element) => {
const childNodeCount = (0, util_1.get)(element, ['style', partition_1.CHILD_NODE_COUNT]);
const cursor = (0, util_1.get)(element, ['style', 'cursor']);
if (cursor !== 'pointer' && childNodeCount) {
element.style.cursor = 'pointer';
const originalAttrs = (0, util_1.pick)(element.attributes, changeStyleKey);
element.addEventListener('mouseenter', () => {
element.attr(state.active);
});
element.addEventListener('mouseleave', () => {
element.attr((0, util_1.deepMix)(originalAttrs, state.inactive));
});
}
});
};
// Animate elements update and add active state.
plotArea.addEventListener('mousemove', createActive);
return () => {
textGroup.remove();
plotArea.removeEventListener('click', createDrillClick);
plotArea.removeEventListener('mousemove', createActive);
};
};
}
//# sourceMappingURL=drillDown.js.map