Add functional metric chart with customisable time scale

This commit is contained in:
JUDIT GRESKOVITS 2017-01-18 17:52:20 +00:00
parent 97fced5728
commit 2c7d751204
11 changed files with 591 additions and 46 deletions

View File

@ -24,6 +24,7 @@
"lodash.isfunction": "^3.0.8", "lodash.isfunction": "^3.0.8",
"lodash.isstring": "^4.0.1", "lodash.isstring": "^4.0.1",
"lodash.isundefined": "^3.0.1", "lodash.isundefined": "^3.0.1",
"moment": "^2.17.1",
"param-case": "^2.1.0", "param-case": "^2.1.0",
"random-natural": "^1.0.3", "random-natural": "^1.0.3",
"react": "^15.4.1", "react": "^15.4.1",

View File

@ -1,33 +0,0 @@
const React = require('react');
const Styled = require('styled-components');
const fns = require('../../shared/functions');
const {
remcalc
} = fns;
const {
default: styled
} = Styled;
const StyledBody = styled.div`
margin: 0;
width: 100%;
height: ${remcalc(264)};
`;
const Body = ({
children
}) => {
return (
<StyledBody name='metric-body'>
{children}
</StyledBody>
);
};
Body.propTypes = {
children: React.PropTypes.node
};
module.exports = Body;

View File

