Skip to content
Snippets Groups Projects
Unverified Commit 522e3df1 authored by Tom Robinson's avatar Tom Robinson
Browse files

Much improved brushing implementation

parent 3c5f4231
No related branches found
No related tags found
No related merge requests found
......@@ -58,6 +58,17 @@
display: none;
}
/* disable pointer events on all chart elements while dragging to avoid weird interactions */
.LineAreaBarChart .dc-chart .dragging .area,
.LineAreaBarChart .dc-chart .dragging .bar,
.LineAreaBarChart .dc-chart .dragging .line,
.LineAreaBarChart .dc-chart .dragging .dot,
.LineAreaBarChart .dc-chart .dragging .row,
.LineAreaBarChart .dc-chart .dragging .bubble,
.LineAreaBarChart .dc-chart .dragging .voronoi {
pointer-events: none !important;
}
/* disable dc default behavior */
.LineAreaBarChart .dc-chart rect.bar:hover {
fill-opacity: 1;
......@@ -73,7 +84,7 @@
opacity: 1 !important;
}
.LineAreaBarChart .enable-dots .dc-tooltip circle.dot {
.LineAreaBarChart .dc-chart .enable-dots .dc-tooltip circle.dot {
r: 3px !important;
fill: white;
stroke: currentColor;
......@@ -88,13 +99,13 @@
stroke: white;
}
.LineAreaBarChart .enable-dots .dc-tooltip circle.dot:hover,
.LineAreaBarChart .enable-dots .dc-tooltip circle.dot.hover {
.LineAreaBarChart .dc-chart .enable-dots .dc-tooltip circle.dot:hover,
.LineAreaBarChart .dc-chart .enable-dots .dc-tooltip circle.dot.hover {
fill: currentColor;
}
.LineAreaBarChart .enable-dots-onhover .dc-tooltip circle.dot:hover,
.LineAreaBarChart .enable-dots-onhover .dc-tooltip circle.dot.hover {
.LineAreaBarChart .dc-chart .enable-dots-onhover .dc-tooltip circle.dot:hover,
.LineAreaBarChart .dc-chart .enable-dots-onhover .dc-tooltip circle.dot.hover {
r: 3px !important;
fill: white;
stroke: currentColor;
......@@ -131,14 +142,10 @@
opacity: 0;
}
.LineAreaBarChart .voronoi {
.LineAreaBarChart .dc-chart .voronoi {
fill: transparent;
}
/*.voronoi path {
fill: rgba(255,0,0,0.05);
}*/
/* we put the brush behind everything so this isn't necessary
/*.LineAreaBarChart .dc-chart .brush {
pointer-events: none;
......
......@@ -36,7 +36,7 @@ import { parseTimestamp } from "metabase/lib/time";
import { datasetContainsNoResults } from "metabase/lib/dataset";
import { addOrUpdateFilter } from "metabase/qb/lib/actions";
import { initBrushParent, initBrushChild } from "./graph/brush";
import { initBrush } from "./graph/brush";
import type { Series, ClickObject } from "metabase/meta/types/Visualization"
......@@ -387,7 +387,7 @@ function applyChartTooltips(chart, series, isStacked, isScalarSeries, onHoverCha
if (onVisualizationClick) {
chart.selectAll(".bar, .dot, .area, .bubble")
.style({ "cursor": "pointer" })
.on("mouseup", function(d) {
.on("click", function(d) {
const seriesIndex = determineSeriesIndexFromElement(this, isStacked);
const card = series[seriesIndex].card;
const isSingleSeriesBar = this.classList.contains("bar") && series.length === 1;
......@@ -583,9 +583,9 @@ function lineAndBarOnRender(chart, settings, onGoalHover, isSplitAxis, isStacked
dispatchUIEvent(e, "mouseleave");
d3.select(e).classed("hover", false);
})
.on("mouseup", ({ point }) => {
.on("click", ({ point }) => {
let e = point[2];
dispatchUIEvent(e, "mouseup");
dispatchUIEvent(e, "click");
})
.order();
......@@ -1039,23 +1039,26 @@ export default function lineAreaBar(element, {
let parent = dc.compositeChart(element);
initChart(parent, element);
let filter = null;
initBrushParent(parent, () => {
// workaround for the filter handler firing on mouse move rather than mouse up
if (filter) {
onChangeCardAndRun(addOrUpdateFilter(series[0].card, filter));
}
});
let isBrushing = false;
let charts = groups.map((group, index) => {
let chart = dc[getDcjsChartType(chartType)](parent);
initBrushChild(chart, (start, end) => {
// fires on mouse move, just update a local variable for now
if (isDimensionTimeseries) {
filter = ["BETWEEN", series[0].data.cols[0].id, moment(start).format(), moment(end).format()]
} else {
filter = ["BETWEEN", series[0].data.cols[0].id, start, end]
initBrush(parent, chart, () => {
isBrushing = true;
}, (range) => {
isBrushing = false;
if (range) {
const [start, end] = range;
let filter;
if (isDimensionTimeseries) {
filter = ["BETWEEN", series[0].data.cols[0].id, moment(start).format(), moment(end).format()];
} else {
filter = ["BETWEEN", series[0].data.cols[0].id, start, end];
}
if (filter) {
onChangeCardAndRun(addOrUpdateFilter(series[0].card, filter));
}
}
});
......@@ -1201,7 +1204,8 @@ export default function lineAreaBar(element, {
const isSplitAxis = (right && right.series.length) && (left && left.series.length > 0);
applyChartTooltips(parent, series, isStacked, isScalarSeries, (hovered) => {
if (onHoverChange) {
// disable tooltips while brushing
if (onHoverChange && !isBrushing) {
// disable tooltips on lines
if (hovered && hovered.element && hovered.element.classList.contains("line")) {
delete hovered.element;
......@@ -1279,7 +1283,7 @@ export function rowRenderer(
}
if (onVisualizationClick) {
chart.selectAll(".row rect").on("mouseup", function(d) {
chart.selectAll(".row rect").on("click", function(d) {
onVisualizationClick({
value: d.value,
column: cols[1],
......
import { KEYCODE_ESC } from "metabase/lib/keyboard";
// TODO: better handle paths
const LEFT_HANDLE_PATH = "M-0.5,76.69275 L-6.5,82.69275 V147.3855 L-0.5,153.3855Z M-2.5,84.69275 V145.3855 M-4.5,84.69275 V145.3855";
const RIGHT_HANDLE_PATH = "M0.5,76.69275 L6.5,82.69275 V147.3855 L0.5,153.3855Z M2.5,84.69275 V145.3855 M4.5,84.69275 V145.3855";
export function initBrush(parent, child, onBrushChange, onBrushEnd) {
if (!child.brushOn) {
return;
}
export function initBrushParent(parent, onBrushEnd) {
parent.on("pretransition", function(chart) {
// move brush to the back so tootips/clicks still work
var brushNode = chart.select("g.brush").node();
if (brushNode && brushNode.parentNode) {
brushNode.parentNode.insertBefore(
brushNode,
brushNode.parentNode.firstChild
);
}
// customize the handles
chart.select(".brush .resize.w path").attr("d", LEFT_HANDLE_PATH);
chart.select(".brush .resize.e path").attr("d", RIGHT_HANDLE_PATH);
// enable brush
child.brushOn(true);
// normally dots are disabled if brush is on but we want them anyway
if (child.xyTipsOn) {
child.xyTipsOn("always");
}
// the brush has been cancelled by pressing escape
let cancelled = false;
// the last updated range when brushing
let range = null;
// start
parent.brush().on("brushstart.custom", () => {
// reset "cancelled" flag
cancelled = false;
// add "dragging" class to chart
parent.svg().classed("dragging", true);
// move the brush element to the front
moveToFront(parent.select(".brush").node());
// add an escape keydown listener
window.addEventListener("keydown", onKeyDown, true);
});
// change
child.addFilterHandler((filters, r) => {
// update "range" with new filter range
range = r;
// emit "onBrushChange" event
onBrushChange(range);
// return filters unmodified
return filters;
});
parent.on("renderlet", chart => {
chart.svg().on("mouseup", () => {
onBrushEnd();
});
// end
parent.brush().on("brushend.custom", () => {
// remove the "dragging" classed
parent.svg().classed("dragging", false)
// reset brush opacity (if the brush was cancelled)
parent.select(".brush").style("opacity", 1);
// move the brush to the back
moveToBack(parent.select(".brush").node());
// remove the escape keydown listener
window.removeEventListener("keydown", onKeyDown, true);
// reset the fitler and redraw
child.filterAll();
parent.redraw();
// if not cancelled, emit the onBrushEnd event with the last filter range
onBrushEnd(cancelled ? null : range);
});
// cancel
const onKeyDown = e => {
if (e.keyCode === KEYCODE_ESC) {
// set the "cancelled" flag
cancelled = true;
// hide the brush
parent.select(".brush").style("opacity", 0);
}
};
parent.on("pretransition.custom", function(chart) {
// move brush to the back so tootips/clicks still work
moveToBack(chart.select(".brush").node());
// remove the handles since we can't adjust them anyway
chart.selectAll(".brush .resize").remove();
});
}
export function initBrushChild(chart, onDragBrush) {
// enable brush
if (chart.brushOn) {
chart.brushOn(true);
function moveToBack(element) {
if (element && element.parentNode) {
element.parentNode.insertBefore(
element,
element.parentNode.firstChild
);
}
// normally dots are disabled if brush is on but we want them anyway
if (chart.xyTipsOn) {
chart.xyTipsOn("always");
}
function moveToFront(element) {
if (element && element.parentNode) {
element.parentNode.appendChild(element);
}
// fires on mouse move
chart.addFilterHandler((filters, [start, end]) => {
onDragBrush(start, end);
// return existing filters, we don't want to actually change them
return filters;
});
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment