Commit dea91c1f by Marcus Andersson Committed by GitHub

Panel: added possibility to group plugins in a panel according to a grid. (#22028)

* POC grid layout of gague.

* added a grid property that can be used to auto size the charts within a panel.

* fill the grid

* fix lint

* change default for stat panel

* avoid empty cells

* Moved to absolute positioning

* Fixed spacing

* Another fix

* Improve layout algorithm

* VizRepeater: Reverted back to so this is auto behavior for some panels

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
parent 5ccefed7
......@@ -72,7 +72,7 @@ export class BarGauge extends PureComponent<Props> {
steps: [],
},
},
itemSpacing: 10,
itemSpacing: 8,
showUnfilled: true,
};
......
import React, { PureComponent, CSSProperties } from 'react';
import { VizOrientation } from '@grafana/data';
import { calculateGridDimensions } from '../../utils/squares';
interface Props<V, D> {
/**
......@@ -21,6 +22,8 @@ interface Props<V, D> {
renderCounter: number; // force update of values & render
orientation: VizOrientation;
itemSpacing?: number;
/** When orientation is set to auto layout items in a grid */
autoGrid?: boolean;
}
export interface VizRepeaterRenderValueProps<V, D = {}> {
......@@ -43,7 +46,7 @@ interface State<V> {
export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>> {
static defaultProps: DefaultProps = {
itemSpacing: 10,
itemSpacing: 8,
};
constructor(props: Props<V, D>) {
......@@ -75,10 +78,61 @@ export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>>
return orientation;
}
renderGrid() {
const { renderValue, height, width, itemSpacing, getAlignmentFactors, orientation } = this
.props as PropsWithDefaults<V, D>;
const { values } = this.state;
const grid = calculateGridDimensions(width, height, itemSpacing, values.length);
const alignmentFactors = getAlignmentFactors ? getAlignmentFactors(values, grid.width, grid.height) : ({} as D);
let xGrid = 0;
let yGrid = 0;
let items: JSX.Element[] = [];
for (let i = 0; i < values.length; i++) {
const value = values[i];
const isLastRow = yGrid === grid.yCount - 1;
const itemWidth = isLastRow ? grid.widthOnLastRow : grid.width;
const itemHeight = grid.height;
const xPos = xGrid * itemWidth + itemSpacing * xGrid;
const yPos = yGrid * itemHeight + itemSpacing * yGrid;
const itemStyles: CSSProperties = {
position: 'absolute',
left: xPos,
top: yPos,
width: `${itemWidth}px`,
height: `${itemHeight}px`,
};
items.push(
<div key={i} style={itemStyles}>
{renderValue({ value, width: itemWidth, height: itemHeight, alignmentFactors, orientation })}
</div>
);
xGrid++;
if (xGrid === grid.xCount) {
xGrid = 0;
yGrid++;
}
}
return <div style={{ position: 'relative' }}>{items}</div>;
}
render() {
const { renderValue, height, width, itemSpacing, getAlignmentFactors } = this.props as PropsWithDefaults<V, D>;
const { renderValue, height, width, itemSpacing, getAlignmentFactors, autoGrid, orientation } = this
.props as PropsWithDefaults<V, D>;
const { values } = this.state;
const orientation = this.getOrientation();
if (autoGrid && orientation === VizOrientation.Auto) {
return this.renderGrid();
}
const itemStyles: React.CSSProperties = {
display: 'flex',
......@@ -91,17 +145,19 @@ export class VizRepeater<V, D = {}> extends PureComponent<Props<V, D>, State<V>>
let vizHeight = height;
let vizWidth = width;
if (orientation === VizOrientation.Horizontal) {
repeaterStyle.flexDirection = 'column';
itemStyles.marginBottom = `${itemSpacing}px`;
vizWidth = width;
vizHeight = height / values.length - itemSpacing + itemSpacing / values.length;
} else {
repeaterStyle.flexDirection = 'row';
repeaterStyle.justifyContent = 'space-between';
itemStyles.marginRight = `${itemSpacing}px`;
vizHeight = height;
vizWidth = width / values.length - itemSpacing + itemSpacing / values.length;
switch (this.getOrientation()) {
case VizOrientation.Horizontal:
repeaterStyle.flexDirection = 'column';
itemStyles.marginBottom = `${itemSpacing}px`;
vizWidth = width;
vizHeight = height / values.length - itemSpacing + itemSpacing / values.length;
break;
case VizOrientation.Vertical:
repeaterStyle.flexDirection = 'row';
repeaterStyle.justifyContent = 'space-between';
itemStyles.marginRight = `${itemSpacing}px`;
vizHeight = height;
vizWidth = width / values.length - itemSpacing + itemSpacing / values.length;
}
itemStyles.width = `${vizWidth}px`;
......
/**
* This function will calculate how many squares we can fit inside a rectangle.
* Please have a look at this post for more details about the implementation:
* https://math.stackexchange.com/questions/466198/algorithm-to-get-the-maximum-size-of-n-squares-that-fit-into-a-rectangle-with-a
*
* @param parentWidth width of the parent container
* @param parentHeight height of the parent container
* @param numberOfChildren number of children that should fit in the parent container
*/
export const calculateGridDimensions = (
parentWidth: number,
parentHeight: number,
itemSpacing: number,
numberOfChildren: number
) => {
const vertical = calculateSizeOfChild(parentWidth, parentHeight, numberOfChildren);
const horizontal = calculateSizeOfChild(parentHeight, parentWidth, numberOfChildren);
const square = Math.max(vertical, horizontal);
let xCount = Math.floor(parentWidth / square);
let yCount = Math.ceil(numberOfChildren / xCount);
// after yCount update xCount to make split between rows more even
xCount = Math.ceil(numberOfChildren / yCount);
const itemsOnLastRow = xCount - (xCount * yCount - numberOfChildren);
const widthOnLastRow = parentWidth / itemsOnLastRow - itemSpacing + itemSpacing / itemsOnLastRow;
return {
width: parentWidth / xCount - itemSpacing + itemSpacing / xCount,
height: parentHeight / yCount - itemSpacing + itemSpacing / yCount,
widthOnLastRow,
xCount,
yCount,
};
};
function calculateSizeOfChild(parentWidth: number, parentHeight: number, numberOfChildren: number): number {
const parts = Math.ceil(Math.sqrt((numberOfChildren * parentWidth) / parentHeight));
if (Math.floor((parts * parentHeight) / parentWidth) * parts < numberOfChildren) {
return parentHeight / Math.ceil((parts * parentHeight) / parentWidth);
}
return parentWidth / parts;
}
......@@ -60,6 +60,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
width={width}
height={height}
source={data}
autoGrid={true}
renderCounter={renderCounter}
orientation={VizOrientation.Auto}
/>
......
......@@ -22,7 +22,6 @@ import {
ReducerID,
getDisplayValueAlignmentFactors,
DisplayValueAlignmentFactors,
VizOrientation,
} from '@grafana/data';
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
......@@ -97,23 +96,9 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
height={height}
source={data}
renderCounter={renderCounter}
orientation={getOrientation(width, height, options.orientation)}
autoGrid={true}
orientation={options.orientation}
/>
);
}
}
/**
* Stat panel custom auto orientation
*/
function getOrientation(width: number, height: number, orientation: VizOrientation): VizOrientation {
if (orientation !== VizOrientation.Auto) {
return orientation;
}
if (width / height > 2) {
return VizOrientation.Vertical;
} else {
return VizOrientation.Horizontal;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment