Skip to content

Commit

Permalink
Ruler interaction replacing Grid mouse listeners for comparative tool…
Browse files Browse the repository at this point in the history
…tips (carbon-design-system#499)

* add ruler and axis tooltip

* move ruler logic to it's own graphFrameComponent

* refactor, add axis tooltip dynamic width, add matching elements hovering

* use getDomainValue instead of hardcoded accessor

* add snap to nearest point

* use getSVGElementSize to get backdrop height

* remove useless code

* add trigger mouseout in hideRuler to only highlight single hovered element

* WIP

* fix categorical scale elements mouseover trigger

* fix close points mouseover/out trigger

* fix import order and package version

* add more test data

* add crosshair cursor when ruler is active to better select data points

* refactor, more docs

* fix axis tooltip baseline

* minor refactor

Co-Authored-By: Zvonimir Fras <[email protected]>

* change ruler color

* a11y bubble

* only show ruler for continuous domain - wip

* add support for top axis

* no need to support discrete scales anymore

* make sure tooltip does not go out of axis bbox

* remove axis tooltip

* only show ruler if there's match with data points

* only show ruler on timescale linechart

* add filter for items getting null values at times, breaking tooltip

* refactor

* fix(core): update import path carbon-components

fix carbon-design-system#546

* v0.30.7

* Merge pull request carbon-design-system#529 from theiliad/tabular-data-format

* add basic tabular data format support

* string updates

* finalize line graph implementation with tabular data support

* cleanup

* finalize scatter graph with support for tabular data

* commented out ideas for the data format

* time-series support for tabular data within line graph

* scatter support

* optimize logic to find main vertical & horizontal axes

* remove primary & secondary labels

* bubble chart support

* grouped bar support

* horizontal grouped bar

* fix legend filtering

* fix bubble and scatter

* fix bar hover errors

* simple bar support

* simple bar time-series support

* horizontal simple bar support

* more graph support

* fix pie tooltip

* fix donut center value

* fix line & scatter demo ranges

* fix bar demos

* stacked data supprot

* backwards data format compatibility

* fix plotting of datums with a value of 0

* fixed stacked bar tooltip

* remove primary, secondary, useAsGraphDomain/Range

* Update packages/core/src/model.ts

Co-Authored-By: natashadecoste <[email protected]>

* apply suggested PR changes

* fix group bar positioning issues

* apply PR suggested changes

* Update packages/core/demo/data/bar.ts

Co-Authored-By: natashadecoste <[email protected]>

* Update packages/core/demo/data/bar.ts

Co-Authored-By: natashadecoste <[email protected]>

* Update packages/core/demo/data/bar.ts

Co-Authored-By: natashadecoste <[email protected]>

* rename "identifier" to "mapsTo"

* add tutorials section to core storybook

* add links to errors

* remove unnecessary comment

* add custom color scale support

* apply PR suggested changes

* fix tests

Co-authored-by: natashadecoste <[email protected]>

* v0.30.8

* Apply suggestions from code review

Co-Authored-By: Eliad Moosavi <[email protected]>

* applied suggestions from review

* fix ruler css

* Update packages/core/src/components/axes/ruler.ts

Co-Authored-By: Eliad Moosavi <[email protected]>

* Update packages/core/src/styles/components/_ruler.scss

Co-Authored-By: Eliad Moosavi <[email protected]>

* Update packages/core/src/components/axes/ruler.ts

Co-Authored-By: Eliad Moosavi <[email protected]>

* add suggestions from review

* Update packages/core/demo/data/line.ts

Co-Authored-By: natashadecoste <[email protected]>

* Apply suggestions from code review

Co-Authored-By: natashadecoste <[email protected]>

* enable ruler on bubble and scatter

* disable mouse listeners on grid

* enable ruler for every scale type

* fix ruler on linear scales, refactor matches logic

Co-authored-by: Zvonimir Fras <[email protected]>
Co-authored-by: Natasha DeCoste <[email protected]>
Co-authored-by: carbon-bot <[email protected]>
Co-authored-by: Eliad Moosavi <[email protected]>
  • Loading branch information
5 people authored and ilariaventurini committed May 11, 2020
1 parent 5c90302 commit 685666b
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 49 deletions.
5 changes: 5 additions & 0 deletions packages/core/demo/data/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ let allDemoGroups = [
chartType: chartTypes.LineChart,
isDemoExample: true
},
{
options: lineDemos.lineTimeSeriesDenseOptions,
data: lineDemos.lineTimeSeriesDenseData,
chartType: chartTypes.LineChart
},
{
options: lineDemos.lineOptions,
data: lineDemos.lineData,
Expand Down
61 changes: 61 additions & 0 deletions packages/core/demo/data/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,67 @@ export const lineTimeSeriesOptions = {
curve: "curveMonotoneX"
};

export const lineTimeSeriesDenseData = [
{ group: "Dataset 1", date: new Date(2019, 0, 1), value: 10000 },
{ group: "Dataset 1", date: new Date(2019, 0, 1, 5), value: 12000 },
{ group: "Dataset 1", date: new Date(2019, 0, 1, 10), value: 14000 },
{ group: "Dataset 1", date: new Date(2019, 0, 2), value: 25000 },
{ group: "Dataset 1", date: new Date(2019, 0, 2, 2), value: 26000 },
{ group: "Dataset 1", date: new Date(2019, 0, 3), value: 10000 },
{ group: "Dataset 1", date: new Date(2019, 0, 3, 5), value: 10000 },
{ group: "Dataset 1", date: new Date(2019, 0, 3, 10), value: 12000 },
{ group: "Dataset 1", date: new Date(2019, 0, 5), value: 45000 },
{ group: "Dataset 1", date: new Date(2019, 0, 7), value: 49000 },
{ group: "Dataset 1", date: new Date(2019, 0, 7, 15), value: 45000 },
{ group: "Dataset 1", date: new Date(2019, 0, 9), value: 50000 },
{ group: "Dataset 1", date: new Date(2019, 0, 9, 5), value: 52000 },
{ group: "Dataset 1", date: new Date(2019, 0, 9, 15), value: 55000 },
{ group: "Dataset 1", date: new Date(2019, 0, 10), value: 50000 },
{ group: "Dataset 1", date: new Date(2019, 0, 12), value: 65000 },
{ group: "Dataset 1", date: new Date(2019, 0, 13), value: 80000 },
{ group: "Dataset 1", date: new Date(2019, 0, 14, 10), value: 85000 },
{ group: "Dataset 1", date: new Date(2019, 0, 15, 7), value: 90000 },
{ group: "Dataset 1", date: new Date(2019, 0, 15, 18), value: 70000 },
{ group: "Dataset 2", date: new Date(2019, 0, 1), value: 20000 },
{ group: "Dataset 2", date: new Date(2019, 0, 1, 3), value: 22000 },
{ group: "Dataset 2", date: new Date(2019, 0, 1, 16), value: 24000 },
{ group: "Dataset 2", date: new Date(2019, 0, 2), value: 35000 },
{ group: "Dataset 2", date: new Date(2019, 0, 2, 7), value: 36000 },
{ group: "Dataset 2", date: new Date(2019, 0, 3), value: 20000 },
{ group: "Dataset 2", date: new Date(2019, 0, 3, 6), value: 20000 },
{ group: "Dataset 2", date: new Date(2019, 0, 3, 18), value: 22000 },
{ group: "Dataset 2", date: new Date(2019, 0, 5), value: 62000 },
{ group: "Dataset 2", date: new Date(2019, 0, 6), value: 52000 },
{ group: "Dataset 2", date: new Date(2019, 0, 7), value: 52000 },
{ group: "Dataset 2", date: new Date(2019, 0, 7, 15), value: 52000 },
{ group: "Dataset 2", date: new Date(2019, 0, 9), value: 60000 },
{ group: "Dataset 2", date: new Date(2019, 0, 9, 5), value: 62000 },
{ group: "Dataset 2", date: new Date(2019, 0, 9, 10), value: 62000 },
{ group: "Dataset 2", date: new Date(2019, 0, 12), value: 65000 },
{ group: "Dataset 2", date: new Date(2019, 0, 14), value: 40000 },
{ group: "Dataset 2", date: new Date(2019, 0, 15, 5), value: 45000 },
{ group: "Dataset 2", date: new Date(2019, 0, 15, 10), value: 35000 },
{ group: "Dataset 2", date: new Date(2019, 0, 15, 18), value: 30000 }
];


export const lineTimeSeriesDenseOptions = {
title: "Line (dense time series)",
axes: {
bottom: {
title: "2019 Annual Sales Figures",
mapsTo: "date",
scaleType: "time"
},
left: {
mapsTo: "value",
title: "Conversion rate",
scaleType: "linear"
}
},
curve: "curveMonotoneX"
};

export const lineTimeSeriesDataRotatedTicks = [
{ group: "Dataset 1", date: new Date(2019, 11, 30), value: 32100 },
{ group: "Dataset 1", date: new Date(2019, 11, 31), value: 23500 },
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/charts/bubble.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Tools } from "../tools";
// Components
import {
Grid,
Line,
Ruler,
Bubble,
TwoDimensionalAxes,
// the imports below are needed because of typescript bug (error TS4029)
Expand Down Expand Up @@ -42,6 +42,7 @@ export class BubbleChart extends AxisChart {
const graphFrameComponents = [
new TwoDimensionalAxes(this.model, this.services),
new Grid(this.model, this.services),
new Ruler(this.model, this.services),
new Bubble(this.model, this.services)
];

Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/charts/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { Tools } from "../tools";
import {
Grid,
Line,
Ruler,
Scatter,
TwoDimensionalAxes,
// the imports below are needed because of typescript bug (error TS4029)
Expand Down Expand Up @@ -42,6 +43,7 @@ export class LineChart extends AxisChart {
const graphFrameComponents = [
new TwoDimensionalAxes(this.model, this.services),
new Grid(this.model, this.services),
new Ruler(this.model, this.services),
new Line(this.model, this.services),
new Scatter(this.model, this.services)
];
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/charts/scatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Tools } from "../tools";
// Components
import {
Grid,
Line,
Ruler,
Scatter,
TwoDimensionalAxes,
// the imports below are needed because of typescript bug (error TS4029)
Expand Down Expand Up @@ -42,6 +42,7 @@ export class ScatterChart extends AxisChart {
const graphFrameComponents = [
new TwoDimensionalAxes(this.model, this.services),
new Grid(this.model, this.services),
new Ruler(this.model, this.services),
new Scatter(this.model, this.services)
];

Expand Down
46 changes: 0 additions & 46 deletions packages/core/src/components/axes/grid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ export class Grid extends Component {

this.drawXGrid();
this.drawYGrid();

if (Tools.getProperty(this.model.getOptions(), "tooltip", "gridline", "enabled")) {
this.addGridEventListeners();
}
}

drawXGrid() {
Expand Down Expand Up @@ -141,48 +137,6 @@ export class Grid extends Component {
return xGridlines;
}

/**
* Adds the listener on the X grid to trigger multiple point tooltips along the x axis.
*/
addGridEventListeners() {
const self = this;
const svg = this.parent;
const grid = DOMUtils.appendOrSelect(svg, "rect.chart-grid-backdrop");

grid.on("mousemove mouseover", function() {
const chartContainer = self.services.domUtils.getMainSVG();
const pos = mouse(chartContainer);
const hoveredElement = select(this);

// remove the styling on the lines
const allgridlines = svg.selectAll(".x.grid .tick");
allgridlines.classed("active", false);

const activeGridline = self.getActiveGridline(pos);
if (activeGridline.empty()) {
return self.services.events.dispatchEvent(Events.Tooltip.HIDE);
}

// set active class to control dasharray and theme colors
activeGridline.classed("active", true);

// get the items that should be highlighted
const itemsToHighlight = self.services.cartesianScales.getDataFromDomain(activeGridline.datum());

self.services.events.dispatchEvent(Events.Tooltip.SHOW, {
hoveredElement,
multidata: itemsToHighlight,
type: TooltipTypes.GRIDLINE
});
})
.on("mouseout", function() {
svg.selectAll(".x.grid .tick")
.classed("active", false);

self.services.events.dispatchEvent(Events.Tooltip.HIDE);
});
}

drawBackdrop() {
const svg = this.parent;

Expand Down
195 changes: 195 additions & 0 deletions packages/core/src/components/axes/ruler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Internal Imports
import { Component } from "../component";
import { DOMUtils } from "../../services";
import { TooltipTypes, ScaleTypes } from "../../interfaces";
import { Tools } from "../../tools";

// D3 Imports
import { mouse, Selection } from "d3-selection";
import { scaleLinear } from "d3-scale";

type GenericSvgSelection = Selection<SVGElement, any, SVGElement, any>;

const THRESHOLD = 5;

/** check if x is inside threshold area extents */
function pointIsWithinThreshold(dx: number, x: number) {
return dx > x - THRESHOLD && dx < x + THRESHOLD;
}

/**
* a compatibility function that accepts ordinal scales too
* as those do not support .invert() by default,
* so a scale clone is created to invert domain with range
*/
function invertedScale(scale) {
if (scale.invert) {
return scale.invert;
}

return scaleLinear()
.domain(scale.range())
.range(scale.domain());
}

export class Ruler extends Component {
type = "ruler";
backdrop: GenericSvgSelection;
hoveredElements: GenericSvgSelection;

render() {
this.drawBackdrop();
this.addBackdropEventListeners();
}

showRuler([x, y]: [number, number]) {
const svg = this.parent;
const ruler = DOMUtils.appendOrSelect(svg, "g.ruler");
const line = DOMUtils.appendOrSelect(ruler, "line.ruler-line");
const dataPointElements: GenericSvgSelection = svg.selectAll(
"[role=graphics-symbol]"
);
const displayData = this.model.getDisplayData();
const domainScale = this.services.cartesianScales.getDomainScale();
const rangeScale = this.services.cartesianScales.getRangeScale();
const [yScaleEnd, yScaleStart] = rangeScale.range();

const scaledData: {domainValue: number, originalData: any}[] = displayData.map((d, i) => ({
domainValue: this.services.cartesianScales.getDomainValue(d, i),
originalData: d
}));

/**
* Find matches, reduce is used instead of filter
* to only get elements which belong to the same axis coordinate
*/
const dataPointsMatchingRulerLine: {domainValue: number, originalData: any}[] =
scaledData.reduce((accum, currentValue) => {
// store the first element of the accumulator array to compare it with current element being processed
const sampleAccumValue = accum[0] ? accum[0].domainValue : undefined;

// if accumulator is not empty and current value is bigger than already existing value in the accumulator, skip current iteration
if (sampleAccumValue !== undefined && currentValue.domainValue > sampleAccumValue) {
return accum;
}

// there's a match and currentValue is either less then or equal to already stored values
if (pointIsWithinThreshold(currentValue.domainValue, x)) {
if (sampleAccumValue !== undefined && currentValue < sampleAccumValue) {
// there's a closer data point in the threshold area, so reinstantiate array
accum = [currentValue];
} else {
// currentValue is equal to already stored values, there's another match on the same coordinate
accum.push(currentValue);
}
}

return accum;
}, []);

// some data point match
if (dataPointsMatchingRulerLine.length > 0) {
const highlightItems = dataPointsMatchingRulerLine.map(d => d.originalData)
.filter(d => {
const rangeIdentifier = this.services.cartesianScales.getRangeIdentifier();
const value = d[rangeIdentifier];
return value !== null && value !== undefined;
});

// get elements on which we should trigger mouse events
const hoveredElements = dataPointElements.filter((d, i) =>
dataPointsMatchingRulerLine.includes(d)
);

/** if we pass from a trigger area to another one
* mouseout on previous elements won't get dispatched
* so we need to do it manually
*/
if (
this.hoveredElements &&
this.hoveredElements.size() > 0 &&
!Tools.isEqual(this.hoveredElements, hoveredElements)
) {
this.hideRuler();
}

hoveredElements.dispatch("mouseover");

// set current hovered elements
this.hoveredElements = hoveredElements;

this.services.events.dispatchEvent("show-tooltip", {
hoveredElement: line,
multidata: highlightItems,
type: TooltipTypes.GRIDLINE
});

ruler.attr("opacity", 1);

// line snaps to matching point
const sampleMatch = dataPointsMatchingRulerLine[0];
line.attr("y1", yScaleStart)
.attr("y2", yScaleEnd)
.attr("x1", sampleMatch.domainValue)
.attr("x2", sampleMatch.domainValue);
} else {
ruler.attr("opacity", 0);
dataPointElements.dispatch("mouseout");
}
}

hideRuler() {
const svg = this.parent;
const ruler = DOMUtils.appendOrSelect(svg, "g.ruler");
const dataPointElements = svg.selectAll("[role=graphics-symbol]");

dataPointElements.dispatch("mouseout");
ruler.attr("opacity", 0);
}

/**
* Adds the listener on the X grid to trigger multiple point tooltips along the x axis.
*/
addBackdropEventListeners() {
const self = this;

this.backdrop
.on("mousemove mouseover", function() {
const chartContainer = self.services.domUtils.getMainSVG();
const pos = mouse(chartContainer);

self.showRuler(pos);
})
.on("mouseout", function() {
self.hideRuler();
self.services.events.dispatchEvent("hide-tooltip");
});
}

drawBackdrop() {
const svg = this.parent;

const domainScale = this.services.cartesianScales.getDomainScale();
const rangeScale = this.services.cartesianScales.getRangeScale();

const [xScaleStart, xScaleEnd] = domainScale.range();
const [yScaleEnd, yScaleStart] = rangeScale.range();

// Get height from the grid
this.backdrop = DOMUtils.appendOrSelect(svg, "svg.chart-grid-backdrop");
const backdropRect = DOMUtils.appendOrSelect(
this.backdrop,
"rect.chart-grid-backdrop"
);

this.backdrop
.merge(backdropRect)
.attr("x", xScaleStart)
.attr("y", yScaleStart)
.attr("width", xScaleEnd - xScaleStart)
.attr("height", yScaleEnd - yScaleStart)
.lower();

backdropRect.attr("width", "100%").attr("height", "100%");
}
}
Loading

0 comments on commit 685666b

Please sign in to comment.