Skip to content

Commit

Permalink
Support full and onelane zoom style in addition of the existing one
Browse files Browse the repository at this point in the history
  • Loading branch information
ekacnet committed May 28, 2024
1 parent ce47c97 commit 180a493
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 41 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ The core of the configuration is the function that you pass to `zoom()`, it take
You can do pretty much anything you want in the zoom function like actually zooming or calling a different URL with a detailed analysis of the zoom time.
There is a function in the `zoom` module that you can reuse for doing the actual zooming: `zoom().zoomTime()`, your source must be able to provide a fresh array of values to handle the zoom, see examples in the stock or random demo.

Zoom also support a zoom style or zoom type, the default one is drawing a rectangle from the point where you click and to the point where you are moving your mouse, `onelane` will start at the top of the horizon that has been clicked and will go to the bottom of this horizon; `full` will start at the top of the whole `cubism` graph till the bottom of it.


## Limitation
Graphite, Cube and GangliaWeb have not been verified yet.
Expand Down
10 changes: 5 additions & 5 deletions cypress/e2e/e2e.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -271,24 +271,24 @@ describe('Stock Check Test With Zoom', function () {
cy.get('div.horizon')
.first()
.then(($div) => {
cy.get('body').trigger('mousemove', 10, 0);
cy.get('body').trigger('mousemove', 10, 70);
cy.get('body').trigger('mousedown', {
eventConstructor: 'MouseEvent',
button: 0,
clientX: 10,
clientY: 0,
clientY: 70,
});
cy.wait(200);
cy.get('body').trigger('mousemove', {
eventConstructor: 'MouseEvent',
clientX: 300,
clientY: 10,
clientY: 80,
});
cy.wait(200);
cy.get('body').trigger('mouseup', {
eventConstructor: 'MouseEvent',
clientX: 300,
clientY: 10,
clientY: 80,
});
// Deal with the delay in rendering after zooming
cy.wait(600);
Expand All @@ -299,7 +299,7 @@ describe('Stock Check Test With Zoom', function () {
.first()
.then(($div) => {
const divOffset = $div.offset();
cy.get('body').trigger('mousemove', 10, 0);
cy.get('body').trigger('mousemove', 10, 70);
});
cy.get('#rule')
.find('div.line')
Expand Down
44 changes: 27 additions & 17 deletions demo/stock.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@
font-family: "Helvetica Neue", Helvetica, sans-serif;
margin: 30px auto;
width: 1280px;
}

#demo {
position: relative;
overflow: hidden;
}

header {
Expand Down Expand Up @@ -43,7 +47,6 @@
background-image: -moz-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
background-image: -webkit-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
background-image: -ms-linear-gradient(top, #fff 0%, rgba(255,255,255,0) 100%);
top: 0px;
padding: 0 0 24px 0;
}

Expand All @@ -53,7 +56,6 @@
background-image: -moz-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
background-image: -webkit-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
background-image: -ms-linear-gradient(bottom, #fff 0%, rgba(255,255,255,0) 100%);
bottom: 0px;
padding: 24px 0 0 0;
}

Expand Down Expand Up @@ -95,15 +97,21 @@
}

.line {
position: relative;
background: #000;
z-index: 2;
}

.rule {
}

</style>
<script src="lib/d3.v7.min.js" charset="utf-8"></script>
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.3/es6-shim.js"></script>-->
<script src="./cubism-es.standalone.js"></script>
<body id="demo">
<body>
<H1> Stock Demo</H1>
<div id="demo">
<script>

var delay = Date.now() - new Date(2012, 4, 2)
Expand All @@ -114,30 +122,30 @@
.size(size)
.stop();

d3.select("#demo").selectAll(".axis")
.data(['top', "bottom"])
.enter().append("div")
.attr("class", function(d) { return d + " axis"; })
.each(function(d) { context.axis().ticks(12).orient(d).render(d3.select(this)) });

const d = d3.select("body").append("div")
.attr("class", "rule")
.attr('id', 'rule');
context.rule().render(d);

d3.select("body").selectAll(".horizon")
d3.select("#demo").selectAll(".horizon")
.data(["AAPL", "BIDU", "SINA", "GOOG", "MSFT", "YHOO", "ADBE", "REDF", "INSP", "IACI", "AVID", "CCUR", "DELL", "DGII", "HPQ", "SGI", "SMCI", "SNDK", "SYNA"].map(stock))
.enter().insert("div", ".bottom")
.attr("class", "horizon");


context.horizon().format(d3.format("+,.2p")).render(d3.selectAll('.horizon'))
const z = d3.select("body").append("div")
const z = d3.select("#demo").append("div")
.attr("class", "zoom");
const d = d3.select("#demo").append("div")
.attr("class", "rule")
.attr('id', 'rule');
context.rule().render(d);
d3.select("#demo").selectAll(".axis")
.data(['top', "bottom"])
.enter().append("div")
.attr("class", function(d) { return d + " axis"; })
.each(function(d) { context.axis().ticks(12).orient(d).render(d3.select(this)) });

context.zoom(function(start, end) {
context.zoom(function(start, end, selection) {
console.log(`Doing a zoom from point ${start} to point ${end}`);
context.zoom().zoomTime(start, end);
context.zoom().zoomTime(start, end, selection);
}).render(z);


Expand Down Expand Up @@ -174,4 +182,6 @@
}

</script>

</div>
</body>
</html>
42 changes: 34 additions & 8 deletions src/context/apiZoom.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ const apiStart = (zoomState) => ({
start: (selection, pos) => {
var x = Math.round(pos[0]);
var y = Math.round(pos[1]);
if (zoomState._zoomStyle === 'onelane') {
y = selection.node().offsetTop;
} else if (zoomState._zoomStyle === 'full') {
y = selection.node().parentNode.offsetTop;
}
zoomState._corner1 = [x, y];
zoomState._selection = selection;
},
});

Expand All @@ -17,7 +23,7 @@ const apiReset = (zoomState) => ({
});

const apiStop = (zoomState) => ({
stop: (selection, pos) => {
stop: (pos) => {
var x = Math.round(pos[0]);
var y = Math.round(pos[1]);
zoomState._corner2 = [x, y];
Expand All @@ -28,18 +34,19 @@ const apiStop = (zoomState) => ({
var start = Math.min(zoomState._corner1[0], zoomState._corner2[0]);
var end = Math.max(zoomState._corner1[0], zoomState._corner2[0]);
if (start !== end) {
zoomState._callback(start, end);
zoomState._callback(start, end, zoomState._selection);
} else {
console.log('Skipping zoom on 1 point');
}
}
// force the zoom indicator to hide
zoomState._corner1 = null;
zoomState._selection = null;
},
});

const apiZoomTime = (zoomState) => ({
zoomTime: (start, stop) => {
zoomTime: (start, stop, selection) => {
const { _context } = zoomState;
const { _step } = _context;

Expand All @@ -50,9 +57,7 @@ const apiZoomTime = (zoomState) => ({
}
var new_start_time = _context._scale.invert(start);
var new_end_time = _context._scale.invert(stop);
console.log('zoomTime() called');
console.log(`${new_start_time} ${new_end_time}`);

var span = selection.select('.' + context.getCSSClass('title'));
// stop the timeout
_context.stop();
var width = (new_end_time - new_start_time) / _context._size;
Expand Down Expand Up @@ -94,6 +99,15 @@ const apiMisc = (zoomState) => ({
getSecondCorner: () => {
return zoomState._corner2;
},
getCurrentCorner: () => {
return zoomState._current_corner;
},
setZoomType: (type) => {
zoomState._zoomStyle = type;
},
getZoomType: () => {
return zoomState._zoomStyle;
},
});

const apiRender = (zoomState) => ({
Expand All @@ -114,7 +128,7 @@ const apiRender = (zoomState) => ({
// All elements that register for events "focus.<something>" will be called when the horizon
// call context.focus()
_context.on('focus.zoom-' + id, (i) => {
if (zoomState._corner1 !== null) {
if (zoomState._corner1 !== null && zoomState._current_corner !== null) {
var x = Math.min(zoomState._corner1[0], zoomState._current_corner[0]);
var y = Math.min(zoomState._corner1[1], zoomState._current_corner[1]);
var width = Math.abs(
Expand Down Expand Up @@ -151,7 +165,17 @@ const apiEnabled = (zoomState) => ({
});

const apiUpdateCurrentCorner = (zoomState) => ({
updateCurrentCorner: (pos) => {
updateCurrentCorner: (pos, selection) => {
var corner = zoomState.getFirstCorner();
if (corner !== null) {
if (zoomState._zoomStyle === 'onelane') {
pos[1] = corner[1] + zoomState._selection.node().clientHeight;
} else if (zoomState._zoomStyle === 'full') {
pos[1] =
zoomState._selection.node().parentNode.offsetTop +
zoomState._selection.node().parentNode.clientHeight;
}
}
// can't use { _current_corner } as it's the value not the address / object
zoomState._current_corner = [Math.round(pos[0]), Math.round(pos[1])];
},
Expand All @@ -177,6 +201,8 @@ const apiZoom = (context) => ({
_enabled: enabled,
_callback: callback,
_current_corner: null,
_selection: null, // the d3 selection that corresponds to the canva that was clicked
_zoomStyle: 'default',
_corner1: null, // the first corner of selection when you press the mouse
_corner2: null, // the second corner of selection when you release the mouse
};
Expand Down
15 changes: 7 additions & 8 deletions src/horizon/apiRender.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,8 @@ const apiRender = (context, state) => ({
.on('mousemove.horizon', function (event) {
context.focus(Math.round(d3.pointer(event)[0]));
if (context.zoom().enabled()) {
context
.zoom()
.updateCurrentCorner(
d3.pointer(event, selection.node().parentNode)
);
var position = d3.pointer(event, selection.node().parentNode);
context.zoom().updateCurrentCorner(position, selection);
}
})
.on('mouseout.horizon', () => {
Expand All @@ -43,13 +40,15 @@ const apiRender = (context, state) => ({
selection
.on('mousedown', (event) => {
if (context.zoom().enabled()) {
console.log('zoom called');
zoom.start(selection, d3.pointer(event, selection.node().parentNode));
var current_node = event.target;
var position = d3.pointer(event, selection.node().parentNode);
zoom.start(d3.select(current_node.parentNode), position);
}
})
.on('mouseup', (event) => {
if (context.zoom().enabled()) {
zoom.stop(selection, d3.pointer(event, selection.node().parentNode));
var position = d3.pointer(event, selection.node().parentNode);
zoom.stop(position);
context.focus(Math.round(d3.pointer(event)[0]));
}
});
Expand Down
59 changes: 56 additions & 3 deletions src/tests/context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,46 @@ describe('apiCSS', () => {
});

describe('apiZoom', () => {
it('should work when mode is full', () => {
const createMockSelection = () => {
const parent = { offsetTop: 72, clientHeight: 7 };
const selection = {
node: jest.fn().mockReturnValue({
parentNode: parent,
clientHeight: 5,
offsetTop: 56,
}),
};
return selection;
};
let sel = createMockSelection();
let cubismContext = context();
cubismContext.zoom().setZoomType('full');
cubismContext.zoom().start(sel, [12, 5]);
expect(cubismContext._zoom.getFirstCorner()).toStrictEqual([12, 72]);
cubismContext.zoom().updateCurrentCorner([24, 10], sel);
expect(cubismContext._zoom.getCurrentCorner()).toStrictEqual([24, 79]);
});
it('should work when mode is onelane', () => {
const createMockSelection = () => {
const parent = { offsetTop: 72, clientHeight: 7 };
const selection = {
node: jest.fn().mockReturnValue({
parentNode: parent,
clientHeight: 5,
offsetTop: 56,
}),
};
return selection;
};
let sel = createMockSelection();
let cubismContext = context();
cubismContext.zoom().setZoomType('onelane');
cubismContext.zoom().start(sel, [12, 5]);
expect(cubismContext._zoom.getFirstCorner()).toStrictEqual([12, 56]);
cubismContext.zoom().updateCurrentCorner([24, 10], sel);
expect(cubismContext._zoom.getCurrentCorner()).toStrictEqual([24, 61]);
});
it('should store the position correctly when start is called', () => {
let cubismContext = context();
cubismContext.zoom().start(null, [12, 5]);
Expand All @@ -64,7 +104,7 @@ describe('apiZoom', () => {
cubismContext._start1 = 100;
cubismContext.zoom().start(null, [12, 5]);
expect(cubismContext._zoom.getFirstCorner()).toStrictEqual([12, 5]);
cubismContext.zoom().stop(null, [24, 7]);
cubismContext.zoom().stop([24, 7]);
expect(cubismContext._zoom.getFirstCorner()).toBe(null);
expect(cubismContext._zoom.getSecondCorner()).toStrictEqual([24, 7]);
});
Expand All @@ -77,7 +117,7 @@ describe('apiZoom', () => {
cubismContext._start1 = 1;
cubismContext._start1 = 100;
cubismContext.zoom().start(null, [24, 5]);
cubismContext.zoom().stop(null, [12, 7]);
cubismContext.zoom().stop([12, 7]);
expect(cubismContext._zoom.getFirstCorner()).toBe(null);
});
it('should not call the callback in stop() if it is the same point', () => {
Expand All @@ -91,10 +131,23 @@ describe('apiZoom', () => {
cubismContext._start1 = 1;
cubismContext._start1 = 100;
cubismContext.zoom().start(null, [24, 5]);
cubismContext.zoom().stop(null, [24, 7]);
cubismContext.zoom().stop([24, 7]);
expect(cubismContext._zoom.getFirstCorner()).toBe(null);
expect(called).toBe(false);
});
it('should work when calling getZoomType', () => {
let cubismContext = context();
let called = false;
cubismContext.zoom((start: number, end: number) => {});
expect(cubismContext.zoom().getZoomType()).toBe('default');
});
it('should work when calling setZoomType', () => {
let cubismContext = context();
let called = false;
cubismContext.zoom((start: number, end: number) => {});
cubismContext.zoom().setZoomType('full');
expect(cubismContext.zoom().getZoomType()).toBe('full');
});
});

describe('apiAxis', () => {
Expand Down

0 comments on commit 180a493

Please sign in to comment.