Skip to content
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

[RFR] Responsive chart #249

Merged
merged 21 commits into from
Jul 12, 2018
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,10 @@ You can either use D3 as a specific import (specifying it in first argument of `

In addition to this configuration object, it also exposes some public methods allowing you to customize your application based on filtered data:
Copy link
Contributor

Choose a reason for hiding this comment

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

s/methods/members/


* **scale()** provides the horizontal scale, allowing you to retrieve bounding dates thanks to `.scale().domain()`,
* **filteredData()** returns an object with both `data` and `fullData` keys containing respectively bounds filtered data and full dataset.
* **draw(config, scale)** redraw chart using given configuration and `d3.scaleTime` scale
* **scale()** provides the horizontal scale, allowing you to retrieve bounding dates thanks to `.scale().domain()`,
* **filteredData()** returns an object with both `data` and `fullData` keys containing respectively bounds filtered data and full dataset.
* **draw(config, scale)** redraws chart using given configuration and `d3.scaleTime` scale
* **destroy** execute this function before to removing the chart from DOM. It prevents some memory leaks due to event listeners.
Copy link
Contributor

Choose a reason for hiding this comment

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

s/execute this function before to removing the chart from DOM/should be executed whenever you remove the chart from DOM/

Copy link
Contributor

Choose a reason for hiding this comment

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

s/destroy/destroy()/


Hence, if you want to display number of displayed data and time bounds as in the [demo](https://marmelab.com/EventDrops/), you can use the following code:

Expand Down
17 changes: 17 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -326,3 +326,20 @@ This parameter configures the minimum zoom level available. Set it to a not-null
_Default: Infinity_

This parameter configures the maximum zoom level available. Set it to a lower value to prevent your users from zooming in too deeply.

### numberDisplayedTicks

\_Default:

```js
const chart = eventDrops({
Copy link
Contributor

Choose a reason for hiding this comment

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

That's not the default value. Display default value only here.

Copy link
Contributor

Choose a reason for hiding this comment

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

Done

numberDisplayedTicks: {
small: 3,
medium: 5,
large: 7,
extra: 12,
},
});

This parameter configures the number of ticks display in the header
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand this sentence. I propose:

When reducing chart width, we need to display less labels on the horizontal axis to keep a readable chart. This parameter aims to solve the issue. Hence, on smallest devices, it displays only 3 labels by default at the same time.

And add a list of breakpoint resolutions to know what small means.

```
22 changes: 21 additions & 1 deletion src/axis.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { timeFormat } from 'd3-time-format';
import breakpoints from './breakpoints';

export const getBreakpointLabel = point =>
Object.keys(breakpoints).reduce((accumulator, label) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just using a for loop? It would prevent browsing every breakpoints for smallest devices.

if (!accumulator.length && point <= breakpoints[label]) {
return label;
}

return accumulator;
}, '');

export const tickFormat = (date, formats, d3) => {
if (d3.timeSecond(date) < date) {
Expand Down Expand Up @@ -33,7 +43,12 @@ export const tickFormat = (date, formats, d3) => {
};

export default (d3, config, xScale) => {
const { label: { width: labelWidth }, axis: { formats }, locale } = config;
const {
label: { width: labelWidth },
axis: { formats },
locale,
numberDisplayedTicks,
} = config;
d3.timeFormatDefaultLocale(locale);
return selection => {
const axis = selection.selectAll('.axis').data(d => d);
Expand All @@ -44,6 +59,11 @@ export default (d3, config, xScale) => {
.axisTop(xScale)
.tickFormat(d => tickFormat(d, formats, d3));

const breakpoint = getBreakpointLabel(window.innerWidth) || 'extra';
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we do it in the resize event instead? We don't need to recompute it everytime if window size is kept the same.

if (numberDisplayedTicks) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need an ifhere, with the default value?

axisTop.ticks(numberDisplayedTicks[breakpoint]);
}

axis
.enter()
.filter((_, i) => !i)
Expand Down
31 changes: 30 additions & 1 deletion src/axis.spec.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import defaultLocale from 'd3-time-format/locale/en-US.json';

import axis, { tickFormat } from './axis';
import axis, { tickFormat, getBreakpointLabel } from './axis';

const defaultConfig = {
label: {
Expand Down Expand Up @@ -120,8 +120,37 @@ describe('Axis', () => {
expect(timeFormatDefaultLocaleSpy).toHaveBeenCalledWith(defaultLocale);
});

it('should display tick using configuration locale', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure why the locale intervenes here. :)

const data = [[{ id: 'foo' }]];
const selection = d3.select('svg').data(data);

let config = {
...defaultConfig,
numberDisplayedTicks: { extra: 9 },
};

axis(d3, config, defaultScale)(selection);
Copy link
Contributor

Choose a reason for hiding this comment

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

Use a test function to not repeat yourself and focus more on what you are testing.

expect(document.querySelectorAll('.tick').length).toBe(9);

config = {
...defaultConfig,
numberDisplayedTicks: { extra: 5 },
};
axis(d3, config, defaultScale)(selection);
expect(document.querySelectorAll('.tick').length).toBe(5);
});

afterEach(() => {
document.body.innerHTML = '';
jest.restoreAllMocks();
});
});

describe('Breakpoint Label', () => {
it('should return breakpoint label correctly depending of current point', () => {
expect(getBreakpointLabel(400)).toBe('small');
expect(getBreakpointLabel(600)).toBe('medium');
expect(getBreakpointLabel(800)).toBe('large');
expect(getBreakpointLabel(1000)).toBe('extra');
});
});
6 changes: 6 additions & 0 deletions src/breakpoints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
small: 576,
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't it be in configuration?

medium: 768,
large: 992,
extra: 1200,
};
6 changes: 6 additions & 0 deletions src/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,10 @@ export default d3 => ({
minimumScale: 0,
maximumScale: Infinity,
},
numberDisplayedTicks: {
small: 3,
medium: 5,
large: 7,
extra: 12,
},
});
22 changes: 17 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ import { withinRange } from './withinRange';

// do not export anything else here to keep window.eventDrops as a function
export default ({ d3 = window.d3, ...customConfiguration }) => {
const chart = selection => {
const initChart = selection => {
selection.selectAll('svg').remove();

const root = selection.selectAll('svg').data(selection.data());
root.exit().remove();

const config = defaultsDeep(
customConfiguration || {},
defaultConfiguration(d3)
Expand Down Expand Up @@ -41,10 +46,6 @@ export default ({ d3 = window.d3, ...customConfiguration }) => {

chart._scale = xScale;

const root = selection.selectAll('svg').data(selection.data());

root.exit().remove();

const svg = root
.enter()
.append('svg')
Expand Down Expand Up @@ -73,8 +74,19 @@ export default ({ d3 = window.d3, ...customConfiguration }) => {
.call(draw(config, xScale));
};

const chart = selection => {
chart._initialize = () => initChart(selection);
chart._initialize();

global.addEventListener('resize', chart._initialize, true);
};

chart.scale = () => chart._scale;
chart.filteredData = () => chart._filteredData;
chart.destroy = (callback = () => {}) => {
global.removeEventListener('resize', chart._initialize, true);
callback();
};

const draw = (config, scale) => selection => {
const { drop: { date: dropDate } } = config;
Expand Down