Merge branch 'master' into bugfix/same-range

pull/1930/head
Andrew Watkins 2017-08-24 14:26:38 -07:00 committed by GitHub
commit 73db221798
20 changed files with 569 additions and 105 deletions

View File

@ -1,9 +1,12 @@
## v1.3.8.0 [unreleased]
### Bug Fixes
1. [#1886](https://github.com/influxdata/chronograf/pull/1886): Fix limit of 100 alert rules on alert rules page
1. [#1930](https://github.com/influxdata/chronograf/pull/1930): Fix graphs when values are of constant values
1. [#1930](https://github.com/influxdata/chronograf/pull/1930): Fix graphs when y-values are constant
### Features
1. [#1928](https://github.com/influxdata/chronograf/pull/1928): Add prefix, suffix, scale, and other y-axis formatting
1. [#1886](https://github.com/influxdata/chronograf/pull/1886): Fix limit of 100 alert rules on alert rules page
### UI Improvements
@ -19,11 +22,21 @@
1. [#1872](https://github.com/influxdata/chronograf/pull/1872): Prevent stats in the legend from wrapping line
1. [#1899](https://github.com/influxdata/chronograf/pull/1899): Fix raw query editor in Data Explorer not using selected time
1. [#1922](https://github.com/influxdata/chronograf/pull/1922): Fix Safari display issues in the Cell Editor display options
1. [#1715](https://github.com/influxdata/chronograf/pull/1715): Chronograf now renders on IE11.
1. [#1866](https://github.com/influxdata/chronograf/pull/1866): Fix missing cell type (and consequently single-stat)
1. [#1866](https://github.com/influxdata/chronograf/pull/1866): Fix data corruption issue with dashboard graph types
**Note**: If you upgraded to 1.3.6.0 and visited any dashboard, you will need to manually reset the graph type for every cell via the cell's caret -> Edit -> Display Options.
1. [#1870](https://github.com/influxdata/chronograf/pull/1870): Fix console error for placing prop on div
1. [#1845](https://github.com/influxdata/chronograf/pull/1845): Fix inaccessible scroll bar in Data Explorer table
1. [#1866](https://github.com/influxdata/chronograf/pull/1866): Fix non-persistence of dashboard graph types
1. [#1872](https://github.com/influxdata/chronograf/pull/1872): Prevent stats in the legend from wrapping line
### Features
1. [#1863](https://github.com/influxdata/chronograf/pull/1863): Improve 'new-sources' server flag example by adding 'type' key
1. [#1898](https://github.com/influxdata/chronograf/pull/1898): Add an input and validation to custom time range calendar dropdowns
1. [#1904](https://github.com/influxdata/chronograf/pull/1904): Add support for selecting template variables with URL params
1. [#1859](https://github.com/influxdata/chronograf/pull/1859): Add y-axis controls to the API for layouts
### UI Improvements
1. [#1862](https://github.com/influxdata/chronograf/pull/1862): Show "Add Graph" button on cells with no queries
@ -46,11 +59,15 @@
1. [#1798](https://github.com/influxdata/chronograf/pull/1798): Fix domain not updating in visualizations when changing time range manually
1. [#1799](https://github.com/influxdata/chronograf/pull/1799): Prevent console error spam from Dygraph's synchronize method when a dashboard has only one graph
1. [#1813](https://github.com/influxdata/chronograf/pull/1813): Guarantee UUID for each Alert Table key to prevent dropping items when keys overlap
1. [#1795](https://github.com/influxdata/chronograf/pull/1795): Fix uptime status on Windows hosts running Telegraf
1. [#1715](https://github.com/influxdata/chronograf/pull/1715): Chronograf now renders properly on IE11.
### Features
1. [#1744](https://github.com/influxdata/chronograf/pull/1744): Add a few time range shortcuts to the custom time range menu
1. [#1714](https://github.com/influxdata/chronograf/pull/1714): Add ability to edit a dashboard graph's y-axis bounds
1. [#1714](https://github.com/influxdata/chronograf/pull/1714): Add ability to edit a dashboard graph's y-axis label
1. [#1744](https://github.com/influxdata/chronograf/pull/1744): Add a few pre-set time range selections to the custom time range menu-- be sure to add a sensible GROUP BY
1. [#1744](https://github.com/influxdata/chronograf/pull/1744): Add a few time range shortcuts to the custom time range menu
### UI Improvements
1. [#1796](https://github.com/influxdata/chronograf/pull/1796): Add spinner write data modal to indicate data is being written

View File

@ -203,6 +203,10 @@ func MarshalDashboard(d chronograf.Dashboard) ([]byte, error) {
axes[a] = &Axis{
Bounds: r.Bounds,
Label: r.Label,
Prefix: r.Prefix,
Suffix: r.Suffix,
Base: r.Base,
Scale: r.Scale,
}
}
@ -281,14 +285,30 @@ func UnmarshalDashboard(data []byte, d *chronograf.Dashboard) error {
axes := make(map[string]chronograf.Axis, len(c.Axes))
for a, r := range c.Axes {
// axis base defaults to 10
if r.Base == "" {
r.Base = "10"
}
if r.Scale == "" {
r.Scale = "linear"
}
if r.Bounds != nil {
axes[a] = chronograf.Axis{
Bounds: r.Bounds,
Label: r.Label,
Prefix: r.Prefix,
Suffix: r.Suffix,
Base: r.Base,
Scale: r.Scale,
}
} else {
axes[a] = chronograf.Axis{
Bounds: []string{},
Base: r.Base,
Scale: r.Scale,
}
}
}

View File

@ -121,6 +121,10 @@ type Axis struct {
LegacyBounds []int64 `protobuf:"varint,1,rep,name=legacyBounds" json:"legacyBounds,omitempty"`
Bounds []string `protobuf:"bytes,2,rep,name=bounds" json:"bounds,omitempty"`
Label string `protobuf:"bytes,3,opt,name=label,proto3" json:"label,omitempty"`
Prefix string `protobuf:"bytes,4,opt,name=prefix,proto3" json:"prefix,omitempty"`
Suffix string `protobuf:"bytes,5,opt,name=suffix,proto3" json:"suffix,omitempty"`
Base string `protobuf:"bytes,6,opt,name=base,proto3" json:"base,omitempty"`
Scale string `protobuf:"bytes,7,opt,name=scale,proto3" json:"scale,omitempty"`
}
func (m *Axis) Reset() { *m = Axis{} }

View File

@ -38,6 +38,10 @@ message Axis {
repeated int64 legacyBounds = 1; // legacyBounds are an ordered 2-tuple consisting of lower and upper axis extents, respectively
repeated string bounds = 2; // bounds are an arbitrary list of client-defined bounds.
string label = 3; // label is a description of this axis
string prefix = 4; // specifies the prefix for axis values
string suffix = 5; // specifies the suffix for axis values
string base = 6; // defines the base for axis values
string scale = 7; // represents the magnitude of the numbers on this axis
}
message Template {

View File

@ -168,6 +168,10 @@ func Test_MarshalDashboard(t *testing.T) {
"y": chronograf.Axis{
Bounds: []string{"0", "3", "1-7", "foo"},
Label: "foo",
Prefix: "M",
Suffix: "m",
Base: "2",
Scale: "roflscale",
},
},
Type: "line",
@ -241,6 +245,8 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
Axes: map[string]chronograf.Axis{
"y": chronograf.Axis{
Bounds: []string{},
Base: "10",
Scale: "linear",
},
},
Type: "line",
@ -260,7 +266,7 @@ func Test_MarshalDashboard_WithLegacyBounds(t *testing.T) {
}
}
func Test_MarshalDashboard_WithNoLegacyBounds(t *testing.T) {
func Test_MarshalDashboard_WithEmptyLegacyBounds(t *testing.T) {
dashboard := chronograf.Dashboard{
ID: 1,
Cells: []chronograf.DashboardCell{
@ -314,6 +320,8 @@ func Test_MarshalDashboard_WithNoLegacyBounds(t *testing.T) {
Axes: map[string]chronograf.Axis{
"y": chronograf.Axis{
Bounds: []string{},
Base: "10",
Scale: "linear",
},
},
Type: "line",

View File

@ -644,6 +644,10 @@ type Axis struct {
Bounds []string `json:"bounds"` // bounds are an arbitrary list of client-defined strings that specify the viewport for a cell
LegacyBounds [2]int64 `json:"-"` // legacy bounds are for testing a migration from an earlier version of axis
Label string `json:"label"` // label is a description of this Axis
Prefix string `json:"prefix"` // Prefix represents a label prefix for formatting axis values
Suffix string `json:"suffix"` // Suffix represents a label suffix for formatting axis values
Base string `json:"base"` // Base represents the radix for formatting axis values
Scale string `json:"scale"` // Scale is the axis formatting scale. Supported: "log", "linear"
}
// DashboardCell holds visual and query information for a cell

View File

@ -76,17 +76,34 @@ func ValidDashboardCellRequest(c *chronograf.DashboardCell) error {
// HasCorrectAxes verifies that only permitted axes exist within a DashboardCell
func HasCorrectAxes(c *chronograf.DashboardCell) error {
for axis, _ := range c.Axes {
switch axis {
case "x", "y", "y2":
// no-op
default:
for label, axis := range c.Axes {
if !oneOf(label, "x", "y", "y2") {
return chronograf.ErrInvalidAxis
}
if !oneOf(axis.Scale, "linear", "log", "") {
return chronograf.ErrInvalidAxis
}
if !oneOf(axis.Base, "10", "2", "") {
return chronograf.ErrInvalidAxis
}
}
return nil
}
// oneOf reports whether a provided string is a member of a variadic list of
// valid options
func oneOf(prop string, validOpts ...string) bool {
for _, valid := range validOpts {
if prop == valid {
return true
}
}
return false
}
// CorrectWidthHeight changes the cell to have at least the
// minimum width and height
func CorrectWidthHeight(c *chronograf.DashboardCell) {

View File

@ -55,6 +55,78 @@ func Test_Cells_CorrectAxis(t *testing.T) {
},
true,
},
{
"linear scale value",
&chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Scale: "linear",
Bounds: []string{"0", "100"},
},
},
},
false,
},
{
"log scale value",
&chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Scale: "log",
Bounds: []string{"0", "100"},
},
},
},
false,
},
{
"invalid scale value",
&chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Scale: "potatoes",
Bounds: []string{"0", "100"},
},
},
},
true,
},
{
"base 10 axis",
&chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Base: "10",
Bounds: []string{"0", "100"},
},
},
},
false,
},
{
"base 2 axis",
&chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Base: "2",
Bounds: []string{"0", "100"},
},
},
},
false,
},
{
"invalid base",
&chronograf.DashboardCell{
Axes: map[string]chronograf.Axis{
"x": chronograf.Axis{
Base: "all your base are belong to us",
Bounds: []string{"0", "100"},
},
},
},
true,
},
}
for _, test := range axisTests {

View File

@ -3720,13 +3720,13 @@
"type": "object",
"properties": {
"x": {
"$ref": "#/definitions/DashboardRange"
"$ref": "#/definitions/Axis"
},
"y": {
"$ref": "#/definitions/DashboardRange"
"$ref": "#/definitions/Axis"
},
"y2": {
"$ref": "#/definitions/DashboardRange"
"$ref": "#/definitions/Axis"
}
}
},
@ -3811,7 +3811,7 @@
}
}
},
"DashboardRange": {
"Axis": {
"type": "object",
"description": "A description of a particular axis for a visualization",
"properties": {
@ -3824,6 +3824,26 @@
"type": "integer",
"format": "int64"
}
},
"label": {
"description": "label is a description of this Axis",
"type": "string"
},
"prefix": {
"description": "Prefix represents a label prefix for formatting axis values.",
"type": "string"
},
"suffix": {
"description": "Suffix represents a label suffix for formatting axis values.",
"type": "string"
},
"base": {
"description": "Base represents the radix for formatting axis values.",
"type": "string"
},
"scale": {
"description": "Scale is the axis formatting scale. Supported: \"log\", \"linear\"",
"type": "string"
}
}
},

View File

@ -1,19 +1,22 @@
import React, {PropTypes} from 'react'
import _ from 'lodash'
import OptIn from 'shared/components/OptIn'
import Input from 'src/dashboards/components/DisplayOptionsInput'
import {Tabber, Tab} from 'src/dashboards/components/Tabber'
import {DISPLAY_OPTIONS, TOOLTIP_CONTENT} from 'src/dashboards/constants'
const {LINEAR, LOG, BASE_2, BASE_10} = DISPLAY_OPTIONS
// TODO: add logic for for Prefix, Suffix, Scale, and Multiplier
const AxesOptions = ({
axes: {y: {bounds, label, prefix, suffix, base, scale, defaultYLabel}},
onSetBase,
onSetScale,
onSetLabel,
onSetPrefixSuffix,
onSetYAxisBoundMin,
onSetYAxisBoundMax,
onSetLabel,
axes,
}) => {
const min = _.get(axes, ['y', 'bounds', '0'], '')
const max = _.get(axes, ['y', 'bounds', '1'], '')
const label = _.get(axes, ['y', 'label'], '')
const defaultYLabel = _.get(axes, ['y', 'defaultYLabel'], '')
const [min, max] = bounds
return (
<div className="display-options--cell">
@ -46,38 +49,48 @@ const AxesOptions = ({
type="number"
/>
</div>
{/* <div className="form-group col-sm-6">
<label htmlFor="prefix">Labels Prefix</label>
<input
className="form-control input-sm"
type="text"
name="prefix"
id="prefix"
<Input
name="prefix"
id="prefix"
value={prefix}
labelText="Y-Value's Prefix"
onChange={onSetPrefixSuffix}
/>
<Input
name="suffix"
id="suffix"
value={suffix}
labelText="Y-Value's Suffix"
onChange={onSetPrefixSuffix}
/>
<Tabber
labelText="Y-Value's Format"
tipID="Y-Values's Format"
tipContent={TOOLTIP_CONTENT.FORMAT}
>
<Tab
text="K/M/B"
isActive={base === BASE_10}
onClickTab={onSetBase(BASE_10)}
/>
</div>
<div className="form-group col-sm-6">
<label htmlFor="prefix">Labels Suffix</label>
<input
className="form-control input-sm"
type="text"
name="suffix"
id="suffix"
<Tab
text="K/M/G"
isActive={base === BASE_2}
onClickTab={onSetBase(BASE_2)}
/>
</div>
<div className="form-group col-sm-6">
<label>Labels Format</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li className="active">K/M/B</li>
<li>K/M/G</li>
</ul>
</div>
<div className="form-group col-sm-6">
<label>Scale</label>
<ul className="nav nav-tablist nav-tablist-sm">
<li className="active">Linear</li>
<li>Logarithmic</li>
</ul>
</div> */}
</Tabber>
<Tabber labelText="Scale">
<Tab
text="Linear"
isActive={scale === LINEAR}
onClickTab={onSetScale(LINEAR)}
/>
<Tab
text="Logarithmic"
isActive={scale === LOG}
onClickTab={onSetScale(LOG)}
/>
</Tabber>
</form>
</div>
)
@ -85,10 +98,26 @@ const AxesOptions = ({
const {arrayOf, func, shape, string} = PropTypes
AxesOptions.defaultProps = {
axes: {
y: {
bounds: ['', ''],
prefix: '',
suffix: '',
base: BASE_10,
scale: LINEAR,
defaultYLabel: '',
},
},
}
AxesOptions.propTypes = {
onSetPrefixSuffix: func.isRequired,
onSetYAxisBoundMin: func.isRequired,
onSetYAxisBoundMax: func.isRequired,
onSetLabel: func.isRequired,
onSetScale: func.isRequired,
onSetBase: func.isRequired,
axes: shape({
y: shape({
bounds: arrayOf(string),

View File

@ -88,6 +88,22 @@ class CellEditorOverlay extends Component {
this.setState({axes: {...axes, y: {...axes.y, label}}})
}
handleSetPrefixSuffix = e => {
const {axes} = this.state
const {prefix, suffix} = e.target.form
this.setState({
axes: {
...axes,
y: {
...axes.y,
prefix: prefix.value,
suffix: suffix.value,
},
},
})
}
handleAddQuery = () => {
const {queriesWorkingDraft} = this.state
const newIndex = queriesWorkingDraft.length
@ -149,6 +165,34 @@ class CellEditorOverlay extends Component {
this.setState({activeQueryIndex})
}
handleSetBase = base => () => {
const {axes} = this.state
this.setState({
axes: {
...axes,
y: {
...axes.y,
base,
},
},
})
}
handleSetScale = scale => () => {
const {axes} = this.state
this.setState({
axes: {
...axes,
y: {
...axes.y,
scale,
},
},
})
}
getActiveQuery = () => {
const {queriesWorkingDraft, activeQueryIndex} = this.state
const activeQuery = queriesWorkingDraft[activeQueryIndex]
@ -230,13 +274,16 @@ class CellEditorOverlay extends Component {
/>
{isDisplayOptionsTabActive
? <DisplayOptions
axes={axes}
onSetBase={this.handleSetBase}
onSetLabel={this.handleSetLabel}
onSetScale={this.handleSetScale}
queryConfigs={queriesWorkingDraft}
selectedGraphType={cellWorkingType}
onSetPrefixSuffix={this.handleSetPrefixSuffix}
onSelectGraphType={this.handleSelectGraphType}
onSetYAxisBoundMin={this.handleSetYAxisBoundMin}
onSetYAxisBoundMax={this.handleSetYAxisBoundMax}
onSetLabel={this.handleSetLabel}
axes={axes}
queryConfigs={queriesWorkingDraft}
/>
: <QueryMaker
source={source}

View File

@ -33,9 +33,12 @@ class DisplayOptions extends Component {
render() {
const {
onSetBase,
onSetScale,
onSetLabel,
selectedGraphType,
onSelectGraphType,
onSetLabel,
onSetPrefixSuffix,
onSetYAxisBoundMin,
onSetYAxisBoundMax,
} = this.props
@ -43,16 +46,19 @@ class DisplayOptions extends Component {
return (
<div className="display-options">
<AxesOptions
axes={axes}
onSetBase={onSetBase}
onSetLabel={onSetLabel}
onSetScale={onSetScale}
onSetPrefixSuffix={onSetPrefixSuffix}
onSetYAxisBoundMin={onSetYAxisBoundMin}
onSetYAxisBoundMax={onSetYAxisBoundMax}
/>
<GraphTypeSelector
selectedGraphType={selectedGraphType}
onSelectGraphType={onSelectGraphType}
/>
<AxesOptions
onSetLabel={onSetLabel}
onSetYAxisBoundMin={onSetYAxisBoundMin}
onSetYAxisBoundMax={onSetYAxisBoundMax}
axes={axes}
/>
</div>
)
}
@ -62,9 +68,12 @@ const {arrayOf, func, shape, string} = PropTypes
DisplayOptions.propTypes = {
selectedGraphType: string.isRequired,
onSelectGraphType: func.isRequired,
onSetPrefixSuffix: func.isRequired,
onSetYAxisBoundMin: func.isRequired,
onSetYAxisBoundMax: func.isRequired,
onSetScale: func.isRequired,
onSetLabel: func.isRequired,
onSetBase: func.isRequired,
axes: shape({}).isRequired,
queryConfigs: arrayOf(shape()).isRequired,
}

View File

@ -0,0 +1,32 @@
import React, {PropTypes} from 'react'
const DisplayOptionsInput = ({id, name, value, onChange, labelText}) =>
<div className="form-group col-sm-6">
<label htmlFor={name}>
{labelText}
</label>
<input
className="form-control input-sm"
type="text"
name={name}
id={id}
value={value}
onChange={onChange}
/>
</div>
const {func, string} = PropTypes
DisplayOptionsInput.defaultProps = {
value: '',
}
DisplayOptionsInput.propTypes = {
name: string.isRequired,
id: string.isRequired,
value: string.isRequired,
onChange: func.isRequired,
labelText: string,
}
export default DisplayOptionsInput

View File

@ -0,0 +1,35 @@
import React, {PropTypes} from 'react'
import QuestionMarkTooltip from 'src/shared/components/QuestionMarkTooltip'
export const Tabber = ({labelText, children, tipID, tipContent}) =>
<div className="form-group col-sm-6">
<label>
{labelText}
{tipID
? <QuestionMarkTooltip tipID={tipID} tipContent={tipContent} />
: null}
</label>
<ul className="nav nav-tablist nav-tablist-sm">
{children}
</ul>
</div>
export const Tab = ({isActive, onClickTab, text}) =>
<li className={isActive ? 'active' : ''} onClick={onClickTab}>
{text}
</li>
const {bool, func, node, string} = PropTypes
Tabber.propTypes = {
children: node.isRequired,
labelText: string,
tipID: string,
tipContent: string,
}
Tab.propTypes = {
onClickTab: func.isRequired,
isActive: bool.isRequired,
text: string.isRequired,
}

View File

@ -94,3 +94,15 @@ export const removeUnselectedTemplateValues = templates => {
return {...template, values: selectedValues}
})
}
export const DISPLAY_OPTIONS = {
LINEAR: 'linear',
LOG: 'log',
BASE_2: '2',
BASE_10: '10',
}
export const TOOLTIP_CONTENT = {
FORMAT:
'<p>K/M/B = Thousand / Million / Billion</p><p>K/M/G = Kilo / Mega / Giga </p>',
}

View File

@ -103,7 +103,7 @@ class ChronoTable extends Component {
const minWidth = 70
const rowHeight = 34
const headerHeight = 30
const stylePixelOffset = 125
const stylePixelOffset = 130
const defaultColumnWidth = 200
const styleAdjustedHeight = height - stylePixelOffset
const width =

View File

@ -7,9 +7,15 @@ import _ from 'lodash'
import Dygraphs from 'src/external/dygraph'
import getRange from 'shared/parsing/getRangeForDygraph'
import {DISPLAY_OPTIONS} from 'src/dashboards/constants'
import {LINE_COLORS, multiColumnBarPlotter} from 'src/shared/graphs/helpers'
import DygraphLegend from 'src/shared/components/DygraphLegend'
import {buildDefaultYLabel} from 'shared/presenters'
import {numberValueFormatter} from 'src/utils/formatting'
const {LINEAR, LOG, BASE_10} = DISPLAY_OPTIONS
const labelWidth = 60
const avgCharPixels = 7
const hasherino = (str, len) =>
str
@ -35,18 +41,11 @@ export default class Dygraph extends Component {
}
}
static defaultProps = {
containerStyle: {},
isGraphFilled: true,
overrideLineColors: null,
dygraphRef: () => {},
}
componentDidMount() {
const timeSeries = this.getTimeSeries()
// dygraphSeries is a legend label and its corresponding y-axis e.g. {legendLabel1: 'y', legendLabel2: 'y2'};
const {
axes,
axes: {y, y2},
dygraphSeries,
ruleValues,
overrideLineColors,
@ -69,8 +68,10 @@ export default class Dygraph extends Component {
hashColorDygraphSeries[seriesName] = {...series, color}
}
const yAxis = _.get(axes, ['y', 'bounds'], [null, null])
const y2Axis = _.get(axes, ['y2', 'bounds'], undefined)
const axisLabelWidth =
labelWidth +
y.prefix.length * avgCharPixels +
y.suffix.length * avgCharPixels
const defaultOptions = {
plugins: isBarGraph
@ -80,6 +81,7 @@ export default class Dygraph extends Component {
direction: 'vertical',
}),
],
logscale: y.scale === LOG,
labelsSeparateLines: false,
labelsKMB: true,
rightGap: 0,
@ -95,10 +97,15 @@ export default class Dygraph extends Component {
series: hashColorDygraphSeries,
axes: {
y: {
valueRange: getRange(timeSeries, yAxis, ruleValues),
valueRange: getRange(timeSeries, y.bounds, ruleValues),
axisLabelFormatter: (yval, __, opts) =>
numberValueFormatter(yval, opts, y.prefix, y.suffix),
axisLabelWidth,
labelsKMB: y.base === '10',
labelsKMG2: y.base === '2',
},
y2: {
valueRange: getRange(timeSeries, y2Axis),
valueRange: getRange(timeSeries, y2.bounds),
},
},
highlightSeriesOpts: {
@ -114,10 +121,10 @@ export default class Dygraph extends Component {
const highlighted = legend.series.find(s => s.isHighlighted)
const prevHighlighted = prevLegend.series.find(s => s.isHighlighted)
const y = highlighted && highlighted.y
const yVal = highlighted && highlighted.y
const prevY = prevHighlighted && prevHighlighted.y
if (legend.x === prevLegend.x && y === prevY) {
if (legend.x === prevLegend.x && yVal === prevY) {
return ''
}
@ -222,7 +229,7 @@ export default class Dygraph extends Component {
componentDidUpdate() {
const {
labels,
axes,
axes: {y, y2},
options,
dygraphSeries,
ruleValues,
@ -237,8 +244,6 @@ export default class Dygraph extends Component {
)
}
const y = _.get(axes, ['y', 'bounds'], [null, null])
const y2 = _.get(axes, ['y2', 'bounds'], undefined)
const timeSeries = this.getTimeSeries()
const ylabel = this.getLabel('y')
const finalLineColors = [...(overrideLineColors || LINE_COLORS)]
@ -253,16 +258,27 @@ export default class Dygraph extends Component {
hashColorDygraphSeries[seriesName] = {...series, color}
}
const axisLabelWidth =
labelWidth +
y.prefix.length * avgCharPixels +
y.suffix.length * avgCharPixels
const updateOptions = {
labels,
file: timeSeries,
ylabel,
logscale: y.scale === LOG,
axes: {
y: {
valueRange: getRange(timeSeries, y, ruleValues),
valueRange: getRange(timeSeries, y.bounds, ruleValues),
axisLabelFormatter: (yval, __, opts) =>
numberValueFormatter(yval, opts, y.prefix, y.suffix),
axisLabelWidth,
labelsKMB: y.base === '10',
labelsKMG2: y.base === '2',
},
y2: {
valueRange: getRange(timeSeries, y2),
valueRange: getRange(timeSeries, y2.bounds),
},
},
stepPlot: options.stepPlot,
@ -275,8 +291,10 @@ export default class Dygraph extends Component {
}
dygraph.updateOptions(updateOptions)
dygraph.resize()
const {w} = this.dygraph.getArea()
this.resize()
this.dygraph.resize()
this.props.setResolution(w)
}
@ -361,6 +379,11 @@ export default class Dygraph extends Component {
handleLegendRef = el => (this.legendRef = el)
resize = () => {
this.dygraph.resizeElements_()
this.dygraph.predraw_()
}
render() {
const {
legend,
@ -386,16 +409,16 @@ export default class Dygraph extends Component {
onSnip={this.handleSnipLabel}
onSort={this.handleSortLegend}
legendRef={this.handleLegendRef}
onInputChange={this.handleLegendInputChange}
onToggleFilter={this.handleToggleFilter}
onInputChange={this.handleLegendInputChange}
/>
<div
ref={r => {
this.graphRef = r
this.props.dygraphRef(r)
}}
style={this.props.containerStyle}
className="dygraph-child-container"
style={this.props.containerStyle}
/>
</div>
)
@ -404,6 +427,27 @@ export default class Dygraph extends Component {
const {array, arrayOf, bool, func, shape, string} = PropTypes
Dygraph.defaultProps = {
axes: {
y: {
bounds: [null, null],
prefix: '',
suffix: '',
base: BASE_10,
scale: LINEAR,
},
y2: {
bounds: undefined,
prefix: '',
suffix: '',
},
},
containerStyle: {},
isGraphFilled: true,
overrideLineColors: null,
dygraphRef: () => {},
}
Dygraph.propTypes = {
axes: shape({
y: shape({

View File

@ -159,12 +159,11 @@ export default React.createClass({
: overrideLineColors
return (
<div className={`dygraph ${this.yLabelClass()}`} style={{height: '100%'}}>
<div className="dygraph graph--hasYLabel" style={{height: '100%'}}>
{isRefreshing ? this.renderSpinner() : null}
<Dygraph
axes={axes}
queries={queries}
dygraphRef={this.dygraphRefFunc}
containerStyle={{width: '100%', height: '100%'}}
overrideLineColors={lineColors}
isGraphFilled={showSingleStat ? false : isGraphFilled}
@ -195,26 +194,6 @@ export default React.createClass({
)
},
yLabelClass() {
const dygraph = this.dygraphRef
if (!dygraph) {
return 'graph--hasYLabel'
}
const label = dygraph.querySelector('.dygraph-ylabel')
if (!label) {
return ''
}
return 'graph--hasYLabel'
},
dygraphRefFunc(dygraphRef) {
this.dygraphRef = dygraphRef
},
renderSpinner() {
return (
<div className="graph-panel__refreshing">

View File

@ -11,7 +11,7 @@ const considerEmpty = (userNumber, number) => {
const getRange = (
timeSeries,
userSelectedRange = [null, null],
ruleValues = {value: null, rangeValue: null}
ruleValues = {value: null, rangeValue: null, operator: ''}
) => {
const {value, rangeValue, operator} = ruleValues
const [userMin, userMax] = userSelectedRange

View File

@ -1,3 +1,114 @@
const KMB_LABELS = ['K', 'M', 'B', 'T', 'Q']
const KMG2_BIG_LABELS = ['k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']
const KMG2_SMALL_LABELS = ['m', 'u', 'n', 'p', 'f', 'a', 'z', 'y']
const pow = (base, exp) => {
if (exp < 0) {
return 1.0 / Math.pow(base, -exp)
}
return Math.pow(base, exp)
}
const round_ = (num, places) => {
const shift = Math.pow(10, places)
return Math.round(num * shift) / shift
}
const floatFormat = (x, optPrecision) => {
// Avoid invalid precision values; [1, 21] is the valid range.
const p = Math.min(Math.max(1, optPrecision || 2), 21)
// This is deceptively simple. The actual algorithm comes from:
//
// Max allowed length = p + 4
// where 4 comes from 'e+n' and '.'.
//
// Length of fixed format = 2 + y + p
// where 2 comes from '0.' and y = # of leading zeroes.
//
// Equating the two and solving for y yields y = 2, or 0.00xxxx which is
// 1.0e-3.
//
// Since the behavior of toPrecision() is identical for larger numbers, we
// don't have to worry about the other bound.
//
// Finally, the argument for toExponential() is the number of trailing digits,
// so we take off 1 for the value before the '.'.
return Math.abs(x) < 1.0e-3 && x !== 0.0
? x.toExponential(p - 1)
: x.toPrecision(p)
}
// taken from https://github.com/danvk/dygraphs/blob/aaec6de56dba8ed712fd7b9d949de47b46a76ccd/src/dygraph-utils.js#L1103
export const numberValueFormatter = (x, opts, prefix, suffix) => {
const sigFigs = opts('sigFigs')
if (sigFigs !== null) {
// User has opted for a fixed number of significant figures.
return floatFormat(x, sigFigs)
}
const digits = opts('digitsAfterDecimal')
const maxNumberWidth = opts('maxNumberWidth')
const kmb = opts('labelsKMB')
const kmg2 = opts('labelsKMG2')
let label
// switch to scientific notation if we underflow or overflow fixed display.
if (
x !== 0.0 &&
(Math.abs(x) >= Math.pow(10, maxNumberWidth) ||
Math.abs(x) < Math.pow(10, -digits))
) {
label = x.toExponential(digits)
} else {
label = `${round_(x, digits)}`
}
if (kmb || kmg2) {
let k
let kLabels = []
let mLabels = []
if (kmb) {
k = 1000
kLabels = KMB_LABELS
}
if (kmg2) {
if (kmb) {
console.error('Setting both labelsKMB and labelsKMG2. Pick one!')
}
k = 1024
kLabels = KMG2_BIG_LABELS
mLabels = KMG2_SMALL_LABELS
}
const absx = Math.abs(x)
let n = pow(k, kLabels.length)
for (let j = kLabels.length - 1; j >= 0; j -= 1, n /= k) {
if (absx >= n) {
label = round_(x / n, digits) + kLabels[j]
break
}
}
if (kmg2) {
const xParts = String(x.toExponential()).split('e-')
if (xParts.length === 2 && xParts[1] >= 3 && xParts[1] <= 24) {
if (xParts[1] % 3 > 0) {
label = round_(xParts[0] / pow(10, xParts[1] % 3), digits)
} else {
label = Number(xParts[0]).toFixed(2)
}
label += mLabels[Math.floor(xParts[1] / 3) - 1]
}
}
}
return `${prefix}${label}${suffix}`
}
export const formatBytes = bytes => {
if (bytes === 0) {
return '0 Bytes'