storybook/stories/small_multiples/9_heatmap.story.tsx (226 lines of code) (raw):
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { boolean, number } from '@storybook/addon-knobs';
import { range } from 'lodash';
import { DateTime } from 'luxon';
import React, { useEffect, useMemo } from 'react';
import type { SmallMultiplesStyle, Margins } from '@elastic/charts';
import {
ScaleType,
Chart,
GroupBy,
SmallMultiples,
Settings,
niceTimeFormatByDay,
timeFormatter,
Heatmap,
} from '@elastic/charts';
import { SeededDataGenerator, getRandomNumberGenerator } from '@elastic/charts/src/mocks/utils';
import type { ChartsStory } from '../../types';
import { useBaseTheme } from '../../use_base_theme';
import { getDebugStateLogger } from '../utils/debug_state_logger';
import { customKnobs } from '../utils/knobs';
import { useHeatmapSelection } from '../utils/use_heatmap_selection';
import { sampleSize } from '../utils/utils';
const rng = getRandomNumberGenerator();
const dg = new SeededDataGenerator(500, 'test');
const numOfDays = 90;
const tickTimeFormatter = timeFormatter(niceTimeFormatByDay(numOfDays));
export const Example: ChartsStory = (_, { title, description }) => {
const debug = boolean('Debug', false);
const debugState = boolean('Enable debug state', true);
const timeBasedData = boolean('Time data', false);
const showLegend = boolean('Show Legend', false);
const vSplit = boolean('v - split', true, 'Data');
const hSplit = boolean('h - split', true, 'Data');
const vSplitCount = number('v - split count', 2, { min: 0 }, 'Data');
const hSplitCount = number('h - split count', 2, { min: 0 }, 'Data');
const vSplitCountAbs = vSplit ? vSplitCount : 1;
const hSplitCountAbs = hSplit ? hSplitCount : 1;
const categories = number('categories', 4, { min: 1, step: 1, range: true }, 'Data');
const density = number('cell density(%)', 75, { min: 5, max: 100, step: 5, range: true }, 'Data') / 100;
const xScaleType = customKnobs.enum.scaleType('xScaleType', ScaleType.Linear, {
include: ['Linear', 'Ordinal'],
group: 'Data',
});
const smStyles: SmallMultiplesStyle = {
horizontalPanelPadding: {
// Note: not fully supported, See https://github.com/elastic/elastic-charts/issues/1992
outer: number(
'Horizontal outer pad',
0,
{
range: true,
min: 0,
max: 0.5,
step: 0.05,
},
'SmallMultiples Styles',
),
inner: number(
'Horizontal inner pad',
0.05,
{
range: true,
min: 0,
max: 0.5,
step: 0.05,
},
'SmallMultiples Styles',
),
},
verticalPanelPadding: {
// Note: not fully supported, See https://github.com/elastic/elastic-charts/issues/1992
outer: number(
'Vertical outer pad',
0,
{
range: true,
min: 0,
max: 0.5,
step: 0.05,
},
'SmallMultiples Styles',
),
inner: number(
'Vertical inner pad',
0.1,
{
range: true,
min: 0,
max: 0.5,
step: 0.05,
},
'SmallMultiples Styles',
),
},
};
const showAxesTitles = boolean('Show axes title', true, 'SmallMultiples Styles');
const showAxesPanelTitles = boolean('Show axes panel titles', true, 'SmallMultiples Styles');
const dataCount = timeBasedData ? numOfDays : 10;
const fullData = useMemo(
() =>
dg.generateSMGroupedSeries(vSplitCountAbs, hSplitCountAbs, () => {
return dg.generateSimpleSeries(dataCount).flatMap((d) =>
range(0, categories, 1).map((y) => {
return {
y,
x: d.x,
value: rng(0, 1000),
t: DateTime.fromISO('2020-01-01T00:00:00Z').plus({ days: d.x }).toMillis(),
};
}),
);
}),
[vSplitCountAbs, hSplitCountAbs, dataCount, categories],
);
const data = useMemo(
() => sampleSize(fullData, vSplitCountAbs * hSplitCountAbs * dataCount * categories * density),
[categories, dataCount, density, fullData, vSplitCountAbs, hSplitCountAbs],
);
const { highlightedData, onElementClick, onBrushEnd, clearSelection } = useHeatmapSelection();
useEffect(() => {
clearSelection();
}, [clearSelection, vSplit, hSplit, vSplitCount, hSplitCount, categories, density, xScaleType]);
const chartMargins: Margins = {
left: number('chart margin left', 0, { range: true, min: 0, max: 50 }, 'Chart Margins and Paddings'),
right: number('chart margin right', 0, { range: true, min: 0, max: 50 }, 'Chart Margins and Paddings'),
top: number('chart margin top', 0, { range: true, min: 0, max: 50 }, 'Chart Margins and Paddings'),
bottom: number('chart margin bottom', 0, { range: true, min: 0, max: 50 }, 'Chart Margins and Paddings'),
};
const chartPaddings: Margins = {
left: number('chart padding left', 0, { range: true, min: 0, max: 50 }, 'Chart Margins and Paddings'),
right: number('chart padding right', 0, { range: true, min: 0, max: 50 }, 'Chart Margins and Paddings'),
top: number('chart padding top', 0, { range: true, min: 0, max: 50 }, 'Chart Margins and Paddings'),
bottom: number('chart padding bottom', 0, { range: true, min: 0, max: 50 }, 'Chart Margins and Paddings'),
};
return (
<Chart title={title} description={description}>
<Settings
debug={debug}
debugState={debugState}
onRenderChange={getDebugStateLogger(debugState)}
onElementClick={onElementClick}
showLegend={showLegend}
baseTheme={useBaseTheme()}
theme={{
axes: {
axisTitle: {
visible: showAxesTitles,
},
axisPanelTitle: {
visible: showAxesPanelTitles,
},
},
heatmap: {
grid: {
stroke: {
width: number(
'Grid stroke',
1,
{
range: true,
min: 1,
max: 10,
step: 1,
},
'SmallMultiples Styles',
),
},
},
cell: {
border: {
strokeWidth: 0,
},
},
},
chartMargins,
chartPaddings,
}}
onBrushEnd={onBrushEnd}
/>
<GroupBy id="v_split" by={(_, { v }) => v} format={(v) => `Metric ${v}`} sort="numDesc" />
<GroupBy id="h_split" by={(_, { h }) => h} format={(v) => `Host ${v}`} sort="numAsc" />
<SmallMultiples
splitVertically={vSplit ? 'v_split' : undefined}
splitHorizontally={hSplit ? 'h_split' : undefined}
style={smStyles}
/>
<Heatmap
id="heatmap1"
colorScale={{
type: 'bands',
bands: [
{ start: -Infinity, end: 200, color: '#d2e9f7' },
{ start: 200, end: 300, color: '#8bc8fb' },
{ start: 300, end: 500, color: '#fdec25' },
{ start: 500, end: 600, color: '#fba740' },
{ start: 800, end: Infinity, color: '#fe5050' },
],
}}
data={data}
xAccessor={timeBasedData ? 't' : 'x'}
yAccessor={(d) => Math.floor(d.y)}
valueAccessor="value"
valueFormatter={(d) => `${Number(d.toFixed(2))}`}
ySortPredicate="numAsc"
xScale={
timeBasedData
? {
type: ScaleType.Time,
interval: {
type: 'calendar',
value: 1,
unit: 'week',
},
}
: {
type: xScaleType,
}
}
xAxisLabelFormatter={timeBasedData ? tickTimeFormatter : (v) => `C${v}`}
yAxisLabelFormatter={(v) => `R${v}`}
timeZone="UTC"
highlightedData={highlightedData}
xAxisTitle="Bottom axis"
yAxisTitle="Left axis"
/>
</Chart>
);
};