Commit ff3c173b authored by James Long's avatar James Long Committed by Commit Bot

Update dependency visualization graph to be completely reactive

Previously, the users of the graph visualization component would need to
ping it whenever there was a data update. Now, the users should supply
as props the properties of pageModel that the graph should be reactive
to. Since the page is now fully reactive, the "update" buttons for the
inbound/outbound components have been removed.

The goal of this is to make it easier to add UI components that
interface with the visualization component.

Bug: 1106107
Change-Id: I0578ad655fc6d13d7cc9cb7e444920846e02438b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2310194
Commit-Queue: James Long <yjlong@google.com>
Reviewed-by: default avatarSamuel Huang <huangs@chromium.org>
Reviewed-by: default avatarMohamed Heikal <mheikal@chromium.org>
Cr-Commit-Position: refs/heads/master@{#790560}
parent 3109a6ee
...@@ -166,18 +166,22 @@ class GraphView { ...@@ -166,18 +166,22 @@ class GraphView {
.force('chargeForce', d3.forceManyBody().strength(-3000)) .force('chargeForce', d3.forceManyBody().strength(-3000))
.force('centerXForce', .force('centerXForce',
d3.forceX(width / 2).strength(node => { d3.forceX(width / 2).strength(node => {
if (node.visualizationState.selectedByFilter) if (node.visualizationState.selectedByFilter) {
return centeringStrengthX * 20; return centeringStrengthX * 20;
if (node.visualizationState.outboundDepth <= 1) }
if (node.visualizationState.outboundDepth <= 1) {
return centeringStrengthX * 5; return centeringStrengthX * 5;
}
return centeringStrengthY; return centeringStrengthY;
})) }))
.force('centerYForce', .force('centerYForce',
d3.forceY(height / 2).strength(node => { d3.forceY(height / 2).strength(node => {
if (node.visualizationState.selectedByFilter) if (node.visualizationState.selectedByFilter) {
return centeringStrengthY * 20; return centeringStrengthY * 20;
if (node.visualizationState.outboundDepth <= 1) }
if (node.visualizationState.outboundDepth <= 1) {
return centeringStrengthY * 5; return centeringStrengthY * 5;
}
return centeringStrengthY; return centeringStrengthY;
})); }));
......
...@@ -28,6 +28,7 @@ import {PagePathName} from '../url_processor.js'; ...@@ -28,6 +28,7 @@ import {PagePathName} from '../url_processor.js';
import LinkToGraph from './link_to_graph.vue'; import LinkToGraph from './link_to_graph.vue';
// @vue/component
const ClassDetailsPanel = { const ClassDetailsPanel = {
components: { components: {
LinkToGraph, LinkToGraph,
......
...@@ -12,15 +12,13 @@ ...@@ -12,15 +12,13 @@
:node-filter-data="pageModel.nodeFilterData" :node-filter-data="pageModel.nodeFilterData"
@[CUSTOM_EVENTS.FILTER_ELEMENT_CLICKED]="removeNodeFromFilter"/> @[CUSTOM_EVENTS.FILTER_ELEMENT_CLICKED]="removeNodeFromFilter"/>
<GraphInboundInput <GraphInboundInput
:inbound-depth-data="pageModel.inboundDepthData" :inbound-depth-data="pageModel.inboundDepthData"/>
@[CUSTOM_EVENTS.INBOUND_DEPTH_UPDATED]="setInboundDepth"/>
<GraphOutboundInput <GraphOutboundInput
:outbound-depth-data="pageModel.outboundDepthData" :outbound-depth-data="pageModel.outboundDepthData"/>
@[CUSTOM_EVENTS.OUTBOUND_DEPTH_UPDATED]="setOutboundDepth"/>
</div> </div>
<div id="graph-and-node-details-container"> <div id="graph-and-node-details-container">
<GraphVisualization <GraphVisualization
:graph-data-update-ticker="graphDataUpdateTicker" :graph-update-triggers="graphUpdateTriggers"
:page-model="pageModel" :page-model="pageModel"
@[CUSTOM_EVENTS.NODE_CLICKED]="graphNodeClicked"/> @[CUSTOM_EVENTS.NODE_CLICKED]="graphNodeClicked"/>
<div id="node-details-container"> <div id="node-details-container">
...@@ -53,9 +51,9 @@ import GraphInboundInput from './graph_inbound_input.vue'; ...@@ -53,9 +51,9 @@ import GraphInboundInput from './graph_inbound_input.vue';
import GraphOutboundInput from './graph_outbound_input.vue'; import GraphOutboundInput from './graph_outbound_input.vue';
import GraphSelectedNodeDetails from './graph_selected_node_details.vue'; import GraphSelectedNodeDetails from './graph_selected_node_details.vue';
import GraphVisualization from './graph_visualization.vue'; import GraphVisualization from './graph_visualization.vue';
import LinkToGraph from './link_to_graph.vue';
import PageUrlGenerator from './page_url_generator.vue'; import PageUrlGenerator from './page_url_generator.vue';
// @vue/component
const ClassGraphPage = { const ClassGraphPage = {
components: { components: {
ClassDetailsPanel, ClassDetailsPanel,
...@@ -65,7 +63,6 @@ const ClassGraphPage = { ...@@ -65,7 +63,6 @@ const ClassGraphPage = {
GraphOutboundInput, GraphOutboundInput,
GraphSelectedNodeDetails, GraphSelectedNodeDetails,
GraphVisualization, GraphVisualization,
LinkToGraph,
PageUrlGenerator, PageUrlGenerator,
}, },
props: { props: {
...@@ -77,9 +74,6 @@ const ClassGraphPage = { ...@@ -77,9 +74,6 @@ const ClassGraphPage = {
* @typedef {Object} ClassPageData * @typedef {Object} ClassPageData
* @property {PageModel} pageModel The data store for the page. * @property {PageModel} pageModel The data store for the page.
* @property {PagePathName} pagePathName The pathname for the page. * @property {PagePathName} pagePathName The pathname for the page.
* @property {number} graphDataUpdateTicker Incremented every time we want to
* trigger a visualization update. See graph_visualization.js for further
* explanation on this variable.
*/ */
/** /**
...@@ -92,12 +86,17 @@ const ClassGraphPage = { ...@@ -92,12 +86,17 @@ const ClassGraphPage = {
return { return {
pageModel, pageModel,
pagePathName: PagePathName.CLASS, pagePathName: PagePathName.CLASS,
graphDataUpdateTicker: 0,
}; };
}, },
computed: { computed: {
CUSTOM_EVENTS: () => CUSTOM_EVENTS, CUSTOM_EVENTS: () => CUSTOM_EVENTS,
PagePathName: () => PagePathName, graphUpdateTriggers: function() {
return [
this.pageModel.nodeFilterData.nodeList,
this.pageModel.inboundDepthData.inboundDepth,
this.pageModel.outboundDepthData.outboundDepth,
];
},
}, },
/** /**
* Parses out data from the current URL to initialize the visualization with. * Parses out data from the current URL to initialize the visualization with.
...@@ -117,7 +116,6 @@ const ClassGraphPage = { ...@@ -117,7 +116,6 @@ const ClassGraphPage = {
} }
this.setOutboundDepth(1); this.setOutboundDepth(1);
this.graphDataUpdateTicker++;
}, },
methods: { methods: {
/** /**
...@@ -125,7 +123,6 @@ const ClassGraphPage = { ...@@ -125,7 +123,6 @@ const ClassGraphPage = {
*/ */
addNodeToFilter: function(nodeName) { addNodeToFilter: function(nodeName) {
this.pageModel.nodeFilterData.addNode(nodeName); this.pageModel.nodeFilterData.addNode(nodeName);
this.graphDataUpdateTicker++;
}, },
/** /**
* Adds all supplied nodes to the node filter, then increments * Adds all supplied nodes to the node filter, then increments
...@@ -136,28 +133,24 @@ const ClassGraphPage = { ...@@ -136,28 +133,24 @@ const ClassGraphPage = {
for (const nodeName of nodeNames) { for (const nodeName of nodeNames) {
this.pageModel.nodeFilterData.addNode(nodeName); this.pageModel.nodeFilterData.addNode(nodeName);
} }
this.graphDataUpdateTicker++;
}, },
/** /**
* @param {string} nodeName The node to remove. * @param {string} nodeName The node to remove.
*/ */
removeNodeFromFilter: function(nodeName) { removeNodeFromFilter: function(nodeName) {
this.pageModel.nodeFilterData.removeNode(nodeName); this.pageModel.nodeFilterData.removeNode(nodeName);
this.graphDataUpdateTicker++;
}, },
/** /**
* @param {number} depth The new inbound depth. * @param {number} depth The new inbound depth.
*/ */
setInboundDepth: function(depth) { setInboundDepth: function(depth) {
this.pageModel.inboundDepthData.inboundDepth = depth; this.pageModel.inboundDepthData.inboundDepth = depth;
this.graphDataUpdateTicker++;
}, },
/** /**
* @param {number} depth The new outbound depth. * @param {number} depth The new outbound depth.
*/ */
setOutboundDepth: function(depth) { setOutboundDepth: function(depth) {
this.pageModel.outboundDepthData.outboundDepth = depth; this.pageModel.outboundDepthData.outboundDepth = depth;
this.graphDataUpdateTicker++;
}, },
/** /**
* @param {?GraphNode} node The selected node. May be `null`, which will * @param {?GraphNode} node The selected node. May be `null`, which will
......
...@@ -18,6 +18,7 @@ import {CUSTOM_EVENTS} from '../vue_custom_events.js'; ...@@ -18,6 +18,7 @@ import {CUSTOM_EVENTS} from '../vue_custom_events.js';
import Autocomplete from '@trevoreyre/autocomplete-vue'; import Autocomplete from '@trevoreyre/autocomplete-vue';
// @vue/component
const GraphFilterInput = { const GraphFilterInput = {
components: { components: {
Autocomplete, Autocomplete,
......
...@@ -16,6 +16,7 @@ ...@@ -16,6 +16,7 @@
<script> <script>
import {CUSTOM_EVENTS} from '../vue_custom_events.js'; import {CUSTOM_EVENTS} from '../vue_custom_events.js';
// @vue/component
const GraphFilterItems = { const GraphFilterItems = {
props: { props: {
nodeFilterData: Object, nodeFilterData: Object,
......
...@@ -7,31 +7,17 @@ ...@@ -7,31 +7,17 @@
<label for="filter-inbound">Change inbound (blue) depth:</label> <label for="filter-inbound">Change inbound (blue) depth:</label>
<input <input
id="filter-inbound" id="filter-inbound"
v-model.number="inboundDepth" v-model.number="inboundDepthData.inboundDepth"
type="number"> type="number">
<button
type="button"
@click="submitInbound">
Update Inbound
</button>
</div> </div>
</template> </template>
<script> <script>
import {CUSTOM_EVENTS} from '../vue_custom_events.js'; // @vue/component
const GraphInboundInput = { const GraphInboundInput = {
props: { props: {
inboundDepthData: Object, inboundDepthData: Object,
}, },
data: function() {
return this.inboundDepthData;
},
methods: {
submitInbound: function() {
this.$emit(CUSTOM_EVENTS.INBOUND_DEPTH_UPDATED, this.inboundDepth);
},
},
}; };
export default GraphInboundInput; export default GraphInboundInput;
......
...@@ -7,31 +7,17 @@ ...@@ -7,31 +7,17 @@
<label for="filter-outbound">Change outbound (yellow) depth:</label> <label for="filter-outbound">Change outbound (yellow) depth:</label>
<input <input
id="filter-outbound" id="filter-outbound"
v-model.number="outboundDepth" v-model.number="outboundDepthData.outboundDepth"
type="number"> type="number">
<button
type="button"
@click="submitOutbound">
Update Outbound
</button>
</div> </div>
</template> </template>
<script> <script>
import {CUSTOM_EVENTS} from '../vue_custom_events.js'; // @vue/component
const GraphOutboundInput = { const GraphOutboundInput = {
props: { props: {
outboundDepthData: Object, outboundDepthData: Object,
}, },
data: function() {
return this.outboundDepthData;
},
methods: {
submitOutbound: function() {
this.$emit(CUSTOM_EVENTS.OUTBOUND_DEPTH_UPDATED, this.outboundDepth);
},
},
}; };
export default GraphOutboundInput; export default GraphOutboundInput;
......
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
<script> <script>
import {CUSTOM_EVENTS} from '../vue_custom_events.js'; import {CUSTOM_EVENTS} from '../vue_custom_events.js';
// @vue/component
const GraphSelectedNodeDetails = { const GraphSelectedNodeDetails = {
props: { props: {
selectedNodeDetailsData: Object, selectedNodeDetailsData: Object,
......
...@@ -13,21 +13,32 @@ ...@@ -13,21 +13,32 @@
import {CUSTOM_EVENTS} from '../vue_custom_events.js'; import {CUSTOM_EVENTS} from '../vue_custom_events.js';
import {GraphView} from '../graph_view.js'; import {GraphView} from '../graph_view.js';
// @vue/component
const GraphVisualization = { const GraphVisualization = {
props: { props: {
graphDataUpdateTicker: Number, /**
* `graphUpdateTriggers` is an array of model properties that trigger an
* update in the graph.
*
* Background: The need to trigger updates make it hard to integrate
* `graph_view.js` into the reactive Vue framework. This is solved with
* `graphUpdateTriggers`, which lists the parts of `pageModel` to observe
* and propagate updates to `graph_view`. This makes the graph "reactive" to
* changes in members of `graphUpdateTriggers`.
*
* Note: Observing `pageModel` in entirety is undesirable since it would
* lead to circular rerendering.
*/
graphUpdateTriggers: Array,
pageModel: Object, pageModel: Object,
}, },
watch: { watch: {
/** graphUpdateTriggers: {
* Watching a "ticker" variable is used for now since we don't always want handler: function() {
* `graphView` to be reactive with respect to `pageModel` (eg. if the user
* is typing but has not submitted yet). This ticker hence becomes the only
* way to force the visualization to update its underlying data.
*/
graphDataUpdateTicker: function() {
this.graphView.updateGraphData(this.pageModel.getDataForD3()); this.graphView.updateGraphData(this.pageModel.getDataForD3());
}, },
deep: true,
},
}, },
/** /**
* Initializes the `GraphView` backing this visualization component. It's * Initializes the `GraphView` backing this visualization component. It's
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
<script> <script>
import {generateUrlFromFilter} from '../url_processor.js'; import {generateUrlFromFilter} from '../url_processor.js';
// @vue/component
const LinkToGraph = { const LinkToGraph = {
props: { props: {
filter: Array, filter: Array,
......
...@@ -29,6 +29,7 @@ import {shortenClassName} from '../chrome_hooks.js'; ...@@ -29,6 +29,7 @@ import {shortenClassName} from '../chrome_hooks.js';
import LinkToGraph from './link_to_graph.vue'; import LinkToGraph from './link_to_graph.vue';
// @vue/component
const PackageDetailsPanel = { const PackageDetailsPanel = {
components: { components: {
LinkToGraph, LinkToGraph,
......
...@@ -12,15 +12,13 @@ ...@@ -12,15 +12,13 @@
:node-filter-data="pageModel.nodeFilterData" :node-filter-data="pageModel.nodeFilterData"
@[CUSTOM_EVENTS.FILTER_ELEMENT_CLICKED]="removeNodeFromFilter"/> @[CUSTOM_EVENTS.FILTER_ELEMENT_CLICKED]="removeNodeFromFilter"/>
<GraphInboundInput <GraphInboundInput
:inbound-depth-data="pageModel.inboundDepthData" :inbound-depth-data="pageModel.inboundDepthData"/>
@[CUSTOM_EVENTS.INBOUND_DEPTH_UPDATED]="setInboundDepth"/>
<GraphOutboundInput <GraphOutboundInput
:outbound-depth-data="pageModel.outboundDepthData" :outbound-depth-data="pageModel.outboundDepthData"/>
@[CUSTOM_EVENTS.OUTBOUND_DEPTH_UPDATED]="setOutboundDepth"/>
</div> </div>
<div id="graph-and-node-details-container"> <div id="graph-and-node-details-container">
<GraphVisualization <GraphVisualization
:graph-data-update-ticker="graphDataUpdateTicker" :graph-update-triggers="graphUpdateTriggers"
:page-model="pageModel" :page-model="pageModel"
@[CUSTOM_EVENTS.NODE_CLICKED]="graphNodeClicked"/> @[CUSTOM_EVENTS.NODE_CLICKED]="graphNodeClicked"/>
<div id="node-details-container"> <div id="node-details-container">
...@@ -55,6 +53,7 @@ import GraphVisualization from './graph_visualization.vue'; ...@@ -55,6 +53,7 @@ import GraphVisualization from './graph_visualization.vue';
import PackageDetailsPanel from './package_details_panel.vue'; import PackageDetailsPanel from './package_details_panel.vue';
import PageUrlGenerator from './page_url_generator.vue'; import PageUrlGenerator from './page_url_generator.vue';
// @vue/component
const PackageGraphPage = { const PackageGraphPage = {
components: { components: {
GraphFilterInput, GraphFilterInput,
...@@ -75,9 +74,6 @@ const PackageGraphPage = { ...@@ -75,9 +74,6 @@ const PackageGraphPage = {
* @typedef {Object} PackagePageData * @typedef {Object} PackagePageData
* @property {PageModel} pageModel The data store for the page. * @property {PageModel} pageModel The data store for the page.
* @property {PagePathName} pagePathName The pathname for the page. * @property {PagePathName} pagePathName The pathname for the page.
* @property {number} graphDataUpdateTicker Incremented every time we want to
* trigger a visualization update. See graph_visualization.js for further
* explanation on this variable.
*/ */
/** /**
...@@ -90,11 +86,17 @@ const PackageGraphPage = { ...@@ -90,11 +86,17 @@ const PackageGraphPage = {
return { return {
pageModel, pageModel,
pagePathName: PagePathName.PACKAGE, pagePathName: PagePathName.PACKAGE,
graphDataUpdateTicker: 0,
}; };
}, },
computed: { computed: {
CUSTOM_EVENTS: () => CUSTOM_EVENTS, CUSTOM_EVENTS: () => CUSTOM_EVENTS,
graphUpdateTriggers: function() {
return [
this.pageModel.nodeFilterData.nodeList,
this.pageModel.inboundDepthData.inboundDepth,
this.pageModel.outboundDepthData.outboundDepth,
];
},
}, },
/** /**
* Parses out data from the current URL to initialize the visualization with. * Parses out data from the current URL to initialize the visualization with.
...@@ -124,7 +126,6 @@ const PackageGraphPage = { ...@@ -124,7 +126,6 @@ const PackageGraphPage = {
*/ */
addNodeToFilter: function(nodeName) { addNodeToFilter: function(nodeName) {
this.pageModel.nodeFilterData.addNode(nodeName); this.pageModel.nodeFilterData.addNode(nodeName);
this.graphDataUpdateTicker++;
}, },
/** /**
* Adds all supplied nodes to the node filter, then increments * Adds all supplied nodes to the node filter, then increments
...@@ -135,28 +136,24 @@ const PackageGraphPage = { ...@@ -135,28 +136,24 @@ const PackageGraphPage = {
for (const nodeName of nodeNames) { for (const nodeName of nodeNames) {
this.pageModel.nodeFilterData.addNode(nodeName); this.pageModel.nodeFilterData.addNode(nodeName);
} }
this.graphDataUpdateTicker++;
}, },
/** /**
* @param {string} nodeName The node to remove. * @param {string} nodeName The node to remove.
*/ */
removeNodeFromFilter: function(nodeName) { removeNodeFromFilter: function(nodeName) {
this.pageModel.nodeFilterData.removeNode(nodeName); this.pageModel.nodeFilterData.removeNode(nodeName);
this.graphDataUpdateTicker++;
}, },
/** /**
* @param {number} depth The new inbound depth. * @param {number} depth The new inbound depth.
*/ */
setInboundDepth: function(depth) { setInboundDepth: function(depth) {
this.pageModel.inboundDepthData.inboundDepth = depth; this.pageModel.inboundDepthData.inboundDepth = depth;
this.graphDataUpdateTicker++;
}, },
/** /**
* @param {number} depth The new outbound depth. * @param {number} depth The new outbound depth.
*/ */
setOutboundDepth: function(depth) { setOutboundDepth: function(depth) {
this.pageModel.outboundDepthData.outboundDepth = depth; this.pageModel.outboundDepthData.outboundDepth = depth;
this.graphDataUpdateTicker++;
}, },
/** /**
* @param {?GraphNode} node The selected node. May be `null`, which will * @param {?GraphNode} node The selected node. May be `null`, which will
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
<script> <script>
import {generateUrlFromFilter} from '../url_processor.js'; import {generateUrlFromFilter} from '../url_processor.js';
// @vue/component
const PageUrlGenerator = { const PageUrlGenerator = {
props: { props: {
pagePathName: String, pagePathName: String,
......
...@@ -8,9 +8,7 @@ const CUSTOM_EVENTS = { ...@@ -8,9 +8,7 @@ const CUSTOM_EVENTS = {
ADD_TO_FILTER_CLICKED: 'add-to-filter-clicked', ADD_TO_FILTER_CLICKED: 'add-to-filter-clicked',
FILTER_ELEMENT_CLICKED: 'filter-element-clicked', FILTER_ELEMENT_CLICKED: 'filter-element-clicked',
FILTER_SUBMITTED: 'filter-submitted', FILTER_SUBMITTED: 'filter-submitted',
INBOUND_DEPTH_UPDATED: 'inbound-depth-updated',
NODE_CLICKED: 'node-clicked', NODE_CLICKED: 'node-clicked',
OUTBOUND_DEPTH_UPDATED: 'outbound-depth-updated',
REMOVE_FROM_FILTER_CLICKED: 'remove-from-filter-clicked', REMOVE_FROM_FILTER_CLICKED: 'remove-from-filter-clicked',
}; };
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment