Skip to content

Commit

Permalink
feat: avoid creating new view when only width or height in spec chang…
Browse files Browse the repository at this point in the history
…es (#95)

* feat: add library

* feat: add isSpecChanged

* feat: detect background change

* refactor: rename updateVegaViewData to updateVegaViewDataset

* feat: avoid creating new view if not necessary

* docs: add storybook demo

* fix: disable padding

* docs: add more data to demo

* build: coverage threshold
  • Loading branch information
kristw committed Feb 6, 2020
1 parent ae2c634 commit d4a87e7
Show file tree
Hide file tree
Showing 12 changed files with 270 additions and 49 deletions.
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,10 @@
],
"coverageThreshold": {
"global": {
"branches": 10,
"functions": 10,
"lines": 10,
"statements": 10
"branches": 5,
"functions": 5,
"lines": 5,
"statements": 5
}
}
},
Expand Down
79 changes: 79 additions & 0 deletions packages/react-vega-demo/stories/ChangingDimensionDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from "react";
import { VegaLite } from "../../react-vega/src";

const values = [];

for (let i = 0; i< 100 ;i++ ) {
values.push({
a: `X${i}`,
b: Math.round(Math.random() * 1000)
})
}

export default class ChangingDimensionDemo extends React.Component<{}, {
width: number;
height: number;
padding: number | {
top?: number;
bottom?: number;
left?: number;
right?: number;
};
grow: boolean;
}> {
interval: NodeJS.Timeout;

constructor(props) {
super(props);
this.state = {
width: 100,
height: 100,
padding: 20,
grow: true,
};
}

componentDidMount() {
this.interval = setInterval(() => {
this.setState(({width, height, padding, grow}) => ({
width: width + (grow ? 1 : -1),
height: height + (grow ? 1 : -1),
grow: grow && width < 400 || !grow && width === 100
}));
}, 10);
}

componentWillUnmount() {
clearInterval(this.interval);
}

render() {
const { width, height, padding } = this.state;

const SPEC = {
$schema: "https://vega.github.io/schema/vega-lite/v4.json",
width,
height,
padding,
data: {
values,
name: "source"
},
selection: {
a: { type: "single" }
},
mark: "bar",
encoding: {
x: { field: "a", type: "ordinal" },
y: { field: "b", type: "quantitative" },
tooltip: { field: "b", type: "quantitative" },
color: {
condition: { selection: "a", value: "steelblue" },
value: "grey"
}
}
} as const;

return <VegaLite spec={SPEC} />;
}
}
4 changes: 3 additions & 1 deletion packages/react-vega-demo/stories/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import React from 'react';
import { storiesOf } from '@storybook/react';
import ReactVegaDemo from './ReactVegaDemo';
import ReactVegaLiteDemo from './ReactVegaLiteDemo';
import ChangingDimensionDemo from './ChangingDimensionDemo';
import './style.css';

