Merge branch 'master' into panel-edit-ux

This commit is contained in:
Torkel Ödegaard 2018-10-28 12:11:47 -07:00
commit 5dd9247762
18 changed files with 594 additions and 38 deletions

View File

@ -927,6 +927,123 @@
"title": "",
"type": "text"
},
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "gdev-testdata",
"editable": true,
"error": false,
"fill": 0,
"gridPos": {
"h": 7,
"w": 16,
"x": 0,
"y": 44
},
"id": 21,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [
{
"alias": "C-series",
"steppedLine": true
}
],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"alias": "",
"hide": false,
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,null,40,null,90,null,null,100,null,null,100,null,null,80,null",
"target": ""
},
{
"alias": "",
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "20,null40,null,null,50,null,70,null,100,null,10,null,30,null",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Null between points",
"tooltip": {
"msResolution": false,
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
},
{
"content": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points",
"editable": true,
"error": false,
"gridPos": {
"h": 7,
"w": 8,
"x": 16,
"y": 44
},
"id": 22,
"links": [],
"mode": "markdown",
"title": "",
"type": "text"
},
{
"aliasColors": {},
"bars": false,
@ -939,7 +1056,7 @@
"h": 7,
"w": 24,
"x": 0,
"y": 44
"y": 51
},
"id": 20,
"legend": {
@ -1024,7 +1141,7 @@
"h": 7,
"w": 12,
"x": 0,
"y": 51
"y": 58
},
"id": 16,
"legend": {
@ -1127,7 +1244,7 @@
"h": 7,
"w": 12,
"x": 12,
"y": 51
"y": 58
},
"id": 17,
"legend": {
@ -1266,7 +1383,7 @@
"h": 7,
"w": 12,
"x": 0,
"y": 58
"y": 65
},
"id": 18,
"legend": {
@ -1370,7 +1487,7 @@
"h": 7,
"w": 12,
"x": 12,
"y": 58
"y": 65
},
"id": 19,
"legend": {
@ -1554,5 +1671,5 @@
"timezone": "browser",
"title": "Panel Tests - Graph",
"uid": "5SdHCadmz",
"version": 3
"version": 1
}

View File

@ -100,12 +100,12 @@ display name, especially if the display name contains spaces or special
characters. Make sure you always use the group or subgroup name as it appears
in the URL of the group or subgroup.
Here's a complete example with `alloed_sign_up` enabled, and access limited to
Here's a complete example with `allow_sign_up` enabled, and access limited to
the `example` and `foo/bar` groups:
```ini
[auth.gitlab]
enabled = false
enabled = true
allow_sign_up = true
client_id = GITLAB_APPLICATION_ID
client_secret = GITLAB_SECRET

View File

@ -19,3 +19,4 @@ export const Label: SFC<Props> = props => {
</span>
);
};

View File

@ -0,0 +1,46 @@
import React, { PureComponent } from 'react';
import _ from 'lodash';
export interface Props {
label: string;
checked: boolean;
labelClass?: string;
switchClass?: string;
onChange: (event) => any;
}
export interface State {
id: any;
}
export class Switch extends PureComponent<Props, State> {
state = {
id: _.uniqueId(),
};
internalOnChange = event => {
event.stopPropagation();
this.props.onChange(event);
};
render() {
const { labelClass, switchClass, label, checked } = this.props;
const labelId = `check-${this.state.id}`;
const labelClassName = `gf-form-label ${labelClass} pointer`;
const switchClassName = `gf-form-switch ${switchClass}`;
return (
<div className="gf-form">
{label && (
<label htmlFor={labelId} className={labelClassName}>
{label}
</label>
)}
<div className={switchClassName}>
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
<label htmlFor={labelId} />
</div>
</div>
);
}
}

View File

@ -373,9 +373,10 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue });
};
onModifyQueries = (action: object, index?: number) => {
onModifyQueries = (action, index?: number) => {
const { datasource } = this.state;
if (datasource && datasource.modifyQuery) {
const preventSubmit = action.preventSubmit;
this.setState(
state => {
const { queries, queryTransactions } = state;
@ -391,16 +392,26 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
nextQueryTransactions = [];
} else {
// Modify query only at index
nextQueries = [
...queries.slice(0, index),
{
key: generateQueryKey(index),
query: datasource.modifyQuery(this.queryExpressions[index], action),
},
...queries.slice(index + 1),
];
// Discard transactions related to row query
nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index);
nextQueries = queries.map((q, i) => {
// Synchronise all queries with local query cache to ensure consistency
q.query = this.queryExpressions[i];
return i === index
? {
key: generateQueryKey(index),
query: datasource.modifyQuery(q.query, action),
}
: q;
});
nextQueryTransactions = queryTransactions
// Consume the hint corresponding to the action
.map(qt => {
if (qt.hints != null && qt.rowIndex === index) {
qt.hints = qt.hints.filter(hint => hint.fix.action !== action);
}
return qt;
})
// Preserve previous row query transaction to keep results visible if next query is incomplete
.filter(qt => preventSubmit || qt.rowIndex !== index);
}
this.queryExpressions = nextQueries.map(q => q.query);
return {
@ -408,7 +419,8 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
queryTransactions: nextQueryTransactions,
};
},
() => this.onSubmit()
// Accepting certain fixes do not result in a well-formed query which should not be submitted
!preventSubmit ? () => this.onSubmit() : null
);
}
};

View File

@ -0,0 +1,72 @@
import PlaceholdersBuffer from './PlaceholdersBuffer';
describe('PlaceholdersBuffer', () => {
it('does nothing if no placeholders are defined', () => {
const text = 'metric';
const buffer = new PlaceholdersBuffer(text);
expect(buffer.hasPlaceholders()).toBe(false);
expect(buffer.toString()).toBe(text);
expect(buffer.getNextMoveOffset()).toBe(0);
});
it('respects the traversal order of placeholders', () => {
const text = 'sum($2 offset $1) by ($3)';
const buffer = new PlaceholdersBuffer(text);
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('sum( offset ) by ()');
expect(buffer.getNextMoveOffset()).toBe(12);
buffer.setNextPlaceholderValue('1h');
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('sum( offset 1h) by ()');
expect(buffer.getNextMoveOffset()).toBe(-10);
buffer.setNextPlaceholderValue('metric');
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('sum(metric offset 1h) by ()');
expect(buffer.getNextMoveOffset()).toBe(16);
buffer.setNextPlaceholderValue('label');
expect(buffer.hasPlaceholders()).toBe(false);
expect(buffer.toString()).toBe('sum(metric offset 1h) by (label)');
expect(buffer.getNextMoveOffset()).toBe(0);
});
it('respects the traversal order of adjacent placeholders', () => {
const text = '$1$3$2$4';
const buffer = new PlaceholdersBuffer(text);
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('');
expect(buffer.getNextMoveOffset()).toBe(0);
buffer.setNextPlaceholderValue('1');
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('1');
expect(buffer.getNextMoveOffset()).toBe(0);
buffer.setNextPlaceholderValue('2');
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('12');
expect(buffer.getNextMoveOffset()).toBe(-1);
buffer.setNextPlaceholderValue('3');
expect(buffer.hasPlaceholders()).toBe(true);
expect(buffer.toString()).toBe('132');
expect(buffer.getNextMoveOffset()).toBe(1);
buffer.setNextPlaceholderValue('4');
expect(buffer.hasPlaceholders()).toBe(false);
expect(buffer.toString()).toBe('1324');
expect(buffer.getNextMoveOffset()).toBe(0);
});
});

View File

@ -0,0 +1,112 @@
/**
* Provides a stateful means of managing placeholders in text.
*
* Placeholders are numbers prefixed with the `$` character (e.g. `$1`).
* Each number value represents the order in which a placeholder should
* receive focus if multiple placeholders exist.
*
* Example scenario given `sum($3 offset $1) by($2)`:
* 1. `sum( offset |) by()`
* 2. `sum( offset 1h) by(|)`
* 3. `sum(| offset 1h) by (label)`
*/
export default class PlaceholdersBuffer {
private nextMoveOffset: number;
private orders: number[];
private parts: string[];
constructor(text: string) {
const result = this.parse(text);
const nextPlaceholderIndex = result.orders.length ? result.orders[0] : 0;
this.nextMoveOffset = this.getOffsetBetween(result.parts, 0, nextPlaceholderIndex);
this.orders = result.orders;
this.parts = result.parts;
}
clearPlaceholders() {
this.nextMoveOffset = 0;
this.orders = [];
}
getNextMoveOffset(): number {
return this.nextMoveOffset;
}
hasPlaceholders(): boolean {
return this.orders.length > 0;
}
setNextPlaceholderValue(value: string) {
if (this.orders.length === 0) {
return;
}
const currentPlaceholderIndex = this.orders[0];
this.parts[currentPlaceholderIndex] = value;
this.orders = this.orders.slice(1);
if (this.orders.length === 0) {
this.nextMoveOffset = 0;
return;
}
const nextPlaceholderIndex = this.orders[0];
// Case should never happen but handle it gracefully in case
if (currentPlaceholderIndex === nextPlaceholderIndex) {
this.nextMoveOffset = 0;
return;
}
const backwardMove = currentPlaceholderIndex > nextPlaceholderIndex;
const indices = backwardMove
? { start: nextPlaceholderIndex + 1, end: currentPlaceholderIndex + 1 }
: { start: currentPlaceholderIndex + 1, end: nextPlaceholderIndex };
this.nextMoveOffset = (backwardMove ? -1 : 1) * this.getOffsetBetween(this.parts, indices.start, indices.end);
}
toString(): string {
return this.parts.join('');
}
private getOffsetBetween(parts: string[], startIndex: number, endIndex: number) {
return parts.slice(startIndex, endIndex).reduce((offset, part) => offset + part.length, 0);
}
private parse(text: string): ParseResult {
const placeholderRegExp = /\$(\d+)/g;
const parts = [];
const orders = [];
let textOffset = 0;
while (true) {
const match = placeholderRegExp.exec(text);
if (!match) {
break;
}
const part = text.slice(textOffset, match.index);
parts.push(part);
// Accounts for placeholders at text boundaries
if (part !== '') {
parts.push('');
}
const order = parseInt(match[1], 10);
orders.push({ index: parts.length - 1, order });
textOffset += part.length + match.length;
}
// Ensures string serialisation still works if no placeholders were parsed
// and also accounts for the remainder of text with placeholders
parts.push(text.slice(textOffset));
return {
// Placeholder values do not necessarily appear sequentially so sort the
// indices to traverse in priority order
orders: orders.sort((o1, o2) => o1.order - o2.order).map(o => o.index),
parts,
};
}
}
type ParseResult = {
/**
* Indices to placeholder items in `parts` in traversal order.
*/
orders: number[];
/**
* Parts comprising the original text with placeholders occupying distinct items.
*/
parts: string[];
};

View File

@ -12,6 +12,7 @@ import NewlinePlugin from './slate-plugins/newline';
import Typeahead from './Typeahead';
import { makeFragment, makeValue } from './Value';
import PlaceholdersBuffer from './PlaceholdersBuffer';
export const TYPEAHEAD_DEBOUNCE = 100;
@ -61,12 +62,15 @@ export interface TypeaheadInput {
class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadFieldState> {
menuEl: HTMLElement | null;
placeholdersBuffer: PlaceholdersBuffer;
plugins: any[];
resetTimer: any;
constructor(props, context) {
super(props, context);
this.placeholdersBuffer = new PlaceholdersBuffer(props.initialValue || '');
// Base plugins
this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins];
@ -76,7 +80,7 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
typeaheadIndex: 0,
typeaheadPrefix: '',
typeaheadText: '',
value: makeValue(props.initialValue || '', props.syntax),
value: makeValue(this.placeholdersBuffer.toString(), props.syntax),
};
}
@ -101,12 +105,14 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
componentWillReceiveProps(nextProps: TypeaheadFieldProps) {
if (nextProps.syntaxLoaded && !this.props.syntaxLoaded) {
// Need a bogus edit to re-render the editor after syntax has fully loaded
this.onChange(
this.state.value
.change()
.insertText(' ')
.deleteBackward()
);
const change = this.state.value
.change()
.insertText(' ')
.deleteBackward();
if (this.placeholdersBuffer.hasPlaceholders()) {
change.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
}
this.onChange(change);
}
}
@ -289,7 +295,17 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
}
const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex);
this.applyTypeahead(change, suggestion);
const nextChange = this.applyTypeahead(change, suggestion);
const insertTextOperation = nextChange.operations.find(operation => operation.type === 'insert_text');
if (insertTextOperation) {
const suggestionText = insertTextOperation.text;
this.placeholdersBuffer.setNextPlaceholderValue(suggestionText);
if (this.placeholdersBuffer.hasPlaceholders()) {
nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus();
}
}
return true;
}
break;
@ -336,6 +352,8 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
// If we dont wait here, menu clicks wont work because the menu
// will be gone.
this.resetTimer = setTimeout(this.resetTypeahead, 100);
// Disrupting placeholder entry wipes all remaining placeholders needing input
this.placeholdersBuffer.clearPlaceholders();
if (onBlur) {
onBlur();
}

View File

@ -1,6 +1,6 @@
import React from 'react';
import { connect } from 'react-redux';
import { Label } from 'app/core/components/Forms/Forms';
import { Label } from 'app/core/components/Label/Label';
import { Team } from '../../types';
import { updateTeam } from './state/actions';
import { getRouteParamsId } from '../../core/selectors/location';

View File

@ -464,6 +464,9 @@ export class PrometheusDatasource {
case 'ADD_RATE': {
return `rate(${query}[5m])`;
}
case 'ADD_SUM': {
return `sum(${query.trim()}) by ($1)`;
}
case 'EXPAND_RULES': {
const mapping = action.mapping;
if (mapping) {

View File

@ -162,16 +162,29 @@ export default class PromQlLanguageProvider extends LanguageProvider {
let refresher: Promise<any> = null;
const suggestions: CompletionItemGroup[] = [];
// sum(foo{bar="1"}) by (|)
const line = value.anchorBlock.getText();
const cursorOffset: number = value.anchorOffset;
// sum(foo{bar="1"}) by (
const leftSide = line.slice(0, cursorOffset);
// Stitch all query lines together to support multi-line queries
let queryOffset;
const queryText = value.document.getBlocks().reduce((text, block) => {
const blockText = block.getText();
if (value.anchorBlock.key === block.key) {
// Newline characters are not accounted for but this is irrelevant
// for the purpose of extracting the selector string
queryOffset = value.anchorOffset + text.length;
}
text += blockText;
return text;
}, '');
const leftSide = queryText.slice(0, queryOffset);
const openParensAggregationIndex = leftSide.lastIndexOf('(');
const openParensSelectorIndex = leftSide.slice(0, openParensAggregationIndex).lastIndexOf('(');
const closeParensSelectorIndex = leftSide.slice(openParensSelectorIndex).indexOf(')') + openParensSelectorIndex;
// foo{bar="1"}
const selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
let selectorString = leftSide.slice(openParensSelectorIndex + 1, closeParensSelectorIndex);
// Range vector syntax not accounted for by subsequent parse so discard it if present
selectorString = selectorString.replace(/\[[^\]]+\]$/, '');
const selector = parseSelector(selectorString, selectorString.length - 2).selector;
const labelKeys = this.labelKeys[selector];

View File

@ -2,6 +2,11 @@ import _ from 'lodash';
import { QueryHint } from 'app/types/explore';
/**
* Number of time series results needed before starting to suggest sum aggregation hints
*/
export const SUM_HINT_THRESHOLD_COUNT = 20;
export function getQueryHints(query: string, series?: any[], datasource?: any): QueryHint[] {
const hints = [];
@ -90,5 +95,24 @@ export function getQueryHints(query: string, series?: any[], datasource?: any):
});
}
}
if (series.length >= SUM_HINT_THRESHOLD_COUNT) {
const simpleMetric = query.trim().match(/^\w+$/);
if (simpleMetric) {
hints.push({
type: 'ADD_SUM',
label: 'Many time series results returned.',
fix: {
label: 'Consider aggregating with sum().',
action: {
type: 'ADD_SUM',
query: query,
preventSubmit: true,
},
},
});
}
}
return hints.length > 0 ? hints : null;
}

View File

@ -198,5 +198,76 @@ describe('Language completion provider', () => {
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
});
it('returns label suggestions inside a multi-line aggregation context', () => {
const instance = new LanguageProvider(datasource, {
labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
});
const value = Plain.deserialize('sum(\nmetric\n)\nby ()');
const aggregationTextBlock = value.document.getBlocksAsArray()[3];
const range = value.selection.moveToStartOf(aggregationTextBlock).merge({ anchorOffset: 4 });
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
wrapperClasses: ['context-aggregation'],
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([
{
items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
label: 'Labels',
},
]);
});
it('returns label suggestions inside an aggregation context with a range vector', () => {
const instance = new LanguageProvider(datasource, {
labelKeys: { '{__name__="metric"}': ['label1', 'label2', 'label3'] },
});
const value = Plain.deserialize('sum(rate(metric[1h])) by ()');
const range = value.selection.merge({
anchorOffset: 26,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
wrapperClasses: ['context-aggregation'],
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([
{
items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
label: 'Labels',
},
]);
});
it('returns label suggestions inside an aggregation context with a range vector and label', () => {
const instance = new LanguageProvider(datasource, {
labelKeys: { '{__name__="metric",label1="value"}': ['label1', 'label2', 'label3'] },
});
const value = Plain.deserialize('sum(rate(metric{label1="value"}[1h])) by ()');
const range = value.selection.merge({
anchorOffset: 42,
});
const valueWithSelection = value.change().select(range).value;
const result = instance.provideCompletionItems({
text: '',
prefix: '',
wrapperClasses: ['context-aggregation'],
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([
{
items: [{ label: 'label1' }, { label: 'label2' }, { label: 'label3' }],
label: 'Labels',
},
]);
});
});
});

View File

@ -1,4 +1,4 @@
import { getQueryHints } from '../query_hints';
import { getQueryHints, SUM_HINT_THRESHOLD_COUNT } from '../query_hints';
describe('getQueryHints()', () => {
it('returns no hints for no series', () => {
@ -79,4 +79,25 @@ describe('getQueryHints()', () => {
},
});
});
it('returns a sum hint when many time series results are returned for a simple metric', () => {
const seriesCount = SUM_HINT_THRESHOLD_COUNT;
const series = Array.from({ length: seriesCount }, _ => ({
datapoints: [[0, 0], [0, 0]],
}));
const hints = getQueryHints('metric', series);
expect(hints.length).toBe(1);
expect(hints[0]).toMatchObject({
type: 'ADD_SUM',
label: 'Many time series results returned.',
fix: {
label: 'Consider aggregating with sum().',
action: {
type: 'ADD_SUM',
query: 'metric',
preventSubmit: true,
},
},
});
});
});

View File

@ -114,7 +114,6 @@ export default class StackdriverDatasource {
if (!queryRes.series) {
return;
}
this.projectName = queryRes.meta.defaultProject;
const unit = this.resolvePanelUnitFromTargets(options.targets);
queryRes.series.forEach(series => {
let timeSerie: any = {

View File

@ -5,6 +5,7 @@ import React, { PureComponent } from 'react';
// Components
import Graph from 'app/viz/Graph';
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
import { Switch } from 'app/core/components/Switch/Switch';
// Types
import { PanelProps, NullValueMode } from 'app/types';
@ -35,10 +36,13 @@ export class Graph2 extends PureComponent<Props> {
}
export class TextOptions extends PureComponent<any> {
onChange = () => {};
render() {
return (
<div className="section gf-form-group">
<h5 className="section-heading">Draw Modes</h5>
<Switch label="Lines" checked={true} onChange={this.onChange} />
</div>
);
}

View File

@ -119,6 +119,7 @@ export interface QueryFix {
export interface QueryFixAction {
type: string;
query?: string;
preventSubmit?: boolean;
}
export interface QueryHint {

View File

@ -2271,9 +2271,51 @@ Licensed under the MIT license.
});
}
function drawOrphanedPoints(series) {
/* Filters series data for points with no neighbors before or after
* and plots single 0.5 radius points for them so that they are displayed.
*/
var abandonedPoints = [];
var beforeX = null;
var afterX = null;
var datapoints = series.datapoints;
// find any points with no neighbors before or after
var emptyPoints = [];
for (var j = 0; j < datapoints.pointsize - 2; j++) {
emptyPoints.push(0);
}
for (var i = 0; i < datapoints.points.length; i += datapoints.pointsize) {
var x = datapoints.points[i], y = datapoints.points[i + 1];
if (i === datapoints.points.length - datapoints.pointsize) {
afterX = null;
} else {
afterX = datapoints.points[i + datapoints.pointsize];
}
if (x !== null && y !== null && beforeX === null && afterX === null) {
abandonedPoints.push(x);
abandonedPoints.push(y);
abandonedPoints.push.apply(abandonedPoints, emptyPoints);
}
beforeX = x;
}
var olddatapoints = datapoints.points
datapoints.points = abandonedPoints;
series.points.radius = series.lines.lineWidth/2;
// plot the orphan points with a radius of lineWidth/2
drawSeriesPoints(series);
// reset old info
datapoints.points = olddatapoints;
}
function drawSeries(series) {
if (series.lines.show)
drawSeriesLines(series);
if (!series.points.show && !series.bars.show) {
// not necessary if user wants points displayed for everything
drawOrphanedPoints(series);
}
if (series.bars.show)
drawSeriesBars(series);
if (series.points.show)