Rendering arrows for color picker, applying color changes to time series

This commit is contained in:
Dominik Prokop 2019-01-18 16:42:05 +01:00
parent e35e266c81
commit c8ac23f3c1
15 changed files with 283 additions and 98 deletions

View File

@ -2,9 +2,9 @@ import React, { Component, createRef } from 'react';
import PopperController from '../Tooltip/PopperController';
import Popper from '../Tooltip/Popper';
import { ColorPickerPopover } from './ColorPickerPopover';
import { Themeable } from '../../types';
import { Themeable, GrafanaTheme } from '../../types';
export interface ColorPickerProps {
export interface ColorPickerProps extends Themeable {
color: string;
onChange: (color: string) => void;
}
@ -13,13 +13,29 @@ export class ColorPicker extends Component<ColorPickerProps & Themeable, any> {
private pickerTriggerRef = createRef<HTMLDivElement>();
render() {
const { theme } = this.props;
return (
<PopperController content={<ColorPickerPopover {...this.props} />}>
<PopperController placement="bottom-start" content={<ColorPickerPopover {...this.props} />}>
{(showPopper, hidePopper, popperProps) => {
return (
<>
{this.pickerTriggerRef.current && (
<Popper {...popperProps} referenceElement={this.pickerTriggerRef.current} className="ColorPicker" />
<Popper
{...popperProps}
referenceElement={this.pickerTriggerRef.current}
className="ColorPicker"
renderArrow={({ arrowProps, placement }) => {
return (
<div
{...arrowProps}
data-placement={placement}
className={`ColorPicker__arrow ColorPicker__arrow--${
theme === GrafanaTheme.Light ? 'light' : 'dark'
}`}
/>
);
}}
/>
)}
<div ref={this.pickerTriggerRef} onClick={showPopper} className="sp-replacer sp-light">
<div className="sp-preview">

View File

@ -1,7 +1,7 @@
import React from 'react';
import NamedColorsPicker from './NamedColorsPicker';
import NamedColorsPicker from './NamedColorsPalette';
import { getColorName } from '../..//utils/colorsPalette';
import { SpectrumPicker } from './SpectrumPicker';
import { SpectrumPalette } from './SpectrumPalette';
import { ColorPickerProps } from './ColorPicker';
import { GrafanaTheme, Themeable } from '../../types';
@ -32,7 +32,7 @@ export class ColorPickerPopover extends React.Component<Props, State> {
const { color, onChange, theme } = this.props;
return activePicker === 'spectrum' ? (
<SpectrumPicker color={color} onColorSelect={this.handleSpectrumColorSelect} options={{}} />
<SpectrumPalette color={color} onColorSelect={this.handleSpectrumColorSelect} options={{}} />
) : (
<NamedColorsPicker color={getColorName(color)} onChange={onChange} theme={theme} />
);

View File

@ -1,11 +1,12 @@
import React, { FunctionComponent } from 'react';
import { find, upperFirst } from 'lodash';
import { Color, ColorsPalete, ColorDefinition, getColorForTheme } from '../../utils/colorsPalette';
import { Themeable } from '../../types';
import { ColorDefinition, getColorForTheme } from '../../utils/colorsPalette';
import { Color } from 'csstype';
import { find, upperFirst } from 'lodash';
type ColorChangeHandler = (color: ColorDefinition) => void;
enum ColorSwatchVariant {
export enum ColorSwatchVariant {
Small = 'small',
Large = 'large',
}
@ -50,14 +51,14 @@ const ColorSwatch: FunctionComponent<ColorSwatchProps> = ({
);
};
interface ColorsGroupProps extends Themeable {
interface NamedColorsGroupProps extends Themeable {
colors: ColorDefinition[];
selectedColor?: Color;
onColorSelect: ColorChangeHandler;
key?: string;
}
const ColorsGroup: FunctionComponent<ColorsGroupProps> = ({
const NamedColorsGroup: FunctionComponent<NamedColorsGroupProps> = ({
colors,
selectedColor,
onColorSelect,
@ -100,37 +101,4 @@ const ColorsGroup: FunctionComponent<ColorsGroupProps> = ({
);
};
interface NamedColorsPickerProps extends Themeable {
color?: Color;
onChange: (colorName: string) => void;
}
const NamedColorsPicker = ({ color, onChange, theme }: NamedColorsPickerProps) => {
const swatches: JSX.Element[] = [];
ColorsPalete.forEach((colors, hue) => {
swatches.push(
<ColorsGroup
key={hue}
theme={theme}
selectedColor={color}
colors={colors}
onColorSelect={color => onChange(color.name)}
/>
);
});
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridRowGap: '32px',
gridColumnGap: '32px',
}}
>
{swatches}
</div>
);
};
export default NamedColorsPicker;
export default NamedColorsGroup;

View File

@ -0,0 +1,42 @@
import React from 'react';
import { Color, ColorsPalette } from '../../utils/colorsPalette';
import { Themeable } from '../../types/index';
import NamedColorsGroup from './NamedColorsGroup';
interface NamedColorsPaletteProps extends Themeable {
color?: Color;
onChange: (colorName: string) => void;
}
const NamedColorsPalette = ({ color, onChange, theme }: NamedColorsPaletteProps) => {
const swatches: JSX.Element[] = [];
ColorsPalette.forEach((colors, hue) => {
swatches.push(
<NamedColorsGroup
key={hue}
theme={theme}
selectedColor={color}
colors={colors}
onColorSelect={color => {
onChange(color.name)
}}
/>
);
});
return (
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gridRowGap: '32px',
gridColumnGap: '32px',
}}
>
{swatches}
</div>
);
};
export default NamedColorsPalette;

View File

@ -3,7 +3,7 @@ import * as PopperJS from 'popper.js';
import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
import PopperController from '../Tooltip/PopperController';
import Popper from '../Tooltip/Popper';
import { Themeable } from '../../types';
import { Themeable, GrafanaTheme } from '../../types';
import { ColorPickerProps } from './ColorPicker';
export interface SeriesColorPickerProps extends ColorPickerProps, Themeable {
@ -37,7 +37,7 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
};
render() {
const { children } = this.props;
const { children, theme } = this.props;
return (
<PopperController placement="bottom-start" content={this.renderPickerTabs()}>
{(showPopper, hidePopper, popperProps) => {
@ -49,10 +49,21 @@ export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
onMouseEnter={showPopper}
onMouseLeave={hidePopper}
referenceElement={this.pickerTriggerRef.current}
className="ColorPicker"
arrowClassName="popper__arrow"
wrapperClassName="ColorPicker"
renderArrow={({ arrowProps, placement }) => {
return (
<div
{...arrowProps}
data-placement={placement}
className={`ColorPicker__arrow ColorPicker__arrow--${
theme === GrafanaTheme.Light ? 'light' : 'dark'
}`}
/>
);
}}
/>
)}
{React.cloneElement(children, {
ref: this.pickerTriggerRef,
onClick: showPopper,

View File

@ -9,7 +9,7 @@ export interface Props {
onColorSelect: (color: string) => void;
}
export class SpectrumPicker extends React.Component<Props, any> {
export class SpectrumPalette extends React.Component<Props, any> {
elem: any;
isMoving: boolean;

View File

@ -1,12 +1,107 @@
$arrowSize: 10px;
.ColorPicker {
.popper__arrow {
border-color: #f7f8fa;
}
@extend .popper;
}
.ColorPicker__arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 0px;
&[data-placement^='top'] {
border-width: $arrowSize $arrowSize 0 $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-bottom-color: transparent;
bottom: -$arrowSize;
left: calc(50% - $arrowSize);
padding-top: $arrowSize;
}
&[data-placement^='bottom'] {
border-width: 0 $arrowSize $arrowSize $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: calc(50% - $arrowSize);
}
&[data-placement^='bottom-start'] {
border-width: 0 $arrowSize $arrowSize $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: $arrowSize;
}
&[data-placement^='bottom-end'] & {
border-width: 0 $arrowSize $arrowSize $arrowSize;
border-left-color: transparent;
border-right-color: transparent;
border-top-color: transparent;
top: 0;
left: calc(100% - $arrowSize);
}
&[data-placement^='right'] {
border-width: $arrowSize $arrowSize $arrowSize 0;
border-left-color: transparent;
border-top-color: transparent;
border-bottom-color: transparent;
left: 0;
top: calc(50% - $arrowSize);
}
&[data-placement^='left'] {
border-width: $arrowSize 0 $arrowSize $arrowSize;
border-top-color: transparent;
border-right-color: transparent;
border-bottom-color: transparent;
right: -$arrowSize;
top: calc(50% - $arrowSize);
}
}
.ColorPicker__arrow--light {
border-color: #ffffff;
}
.ColorPicker__arrow--dark {
border-color: #1e2028;
}
// Top
.ColorPicker[data-placement^='top'] {
padding-bottom: $arrowSize;
}
// Bottom
.ColorPicker[data-placement^='bottom'] {
padding-top: $arrowSize;
}
.ColorPicker[data-placement^='bottom-start'] {
padding-top: $arrowSize;
}
.ColorPicker[data-placement^='bottom-end'] {
padding-top: $arrowSize;
}
// Right
.ColorPicker[data-placement^='right'] {
padding-left: $arrowSize;
}
// Left
.ColorPicker[data-placement^='left'] {
padding-right: $arrowSize;
}
.ColorPickerPopover {
border-radius: 3px;
}
@ -93,7 +188,7 @@
}
.gf-color-picker__body {
padding-bottom: 10px;
padding-bottom: $arrowSize;
padding-left: 6px;
}

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import * as PopperJS from 'popper.js';
import { Manager, Popper as ReactPopper } from 'react-popper';
import { Manager, Popper as ReactPopper, PopperArrowProps } from 'react-popper';
import { Portal } from '@grafana/ui';
import Transition from 'react-transition-group/Transition';
import { PopperContent } from './PopperController';
@ -22,12 +22,18 @@ interface Props extends React.HTMLAttributes<HTMLDivElement> {
placement?: PopperJS.Placement;
content: PopperContent;
referenceElement: PopperJS.ReferenceObject;
arrowClassName?: string;
wrapperClassName?: string;
renderArrow?: (
props: {
arrowProps: PopperArrowProps;
placement: string;
}
) => JSX.Element;
}
class Popper extends PureComponent<Props> {
render() {
const { show, placement, onMouseEnter, onMouseLeave, className, arrowClassName } = this.props;
const { show, placement, onMouseEnter, onMouseLeave, className, wrapperClassName, renderArrow } = this.props;
const { content } = this.props;
return (
@ -53,16 +59,15 @@ class Popper extends PureComponent<Props> {
...transitionStyles[transitionState],
}}
data-placement={placement}
className={`popper`}
className={`${wrapperClassName}`}
>
<div className={className}>
{content}
<div
ref={arrowProps.ref}
style={{ ...arrowProps.style }}
data-placement={placement}
className={arrowClassName}
/>
{renderArrow &&
renderArrow({
arrowProps,
placement,
})}
</div>
</div>
);

View File

@ -27,8 +27,11 @@ export const Tooltip = ({ children, theme, ...controllerProps }: TooltipProps) =
onMouseEnter={showPopper}
onMouseLeave={hidePopper}
referenceElement={tooltipTriggerRef.current}
wrapperClassName='popper'
className={popperBackgroundClassName}
arrowClassName={'popper__arrow'}
renderArrow={({ arrowProps, placement }) => (
<div className="popper__arrow" data-placement={placement} {...arrowProps} />
)}
/>
)}
{React.cloneElement(children, {

View File

@ -44,7 +44,6 @@ $popper-margin-from-ref: 5px;
margin: 0px;
}
// Top
.popper[data-placement^='top'] {
padding-bottom: $popper-margin-from-ref;

View File

@ -1,11 +1,7 @@
import { getColorName, getColorDefinition, ColorsPalete, buildColorDefinition } from './colorsPalette';
import { getColorName, getColorDefinition, getColorByName, SemiDarkBlue, getColorFromHexRgbOrName } from './colorsPalette';
import { GrafanaTheme } from '../types';
describe('colors', () => {
const FakeBlue = buildColorDefinition('blue', 'blue', ['#0000ff', '#00000ee']);
beforeAll(() => {
ColorsPalete.set('blue', [FakeBlue]);
});
describe('getColorDefinition', () => {
it('returns undefined for unknown hex', () => {
@ -13,8 +9,8 @@ describe('colors', () => {
});
it('returns definition for known hex', () => {
expect(getColorDefinition(FakeBlue.variants.light)).toEqual(FakeBlue);
expect(getColorDefinition(FakeBlue.variants.dark)).toEqual(FakeBlue);
expect(getColorDefinition(SemiDarkBlue.variants.light)).toEqual(SemiDarkBlue);
expect(getColorDefinition(SemiDarkBlue.variants.dark)).toEqual(SemiDarkBlue);
});
});
@ -24,8 +20,39 @@ describe('colors', () => {
});
it('returns name for known hex', () => {
expect(getColorName(FakeBlue.variants.light)).toEqual(FakeBlue.name);
expect(getColorName(FakeBlue.variants.dark)).toEqual(FakeBlue.name);
expect(getColorName(SemiDarkBlue.variants.light)).toEqual(SemiDarkBlue.name);
expect(getColorName(SemiDarkBlue.variants.dark)).toEqual(SemiDarkBlue.name);
});
});
describe('getColorByName', () => {
it('returns undefined for unknown color', () => {
expect(getColorByName('aruba-sunshine')).toBeUndefined();
});
it('returns color definiton for known color', () => {
expect(getColorByName(SemiDarkBlue.name)).toBe(SemiDarkBlue);
});
});
describe('getColorFromHexRgbOrName', () => {
it('returns undefined for unknown color', () => {
expect(() => getColorFromHexRgbOrName('aruba-sunshine')).toThrow();
});
it('returns dark hex variant for known color if theme not specified', () => {
expect(getColorFromHexRgbOrName(SemiDarkBlue.name)).toBe(SemiDarkBlue.variants.dark);
});
it('returns correct variant\'s hex for known color if theme specified', () => {
expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaTheme.Light)).toBe(SemiDarkBlue.variants.light);
});
it('returns color if specified as hex or rgb/a', () => {
expect(getColorFromHexRgbOrName('ff0000')).toBe('ff0000');
expect(getColorFromHexRgbOrName('#ff0000')).toBe('#ff0000');
expect(getColorFromHexRgbOrName('rgb(0,0,0)')).toBe('rgb(0,0,0)');
expect(getColorFromHexRgbOrName('rgba(0,0,0,1)')).toBe('rgba(0,0,0,1)');
});
});
});

View File

@ -47,7 +47,7 @@ export type ColorDefinition = {
variants: ThemeVariants;
};
export const ColorsPalete = new Map<Hue, ColorDefinition[]>();
export const ColorsPalette = new Map<Hue, ColorDefinition[]>();
export const buildColorDefinition = (
hue: Hue,
@ -107,15 +107,15 @@ const blues = [BasicBlue, DarkBlue, SemiDarkBlue, LightBlue, SuperLightBlue];
const oranges = [BasicOrange, DarkOrange, SemiDarkOrange, LightOrange, SuperLightOrange];
const purples = [BasicPurple, DarkPurple, SemiDarkPurple, LightPurple, SuperLightPurple];
ColorsPalete.set('green', greens);
ColorsPalete.set('yellow', yellows);
ColorsPalete.set('red', reds);
ColorsPalete.set('blue', blues);
ColorsPalete.set('orange', oranges);
ColorsPalete.set('purple', purples);
ColorsPalette.set('green', greens);
ColorsPalette.set('yellow', yellows);
ColorsPalette.set('red', reds);
ColorsPalette.set('blue', blues);
ColorsPalette.set('orange', oranges);
ColorsPalette.set('purple', purples);
export const getColorDefinition = (hex: string): ColorDefinition | undefined => {
return flatten(Array.from(ColorsPalete.values())).filter(definition =>
return flatten(Array.from(ColorsPalette.values())).filter(definition =>
some(values(definition.variants), color => color === hex)
)[0];
};
@ -137,6 +137,25 @@ export const getColorName = (color: string): Color | undefined => {
return color as Color;
};
export const getColorByName = (colorName: string) => {
const definition = flatten(Array.from(ColorsPalette.values())).filter(definition => definition.name === colorName);
return definition.length > 0 ? definition[0] : undefined;
};
export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaTheme): string => {
if (color.indexOf('rgb') > -1 || isHex(color)) {
return color;
}
const colorDefinition = getColorByName(color);
if (!colorDefinition) {
throw new Error('Unknown color');
}
return theme ? colorDefinition.variants[theme] : colorDefinition.variants.dark;
};
export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaTheme) => {
return theme ? color.variants[theme] : color.variants.dark;
};

View File

@ -1,7 +1,6 @@
import kbn from 'app/core/utils/kbn';
import { getFlotTickDecimals } from 'app/core/utils/ticks';
import _ from 'lodash';
import { ColorDefinition } from '@grafana/ui/src/utils/colorsPalette';
function matchSeriesOverride(aliasOrRegex, seriesAlias) {
if (!aliasOrRegex) {
@ -357,13 +356,8 @@ export default class TimeSeries {
return false;
}
setColor(color: string | ColorDefinition) {
if (typeof color === 'string') {
this.color = color;
this.bars.fillColor = color;
} else {
this.color = color.variants.dark;
this.bars.fillColor = color.variants.dark;
}
setColor(color: string) {
this.color = color;
this.bars.fillColor = color;
}
}

View File

@ -1,7 +1,9 @@
import _ from 'lodash';
import { colors } from '@grafana/ui';
import { colors, GrafanaTheme } from '@grafana/ui';
import TimeSeries from 'app/core/time_series2';
import { getColorFromHexRgbOrName } from '@grafana/ui/src/utils/colorsPalette';
import config from 'app/core/config';
export class DataProcessor {
constructor(private panel) {}
@ -107,12 +109,13 @@ export class DataProcessor {
const alias = seriesData.target;
const colorIndex = index % colors.length;
const color = this.panel.aliasColors[alias] || colors[colorIndex];
const series = new TimeSeries({
datapoints: datapoints,
alias: alias,
color: color,
color: getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark),
unit: seriesData.unit,
});

View File

@ -9,6 +9,9 @@ import _ from 'lodash';
import { MetricsPanelCtrl } from 'app/plugins/sdk';
import { DataProcessor } from './data_processor';
import { axesEditorComponent } from './axes_editor';
import { getColorFromHexRgbOrName } from '@grafana/ui/src/utils/colorsPalette';
import config from 'app/core/config';
import { GrafanaTheme } from '@grafana/ui';
class GraphCtrl extends MetricsPanelCtrl {
static template = template;
@ -242,8 +245,8 @@ class GraphCtrl extends MetricsPanelCtrl {
}
onColorChange = (series, color) => {
series.setColor(color);
this.panel.aliasColors[series.alias] = series.color;
series.setColor(getColorFromHexRgbOrName(color, config.bootData.user.lightTheme ? GrafanaTheme.Light : GrafanaTheme.Dark));
this.panel.aliasColors[series.alias] = color;
this.render();
};