@ -0,0 +1,174 @@
const React = require('react');
const Styled = require('styled-components');
const moment = require('moment');
const Chart = require('chart.js');
const whisker = require('chartjs-chart-box-plot');
whisker(Chart);
const {
default: styled
} = Styled;
const Container = styled.div`
position: relative;
height: 100%;
width: 100%;
`;
const Canvas = styled.canvas`
`;
class Graph extends React.Component {
componentDidMount() {
const {
yMax = 100,
yMin = 0,
yMeasurement = '%'
} = this.props;
const {
data,
xMax,
xMin,
xUnitStepSize
} = this.processData(this.props);
this._chart = new Chart(this._refs.component, {
type: 'whisker',
responsive: true,
maintainAspectRatio: true,
data: {
datasets: [{
data: data
}]
},
options: {
layout: {
padding: 10
},
scales: {
xAxes: [{
display: true,
type: 'time',
time: {
unit: 'minute',
unitStepSize: xUnitStepSize,
max: xMax,
min: xMin,
/*displayFormats: {
hour: 'MMM D, hA'
}*/
},
}],
yAxes: [{
display: true,
ticks: {
min: yMin,
max: yMax,
callback: (value, index, values) => {
return `${value.toFixed(2)}${yMeasurement}`;
}
}
}]
},
legend: {
display: false
}
},
});
}
componentWillReceiveProps(nextProps) {
const {
data,
xMax,
xMin,
xUnitStepSize
} = this.processData(nextProps);
this._chart.data.datasets = [{
data
}];
this._chart.options.scales.xAxes[0].time.max = xMax;
this._chart.options.scales.xAxes[0].time.min = xMin;
this._chart.options.scales.xAxes[0].time.unitStepSize = xUnitStepSize;
this._chart.update(0);
}
processData(props) {
const {
data = [],
duration = 360
} = this.props;
// I'm going to assume that data will be structured in 10min intervals...
// And that newest data will be at the end...
// Let's rock and roll!
// All this shizzle below needs to be recalculated on new props, yay!
const now = moment();
// first time on scale x
const before = moment().subtract(duration, 'minutes');
// remove leading data before first time on scale x
const totalData = data.slice(data.length - 1 - duration/10);
// adjust time of first data, if there's less data than would fill the chart
const start = moment(before)
.add(duration - (totalData.length-1)*10, 'minutes');
// add times to data
const dataWithTime = totalData.map((d, i) => {
const add = i*10;
return Object.assign(
{},
d,
{
x: moment(start).add(add, 'minutes').toDate()
}
);
});
// set min and max
const xMax = now.toDate();
const xMin = before.toDate();
// calculate stepsize
const xUnitStepSize = duration/6;
return {
data: dataWithTime,
xMax,
xMin,
xUnitStepSize
};
}
ref(name) {
this._refs = this._refs || {};
return (el) => {
this._refs[name] = el;
};
}
render() {
return (
<Container name='metric-body'>
<Canvas
height={262}
innerRef={this.ref('component')}
width={940}
/>
</Container>
);
}
}
Graph.propTypes = {
data: React.PropTypes.array,
duration: React.PropTypes.number,
yMax: React.PropTypes.number,
yMeasurement: React.PropTypes.string,
yMin: React.PropTypes.number
};
module.exports = Graph;

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 41.2 (35397) - http://www.bohemiancoding.com/sketch -->
<title>icon: settings </title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="project:-adding-metrics-1.4" transform="translate(-979.000000, -413.000000)" fill="#FFFFFF">
<g id="metric:-cpu" transform="translate(208.000000, 391.000000)">
<g id="metric-head" transform="translate(5.000000, 0.000000)">
<g id="button:-settinga" transform="translate(742.000000, 0.000000)">
<path d="M33,34 C31.3425,34 30,32.6575 30,31 C30,29.3425 31.3425,28 33,28 C34.6575,28 36,29.3425 36,31 C36,32.6575 34.6575,34 33,34 L33,34 Z M42,32.5 L42,29.5 L40.3485,29.5 C40.155,28.543 39.774,27.655 39.252,26.869 L40.425,25.696 L38.304,23.575 L37.131,24.748 C36.345,24.226 35.457,23.845 34.5,23.6515 L34.5,22 L31.5,22 L31.5,23.6515 C30.543,23.845 29.655,24.226 28.869,24.748 L27.696,23.575 L25.575,25.696 L26.748,26.869 C26.226,27.655 25.845,28.543 25.6515,29.5 L24,29.5 L24,32.5 L25.6515,32.5 C25.845,33.457 26.226,34.345 26.748,35.131 L25.575,36.304 L27.696,38.425 L28.869,37.252 C29.655,37.774 30.543,38.155 31.5,38.3485 L31.5,40 L34.5,40 L34.5,38.3485 C35.457,38.155 36.345,37.774 37.131,37.252 L38.304,38.425 L40.425,36.304 L39.252,35.131 C39.774,34.345 40.155,33.457 40.3485,32.5 L42,32.5 Z" id="icon:-settings-"></path>
</g>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,5 +1,5 @@
module.exports = { module.exports = {
MetricBody: require('./body'), MetricGraph: require('./graph'),
MetricCloseButton: require('./close-button'), MetricCloseButton: require('./close-button'),
MetricHeader: require('./header'), MetricHeader: require('./header'),
MetricSelect: require('./select'), MetricSelect: require('./select'),

View File

@ -0,0 +1,307 @@
module.exports = [{
firstQuartile: 15,
thirdQuartile: 15,
median: 15,
max: 15,
min: 15,
}, {
firstQuartile: 26,
thirdQuartile: 26,
median: 26,
max: 26,
min: 26,
}, {
firstQuartile: 17,
thirdQuartile: 17,
median: 17,
max: 17,
min: 17,
}, {
firstQuartile: 15,
thirdQuartile: 25,
median: 19,
max: 19,
min: 20,
}, {
firstQuartile: 19,
thirdQuartile: 25,
median: 21,
max: 20,
min: 25,
}, {
firstQuartile: 24,
thirdQuartile: 30,
median: 25,
max: 26,
min: 27,
}, {
firstQuartile: 28,
thirdQuartile: 34,
median: 30,
max: 30,
min: 30,
}, {
firstQuartile: 30,
thirdQuartile: 45,
median: 35,
max: 40,
min: 40,
}, {
firstQuartile: 20,
thirdQuartile: 55,
median: 45,
max: 44,
min: 44,
}, {
firstQuartile: 55,
thirdQuartile: 55,
median: 55,
max: 55,
min: 55,
}, {
firstQuartile: 57,
thirdQuartile: 56,
median: 57,
max: 58,
min: 57,
}, {
firstQuartile: 57,
thirdQuartile: 56,
median: 56,
max: 56,
min: 56,
}, {
firstQuartile: 60,
thirdQuartile: 56,
median: 60,
max: 60,
min: 60,
}, {
firstQuartile: 57,
thirdQuartile: 57,
median: 57,
max: 57,
min: 57,
}, {
firstQuartile: 57,
thirdQuartile: 55,
median: 55,
max: 55,
min: 55,
}, {
firstQuartile: 20,
thirdQuartile: 45,
median: 45,
max: 45,
min: 45,
}, {
firstQuartile: 15,
thirdQuartile: 40,
median: 30,
max: 49,
min: 30,
}, {
firstQuartile: 15,
thirdQuartile: 15,
median: 15,
max: 15,
min: 15,
}, {
firstQuartile: 26,
thirdQuartile: 26,
median: 26,
max: 26,
min: 26,
}, {
firstQuartile: 17,
thirdQuartile: 17,
median: 17,
max: 17,
min: 17,
}, {
firstQuartile: 15,
thirdQuartile: 25,
median: 19,
max: 19,
min: 20,
}, {
firstQuartile: 19,
thirdQuartile: 25,
median: 21,
max: 20,
min: 25,
}, {
firstQuartile: 24,
thirdQuartile: 30,
median: 25,
max: 26,
min: 10,
}, {
firstQuartile: 28,
thirdQuartile: 34,
median: 30,
max: 30,
min: 30,
}, {
firstQuartile: 30,
thirdQuartile: 45,
median: 35,
max: 40,
min: 40,
}, {
firstQuartile: 20,
thirdQuartile: 55,
median: 45,
max: 44,
min: 44,
}, {
firstQuartile: 55,
thirdQuartile: 55,
median: 55,
max: 55,
min: 55,
}, {
firstQuartile: 57,
thirdQuartile: 56,
median: 57,
max: 58,
min: 57,
}, {
firstQuartile: 57,
thirdQuartile: 56,
median: 56,
max: 56,
min: 56,
}, {
firstQuartile: 60,
thirdQuartile: 56,
median: 60,
max: 60,
min: 60,
}, {
firstQuartile: 57,
thirdQuartile: 57,
median: 57,
max: 57,
min: 57,
}, {
firstQuartile: 57,
thirdQuartile: 55,
median: 55,
max: 55,
min: 55,
}, {
firstQuartile: 20,
thirdQuartile: 45,
median: 45,
max: 45,
min: 45,
}, {
firstQuartile: 15,
thirdQuartile: 40,
median: 30,
max: 49,
min: 30,
}, {
firstQuartile: 15,
thirdQuartile: 15,
median: 15,
max: 15,
min: 15,
}, {
firstQuartile: 26,
thirdQuartile: 26,
median: 26,
max: 26,
min: 26,
}, {
firstQuartile: 17,
thirdQuartile: 17,
median: 17,
max: 17,
min: 17,
}, {
firstQuartile: 15,
thirdQuartile: 25,
median: 19,
max: 19,
min: 20,
}, {
firstQuartile: 19,
thirdQuartile: 25,
median: 21,
max: 20,
min: 25,
}, {
firstQuartile: 24,
thirdQuartile: 30,
median: 25,
max: 26,
min: 27,
}, {
firstQuartile: 28,
thirdQuartile: 34,
median: 30,
max: 30,
min: 30,
}, {
firstQuartile: 30,
thirdQuartile: 45,
median: 35,
max: 40,
min: 40,
}, {
firstQuartile: 20,
thirdQuartile: 55,
median: 45,
max: 44,
min: 44,
}, {
firstQuartile: 55,
thirdQuartile: 55,
median: 55,
max: 55,
min: 55,
}, {
firstQuartile: 57,
thirdQuartile: 56,
median: 57,
max: 58,
min: 57,
}, {
firstQuartile: 57,
thirdQuartile: 56,
median: 56,
max: 56,
min: 56,
}, {
firstQuartile: 60,
thirdQuartile: 56,
median: 60,
max: 60,
min: 60,
}, {
firstQuartile: 57,
thirdQuartile: 57,
median: 57,
max: 57,
min: 57,
}, {
firstQuartile: 57,
thirdQuartile: 55,
median: 55,
max: 55,
min: 55,
}, {
firstQuartile: 20,
thirdQuartile: 45,
median: 45,
max: 45,
min: 45,
}, {
firstQuartile: 15,
thirdQuartile: 40,
median: 30,
max: 49,
min: 30,
}];

View File

@ -59,8 +59,10 @@ const Select = ({
form, form,
id = rndId(), id = rndId(),
name, name,
onChange,
required, required,
selected selected,
value
}) => { }) => {
return ( return (
<SelectWrapper> <SelectWrapper>
@ -70,8 +72,10 @@ const Select = ({
form={form} form={form}
id={id} id={id}
name={name} name={name}
onChange={onChange}
required={required} required={required}
selected={selected} selected={selected}
value={value}
> >
{children} {children}
</StyledSelect> </StyledSelect>
@ -86,8 +90,10 @@ Select.propTypes = {
form: React.PropTypes.string, form: React.PropTypes.string,
id: React.PropTypes.string, id: React.PropTypes.string,
name: React.PropTypes.string, name: React.PropTypes.string,
onChange: React.PropTypes.func,
required: React.PropTypes.bool, required: React.PropTypes.bool,
selected: React.PropTypes.bool selected: React.PropTypes.bool,
value: React.PropTypes.string
}; };
module.exports = Select; module.exports = Select;

View File

@ -3,7 +3,8 @@ const Styled = require('styled-components');
const fns = require('../../shared/functions'); const fns = require('../../shared/functions');
const constants = require('../../shared/constants'); const constants = require('../../shared/constants');
const Button = require('../button'); const Button = require('../button');
const SettingsIcon = require('!babel!svg-react!./close.svg?name=SettingsIcon'); const SettingsIcon =
require('!babel!svg-react!./icon-settings.svg?name=SettingsIcon');
const { const {
default: styled default: styled

View File

@ -6,7 +6,7 @@ const {
} = require('@kadira/storybook'); } = require('@kadira/storybook');
const { const {
MetricBody, MetricGraph,
MetricCloseButton, MetricCloseButton,
MetricHeader, MetricHeader,
MetricSelect, MetricSelect,
@ -15,7 +15,42 @@ const {
MetricView MetricView
} = require('./'); } = require('./');
const MetricData = require('./metric-data');
const onButtonClick = () => {}; const onButtonClick = () => {};
const onMetricSelect = () => {};
const hour = 60; // in minutes - for moment
const sixHours = 6*hour;
const twelveHours = 12*hour;
const oneDay = 24*hour;
const twoDays = 48*hour;
const withinRange = (
value,
newMin,
newMax,
precision = 2,
oldMin = 0,
oldMax = 100
) => {
const normalisedValue = value-oldMin;
const newRange = newMax-newMin;
const oldRange = oldMax-oldMin;
const newValue = newMin + normalisedValue*newRange/oldRange;
return newValue.toFixed(2);
};
const percentageMetricData = MetricData;
const kbMetricData = MetricData.map(m => {
return {
firstQuartile: withinRange(m.firstQuartile, 1.55, 2.0),
thirdQuartile: withinRange(m.thirdQuartile, 1.55, 2.0),
median: withinRange(m.median, 1.55, 2.0),
max: withinRange(m.max, 1.55, 2.0),
min: withinRange(m.min, 1.55, 2.0),
};
});
storiesOf('Metric', module) storiesOf('Metric', module)
.add('Metric', () => ( .add('Metric', () => (
@ -23,16 +58,46 @@ storiesOf('Metric', module)
<MetricView> <MetricView>
<MetricHeader> <MetricHeader>
<MetricTitle>Aggregated CPU usage</MetricTitle> <MetricTitle>Aggregated CPU usage</MetricTitle>
<MetricSelect> <MetricSelect onChange={onMetricSelect} value={sixHours}>
<option selected>6 hours</option> <option value={sixHours}>6 hours</option>
<option>12 hours</option> <option value={twelveHours}>12 hours</option>
<option>24 hours</option> <option value={oneDay}>24 hours</option>
<option>Two days</option> <option value={twoDays}>Two days</option>
</MetricSelect> </MetricSelect>
<MetricSettingsButton>Settings</MetricSettingsButton> <MetricSettingsButton onClick={onButtonClick}>
Settings
</MetricSettingsButton>
<MetricCloseButton onClick={onButtonClick} /> <MetricCloseButton onClick={onButtonClick} />
</MetricHeader> </MetricHeader>
<MetricBody /> <MetricGraph
data={percentageMetricData}
duration={sixHours}
yMax={100}
yMeasurement='%'
yMin={0}
/>
</MetricView>
<MetricView>
<MetricHeader>
<MetricTitle>Aggregated CPU usage</MetricTitle>
<MetricSelect onChange={onMetricSelect} value={twoDays}>
<option value={sixHours}>6 hours</option>
<option value={twelveHours}>12 hours</option>
<option value={oneDay}>24 hours</option>
<option value={twoDays}>Two days</option>
</MetricSelect>
<MetricSettingsButton onClick={onButtonClick}>
Settings
</MetricSettingsButton>
<MetricCloseButton onClick={onButtonClick} />
</MetricHeader>
<MetricGraph
data={kbMetricData}
duration={oneDay}
yMax={2.0}
yMeasurement='kb'
yMin={1.55}
/>
</MetricView> </MetricView>
</Base> </Base>
)); ));

View File

@ -1,4 +1,5 @@
const constants = require('../../shared/constants'); const constants = require('../../shared/constants');
const fns = require('../../shared/functions');
const React = require('react'); const React = require('react');
const Styled = require('styled-components'); const Styled = require('styled-components');
@ -7,6 +8,10 @@ const {
colors colors
} = constants; } = constants;
const {
remcalc
} = fns;
const { const {
default: styled default: styled
} = Styled; } = Styled;
@ -15,6 +20,7 @@ const Container = styled.div`
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
max-width: ${remcalc(940)};
box-shadow: ${boxes.bottomShaddow}; box-shadow: ${boxes.bottomShaddow};
border: 1px solid ${colors.borderSecondary}; border: 1px solid ${colors.borderSecondary};
`; `;

View File

@ -4343,7 +4343,7 @@ mobx@^2.3.4:
version "2.7.0" version "2.7.0"
resolved "https://registry.yarnpkg.com/mobx/-/mobx-2.7.0.tgz#cf3d82d18c0ca7f458d8f2a240817b3dc7e54a01" resolved "https://registry.yarnpkg.com/mobx/-/mobx-2.7.0.tgz#cf3d82d18c0ca7f458d8f2a240817b3dc7e54a01"
moment@^2.10.6: moment@^2.10.6, moment@^2.17.1:
version "2.17.1" version "2.17.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82" resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82"