diff --git a/.playground/index.html b/.playground/index.html index d85f065a43..a8f3a27002 100644 --- a/.playground/index.html +++ b/.playground/index.html @@ -2,6 +2,7 @@ + Charts Playground Document diff --git a/.playground/playgroud.tsx b/.playground/playgroud.tsx index bf77de95a6..ea06707393 100644 --- a/.playground/playgroud.tsx +++ b/.playground/playgroud.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { loremIpsum } from 'lorem-ipsum'; import { Axis, @@ -9,88 +10,129 @@ import { Position, ScaleType, Settings, - LineSeries, + AreaSeries, + mergeWithDefaultTheme, } from '../src'; import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana'; -import { CursorEvent } from '../src/specs/settings'; -import { CursorUpdateListener } from '../src/chart_types/xy_chart/store/chart_state'; export class Playground extends React.Component { - ref1 = React.createRef(); - ref2 = React.createRef(); - ref3 = React.createRef(); - - onCursorUpdate: CursorUpdateListener = (event?: CursorEvent) => { - this.ref1.current!.dispatchExternalCursorEvent(event); - this.ref2.current!.dispatchExternalCursorEvent(event); - this.ref3.current!.dispatchExternalCursorEvent(event); - }; - render() { - return ( - <> - {renderChart( - '1', - this.ref1, - KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 15), - this.onCursorUpdate, - true, - )} - {renderChart( - '2', - this.ref2, - KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(0, 15), - this.onCursorUpdate, - true, - )} - {renderChart('2', this.ref3, KIBANA_METRICS.metrics.kibana_os_load[1].data.slice(15, 30), this.onCursorUpdate)} - - ); + return <>{this.renderChart(Position.Bottom)}; } -} - -function renderChart( - key: string, - ref: React.RefObject, - data: any, - onCursorUpdate?: CursorUpdateListener, - timeSeries: boolean = false, -) { - return ( -
- - - - d.toFixed(2)} /> - { + const random = Math.floor(Math.random() * 3) + 1; + const id = loremIpsum({ count: random, units: 'words' }); + return ( + - - -
- ); + ); + }; + const theme = mergeWithDefaultTheme({ + lineSeriesStyle: { + line: { + stroke: 'violet', + strokeWidth: 4, + }, + point: { + fill: 'yellow', + stroke: 'black', + strokeWidth: 2, + radius: 6, + }, + }, + }); + return ( +
+ + + + d.toFixed(2)} /> + + + + + {Array(10) + .fill(null) + .map(renderMore)} + +
+ ); + } } diff --git a/package.json b/package.json index d9e74db788..70b1940e0e 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "husky": "^1.3.1", "jest": "^24.1.0", "jest-environment-jsdom-fourteen": "^0.1.0", + "lorem-ipsum": "^2.0.3", "node-sass": "^4.11.0", "postcss-cli": "^6.1.3", "prettier": "1.16.4", diff --git a/src/chart_types/xy_chart/store/chart_state.test.ts b/src/chart_types/xy_chart/store/chart_state.test.ts index de7e3f7d87..a9be86e279 100644 --- a/src/chart_types/xy_chart/store/chart_state.test.ts +++ b/src/chart_types/xy_chart/store/chart_state.test.ts @@ -108,14 +108,6 @@ describe('Chart Store', () => { expect(axesTicks.get(AXIS_ID)).not.toBeUndefined(); }); - test('can toggle legend visibility', () => { - store.toggleLegendCollapsed(); - expect(store.legendCollapsed.get()).toBe(true); - - store.toggleLegendCollapsed(); - expect(store.legendCollapsed.get()).toBe(false); - }); - test('can set legend visibility', () => { store.showLegend.set(false); store.setShowLegend(true); @@ -509,7 +501,7 @@ describe('Chart Store', () => { }; localStore.computeChart(); - expect(localStore.initialized.get()).toBe(false); + expect(localStore.chartInitialized.get()).toBe(false); }); test('only computes chart if series specs exist', () => { @@ -524,7 +516,7 @@ describe('Chart Store', () => { localStore.seriesSpecs = new Map(); localStore.computeChart(); - expect(localStore.initialized.get()).toBe(false); + expect(localStore.chartInitialized.get()).toBe(false); }); test('can set the color for a series', () => { diff --git a/src/chart_types/xy_chart/store/chart_state.ts b/src/chart_types/xy_chart/store/chart_state.ts index 46653097ea..268378da3c 100644 --- a/src/chart_types/xy_chart/store/chart_state.ts +++ b/src/chart_types/xy_chart/store/chart_state.ts @@ -119,7 +119,8 @@ export class ChartStore { debug = false; id = uuid.v4(); specsInitialized = observable.box(false); - initialized = observable.box(false); + chartInitialized = observable.box(false); + legendInitialized = observable.box(false); enableHistogramMode = observable.box(false); parentDimensions: Dimensions = { @@ -232,15 +233,9 @@ export class ChartStore { canDataBeAnimated = false; showLegend = observable.box(false); - legendCollapsed = observable.box(false); - legendPosition: Position | undefined; + legendPosition = observable.box(Position.Right); showLegendDisplayValue = observable.box(true); - toggleLegendCollapsed = action(() => { - this.legendCollapsed.set(!this.legendCollapsed.get()); - this.computeChart(); - }); - /** * determine if crosshair cursor should be visible based on cursor position and brush enablement */ @@ -833,7 +828,7 @@ export class ChartStore { } computeChart() { - this.initialized.set(false); + this.chartInitialized.set(false); // compute only if parent dimensions are computed if (this.parentDimensions.width === 0 || this.parentDimensions.height === 0) { return; @@ -879,6 +874,14 @@ export class ChartStore { this.deselectedDataSeries, ); + if (!this.legendInitialized.get()) { + this.legendInitialized.set(true); + + if (this.legendItems.size > 0 && this.showLegend.get()) { + return; + } + } + this.isChartEmpty = isAllSeriesDeselected(this.legendItems); const { xDomain, yDomain, formattedDataSeries } = this.seriesDomainsAndData; @@ -913,14 +916,12 @@ export class ChartStore { }); bboxCalculator.destroy(); - // // compute chart dimensions + // compute chart dimensions const computedChartDims = computeChartDimensions( this.parentDimensions, this.chartTheme, this.axesTicksDimensions, this.axesSpecs, - this.showLegend.get() && !this.legendCollapsed.get(), - this.legendPosition, ); this.chartDimensions = computedChartDims.chartDimensions; @@ -940,7 +941,6 @@ export class ChartStore { this.enableHistogramMode.get(), ); - // tslint:disable-next-line:no-console this.geometries = seriesGeometries.geometries; this.xScale = seriesGeometries.scales.xScale; @@ -958,17 +958,15 @@ export class ChartStore { computedChartDims, this.chartTheme, this.chartRotation, - this.showLegend.get() && !this.legendCollapsed.get(), this.axesSpecs, this.axesTicksDimensions, xDomain, yDomain, totalBarsInCluster, this.enableHistogramMode.get(), - this.legendPosition, barsPadding, ); - // tslint:disable-next-line:no-console + this.axesPositions = axisTicksPositions.axisPositions; this.axesTicks = axisTicksPositions.axisTicks; this.axesVisibleTicks = axisTicksPositions.axisVisibleTicks; @@ -992,6 +990,6 @@ export class ChartStore { // temporary disabled until // https://github.com/elastic/elastic-charts/issues/89 and https://github.com/elastic/elastic-charts/issues/41 this.canDataBeAnimated = false; - this.initialized.set(true); + this.chartInitialized.set(true); } } diff --git a/src/chart_types/xy_chart/utils/axis_utils.test.ts b/src/chart_types/xy_chart/utils/axis_utils.test.ts index 913f7102a5..55dfd19334 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.test.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.test.ts @@ -33,11 +33,6 @@ import { import { CanvasTextBBoxCalculator } from '../../../utils/bbox/canvas_text_bbox_calculator'; import { SvgTextBBoxCalculator } from '../../../utils/bbox/svg_text_bbox_calculator'; -// const chartScalesConfig: ScalesConfig = { -// ordinal: { -// padding: 0, -// }, -// }; describe('Axis computational utils', () => { const mockedRect = { x: 0, @@ -774,7 +769,6 @@ describe('Axis computational utils', () => { test('should compute axis ticks positions with title', () => { const chartRotation = 0; - const showLegend = false; // validate assumptions for test expect(verticalAxisSpec.id).toEqual(verticalAxisSpecWTitle.id); @@ -792,7 +786,6 @@ describe('Axis computational utils', () => { }, LIGHT_THEME, chartRotation, - showLegend, axisSpecs, axisDims, xDomain, @@ -819,7 +812,6 @@ describe('Axis computational utils', () => { }, LIGHT_THEME, chartRotation, - showLegend, axisSpecs, axisDims, xDomain, @@ -982,9 +974,6 @@ describe('Axis computational utils', () => { test('should not compute axis ticks positions if missaligned specs', () => { const chartRotation = 0; - const showLegend = true; - const leftLegendPosition = Position.Left; - const axisSpecs = new Map(); axisSpecs.set(verticalAxisSpec.id, verticalAxisSpec); @@ -998,14 +987,12 @@ describe('Axis computational utils', () => { }, LIGHT_THEME, chartRotation, - showLegend, axisSpecs, axisDims, xDomain, [yDomain], 1, false, - leftLegendPosition, ); expect(axisTicksPosition.axisPositions.size).toBe(0); expect(axisTicksPosition.axisTicks.size).toBe(0); @@ -1015,10 +1002,6 @@ describe('Axis computational utils', () => { test('should compute axis ticks positions', () => { const chartRotation = 0; - const showLegend = true; - const leftLegendPosition = Position.Left; - const topLegendPosition = Position.Top; - const axisSpecs = new Map(); axisSpecs.set(verticalAxisSpec.id, verticalAxisSpec); @@ -1032,14 +1015,12 @@ describe('Axis computational utils', () => { }, LIGHT_THEME, chartRotation, - showLegend, axisSpecs, axisDims, xDomain, [yDomain], 1, false, - leftLegendPosition, ); const expectedVerticalAxisGridLines = [ @@ -1065,14 +1046,12 @@ describe('Axis computational utils', () => { }, LIGHT_THEME, chartRotation, - showLegend, axisSpecs, axisDims, xDomain, [yDomain], 1, false, - topLegendPosition, ); const expectedPositionWithTopLegend = { @@ -1095,14 +1074,12 @@ describe('Axis computational utils', () => { }, LIGHT_THEME, chartRotation, - showLegend, invalidSpecs, axisDims, xDomain, [yDomain], 1, false, - leftLegendPosition, ); }; diff --git a/src/chart_types/xy_chart/utils/axis_utils.ts b/src/chart_types/xy_chart/utils/axis_utils.ts index f40705a0d4..1417a35262 100644 --- a/src/chart_types/xy_chart/utils/axis_utils.ts +++ b/src/chart_types/xy_chart/utils/axis_utils.ts @@ -535,18 +535,15 @@ export function getAxisTicksPositions( }, chartTheme: Theme, chartRotation: Rotation, - showLegend: boolean, axisSpecs: Map, axisDimensions: Map, xDomain: XDomain, yDomain: YDomain[], totalGroupsCount: number, enableHistogramMode: boolean, - legendPosition?: Position, barsPadding?: number, ) { const { chartPaddings, chartMargins } = chartTheme; - const legendStyle = chartTheme.legend; const axisPositions: Map = new Map(); const axisVisibleTicks: Map = new Map(); const axisTicks: Map = new Map(); @@ -556,16 +553,6 @@ export function getAxisTicksPositions( let cumBottomSum = chartPaddings.bottom; let cumLeftSum = computedChartDims.leftMargin; let cumRightSum = chartPaddings.right; - if (showLegend) { - switch (legendPosition) { - case Position.Left: - cumLeftSum += legendStyle.verticalWidth; - break; - case Position.Top: - cumTopSum += legendStyle.horizontalHeight; - break; - } - } axisDimensions.forEach((axisDim, id) => { const axisSpec = axisSpecs.get(id); @@ -654,7 +641,7 @@ export function isVertical(position: Position) { } export function isHorizontal(position: Position) { - return !isVertical(position); + return position === Position.Top || position === Position.Bottom; } export function isLowerBound(domain: Partial): domain is LowerBoundedDomain { diff --git a/src/chart_types/xy_chart/utils/dimensions.test.ts b/src/chart_types/xy_chart/utils/dimensions.test.ts index 785c2c5f8c..dbf7e330bb 100644 --- a/src/chart_types/xy_chart/utils/dimensions.test.ts +++ b/src/chart_types/xy_chart/utils/dimensions.test.ts @@ -50,8 +50,8 @@ describe('Computed chart dimensions', () => { const legend: LegendStyle = { verticalWidth: 10, horizontalHeight: 10, + spacingBuffer: 10, }; - const showLegend = false; const defaultTheme = LIGHT_THEME; const chartTheme = { ...defaultTheme, @@ -67,7 +67,7 @@ describe('Computed chart dimensions', () => { test('should be equal to parent dimension with no axis minus margins', () => { const axisDims = new Map(); const axisSpecs = new Map(); - const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs, showLegend); + const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs); expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); expect(chartDimensions).toMatchSnapshot(); @@ -79,7 +79,7 @@ describe('Computed chart dimensions', () => { const axisSpecs = new Map(); axisDims.set(getAxisId('axis_1'), axis1Dims); axisSpecs.set(getAxisId('axis_1'), axisLeftSpec); - const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs, showLegend); + const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs); expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); expect(chartDimensions).toMatchSnapshot(); @@ -91,7 +91,7 @@ describe('Computed chart dimensions', () => { const axisSpecs = new Map(); axisDims.set(getAxisId('axis_1'), axis1Dims); axisSpecs.set(getAxisId('axis_1'), { ...axisLeftSpec, position: Position.Right }); - const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs, showLegend); + const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs); expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); expect(chartDimensions).toMatchSnapshot(); @@ -106,7 +106,7 @@ describe('Computed chart dimensions', () => { ...axisLeftSpec, position: Position.Top, }); - const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs, showLegend); + const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs); expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); expect(chartDimensions).toMatchSnapshot(); @@ -121,7 +121,7 @@ describe('Computed chart dimensions', () => { ...axisLeftSpec, position: Position.Bottom, }); - const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs, showLegend); + const { chartDimensions } = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs); expect(chartDimensions.left + chartDimensions.width).toBeLessThanOrEqual(parentDim.width); expect(chartDimensions.top + chartDimensions.height).toBeLessThanOrEqual(parentDim.height); expect(chartDimensions).toMatchSnapshot(); @@ -134,7 +134,7 @@ describe('Computed chart dimensions', () => { ...axisLeftSpec, position: Position.Bottom, }); - const chartDimensions = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs, showLegend); + const chartDimensions = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs); const expectedDims = { chartDimensions: { @@ -156,7 +156,7 @@ describe('Computed chart dimensions', () => { hide: true, position: Position.Bottom, }); - const hiddenAxisChartDimensions = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs, showLegend); + const hiddenAxisChartDimensions = computeChartDimensions(parentDim, chartTheme, axisDims, axisSpecs); expect(hiddenAxisChartDimensions).toEqual(expectedDims); }); diff --git a/src/chart_types/xy_chart/utils/dimensions.ts b/src/chart_types/xy_chart/utils/dimensions.ts index 784782612e..0d8d10d613 100644 --- a/src/chart_types/xy_chart/utils/dimensions.ts +++ b/src/chart_types/xy_chart/utils/dimensions.ts @@ -19,14 +19,11 @@ export function computeChartDimensions( chartTheme: Theme, axisDimensions: Map, axisSpecs: Map, - showLegend: boolean, - legendPosition?: Position, ): { chartDimensions: Dimensions; leftMargin: number; } { const { chartMargins, chartPaddings } = chartTheme; - const legendStyle = chartTheme.legend; const { axisTitleStyle } = chartTheme.axes; const axisTitleHeight = axisTitleStyle.fontSize + axisTitleStyle.padding; @@ -75,41 +72,16 @@ export function computeChartDimensions( const chartWidth = parentDimensions.width - chartLeftAxisMaxWidth - chartRightAxisMaxWidth; const chartHeight = parentDimensions.height - chartTopAxisMaxHeight - chartBottomAxisMaxHeight; - let vMargin = 0; - let hMargin = 0; - - // add space for legend - let legendTopMargin = 0; - let legendLeftMargin = 0; - if (showLegend) { - switch (legendPosition) { - case Position.Right: - hMargin += legendStyle.verticalWidth; - break; - case Position.Left: - hMargin += legendStyle.verticalWidth; - legendLeftMargin = legendStyle.verticalWidth; - break; - case Position.Top: - vMargin += legendStyle.horizontalHeight; - legendTopMargin = legendStyle.horizontalHeight; - break; - case Position.Bottom: - vMargin += legendStyle.horizontalHeight; - break; - } - } - - let top = chartTopAxisMaxHeight + chartPaddings.top + legendTopMargin; - let left = chartLeftAxisMaxWidth + chartPaddings.left + legendLeftMargin; + let top = chartTopAxisMaxHeight + chartPaddings.top; + let left = chartLeftAxisMaxWidth + chartPaddings.left; return { leftMargin: chartLeftAxisMaxWidth - vLeftAxisSpecWidth, chartDimensions: { top, left, - width: chartWidth - hMargin - chartPaddings.left - chartPaddings.right, - height: chartHeight - vMargin - chartPaddings.top - chartPaddings.bottom, + width: chartWidth - chartPaddings.left - chartPaddings.right, + height: chartHeight - chartPaddings.top - chartPaddings.bottom, }, }; } diff --git a/src/components/_container.scss b/src/components/_container.scss index a1dd5b6068..ad0bad3076 100644 --- a/src/components/_container.scss +++ b/src/components/_container.scss @@ -3,17 +3,19 @@ */ .echContainer { + flex: 1; position: relative; - width: 100%; +} + +.echChart { + display: flex; height: 100%; - &:hover { - .echLegend__toggle { - opacity: 1; - } + &--isBrushEnabled { + cursor: crosshair; } -} -.echChart--isBrushEnabled { - cursor: crosshair; + &--column { + flex-direction: column; + } } diff --git a/src/components/_global.scss b/src/components/_global.scss new file mode 100644 index 0000000000..8099c94c55 --- /dev/null +++ b/src/components/_global.scss @@ -0,0 +1,3 @@ +.invisible { + visibility: hidden; +} diff --git a/src/components/_index.scss b/src/components/_index.scss index 857235a188..de8ee6711d 100644 --- a/src/components/_index.scss +++ b/src/components/_index.scss @@ -1,3 +1,4 @@ +@import 'global'; @import 'container'; @import 'annotation'; @import 'crosshair'; diff --git a/src/components/_tooltip.scss b/src/components/_tooltip.scss index 02338a29cd..9200337d28 100644 --- a/src/components/_tooltip.scss +++ b/src/components/_tooltip.scss @@ -26,7 +26,7 @@ &__label { @include euiTextOverflowWrap; min-width: 1px; - flex: 1; + flex: 1 1 auto; } &__value { diff --git a/src/components/chart.tsx b/src/components/chart.tsx index a2bafc6d6a..8d58628155 100644 --- a/src/components/chart.tsx +++ b/src/components/chart.tsx @@ -1,17 +1,18 @@ +import React, { CSSProperties } from 'react'; import classNames from 'classnames'; import { Provider } from 'mobx-react'; -import React, { CSSProperties, Fragment } from 'react'; + import { SpecsParser } from '../specs/specs_parser'; import { ChartStore } from '../chart_types/xy_chart/store/chart_state'; -import { htmlIdGenerator } from '../utils/commons'; import { AnnotationTooltip } from './annotation_tooltips'; import { ChartResizer } from './chart_resizer'; import { Crosshair } from './crosshair'; import { Highlighter } from './highlighter'; import { Legend } from './legend/legend'; -import { LegendButton } from './legend/legend_button'; import { ReactiveChart as ReactChart } from './react_canvas/reactive_chart'; import { Tooltips } from './tooltips'; +import { isHorizontal } from '../chart_types/xy_chart/utils/axis_utils'; +import { Position } from '../chart_types/xy_chart/utils/specs'; import { CursorEvent } from '../specs/settings'; import { ChartSize, getChartSize } from '../utils/chart_size'; @@ -24,18 +25,39 @@ interface ChartProps { className?: string; } -export class Chart extends React.Component { +interface ChartState { + legendPosition: Position; +} + +export class Chart extends React.Component { static defaultProps: ChartProps = { renderer: 'canvas', }; private chartSpecStore: ChartStore; - private legendId: string; constructor(props: any) { super(props); this.chartSpecStore = new ChartStore(); - this.legendId = htmlIdGenerator()('legend'); + this.state = { + legendPosition: this.chartSpecStore.legendPosition.get(), + }; + // value is set to chart_store in settings so need to watch the value + this.chartSpecStore.legendPosition.observe(({ newValue: legendPosition }) => { + this.setState({ + legendPosition, + }); + }); } + static getContainerStyle = (size: any): CSSProperties => { + if (size) { + return { + position: 'relative', + ...getChartSize(size), + }; + } + return {}; + }; + dispatchExternalCursorEvent(event?: CursorEvent) { this.chartSpecStore.setActiveChartId(event && event.chartId); const isActiveChart = this.chartSpecStore.isActiveChart.get(); @@ -57,21 +79,18 @@ export class Chart extends React.Component { render() { const { renderer, size, className } = this.props; - let containerStyle: CSSProperties; - if (size) { - containerStyle = { - position: 'relative', - ...getChartSize(size), - }; - } else { - containerStyle = {}; - } - const chartClass = classNames('echContainer', className); + const containerStyle = Chart.getContainerStyle(size); + const Horizontal = isHorizontal(this.state.legendPosition); + const chartClassNames = classNames('echChart', className, { + 'echChart--column': Horizontal, + }); + return ( - +
+ {this.props.children} -
+
{// TODO reenable when SVG rendered is aligned with canvas one @@ -79,11 +98,9 @@ export class Chart extends React.Component { {renderer === 'canvas' && } - -
- +
); } diff --git a/src/components/legend/_index.scss b/src/components/legend/_index.scss index 8caba8f13e..6b6f3997ff 100644 --- a/src/components/legend/_index.scss +++ b/src/components/legend/_index.scss @@ -1,5 +1,3 @@ @import 'variables'; @import 'legend'; -@import 'legend_button'; -@import 'legend_list'; @import 'legend_item'; diff --git a/src/components/legend/_legend.scss b/src/components/legend/_legend.scss index 32703da480..fcb5d4671c 100644 --- a/src/components/legend/_legend.scss +++ b/src/components/legend/_legend.scss @@ -1,67 +1,46 @@ -// Legend - .echLegend { - // Margin supplied in JS to match chart margins - position: absolute !important; // Override shadow - overflow-y: hidden; -} - -.echLegend--collapsed { - display: none; -} - -.echLegend--debug { - background: red; -} - -.echLegend--top, -.echLegend--bottom { - left: $euiSizeL; - right: 0; - height: $echLegendMaxHeight; - - .echLegendList { - flex-direction: row; - flex-wrap: wrap; + &--top, + &--bottom { + .echLegendList { + display: grid; + grid-column-gap: $echLegendColumnGap; + grid-row-gap: $echLegendRowGap; + margin-top: $echLegendRowGap; + margin-bottom: $echLegendRowGap; + + @include internetExplorerOnly { + display: flex; + flex-direction: row; + flex-wrap: wrap; + } + } } - .echLegendItem { - margin-right: $euiSizeL; - width: $echLegendMaxWidth; + &--left, + &--right { + .echLegendList { + flex-direction: column; + } } -} -.echLegend--top { - top: 0; -} -.echLegend--bottom { - bottom: 0; -} + &--top, + &--left { + order: 0; + } -.echLegend--left, -.echLegend--right { - top: 0; - bottom: 0; - width: $echLegendMaxWidth; + &--bottom, + &--right { + order: 1; + } - .echLegendList { - flex-direction: column; + &--debug { + background: red; } - .echLegendItem { + .echLegendListContainer { + @include euiYScrollWithShadows; width: 100%; + overflow-y: auto; + overflow-x: hidden; } } - -.echLegend--left { - left: 0; -} -.echLegend--right { - right: 0; -} - -.echLegendListContainer { - @include euiYScrollWithShadows; - width: 100%; - overflow-y: auto; -} diff --git a/src/components/legend/_legend_button.scss b/src/components/legend/_legend_button.scss deleted file mode 100644 index e634285241..0000000000 --- a/src/components/legend/_legend_button.scss +++ /dev/null @@ -1,23 +0,0 @@ -.echLegendButton { - padding: $euiSizeXS; - line-height: 1; - opacity: 0.35; - border-radius: $euiBorderRadius; - background-color: $euiColorEmptyShade; - position: absolute; - bottom: 0; - left: 0; - transition: - opacity $euiAnimSpeedFast $euiAnimSlightResistance, - background-color $euiAnimSpeedFast $euiAnimSlightResistance; - - &:hover, - &:focus { - opacity: 1; - background-color: $euiFocusBackgroundColor; - } -} - -.echLegendButton--isOpen { - opacity: 1; -} diff --git a/src/components/legend/_legend_item.scss b/src/components/legend/_legend_item.scss index cd3c4e31a7..e312e19eb3 100644 --- a/src/components/legend/_legend_item.scss +++ b/src/components/legend/_legend_item.scss @@ -1,62 +1,81 @@ +$legendItemVerticalPadding: $echLegendRowGap / 2; + .echLegendItem { color: $euiTextColor; - height: $echLegendItemHeight; - width: 100%; display: flex; flex-wrap: nowrap; justify-content: space-between; user-select: none; align-items: center; + width: 100%; &:hover { .echLegendItem__title { text-decoration: underline; } } -} -.echLegendItem__color { - margin-right: $euiSizeXS; -} + &__color { + margin-right: $euiSizeXS; + } -.echLegendItem__visibility { - margin-right: $euiSizeXS; + &__visibility { + margin-right: $euiSizeXS; - &:hover { - cursor: pointer; + &:hover { + cursor: pointer; + } } -} -.echLegendItem__title { - @include euiFontSizeXS; - @include euiTextTruncate; - margin-right: $euiSizeXS; - flex: 1; - &:hover { - cursor: pointer; + &__title { + @include euiFontSizeXS; + @include euiTextTruncate; + flex: 1 1 auto; + + &:hover { + cursor: pointer; + } } -} -.echLegendItem__title--selected { - text-decoration: underline; -} + &__title--selected { + text-decoration: underline; + } -.echLegendItem__title--hasClickListener { - &:hover { - cursor: pointer; + &__title--hasClickListener { + &:hover { + cursor: pointer; + } } -} -.echLegendItem__displayValue { - @include euiFontSizeXS; - text-align: right; - font-feature-settings: 'tnum'; + &__displayValue { + @include euiFontSizeXS; + text-align: right; + margin-left: $euiSizeXS; + font-feature-settings: 'tnum'; - &--hidden { - display: none; + &--hidden { + display: none; + } } -} -.echLegendItem-isHidden { - color: $euiColorDarkShade; + &--right, + &--left { + padding-top: $legendItemVerticalPadding; + padding-bottom: $legendItemVerticalPadding; + } + + @include internetExplorerOnly { + &--bottom, + &--top { + width: $echLegendMaxWidth; + margin-right: $euiSizeL; + } + + padding-top: $legendItemVerticalPadding; + padding-bottom: $legendItemVerticalPadding; + } + + &--hidden { + color: $euiColorDarkShade; + } } diff --git a/src/components/legend/_legend_list.scss b/src/components/legend/_legend_list.scss deleted file mode 100644 index 41d95552fe..0000000000 --- a/src/components/legend/_legend_list.scss +++ /dev/null @@ -1,3 +0,0 @@ -.echLegendList { - display: flex; -} diff --git a/src/components/legend/_variables.scss b/src/components/legend/_variables.scss index 9eb2df5292..c8543808be 100644 --- a/src/components/legend/_variables.scss +++ b/src/components/legend/_variables.scss @@ -1,3 +1,3 @@ $echLegendMaxWidth: 200px; -$echLegendMaxHeight: $euiSizeXL * 2; -$echLegendItemHeight: ($echLegendMaxHeight / 2) - 6px; +$echLegendRowGap: 8px; +$echLegendColumnGap: 24px; diff --git a/src/components/legend/legend.tsx b/src/components/legend/legend.tsx index 79049e824e..751c580ca6 100644 --- a/src/components/legend/legend.tsx +++ b/src/components/legend/legend.tsx @@ -1,59 +1,133 @@ import classNames from 'classnames'; import { inject, observer } from 'mobx-react'; -import React from 'react'; -import { isVertical } from '../../chart_types/xy_chart/utils/axis_utils'; +import React, { createRef } from 'react'; +import { isVertical, isHorizontal } from '../../chart_types/xy_chart/utils/axis_utils'; import { LegendItem as SeriesLegendItem } from '../../chart_types/xy_chart/legend/legend'; import { ChartStore } from '../../chart_types/xy_chart/store/chart_state'; +import { Position } from '../../chart_types/xy_chart/utils/specs'; import { LegendItem } from './legend_item'; +import { Theme } from '../../utils/themes/theme'; interface LegendProps { chartStore?: ChartStore; // FIX until we find a better way on ts mobx - legendId: string; } -class LegendComponent extends React.Component { +interface LegendState { + width?: number; +} + +interface LegendStyle { + maxHeight?: string; + maxWidth?: string; + width?: string; +} + +interface LegendListStyle { + paddingTop?: number | string; + paddingBottom?: number | string; + paddingLeft?: number | string; + paddingRight?: number | string; + gridTemplateColumns?: string; +} + +class LegendComponent extends React.Component { static displayName = 'Legend'; - onCollapseLegend = () => { - this.props.chartStore!.toggleLegendCollapsed(); + state = { + width: undefined, }; + private echLegend = createRef(); + + componentDidUpdate() { + const { chartInitialized, chartTheme, legendPosition } = this.props.chartStore!; + if ( + this.echLegend.current && + isVertical(legendPosition.get()) && + this.state.width === undefined && + !chartInitialized.get() + ) { + const buffer = chartTheme.legend.spacingBuffer; + + this.setState({ + width: this.echLegend.current.offsetWidth + buffer, + }); + } + } + render() { - const { legendId } = this.props; const { - initialized, + legendInitialized, + chartInitialized, legendItems, legendPosition, showLegend, - legendCollapsed, debug, chartTheme, } = this.props.chartStore!; + const postion = legendPosition.get(); - if (!showLegend.get() || !initialized.get() || legendItems.size === 0 || legendPosition === undefined) { + if (!showLegend.get() || !legendInitialized.get() || legendItems.size === 0) { return null; } - const legendClasses = classNames('echLegend', `echLegend--${legendPosition}`, { - 'echLegend--collapsed': legendCollapsed.get(), + const legendContainerStyle = this.getLegendStyle(postion, chartTheme); + const legendListStyle = this.getLegendListStyle(postion, chartTheme); + const legendClasses = classNames('echLegend', `echLegend--${postion}`, { 'echLegend--debug': debug, + invisible: !chartInitialized.get(), }); - let paddingStyle; - if (isVertical(legendPosition)) { - paddingStyle = { - paddingTop: chartTheme.chartMargins.top, - paddingBottom: chartTheme.chartMargins.bottom, - }; - } + return ( -
-
-
{[...legendItems.values()].map(this.renderLegendElement)}
+
+
+
+ {[...legendItems.values()].map(this.renderLegendElement)} +
); } + getLegendListStyle = (position: Position, { chartMargins, legend }: Theme): LegendListStyle => { + const { top: paddingTop, bottom: paddingBottom, left: paddingLeft, right: paddingRight } = chartMargins; + + if (isHorizontal(position)) { + return { + paddingLeft, + paddingRight, + gridTemplateColumns: `repeat(auto-fill, minmax(${legend.verticalWidth}px, 1fr))`, + }; + } + + return { + paddingTop, + paddingBottom, + }; + }; + + getLegendStyle = (position: Position, { legend }: Theme): LegendStyle => { + if (isVertical(position)) { + if (this.state.width !== undefined) { + const threshold = Math.min(this.state.width!, legend.verticalWidth); + const width = `${threshold}px`; + + return { + width, + maxWidth: width, + }; + } + + return { + maxWidth: `${legend.verticalWidth}px`, + }; + } + + return { + maxHeight: `${legend.horizontalHeight}px`, + }; + }; + onLegendItemMouseover = (legendItemKey: string) => () => { this.props.chartStore!.onLegendItemOver(legendItemKey); }; @@ -64,7 +138,8 @@ class LegendComponent extends React.Component { private renderLegendElement = (item: SeriesLegendItem) => { const { key, displayValue } = item; - const tooltipValues = this.props.chartStore!.legendItemTooltipValues.get(); + const { legendPosition, legendItemTooltipValues } = this.props.chartStore!; + const tooltipValues = legendItemTooltipValues.get(); let tooltipValue; if (tooltipValues && tooltipValues.get(key)) { @@ -78,6 +153,7 @@ class LegendComponent extends React.Component { {...item} key={key} legendItemKey={key} + legendPosition={legendPosition.get()} displayValue={newDisplayValue} onMouseEnter={this.onLegendItemMouseover(key)} onMouseLeave={this.onLegendItemMouseout} diff --git a/src/components/legend/legend_button.tsx b/src/components/legend/legend_button.tsx deleted file mode 100644 index 7fd0cd9466..0000000000 --- a/src/components/legend/legend_button.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import classNames from 'classnames'; -import { inject, observer } from 'mobx-react'; -import React from 'react'; -import { ChartStore } from '../../chart_types/xy_chart/store/chart_state'; -import { Icon } from '../icons/icon'; - -interface LegendButtonProps { - chartStore?: ChartStore; - legendId: string; -} - -class LegendButtonComponent extends React.Component { - static displayName = 'Legend'; - onCollapseLegend = () => { - this.props.chartStore!.toggleLegendCollapsed(); - }; - - render() { - const { initialized, legendItems, legendCollapsed, showLegend } = this.props.chartStore!; - - if (!showLegend.get() || !initialized.get() || legendItems.size === 0) { - return null; - } - const isOpen = !legendCollapsed.get(); - const className = classNames('echLegendButton', { - 'echLegendButton--isOpen': isOpen, - }); - return ( - - ); - } -} - -export const LegendButton = inject('chartStore')(observer(LegendButtonComponent)); diff --git a/src/components/legend/legend_item.tsx b/src/components/legend/legend_item.tsx index d23ac9df9c..6dc2cb6c3b 100644 --- a/src/components/legend/legend_item.tsx +++ b/src/components/legend/legend_item.tsx @@ -4,10 +4,12 @@ import React from 'react'; import { Icon } from '../icons/icon'; import { ChartStore } from '../../chart_types/xy_chart/store/chart_state'; +import { Position } from '../../chart_types/xy_chart/utils/specs'; interface LegendItemProps { chartStore?: ChartStore; // FIX until we find a better way on ts mobx legendItemKey: string; + legendPosition: Position; color: string | undefined; label: string | undefined; isSeriesVisible?: boolean; @@ -44,17 +46,26 @@ class LegendItemComponent extends React.Component { } render() { - const { initialized } = this.props.chartStore!; - if (!initialized.get()) { + const { chartInitialized } = this.props.chartStore!; + if (!chartInitialized.get()) { return null; } @@ -354,29 +354,12 @@ class Chart extends React.Component { debug, setCursorPosition, isChartEmpty, - legendCollapsed, - legendPosition, - chartTheme, } = this.props.chartStore!; if (isChartEmpty) { - const isLegendCollapsed = legendCollapsed.get(); - const { verticalWidth, horizontalHeight } = chartTheme.legend; - - const paddingStyle = - legendPosition && isVertical(legendPosition) - ? legendPosition === Position.Right - ? { paddingLeft: -verticalWidth } - : { paddingLeft: verticalWidth } - : legendPosition === Position.Top - ? { paddingTop: horizontalHeight } - : { paddingTop: -horizontalHeight }; - - const style = isLegendCollapsed ? undefined : paddingStyle; - return (
-

No data to display

+

No data to display

); } diff --git a/src/specs/settings.test.tsx b/src/specs/settings.test.tsx index f5d1b0525f..6844b86f55 100644 --- a/src/specs/settings.test.tsx +++ b/src/specs/settings.test.tsx @@ -47,7 +47,7 @@ describe('Settings spec component', () => { expect(chartStore.showLegend.get()).toEqual(true); expect(chartStore.tooltipType.get()).toEqual(TooltipType.None); expect(chartStore.tooltipSnap.get()).toEqual(false); - expect(chartStore.legendPosition).toBe(Position.Bottom); + expect(chartStore.legendPosition.get()).toBe(Position.Bottom); expect(chartStore.showLegendDisplayValue.get()).toEqual(false); expect(chartStore.debug).toBe(true); expect(chartStore.customXDomain).toEqual({ min: 0, max: 10 }); @@ -64,7 +64,7 @@ describe('Settings spec component', () => { expect(chartStore.tooltipType.get()).toEqual(DEFAULT_TOOLTIP_TYPE); expect(chartStore.tooltipSnap.get()).toEqual(DEFAULT_TOOLTIP_SNAP); expect(chartStore.showLegendDisplayValue.get()).toEqual(true); - expect(chartStore.legendPosition).toBeUndefined(); + expect(chartStore.legendPosition.get()).toBe(Position.Right); expect(chartStore.debug).toBe(false); expect(chartStore.customXDomain).toBeUndefined(); @@ -93,7 +93,7 @@ describe('Settings spec component', () => { expect(chartStore.showLegend.get()).toEqual(true); expect(chartStore.tooltipType.get()).toEqual(TooltipType.None); expect(chartStore.tooltipSnap.get()).toEqual(false); - expect(chartStore.legendPosition).toBe(Position.Bottom); + expect(chartStore.legendPosition.get()).toBe(Position.Bottom); expect(chartStore.showLegendDisplayValue.get()).toEqual(false); expect(chartStore.debug).toBe(true); expect(chartStore.customXDomain).toEqual({ min: 0, max: 10 }); diff --git a/src/specs/settings.tsx b/src/specs/settings.tsx index 3d5c80465b..1bd412aef9 100644 --- a/src/specs/settings.tsx +++ b/src/specs/settings.tsx @@ -130,7 +130,10 @@ function updateChartStore(props: SettingSpecProps) { } chartStore.setShowLegend(showLegend); - chartStore.legendPosition = legendPosition; + + if (legendPosition) { + chartStore.legendPosition.set(legendPosition); + } chartStore.showLegendDisplayValue.set(showLegendDisplayValue); chartStore.customXDomain = xDomain; diff --git a/src/specs/specs_parser.test.tsx b/src/specs/specs_parser.test.tsx index 1423438bbe..c68e8bdd1d 100644 --- a/src/specs/specs_parser.test.tsx +++ b/src/specs/specs_parser.test.tsx @@ -28,9 +28,9 @@ describe('Specs parser', () => { }); test('updates initialization state on unmount', () => { const chartStore = new ChartStore(); - chartStore.initialized.set(true); + chartStore.chartInitialized.set(true); const component = mount(); component.unmount(); - expect(chartStore.initialized.get()).toBe(false); + expect(chartStore.chartInitialized.get()).toBe(false); }); }); diff --git a/src/specs/specs_parser.tsx b/src/specs/specs_parser.tsx index b7412cdddb..aaa827e891 100644 --- a/src/specs/specs_parser.tsx +++ b/src/specs/specs_parser.tsx @@ -7,11 +7,6 @@ export interface SpecProps { } export class SpecsSpecRootComponent extends PureComponent { - static getDerivedStateFromProps(props: SpecProps) { - props.chartStore!.specsInitialized.set(false); - return null; - } - state = {}; componentDidMount() { this.props.chartStore!.specsInitialized.set(true); this.props.chartStore!.computeChart(); @@ -21,7 +16,7 @@ export class SpecsSpecRootComponent extends PureComponent { this.props.chartStore!.computeChart(); } componentWillUnmount() { - this.props.chartStore!.initialized.set(false); + this.props.chartStore!.chartInitialized.set(false); } render() { return this.props.children || null; diff --git a/src/utils/themes/dark_theme.ts b/src/utils/themes/dark_theme.ts index a88486918e..ea9a64f829 100644 --- a/src/utils/themes/dark_theme.ts +++ b/src/utils/themes/dark_theme.ts @@ -97,6 +97,7 @@ export const DARK_THEME: Theme = { legend: { verticalWidth: 200, horizontalHeight: 64, + spacingBuffer: 40, }, crosshair: { band: { diff --git a/src/utils/themes/light_theme.ts b/src/utils/themes/light_theme.ts index 2f07bf1526..01ef26cd8d 100644 --- a/src/utils/themes/light_theme.ts +++ b/src/utils/themes/light_theme.ts @@ -97,6 +97,7 @@ export const LIGHT_THEME: Theme = { legend: { verticalWidth: 200, horizontalHeight: 64, + spacingBuffer: 40, }, crosshair: { band: { diff --git a/src/utils/themes/theme.ts b/src/utils/themes/theme.ts index dce8319318..cddfb4b416 100644 --- a/src/utils/themes/theme.ts +++ b/src/utils/themes/theme.ts @@ -75,8 +75,24 @@ export interface ColorConfig { defaultVizColor: string; } export interface LegendStyle { + /** + * Max width used for left/right legend + * + * or + * + * Width of `LegendItem` for top/bottom legend + */ verticalWidth: number; + /** + * Max height used for top/bottom legend + */ horizontalHeight: number; + /** + * Added buffer between label and value. + * + * Smaller values render a more compact legend + */ + spacingBuffer: number; } export interface Theme { /** diff --git a/stories/legend.tsx b/stories/legend.tsx index 1886b671a6..7eea1fa8d9 100644 --- a/stories/legend.tsx +++ b/stories/legend.tsx @@ -1,4 +1,4 @@ -import { array, boolean, select } from '@storybook/addon-knobs'; +import { array, boolean, select, number } from '@storybook/addon-knobs'; import { storiesOf } from '@storybook/react'; import React from 'react'; import { @@ -13,6 +13,7 @@ import { Position, ScaleType, Settings, + PartialTheme, } from '../src/'; import * as TestDatasets from '../src/utils/data_samples/test_dataset'; import { TSVB_DATASET } from '../src/utils/data_samples/test_dataset_tsvb'; @@ -227,4 +228,48 @@ storiesOf('Legend', module) {seriesComponents} ); - }); + }) + .add( + 'legend spacingBuffer', + () => { + const theme: PartialTheme = { + legend: { + spacingBuffer: number('legend buffer value', 80), + }, + }; + + return ( + + + + Number(d).toFixed(2)} + /> + + + + + ); + }, + { + info: + 'For high variability in values it may be necessary to increase the `spacingBuffer` to account for larger numbers.', + }, + ); diff --git a/yarn.lock b/yarn.lock index 0fd779fe4a..a91051f487 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4765,7 +4765,7 @@ commander@2.17.x: resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf" integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg== -commander@^2.19.0, commander@^2.20.0, commander@~2.20.0: +commander@^2.17.1, commander@^2.19.0, commander@^2.20.0, commander@~2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" integrity sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ== @@ -9686,6 +9686,13 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.3 dependencies: js-tokens "^3.0.0 || ^4.0.0" +lorem-ipsum@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/lorem-ipsum/-/lorem-ipsum-2.0.3.tgz#9f1fa634780c9f58a349d4e091c3ba74f733164e" + integrity sha512-CX2r84DMWjW/DWiuzicTI9aRaJPAw2cvAGMJYZh/nx12OkTGqloj8y8FU0S8ZkKwOdqhfxEA6Ly8CW2P6Yxjwg== + dependencies: + commander "^2.17.1" + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"