Skip to content

Commit

Permalink
Merge pull request #131 from redbearsam/feature/legend_resizable
Browse files Browse the repository at this point in the history
Feature/legend resizable
  • Loading branch information
redbearsam authored Apr 5, 2019
2 parents b70e3cb + 84065c6 commit 7c6d77f
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 51 deletions.
5 changes: 1 addition & 4 deletions packages/perspective-viewer-d3fc/src/js/legend/legend.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import scrollableLegend from "./scrollableLegend";
import {withoutOpacity} from "../series/seriesColors";
import {getChartElement} from "../plugin/root";
import {getOrCreateElement} from "../utils/utils";
import {draggableComponent} from "./styling/draggableComponent";

const scrollColorLegend = settings =>
scrollableLegend(
Expand All @@ -40,6 +39,7 @@ function symbolScale(fromScale) {

const domain = fromScale.domain();
const range = fromScale.range().map(r => d3.symbol().type(r)());

return d3
.scaleOrdinal()
.domain(domain)
Expand All @@ -50,7 +50,6 @@ function legendComponent(scrollLegendComponent, scaleModifier) {
let settings = {};
let scale = null;
let color = null;
let draggable = draggableComponent();

function legend(container) {
if (scale && scale.range().length > 1) {
Expand Down Expand Up @@ -101,8 +100,6 @@ function legendComponent(scrollLegendComponent, scaleModifier) {
.attr("borderbox-on-hover", true)
.style("z-index", "2")
.call(scrollLegend);

draggable(legendSelection);
}
}

Expand Down
41 changes: 33 additions & 8 deletions packages/perspective-viewer-d3fc/src/js/legend/scrollableLegend.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,37 @@ import {rebindAll} from "d3fc";
import {getOrCreateElement} from "../utils/utils";
import legendControlsTemplate from "../../html/legend-controls.html";
import {cropCellContents} from "./styling/cropCellContents";
import {draggableComponent} from "./styling/draggableComponent";
import {resizableComponent} from "./styling/resizableComponent";

const averageCellHeightPx = 16;
const controlsHeightPx = 20;

export default (fromLegend, settings) => {
const legend = fromLegend || d3Legend.legendColor();

let domain = [];
let pageCount = 1;
let pageSize = 15;
let pageSize;
let pageIndex = settings.legend ? settings.legend.pageIndex : 0;
let decorate = () => {};
let draggable = draggableComponent();
let resizable;

const scrollableLegend = selection => {
const domain = legend.scale().domain();
pageCount = Math.ceil(domain.length / pageSize);

domain = legend.scale().domain();
render(selection);

resizable = resizableComponent()
.maxHeight(domain.length * averageCellHeightPx + controlsHeightPx)
.on("resize", () => render(selection));

resizable(selection);
draggable(selection);
};

const render = selection => {
calculatePageSize(selection);
renderControls(selection);
renderLegend(selection);
cropCellContents(selection);
Expand Down Expand Up @@ -66,11 +81,11 @@ export default (fromLegend, settings) => {
const legendElement = getLegendElement(selection);
legendElement.call(legend);

const cellSize = selection
const cellContainerSize = selection
.select("g.legendCells")
.node()
.getBBox();
legendElement.attr("height", cellSize.height + 20);
legendElement.attr("height", cellContainerSize.height + controlsHeightPx);

decorate(selection);
};
Expand All @@ -80,10 +95,20 @@ export default (fromLegend, settings) => {
settings.legend = {pageIndex};
};

const cellFilter = () => {
return (_, i) => i >= pageSize * pageIndex && i < pageSize * pageIndex + pageSize;
const cellFilter = () => (_, i) => i >= pageSize * pageIndex && i < pageSize * pageIndex + pageSize;

const calculatePageSize = selection => {
const legendContainerRect = selection.node().getBoundingClientRect();
let proposedPageSize = Math.floor(legendContainerRect.height / averageCellHeightPx) - 1;

//if page size is less than all legend items, leave space for the legend controls
pageSize = proposedPageSize < domain.length ? proposedPageSize - 1 : proposedPageSize;
pageCount = calculatePageCount(proposedPageSize);
pageIndex = Math.min(pageIndex, pageCount - 1);
};

const calculatePageCount = pageSize => Math.ceil(domain.length / pageSize);

const getLegendControls = container =>
getOrCreateElement(container, ".legend-controls", () =>
container
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,9 @@
*/

import * as d3 from "d3";
import {isElementOverflowing} from "../../utils/utils";
import {getChartContainer} from "../../plugin/root";
import {enforceContainerBoundaries, margin} from "./enforceContainerBoundaries";

const margin = 10;
const resizeForDraggingEvent = "resize.for-dragging";

export function draggableComponent() {
Expand All @@ -22,17 +21,16 @@ export function draggableComponent() {
node.style.cursor = "move";

const drag = d3.drag().on("drag", function() {
const [offsetX, offsetY] = enforceContainerBoundaries(this, d3.event.dx, d3.event.dy);
this.style.left = `${this.offsetLeft + offsetX}px`;
this.style.top = `${this.offsetTop + offsetY}px`;
const offsets = enforceContainerBoundaries(this, d3.event.dx, d3.event.dy);
this.style.left = `${this.offsetLeft + offsets.x}px`;
this.style.top = `${this.offsetTop + offsets.y}px`;

const element = d3.select(this);
if (isNodeInTopRight(node)) {
pinned = pinNodeToTopRight(node);
return;
}

pinned = unpinNodeFromTopRight(node, element, pinned);
pinned = unpinNodeFromTopRight(node, pinned);
});

element.call(drag);
Expand All @@ -41,14 +39,14 @@ export function draggableComponent() {
return draggable;
}

function unpinNodeFromTopRight(node, element, pinned) {
function unpinNodeFromTopRight(node, pinned) {
if (pinned !== false) {
// Default behaviour for the legend is to remain pinned to the top right hand corner with a specific margin.
// Once the legend has moved we cannot continue to use that css based approach.
d3.select(window).on(resizeForDraggingEvent, function() {
const [offsetX, offsetY] = enforceContainerBoundaries(node, 0, 0);
node.style.left = `${node.offsetLeft + offsetX}px`;
node.style.top = `${node.offsetTop + offsetY}px`;
const offsets = enforceContainerBoundaries(node, 0, 0);
node.style.left = `${node.offsetLeft + offsets.x}px`;
node.style.top = `${node.offsetTop + offsets.y}px`;
});
}
return false;
Expand All @@ -71,31 +69,3 @@ function isNodeInTopRight(node) {

return nodeRect.right + margin + fuzz >= containerRect.right && nodeRect.top - margin - fuzz <= containerRect.top;
}

function enforceContainerBoundaries(innerNode, offsetX, offsetY) {
const chartNodeRect = d3
.select(getChartContainer(innerNode))
.node()
.getBoundingClientRect();

const legendNodeRect = innerNode.getBoundingClientRect();

const draggedLegendNodeRect = {
top: legendNodeRect.top + offsetY - margin,
right: legendNodeRect.right + offsetX + margin,
bottom: legendNodeRect.bottom + offsetY + margin,
left: legendNodeRect.left + offsetX - margin
};

const adjustedOffsets = {x: offsetX, y: offsetY};
const boundaries = [{edge: "right", dimension: "x"}, {edge: "left", dimension: "x"}, {edge: "top", dimension: "y"}, {edge: "bottom", dimension: "y"}];

boundaries.forEach(bound => {
if (isElementOverflowing(chartNodeRect, draggedLegendNodeRect, bound.edge)) {
const adjustment = draggedLegendNodeRect[bound.edge] - chartNodeRect[bound.edge];
adjustedOffsets[bound.dimension] = adjustedOffsets[bound.dimension] - adjustment;
}
});

return [adjustedOffsets.x, adjustedOffsets.y];
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/******************************************************************************
*
* Copyright (c) 2017, the Perspective Authors.
*
* This file is part of the Perspective library, distributed under the terms of
* the Apache License 2.0. The full license can be found in the LICENSE file.
*
*/

import * as d3 from "d3";
import {isElementOverflowing} from "../../utils/utils";
import {getChartElement} from "../../plugin/root";

export const margin = 10;

export function enforceContainerBoundaries(innerNode, offsetX, offsetY) {
const chartNodeRect = d3
.select(getChartElement(innerNode).getContainer())
.node()
.getBoundingClientRect();

const innerNodeRect = innerNode.getBoundingClientRect();

const draggedInnerNodeRect = {
top: innerNodeRect.top + offsetY - margin,
right: innerNodeRect.right + offsetX + margin,
bottom: innerNodeRect.bottom + offsetY + margin,
left: innerNodeRect.left + offsetX - margin
};

const adjustedOffsets = {x: offsetX, y: offsetY};
const boundaries = [{edge: "right", dimension: "x"}, {edge: "left", dimension: "x"}, {edge: "top", dimension: "y"}, {edge: "bottom", dimension: "y"}];

boundaries.forEach(bound => {
if (isElementOverflowing(chartNodeRect, draggedInnerNodeRect, bound.edge)) {
const adjustment = draggedInnerNodeRect[bound.edge] - chartNodeRect[bound.edge];
adjustedOffsets[bound.dimension] = adjustedOffsets[bound.dimension] - adjustment;
}
});

return adjustedOffsets;
}
Loading

0 comments on commit 7c6d77f

Please sign in to comment.