Skip to content

Match vision tab overhaul #469

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

Merged
merged 23 commits into from
Nov 14, 2016
Merged

Conversation

micaelbergeron
Copy link

@micaelbergeron micaelbergeron commented Nov 11, 2016

fixes #342

This PR addresses #342 and will add a bunch of new features to the Vision Tab, including:

  • Dynamic ward filtering
  • Player/Ward type filtering
  • Time slider (controls visible wards on the map)
  • Ward log
  • Counterwarding (require Add a warding processor parser#3)

Keep in mind that this is a work in progress.
I've also refactored certain components while working on this page.

There are still refactoring/extracting that needs to be done.

Comments welcome.

micael.bergeron added 7 commits October 27, 2016 14:15
remove the usage of redux's Connect from the vision page by using the
PlainTable instead of the default Table (TableWithOptions).

there are still performance issues, buut it is usable.
i've decided to not use a <table> anymore to host this data as it seems
the material-ui table causes some issues with transitions and animating
table row is hacky at best

a simple <ul> implementation seems to be more performant
# Conflicts:
#	src/components/Match/Match.css
#	src/components/Match/VisionMap.jsx
#	src/components/Match/matchColumns.jsx
#	src/components/TabBar/TabBar.jsx
#	src/components/Table/Table.css
#	src/components/Table/Table.jsx
#	src/components/Visualizations/Table/HeroImage.jsx
#	webpack.config.js
enabledIndex: {},
});
shouldComponentUpdate(newProps) {
if (newProps.wardsLog.length == this.props.wardsLog.length) return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use ===

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a boolean so you can return it directly

enabledIndex: {},
});
shouldComponentUpdate(newProps) {
if (newProps.wardsLog.length == this.props.wardsLog.length) return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a boolean so you can return it directly

this.updateMap = this.updateMap.bind(this);

componentDidMount() {
window.addEventListener("resize", () => this.forceUpdate());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @mike-robertson will advise that you use redux-responsive's state.browser.width for this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's iterate on this once the component is shipped.

class PlayerFilter extends React.PureComponent {
constructor(props) {
super(props);
this.getObserverCount = () => this.props.player.obs_log.length;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'd advise making these functions outside the class, and take props as a parameter.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component will be refactored.

<Col xs>
<Button {...this.getMuiThemeProps()}
label={obs_count}
disabled={obs_count == 0}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use !obs_count

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

backgroundColor={this.generateFilterKey("observer") in this.props.activeFilters ? filterOff : filterOn}
style={{opacity: obs_count > 0 ? opacityOn : opacityOff}}
onClick={() => onFilterClick(this.generateFilterKey("observer"), player.player_slot, "observer")}
icon={<Avatar size={24} src="http://a19a1164.ngrok.io/apps/dota2/images/items/ward_observer_lg.png" />}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use API_HOST

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prototype left-overs.


class VisionPage extends React.Component {
componentWillMount() {
Perf.start();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be disabled for production?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't react-addons-perf already doing this under the hood?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be removed.

return _.rangeStep(interval, 0, this.props.match.duration);
}

_handleViewportChange(value) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think eslint will complain about the use of underscores since javascript doesn't really have any idea of private properties.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that was to test quickly the debounce, I'll figure out a better name


// a simple functor that will call the correct function depending on value
const threshold = _.curry((start, limits, values, value) => {
if (limits.length != values.length) throw "Limits must be the same as functions.";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!==

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to extract this functor in utility

}
};

export const Pure = (Component) => class extends React.PureComponent {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's the difference between this and a function component?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no difference, it just reads better when using an existing component.

const PureRow = Pure(Row);

@@ -0,0 +1,16 @@
import React from 'react';

export const Fixed = (Component) => class extends React.Component {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't the Redux store take care of this kind of comparison already?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Vision filters are in the local state r/n.

@micaelbergeron
Copy link
Author

I must lint this.

@micaelbergeron
Copy link
Author

I think this is ready to ship.

@howardchung
Copy link
Member

Reviewers:
@albertcui
@blukai
@mike-robertson
@dartharaknus

Copy link
Member

@blukai blukai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it wont work for me VisionPage.jsx?6a5e:62 - Uncaught TypeError: Cannot assign to read only property 'key' of object '#<Object>'

@@ -1,5 +1,16 @@
@import "../palette.css";

:root {
--ward-log-height: 50px;
--slider-ticks-color: #757575;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this colors should be defined in palette.css

"material-ui": "0.16.1",
"moment-duration-format": "^1.3.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this new 7 dependencies?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let me explain why those have been added:

  • Lodash: I don't feel like implementing my how flow() to compose my filters. I suggest we use the 'lodash/fp' import that is more geared toward functional programming. (I also use: inRange, sortByIndex, map/filter).
  • moment-duration-format: I don't think I'm using it, so I guess I'll remove it. Good catch.
  • react-addons-css-transition-group: de facto transition component for React
  • react-measure: This is to deal with the resizing of the page. @howardchung suggested I use the Redux store size, but I don't see how that can integrate with the flexgrid sizing. @mike-robertson you are welcome to comment.
  • seamless-immutable: okay that is a subject of its own, but this manage immutable Object/Array and make it so deeply nested values can be compared with a simple reference equality check. Having immutable objects in the Redux store is considered a good practice.
  • react-addons-perf: That is very helpful to dig down React rendering and find performance issues
  • react-perf-tool: I think that expose it to the Chrome extension

@@ -148,6 +159,117 @@
vertical-align: middle;
}

.ward-log {
width: 100%;
font-size: .8em;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have some defined font sizes in palette.css. I guess 0.8em is somehing like 12px so u can use --fontSizeSmall

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll oblige but I think that using px as font size is a bad practice for mobility. I'd rather leave it that way or refactor to use em/rem all around.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should link it to --fontSizeSmall then convert that em then

Copy link
Member

@blukai blukai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Some things that we should fix someday:

Also it can be done in this pr

const [opacityOn, opacityOff] = [1, 0.4];
return (
<Row
className={styles['filter-row']}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We use camelcase in the css files so we don't need to do things like this. I know camelCase isn't standard css, but for css-modules, it is.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

});
}

generateFilterKey(type) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't you need to bind this in the constructor so you don't lose 'this' context?

Copy link
Author

@micaelbergeron micaelbergeron Nov 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be static.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seems so.

{props.ticks.map((tick) => {
const [t, min, max] = [tick, props.min, props.max];
const percent = 100 * ((t - min) / (max - min));
const cls = [styles['slider-tick']];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use camelCase to avoid this

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

);

const PipelineFilter = (filters, data, iter = Array.prototype.filter) => {
const filtered = filters.map(f => iter.call(data, f))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const filteredData = filters.reduce(
  (arr, filt) => arr.filter(filt),
  data
);
return data.filter(obj => !filteredData.includes(obj));

Doesn't this do the same thing but is easier to understand? Plus no lodash, and you don't need to have a reference to the Array.prototype.filter function.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% sure the implementation is does exactly the same thing.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can remove the prototype reference.

this.ticks = this.computeTick();
this.findPivot = value => _.flow(_.map(x => x.entered.time),
_.sortedIndex(value))(this.state.wardsLog);
this.handleViewportChange = _.debounce(50, this.viewportChange);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not just do:

this.handleViewportChange = () => setTimeout(this.viewportChange, 50);

No reason to drag in a third party library for that, right?

Copy link
Author

@micaelbergeron micaelbergeron Nov 12, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not debouncing, this is deferring. You'll still call on every change event, 50ms after the call stack has been emptied.


const observers = extractWardLog('observer', player.obs_log, player.obs_left_log);
const sentries = extractWardLog('sentry', player.sen_log, player.sen_left_log);
return _.concat(observers, sentries);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

observers.concat(sentries)

_.flatten,
_.sortBy(xs => xs.entered.time),
imap((x, i) => ({ ...x, key: i })),
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this can avoid using lodash, I would prefer that, but I don't know exactly what 'convert' is doing here. It seems weird to me that we would need to flatten this array one level, but I don't know what the data looks like.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fp flavor cap the iteratee to a certain arrity, this removes that cap. It is basically to have the index in the iteratee function.

newPlayer.objective_damage[identifier] + player.damage[key] :
player.damage[key];
newPlayer.objective_damage[identifier] + player.damage[key] :
player.damage[key];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure I am a fan of this tabbing, but if others are I won't fight it.

@@ -360,3 +373,12 @@ export function unpackPositionData(input) {
}
return input;
}

export const threshold = _.curry((start, limits, values, value) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment here, you could just invoke this function like threshold()()()() and manually curry the function definition but it you are against doing that (so you can do threshold(1, 2, 3)(4)), I'll defer to your judgement.


const limitsWithStart = limits.slice(0);
limitsWithStart.unshift(start);
return findLast(values, (v, i) => _.inRange(limitsWithStart[i], limitsWithStart[i + 1], value));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useful method, but I don't know if it is worth bringing in all of lodash for. Though if we can get away with the 4kb gzipped lodash and you want to use these, I'm down with that. I'm mostly against using lodash when we could instead use native implementations at essentially no extra cost.

@howardchung
Copy link
Member

will this integrate #445 ?

@micaelbergeron
Copy link
Author

Let's merge this first then iterate on it if you guys don't mind. #445 is easy to add after.

@micaelbergeron
Copy link
Author

I didn't remove lodash usage but I tried to use native methods when available and not changing the implementation.

})
;

const observers = extractWardLog('observer', player.obs_log, player.obs_left_log);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this crash if !player.obs_log (unparsed match?)

Copy link
Author

@micaelbergeron micaelbergeron Nov 14, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed this, I really thought this was handled up in the Component hierarchy.

@howardchung howardchung merged commit 4443323 into odota:master Nov 14, 2016
masad-frost pushed a commit to masad-frost/ui that referenced this pull request Jan 20, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

use ward logs for ward map
5 participants