storiesOf('react-vega', module)
.add('Vega', () => <ReactVegaDemo />)
.add('VegaLite', () => <ReactVegaLiteDemo />);
.add('VegaLite', () => <ReactVegaLiteDemo />)
.add('Changing dimension', () => <ChangingDimensionDemo />);
3 changes: 2 additions & 1 deletion packages/react-vega/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
],
"dependencies": {
"@types/react": "^16.9.19",
"vega-embed": "^6.2.2"
"vega-embed": "^6.2.2",
"fast-deep-equal": "^3.1.1"
},
"peerDependencies": {
"react": "^16.10.0",
Expand Down
29 changes: 4 additions & 25 deletions packages/react-vega/src/Vega.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,14 @@
import React from 'react';
import { vega } from 'vega-embed';
import shallowEqual from './utils/shallowEqual';
import updateMultipleDatasetsInView from './utils/updateMultipleDatasetsInView';
import VegaEmbed, { VegaEmbedProps } from './VegaEmbed';
import isFunction from './utils/isFunction';
import { PlainObject, View, ViewListener } from './types';
import shallowEqual from './utils/shallowEqual';
import { NOOP } from './constants';

export type VegaProps = VegaEmbedProps & {
data?: PlainObject;
};

function updateData(view: View, name: string, value: unknown) {
if (value) {
if (isFunction(value)) {
value(view.data(name));
} else {
view.change(
name,
vega
.changeset()
.remove(() => true)
.insert(value),
);
}
}
}

const EMPTY = {};

export default class Vega extends React.PureComponent<VegaProps> {
Expand Down Expand Up @@ -55,13 +38,9 @@ export default class Vega extends React.PureComponent<VegaProps> {
const { data } = this.props;

if (data) {
const datasetNames = Object.keys(data);

if (this.vegaEmbed.current && datasetNames.length > 0) {
if (this.vegaEmbed.current && Object.keys(data).length > 0) {
this.vegaEmbed.current.modifyView(view => {
datasetNames.forEach(name => {
updateData(view, name, data[name]);
});
updateMultipleDatasetsInView(view, data);
view.resize().run();
});
}
Expand Down
69 changes: 53 additions & 16 deletions packages/react-vega/src/VegaEmbed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { ViewListener, View, SignalListeners } from './types';
import shallowEqual from './utils/shallowEqual';
import getUniqueFieldNames from './utils/getUniqueFieldNames';
import { NOOP } from './constants';
import addSignalListenersToView from './utils/addSignalListenersToView';
import computeSpecChanges from './utils/computeSpecChanges';
import removeSignalListenersFromView from './utils/removeSignalListenersFromView';

export type VegaEmbedProps = {
className?: string;
Expand All @@ -26,16 +29,60 @@ export default class VegaEmbed extends React.PureComponent<VegaEmbedProps> {
componentDidUpdate(prevProps: VegaEmbedProps) {
const fieldSet = getUniqueFieldNames([this.props, prevProps]) as Set<keyof VegaEmbedProps>;
fieldSet.delete('className');
fieldSet.delete('style');
fieldSet.delete('signalListeners');
fieldSet.delete('spec');
fieldSet.delete('style');

// Only create a new view if necessary
if (
Array.from(fieldSet).some(f => this.props[f] !== prevProps[f]) ||
!shallowEqual(this.props.signalListeners, prevProps.signalListeners)
) {
if (Array.from(fieldSet).some(f => this.props[f] !== prevProps[f])) {
this.clearView();
this.createView();
} else {
const specChanges = computeSpecChanges(this.props.spec, prevProps.spec);

const { signalListeners: newSignalListeners } = this.props;
const { signalListeners: oldSignalListeners } = prevProps;

if (specChanges) {
if (specChanges.isExpensive) {
this.clearView();
this.createView();
} else {
const areSignalListenersChanged = !shallowEqual(newSignalListeners, oldSignalListeners);
this.modifyView(view => {
if (specChanges.width !== false) {
view.width(specChanges.width);
}
if (specChanges.height !== false) {
view.height(specChanges.height);
}
if (areSignalListenersChanged) {
if (oldSignalListeners) {
removeSignalListenersFromView(view, oldSignalListeners);
}
if (newSignalListeners) {
addSignalListenersToView(view, newSignalListeners);
}
}

view.run();
});
}
} else {
const areSignalListenersChanged = !shallowEqual(newSignalListeners, oldSignalListeners);
this.modifyView(view => {
if (areSignalListenersChanged) {
if (oldSignalListeners) {
removeSignalListenersFromView(view, oldSignalListeners);
}
if (newSignalListeners) {
addSignalListenersToView(view, newSignalListeners);
}
}

view.run();
});
}
}
}

Expand Down Expand Up @@ -71,19 +118,9 @@ export default class VegaEmbed extends React.PureComponent<VegaEmbedProps> {
if (this.containerRef.current) {
this.viewPromise = vegaEmbed(this.containerRef.current, spec, options)
.then(({ view }) => {
const signalNames = Object.keys(signalListeners);
signalNames.forEach(signalName => {
try {
view.addSignalListener(signalName, signalListeners[signalName]);
} catch (error) {
// eslint-disable-next-line no-console
console.warn('Cannot add invalid signal handler >>', error);
}
});
if (signalNames.length > 0) {
if (addSignalListenersToView(view, signalListeners)) {
view.run();
}

return view;
})
.catch(this.handleError);
Expand Down
15 changes: 15 additions & 0 deletions packages/react-vega/src/utils/addSignalListenersToView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { View, SignalListeners } from '../types';

export default function addSignalListenersToView(view: View, signalListeners: SignalListeners) {
const signalNames = Object.keys(signalListeners);
signalNames.forEach(signalName => {
try {
view.addSignalListener(signalName, signalListeners[signalName]);
} catch (error) {
// eslint-disable-next-line no-console
console.warn('Cannot add invalid signal listener.', error);
}
});

return signalNames.length > 0;
}
62 changes: 62 additions & 0 deletions packages/react-vega/src/utils/computeSpecChanges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { VisualizationSpec } from 'vega-embed';
import equal from 'fast-deep-equal';
import getUniqueFieldNames from './getUniqueFieldNames';

interface SpecChanges {
width: false | number;
height: false | number;
isExpensive: boolean;
}

export default function computeSpecChanges(newSpec: VisualizationSpec, oldSpec: VisualizationSpec) {
if (newSpec === oldSpec) return false;

const changes: SpecChanges = {
width: false,
height: false,
isExpensive: false,
};

const fieldNames = getUniqueFieldNames([newSpec, oldSpec]);

if (
fieldNames.has('width') &&
(!('width' in newSpec) || !('width' in oldSpec) || newSpec.width !== oldSpec.width)
) {
if ('width' in newSpec && typeof newSpec.width === 'number') {
changes.width = newSpec.width;
} else {
changes.isExpensive = true;
}
}

if (
fieldNames.has('height') &&
(!('height' in newSpec) || !('height' in oldSpec) || newSpec.height !== oldSpec.height)
) {
if ('height' in newSpec && typeof newSpec.height === 'number') {
changes.height = newSpec.height;
} else {
changes.isExpensive = true;
}
}

// Delete cheap fields
fieldNames.delete('width');
fieldNames.delete('height');

if (
[...fieldNames].some(
field =>
!(field in newSpec) ||
!(field in oldSpec) ||
!equal(newSpec[field as keyof typeof newSpec], oldSpec[field as keyof typeof oldSpec]),
)
) {
changes.isExpensive = true;
}

return changes.width !== false || changes.height !== false || changes.isExpensive
? changes
: false;
}
5 changes: 3 additions & 2 deletions packages/react-vega/src/utils/getUniqueFieldNames.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { VisualizationSpec } from 'vega-embed';
import { PlainObject } from '../types';

export default function getUniqueFieldNames(objects: PlainObject[]) {
const fields = new Set();
export default function getUniqueFieldNames(objects: (PlainObject | VisualizationSpec)[]) {
const fields = new Set<string>();
objects.forEach(o => {
Object.keys(o).forEach(field => {
fields.add(field);
Expand Down
18 changes: 18 additions & 0 deletions packages/react-vega/src/utils/removeSignalListenersFromView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { View, SignalListeners } from '../types';

export default function removeSignalListenersFromView(
view: View,
signalListeners: SignalListeners,
) {
const signalNames = Object.keys(signalListeners);
signalNames.forEach(signalName => {
try {
view.removeSignalListener(signalName, signalListeners[signalName]);
} catch (error) {
// eslint-disable-next-line no-console
console.warn('Cannot remove invalid signal listener.', error);
}
});

return signalNames.length > 0;
}
8 changes: 8 additions & 0 deletions packages/react-vega/src/utils/updateMultipleDatasetsInView.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import updateSingleDatasetInView from './updateSingleDatasetInView';
import { PlainObject, View } from '../types';

export default function updateMultipleDatasetsInView(view: View, data: PlainObject) {
Object.keys(data).forEach(name => {
updateSingleDatasetInView(view, name, data[name]);
});
}
Loading

0 comments on commit d4a87e7

Please sign in to comment.