-
Notifications
You must be signed in to change notification settings - Fork 472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
basic brushing and zooming feature for line chart and point chart #851
Conversation
@cnwangjie great start! a few things:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just some minor coding style comments based on a quick once over. I may request other changes later. :)
src/js/common/brush.js
Outdated
@@ -0,0 +1,95 @@ | |||
function mg_create_brushing_pattern(args, range) { | |||
var extentRect; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please use const
and the ternary operator here:
const extentRect = (d3.select(args.target).select('.mg-extent').size()) ?
d3.select(args.target).select('.mg-extent') :
d3.select(args.target)
.select('.mg-rollover-rect, .mg-voronoi')
.insert('g', '*')
.classed('mg-brush', true)
.append('rect')
.classed('mg-extent', true);
In general we should try to use let
or const
for new code (http://airbnb.io/javascript/#references--prefer-const)
src/js/common/brush.js
Outdated
var mouseDown = false; | ||
var origin = []; | ||
rollover.classed('mg-brush-container', true); | ||
rollover.on('mousedown', function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New functions should be defined using the arrow syntax:
src/js/common/zoom.js
Outdated
var xScale = args.scales.X; | ||
domain.x = []; | ||
domain.x[0] = +xScale.invert(range.x[0]); | ||
domain.x[1] = +xScale.invert(range.x[1]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to define the array like this:
domain.x = range.x.map(v => +xScale.invert(v));
@wlach Thank you for your review. I have updated this PR, following are changes I made. fixes some obvious bugsFor the bug you say:
I think it is because the selection range is too small (no any data point in this range). When the mouse leaves the chart and up, there will not be But for zooming does not work for point charts, it can work on me. Could you try it again? If it still doesn't work, I will make some changes for it. I added two attributes I've added these two files to the I changed the two files I added to ES6 styleYou want to use the keywords I also hope that this project can be more modular. And I'm happy to make changes for this for a long time. Can you look it again? If there are no other problems and you think it is ready to be merged, I would squash these commits. |
@cnwangjie hmm, so I downloaded your latest changes and it looks like things are working better with point charts, but still not quite working. I can drag a selection but nothing seems to actually select. Here's the js I have: //modify away!
MG.data_graphic({
title: "UFO Sightings",
description: "Yearly UFO sightings from 1945 to 2010.",
data: JSON.parse(document.querySelector('.data textarea').value),
markers: [{'year': 1964, 'label': '"The Creeping Terror" released'}],
width: 400,
height: 250,
target: ".result",
x_accessor: "year",
y_accessor: "sightings",
xax_count: 3,
chart_zoom: "xy",
chart_type: "point"
}); Here's the error I'm seeing in the console: The code itself is looking pretty good! Most of the code is indeed written in ES5 style, but we're gradually moving things over. I'm hoping we'll have the process complete by end of summer or so. |
src/js/common/zoom.js
Outdated
} | ||
|
||
return d => { | ||
const x_in_range = ('x' in range) ? is_data_in_range(d[args.x_accessor], range.x) : true; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be restated more simply as:
const x_in_range = !('x' in range) || is_data_in_range(d[args.x_accessor], range.x);
Same with the statement just below.
@wlach I spent some time finding the reasons for this case. The direct reason is the type of "year" is string, so I can infer the range of the data that needs to be displayed for this situation. But I'm not sure if it's appropriate to add brushing function to this type ( If we convert this data to number another problem (same as #822) will be displayed. I wrote this problem in detail under that issue. But I'm not sure what the auhtor intended, so I just put forward some of my ideas and did not directly modify it. I hope to discuss this with you and other members. |
@cnwangjie oh right! that would explain it. for a first pass, it might just make sense to disable/ignore zooming on this type of chart (categorical). @hamilton -- do you have any opinions here? |
I concur - let's disable / ignore zooming for categorical charts for now. |
src/js/common/brush.js
Outdated
if (mouseDown) { | ||
isDragging = true; | ||
rollover.classed('mg-brushing', true); | ||
const range = calculateSelectionRange(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need to create this variable, just pass the value of calculateSelectionRange()
directly to the function below.
src/js/common/brush.js
Outdated
MG.zoom_to_raw_range(args); | ||
} | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this newline
src/js/common/zoom.js
Outdated
|
||
const zoom_to_raw_range = args => { | ||
if (!('raw_domain' in args)) return; | ||
const range = args.raw_domain; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Again, no need for a temporary here.
src/js/common/zoom.js
Outdated
const zoom_to_data_domain = (args, range) => { | ||
const raw_data = args.raw_data || args.data; | ||
if (!('raw_data' in args)) { | ||
args.raw_domain = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would probably put raw_domain
and raw_data
under args.processed
, since they don't represent user input.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think so, but there are many variables under args.
src/js/common/zoom.js
Outdated
args.data = raw_data.filter(filter_in_range_data(args, range)); | ||
} | ||
} | ||
if ('x' in range) args.processed.zoom_x = range.x; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe try this approach instead:
['x', 'y'].forEach(dim => {
if (dim in range) {
args.processed[`zoom_${dim}`] = range[dim];
} else {
delete args.processed[`zoom_${dim}`];
}
});
src/js/common/zoom.js
Outdated
const yScale = args.scales.Y; | ||
domain.y = range.y.map(v => +yScale.invert(v)).reverse(); | ||
} | ||
return domain; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could probably do something similar to what I suggested just above with a forEach loop here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you need to reduce the number of lines of code this function can be written as follows. And just only avoid one repeat.
const convert_range_to_domain = (args, range) =>
['x', 'y'].reduce((domain, dim) => {
if (!(dim in range)) return domain;
domain[dim] = range[dim].map(v => +args.scales[dim.toUpperCase()].invert(v));
if (dim === 'y') domain[dim].reverse();
return domain;
}, {});
Also it is probably to change
return d => {
const x_in_range = !('x' in range) || is_data_in_range(d[args.x_accessor], range.x);
const y_in_range = !('y' in range) || is_data_in_range(d[args.y_accessor], range.y);
return x_in_range && y_in_range;
}
to
return d => ['x', 'y'].every(dim => !(dim in range) || is_data_in_range(d[args[`${dim}_accessor`]], range[dim]))
Also some others.
But does this make the code a bit difficult to understand? 😕
I'm not sure if this is necessary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cnwangjie I think it's pretty understandable, at least to someone who knows what they're looking for. In general I prefer this kind of "denser" style of code-- it requires a bit more functional programming knowledge to understand, but it's much more concise.
If you think a particular section is unclear, it never hurts to put in a comment like this:
// Converts a selection range into a set of coordinates that we can use to zoom
// the chart to a particular region
const convert_range_to_domain = (args, range) =>
...
src/js/common/brush.js
Outdated
|
||
const add_brush_function = args => { | ||
if (!args.zoom_target) args.zoom_target = args; | ||
const zoom_target = args.zoom_target; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you need this temporary variable.
@wlach I have modified my code based on your suggestion. And display warning when axis type is 'categorical'. But I have some confusion about the necessity of doing so. I used to think that the code should be easy to understand, so I like to use some temporary variables. And expect the compiler to optimize the performance of the code. But if you think that it should be done, I'm also happy to make some changes to do it. 😂 |
@cnwangjie yes, of course the compiler with optimize the variables. but I think it's still a bit unnecessary to use so many temporaries. This is largely a matter of preference, but I'd encourage you to give it a try for a little while and see how you like it. :) As I mention above, comments can often help explain more difficult sections. |
@cnwangjie ok, I'm pretty happy with things now! One last thing before we merge, can you update https://github.com/metricsgraphics/metrics-graphics/blob/master/examples/charts/addons.htm Probably best to rename the file to "brushing_zooming.htm" and "addons" to "brushing & zooming" |
34cef99
to
d2e9390
Compare
@wlach OK. I have updated the examples and supplied comments for some functions, and squashed unnecessary history commits. Besides, I added some simple descriptions for two new options in wiki "List of Options" & "Graphic" pages. I think I can add a link to the example in the wiki after the example pages are updated. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good! Just a few minor changes requested
examples/charts/brushing_zooming.htm
Outdated
} | ||
const main = { | ||
title: "Overview Plot", | ||
description: "This is a simple for creating an overview plot. You can create an overview plot easily by creating another chart with 'zoom_target' option and set it as the object of main chart. Besides, you can hide axis and active point to simplify the overview plot.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"This is a simple for creating an overview plot. You can create an overview plot easily by creating another chart with 'zoom_target' option and set it as the object of main chart. Besides, you can hide axis and active point to simplify the overview plot."
=>
"This is a simple example of an overview plot. You can create an overview plot by creating another chart with 'zoom_target' option and then setting it as the object of the main chart."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P.S. I didn't realize you left the overview plot functionality in. :) That's ok-- there are a few features I would like to add to that in a followup. We can discuss next week. :)
examples/charts/brushing_zooming.htm
Outdated
} | ||
MG.data_graphic({ | ||
title: "Basic Brushing & Zooming", | ||
description: "This is a simple for brushing and zooming feature. You can set 'brush' as 'xy', 'x', 'y' to restrict axis.", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"This is a simple for brushing and zooming feature. You can set 'brush' as 'xy', 'x', 'y' to restrict axis." => "This is a simple example of brushing and zooming. You can set 'brush' as 'xy', 'x', 'y' to specify the axis(es) that may be brushed."
examples/examples.htm
Outdated
@@ -100,7 +102,7 @@ | |||
<li><a href='#' id='goto-updating' class='pill'>Updating</a></li> | |||
<li><a href='#' id='goto-other' class='pill'>Other</a></li> | |||
<li><a href='#' id='goto-experimental' class='pill'>Experimental</a></li> | |||
<li><a href='#' id='goto-addons' class='pill'>Addons</a></li> | |||
<li><a href='#' id='goto-brushing_zooming' class='pill'>Brushing & Zooming</a></li> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you put this before experimental
? Also, you should generally use &
for specifying the ampersand in html :)
- change some syntax to ES6 & fix some obvious problems (5.5) - change some format & warning on categorical (5.17) - display warning when axis type is 'categorical' - add some comment - update the example page for this feature and remove outdated addons (5.18)
@wlach ok, I've changed. |
Awesome! It's a holiday weekend here in Canada, I'll do one last review if I have the time, otherwise I will get to this first thing Tuesday. |
Thanks @cnwangjie. As mentioned there are a few tweaks i would like to make to this implementation (e.g. I would like to persist the selection in the case that we're brushing over an overview chart), but i think this is a good incremental step. Impressive start to the GSOC project! |
This basically implements the brushing and zooming feature like
mg-line-brushing
. It may be not ready to be merged into master branch. But I hope to share some problems I found and to get some suggestions.Some difference with the addons
descriptor
to redraw the chart for avoiding repeated call hooks.Some problems
For the line chart, we need not to filter the data out of range in order to ensure the integrity of the line.
But for the point chart, we should drop those data out of range, if not there will be some points drawn outside the chart because of the different ways of drawing. I tried to filter them in the
mainPlot
method in point.js but the axis will still be drawn. So I filter the data when the chart type is point. I don’t know if this is appropriate?Because different types of charts are drawn in different ways, some changes need to be made for them. Do I need to support this function for other charts except line and point?
I largely borrowed code of the addons, so that some operations I'm not sure whether it has the right effect. Such as convert mouse selection range to data domain and set the range of selection graphic.
Here is my effect on manual testing of some data.
![201805041216561525364216594_small](https://user-images.githubusercontent.com/7459652/39609878-cffffa1e-4f7e-11e8-90e2-6d33ed95c79c.gif)
Now I only added
brush
andzoom_target
. This is enough to restrict brushing axis and add an overview plot. Should I add more options or is there any better option?For setting a zooming target chart, I don't know how to do it better. The way I am now taking is to set an args as a variable and set it to another option. Like following way. Are there any better ways?