mirror of https://github.com/grafana/grafana
Scatter: support bubble and line charts with out-of-order data (alpha) (#39377)
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>pull/39637/head^2
parent
6aa006b699
commit
4c8c2f6c96
@ -0,0 +1,12 @@ |
||||
import React, { FC } from 'react'; |
||||
import { StandardEditorProps } from '@grafana/data'; |
||||
|
||||
import { XYChartOptions, ScatterFieldConfig } from './models.gen'; |
||||
|
||||
export const ExplicitEditor: FC<StandardEditorProps<ScatterFieldConfig[], any, XYChartOptions>> = ({ |
||||
value, |
||||
onChange, |
||||
context, |
||||
}) => { |
||||
return <div>TODO: explicit scatter config</div>; |
||||
}; |
@ -0,0 +1,59 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { stylesFactory } from '@grafana/ui'; |
||||
import { DataFrame, Field, formattedValueToString, getFieldDisplayName, GrafanaTheme2 } from '@grafana/data'; |
||||
import { css } from '@emotion/css'; |
||||
import { config } from 'app/core/config'; |
||||
import { ScatterSeries } from './types'; |
||||
|
||||
export interface Props { |
||||
series: ScatterSeries; |
||||
data: DataFrame[]; // source data
|
||||
rowIndex?: number; // the hover row
|
||||
} |
||||
|
||||
export class TooltipView extends PureComponent<Props> { |
||||
style = getStyles(config.theme2); |
||||
|
||||
render() { |
||||
const { series, data, rowIndex } = this.props; |
||||
if (!series || rowIndex == null) { |
||||
return null; |
||||
} |
||||
const frame = series.frame(data); |
||||
const y = undefined; // series.y(frame);
|
||||
|
||||
return ( |
||||
<table className={this.style.infoWrap}> |
||||
<tbody> |
||||
{frame.fields.map((f, i) => ( |
||||
<tr key={`${i}/${rowIndex}`} className={f === y ? this.style.highlight : ''}> |
||||
<th>{getFieldDisplayName(f, frame)}:</th> |
||||
<td>{fmt(f, rowIndex)}</td> |
||||
</tr> |
||||
))} |
||||
</tbody> |
||||
</table> |
||||
); |
||||
} |
||||
} |
||||
|
||||
function fmt(field: Field, row: number): string { |
||||
const v = field.values.get(row); |
||||
if (field.display) { |
||||
return formattedValueToString(field.display(v)); |
||||
} |
||||
return `${v}`; |
||||
} |
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => ({ |
||||
infoWrap: css` |
||||
padding: 8px; |
||||
th { |
||||
font-weight: ${theme.typography.fontWeightMedium}; |
||||
padding: ${theme.spacing(0.25, 2)}; |
||||
} |
||||
`,
|
||||
highlight: css` |
||||
background: ${theme.colors.action.hover}; |
||||
`,
|
||||
})); |
@ -0,0 +1,71 @@ |
||||
import React, { useMemo } from 'react'; |
||||
import { LegendDisplayMode, UPlotChart, useTheme2, VizLayout, VizLegend, VizLegendItem } from '@grafana/ui'; |
||||
import { PanelProps } from '@grafana/data'; |
||||
import { XYChartOptions } from './models.gen'; |
||||
import { prepData, prepScatter } from './scatter'; |
||||
|
||||
interface XYChartPanelProps extends PanelProps<XYChartOptions> {} |
||||
|
||||
export const XYChartPanel: React.FC<XYChartPanelProps> = ({ |
||||
data, |
||||
width, |
||||
height, |
||||
options, |
||||
fieldConfig, |
||||
timeRange, |
||||
//onFieldConfigChange, |
||||
}) => { |
||||
const theme = useTheme2(); |
||||
|
||||
const info = useMemo(() => { |
||||
console.log('prepScatter!'); |
||||
return prepScatter(options, data, theme, () => {}); |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps |
||||
}, [data.structureRev, options]); |
||||
|
||||
// preps data in various shapes...aligned, stacked, merged, interpolated, etc.. |
||||
const scatterData = useMemo(() => { |
||||
console.log('prepData!'); |
||||
return prepData(info, data.series); |
||||
}, [info, data.series]); |
||||
|
||||
const legend = useMemo(() => { |
||||
const items: VizLegendItem[] = []; |
||||
for (const s of info.series) { |
||||
const frame = s.frame(data.series); |
||||
if (frame) { |
||||
for (const item of s.legend(frame)) { |
||||
items.push(item); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<VizLayout.Legend placement="bottom"> |
||||
<VizLegend placement="bottom" items={items} displayMode={LegendDisplayMode.List} /> |
||||
</VizLayout.Legend> |
||||
); |
||||
// eslint-disable-next-line react-hooks/exhaustive-deps |
||||
}, [info]); |
||||
|
||||
if (info.error) { |
||||
return ( |
||||
<div className="panel-empty"> |
||||
<p>{info.error}</p> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<VizLayout width={width} height={height} legend={legend}> |
||||
{(vizWidth: number, vizHeight: number) => ( |
||||
// <pre style={{ width: vizWidth, height: vizHeight, border: '1px solid green', margin: '0px' }}> |
||||
// {JSON.stringify(scatterData, null, 2)} |
||||
// </pre> |
||||
<UPlotChart config={info.builder!} data={scatterData} width={vizWidth} height={vizHeight} timeRange={timeRange}> |
||||
{/*children ? children(config, alignedFrame) : null*/} |
||||
</UPlotChart> |
||||
)} |
||||
</VizLayout> |
||||
); |
||||
}; |
@ -1,69 +0,0 @@ |
||||
import React, { useCallback, useMemo } from 'react'; |
||||
import { Button, GraphNGLegendEvent, TimeSeries, TooltipPlugin } from '@grafana/ui'; |
||||
import { PanelProps } from '@grafana/data'; |
||||
import { Options } from './types'; |
||||
import { hideSeriesConfigFactory } from '../timeseries/overrides/hideSeriesConfigFactory'; |
||||
import { getXYDimensions } from './dims'; |
||||
|
||||
interface XYChartPanelProps extends PanelProps<Options> {} |
||||
|
||||
export const XYChartPanel: React.FC<XYChartPanelProps> = ({ |
||||
data, |
||||
timeRange, |
||||
timeZone, |
||||
width, |
||||
height, |
||||
options, |
||||
fieldConfig, |
||||
onFieldConfigChange, |
||||
}) => { |
||||
const dims = useMemo(() => getXYDimensions(options.dims, data.series), [options.dims, data.series]); |
||||
|
||||
const frames = useMemo(() => [dims.frame], [dims]); |
||||
|
||||
const onLegendClick = useCallback( |
||||
(event: GraphNGLegendEvent) => { |
||||
onFieldConfigChange(hideSeriesConfigFactory(event, fieldConfig, frames)); |
||||
}, |
||||
[fieldConfig, onFieldConfigChange, frames] |
||||
); |
||||
|
||||
if (dims.error) { |
||||
return ( |
||||
<div> |
||||
<div>ERROR: {dims.error}</div> |
||||
{dims.hasData && ( |
||||
<div> |
||||
<Button onClick={() => alert('TODO, switch vis')}>Show as Table</Button> |
||||
{dims.hasTime && <Button onClick={() => alert('TODO, switch vis')}>Show as Time series</Button>} |
||||
</div> |
||||
)} |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<TimeSeries |
||||
frames={frames} |
||||
structureRev={data.structureRev} |
||||
fields={dims.fields} |
||||
timeRange={timeRange} |
||||
timeZone={timeZone} |
||||
width={width} |
||||
height={height} |
||||
legend={options.legend} |
||||
onLegendClick={onLegendClick} |
||||
> |
||||
{(config, alignedDataFrame) => { |
||||
return ( |
||||
<TooltipPlugin |
||||
config={config} |
||||
data={alignedDataFrame} |
||||
mode={options.tooltip.mode as any} |
||||
timeZone={timeZone} |
||||
/> |
||||
); |
||||
}} |
||||
</TimeSeries> |
||||
); |
||||
}; |
@ -0,0 +1,126 @@ |
||||
import React, { PureComponent } from 'react'; |
||||
import { PanelProps } from '@grafana/data'; |
||||
import { XYChartOptions } from './models.gen'; |
||||
import { ScatterHoverEvent, ScatterSeries } from './types'; |
||||
import { |
||||
LegendDisplayMode, |
||||
Portal, |
||||
UPlotChart, |
||||
UPlotConfigBuilder, |
||||
VizLayout, |
||||
VizLegend, |
||||
VizLegendItem, |
||||
VizTooltipContainer, |
||||
} from '@grafana/ui'; |
||||
import { FacetedData } from '@grafana/ui/src/components/uPlot/types'; |
||||
import { prepData, prepScatter } from './scatter'; |
||||
import { config } from '@grafana/runtime'; |
||||
import { TooltipView } from './TooltipView'; |
||||
|
||||
type Props = PanelProps<XYChartOptions>; |
||||
type State = { |
||||
error?: string; |
||||
series: ScatterSeries[]; |
||||
builder?: UPlotConfigBuilder; |
||||
facets?: FacetedData; |
||||
hover?: ScatterHoverEvent; |
||||
}; |
||||
|
||||
export class XYChartPanel2 extends PureComponent<Props, State> { |
||||
state: State = { |
||||
series: [], |
||||
}; |
||||
|
||||
componentDidMount() { |
||||
this.initSeries(); // also data
|
||||
} |
||||
|
||||
componentDidUpdate(oldProps: Props) { |
||||
const { options, data } = this.props; |
||||
const configsChanged = options !== oldProps.options || data.structureRev !== oldProps.data.structureRev; |
||||
|
||||
if (configsChanged) { |
||||
this.initSeries(); |
||||
} else if (data !== oldProps.data) { |
||||
this.initFacets(); |
||||
} |
||||
} |
||||
|
||||
scatterHoverCallback = (hover?: ScatterHoverEvent) => { |
||||
this.setState({ hover }); |
||||
}; |
||||
|
||||
getData = () => { |
||||
return this.props.data.series; |
||||
}; |
||||
|
||||
initSeries = () => { |
||||
const { options, data } = this.props; |
||||
const info: State = prepScatter(options, this.getData, config.theme2, this.scatterHoverCallback); |
||||
if (info.series.length && data.series) { |
||||
info.facets = prepData(info, data.series); |
||||
info.error = undefined; |
||||
} |
||||
this.setState(info); |
||||
}; |
||||
|
||||
initFacets = () => { |
||||
this.setState({ |
||||
facets: prepData(this.state, this.props.data.series), |
||||
}); |
||||
}; |
||||
|
||||
renderLegend = () => { |
||||
const { data } = this.props; |
||||
const { series } = this.state; |
||||
const items: VizLegendItem[] = []; |
||||
for (const s of series) { |
||||
const frame = s.frame(data.series); |
||||
if (frame) { |
||||
for (const item of s.legend(frame)) { |
||||
items.push(item); |
||||
} |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<VizLayout.Legend placement="bottom"> |
||||
<VizLegend placement="bottom" items={items} displayMode={LegendDisplayMode.List} /> |
||||
</VizLayout.Legend> |
||||
); |
||||
}; |
||||
|
||||
render() { |
||||
const { width, height, timeRange, data } = this.props; |
||||
const { error, facets, builder, hover, series } = this.state; |
||||
if (error || !builder) { |
||||
return ( |
||||
<div className="panel-empty"> |
||||
<p>{error}</p> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<VizLayout width={width} height={height} legend={this.renderLegend()}> |
||||
{(vizWidth: number, vizHeight: number) => ( |
||||
// <pre style={{ width: vizWidth, height: vizHeight, border: '1px solid green', margin: '0px' }}>
|
||||
// {JSON.stringify(scatterData, null, 2)}
|
||||
// </pre>
|
||||
<UPlotChart config={builder} data={facets!} width={vizWidth} height={vizHeight} timeRange={timeRange}> |
||||
{/*children ? children(config, alignedFrame) : null*/} |
||||
</UPlotChart> |
||||
)} |
||||
</VizLayout> |
||||
<Portal> |
||||
{hover && ( |
||||
<VizTooltipContainer position={{ x: hover.pageX, y: hover.pageY }} offset={{ x: 10, y: 10 }}> |
||||
<TooltipView series={series[hover.scatterIndex]} rowIndex={hover.xIndex} data={data.series} /> |
||||
</VizTooltipContainer> |
||||
)} |
||||
</Portal> |
||||
</> |
||||
); |
||||
} |
||||
} |
@ -0,0 +1,94 @@ |
||||
import { |
||||
FieldColorModeId, |
||||
FieldConfigProperty, |
||||
FieldType, |
||||
identityOverrideProcessor, |
||||
SetFieldConfigOptionsArgs, |
||||
} from '@grafana/data'; |
||||
import { LineStyle, VisibilityMode } from '@grafana/schema'; |
||||
|
||||
import { commonOptionsBuilder, graphFieldOptions } from '@grafana/ui'; |
||||
import { LineStyleEditor } from '../timeseries/LineStyleEditor'; |
||||
import { ScatterFieldConfig, ScatterLineMode } from './models.gen'; |
||||
|
||||
const categoryStyles = undefined; // ['Scatter styles'];
|
||||
|
||||
export function getScatterFieldConfig(cfg: ScatterFieldConfig): SetFieldConfigOptionsArgs<ScatterFieldConfig> { |
||||
return { |
||||
standardOptions: { |
||||
[FieldConfigProperty.Color]: { |
||||
settings: { |
||||
byValueSupport: true, |
||||
bySeriesSupport: true, |
||||
preferThresholdsMode: false, |
||||
}, |
||||
defaultValue: { |
||||
mode: FieldColorModeId.PaletteClassic, |
||||
}, |
||||
}, |
||||
}, |
||||
|
||||
useCustomConfig: (builder) => { |
||||
builder |
||||
.addRadio({ |
||||
path: 'point', |
||||
name: 'Points', |
||||
category: categoryStyles, |
||||
defaultValue: cfg.point, |
||||
settings: { |
||||
options: graphFieldOptions.showPoints, |
||||
}, |
||||
}) |
||||
.addSliderInput({ |
||||
path: 'pointSize.fixed', |
||||
name: 'Point size', |
||||
category: categoryStyles, |
||||
defaultValue: cfg.pointSize?.fixed, |
||||
settings: { |
||||
min: 1, |
||||
max: 100, |
||||
step: 1, |
||||
}, |
||||
showIf: (c) => c.point !== VisibilityMode.Never, |
||||
}) |
||||
.addRadio({ |
||||
path: 'line', |
||||
name: 'Lines', |
||||
category: categoryStyles, |
||||
defaultValue: cfg.line, |
||||
settings: { |
||||
options: [ |
||||
{ label: 'None', value: ScatterLineMode.None }, |
||||
{ label: 'Linear', value: ScatterLineMode.Linear }, |
||||
], |
||||
}, |
||||
}) |
||||
.addCustomEditor<void, LineStyle>({ |
||||
id: 'lineStyle', |
||||
path: 'lineStyle', |
||||
name: 'Line style', |
||||
category: categoryStyles, |
||||
showIf: (c) => c.line !== ScatterLineMode.None, |
||||
editor: LineStyleEditor, |
||||
override: LineStyleEditor, |
||||
process: identityOverrideProcessor, |
||||
shouldApply: (f) => f.type === FieldType.number, |
||||
}) |
||||
.addSliderInput({ |
||||
path: 'lineWidth', |
||||
name: 'Line width', |
||||
category: categoryStyles, |
||||
defaultValue: cfg.lineWidth, |
||||
settings: { |
||||
min: 0, |
||||
max: 10, |
||||
step: 1, |
||||
}, |
||||
showIf: (c) => c.line !== ScatterLineMode.None, |
||||
}); |
||||
|
||||
commonOptionsBuilder.addAxisConfig(builder, cfg); |
||||
commonOptionsBuilder.addHideFrom(builder); |
||||
}, |
||||
}; |
||||
} |
@ -0,0 +1,72 @@ |
||||
import { |
||||
OptionsWithTooltip, |
||||
OptionsWithLegend, |
||||
LineStyle, |
||||
VisibilityMode, |
||||
HideableFieldConfig, |
||||
AxisConfig, |
||||
AxisPlacement, |
||||
} from '@grafana/schema'; |
||||
import { |
||||
ColorDimensionConfig, |
||||
DimensionSupplier, |
||||
ScaleDimensionConfig, |
||||
TextDimensionConfig, |
||||
} from 'app/features/dimensions'; |
||||
|
||||
export enum ScatterLineMode { |
||||
None = 'none', |
||||
Linear = 'linear', |
||||
// Smooth
|
||||
// r2, etc
|
||||
} |
||||
|
||||
export interface ScatterFieldConfig extends HideableFieldConfig, AxisConfig { |
||||
line?: ScatterLineMode; |
||||
lineWidth?: number; |
||||
lineStyle?: LineStyle; |
||||
lineColor?: ColorDimensionConfig; |
||||
|
||||
point?: VisibilityMode; |
||||
pointSize?: ScaleDimensionConfig; // only 'fixed' is exposed in the UI
|
||||
pointColor?: ColorDimensionConfig; |
||||
pointSymbol?: DimensionSupplier<string>; |
||||
|
||||
label?: VisibilityMode; |
||||
labelValue?: TextDimensionConfig; |
||||
} |
||||
|
||||
/** Configured in the panel level */ |
||||
export interface ScatterSeriesConfig extends ScatterFieldConfig { |
||||
x?: string; |
||||
y?: string; |
||||
} |
||||
|
||||
export const defaultScatterConfig: ScatterFieldConfig = { |
||||
line: ScatterLineMode.None, // no line
|
||||
lineWidth: 1, |
||||
lineStyle: { |
||||
fill: 'solid', |
||||
}, |
||||
point: VisibilityMode.Auto, |
||||
pointSize: { |
||||
fixed: 5, |
||||
min: 1, |
||||
max: 20, |
||||
}, |
||||
axisPlacement: AxisPlacement.Auto, |
||||
}; |
||||
|
||||
/** Old config saved with 8.0+ */ |
||||
export interface XYDimensionConfig { |
||||
frame: number; |
||||
x?: string; // name | first
|
||||
exclude?: string[]; // all other numbers except
|
||||
} |
||||
|
||||
export interface XYChartOptions extends OptionsWithLegend, OptionsWithTooltip { |
||||
mode?: 'xy' | 'explicit'; |
||||
dims: XYDimensionConfig; |
||||
|
||||
series?: ScatterSeriesConfig[]; |
||||
} |
@ -1,25 +1,69 @@ |
||||
import { GraphFieldConfig, GraphDrawStyle } from '@grafana/schema'; |
||||
import { PanelPlugin } from '@grafana/data'; |
||||
import { commonOptionsBuilder } from '@grafana/ui'; |
||||
import { XYChartPanel } from './XYChartPanel'; |
||||
import { Options } from './types'; |
||||
import { defaultScatterConfig, XYChartOptions, ScatterFieldConfig } from './models.gen'; |
||||
import { getScatterFieldConfig } from './config'; |
||||
import { XYDimsEditor } from './XYDimsEditor'; |
||||
import { getGraphFieldConfig, defaultGraphConfig } from '../timeseries/config'; |
||||
import { XYChartPanel2 } from './XYChartPanel2'; |
||||
import { ColorDimensionEditor, ScaleDimensionEditor } from 'app/features/dimensions/editors'; |
||||
|
||||
export const plugin = new PanelPlugin<Options, GraphFieldConfig>(XYChartPanel) |
||||
.useFieldConfig( |
||||
getGraphFieldConfig({ |
||||
...defaultGraphConfig, |
||||
drawStyle: GraphDrawStyle.Points, |
||||
}) |
||||
) |
||||
export const plugin = new PanelPlugin<XYChartOptions, ScatterFieldConfig>(XYChartPanel2) |
||||
.useFieldConfig(getScatterFieldConfig(defaultScatterConfig)) |
||||
.setPanelOptions((builder) => { |
||||
builder.addCustomEditor({ |
||||
id: 'xyPlotConfig', |
||||
path: 'dims', |
||||
name: 'Data', |
||||
editor: XYDimsEditor, |
||||
}); |
||||
builder |
||||
.addRadio({ |
||||
path: 'mode', |
||||
name: 'Mode', |
||||
defaultValue: 'single', |
||||
settings: { |
||||
options: [ |
||||
{ value: 'xy', label: 'XY', description: 'No changes to saved model since 8.0' }, |
||||
{ value: 'explicit', label: 'Explicit' }, |
||||
], |
||||
}, |
||||
}) |
||||
.addCustomEditor({ |
||||
id: 'xyPlotConfig', |
||||
path: 'dims', |
||||
name: 'Data', |
||||
editor: XYDimsEditor, |
||||
showIf: (cfg) => cfg.mode === 'xy', |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'series[0].x', |
||||
name: 'X Field', |
||||
showIf: (cfg) => cfg.mode === 'explicit', |
||||
}) |
||||
.addFieldNamePicker({ |
||||
path: 'series[0].y', |
||||
name: 'Y Field', |
||||
showIf: (cfg) => cfg.mode === 'explicit', |
||||
}) |
||||
.addCustomEditor({ |
||||
id: 'seriesZerox.pointColor', |
||||
path: 'series[0].pointColor', |
||||
name: 'Point color', |
||||
editor: ColorDimensionEditor, |
||||
settings: {}, |
||||
defaultValue: {}, |
||||
showIf: (cfg) => cfg.mode === 'explicit', |
||||
}) |
||||
.addCustomEditor({ |
||||
id: 'seriesZerox.pointSize', |
||||
path: 'series[0].pointSize', |
||||
name: 'Point size', |
||||
editor: ScaleDimensionEditor, |
||||
settings: { |
||||
min: 1, |
||||
max: 50, |
||||
}, |
||||
defaultValue: { |
||||
fixed: 5, |
||||
min: 1, |
||||
max: 50, |
||||
}, |
||||
showIf: (cfg) => cfg.mode === 'explicit', |
||||
}); |
||||
|
||||
commonOptionsBuilder.addTooltipOptions(builder); |
||||
commonOptionsBuilder.addLegendOptions(builder); |
||||
}); |
||||
|
@ -0,0 +1,674 @@ |
||||
import { |
||||
DataFrame, |
||||
FieldColorModeId, |
||||
fieldColorModeRegistry, |
||||
getDisplayProcessor, |
||||
getFieldColorModeForField, |
||||
getFieldDisplayName, |
||||
getFieldSeriesColor, |
||||
GrafanaTheme2, |
||||
} from '@grafana/data'; |
||||
import { AxisPlacement, ScaleDirection, ScaleOrientation, VisibilityMode } from '@grafana/schema'; |
||||
import { UPlotConfigBuilder } from '@grafana/ui'; |
||||
import { FacetedData, FacetSeries } from '@grafana/ui/src/components/uPlot/types'; |
||||
import { |
||||
findFieldIndex, |
||||
getScaledDimensionForField, |
||||
ScaleDimensionConfig, |
||||
ScaleDimensionMode, |
||||
} from 'app/features/dimensions'; |
||||
import { config } from '@grafana/runtime'; |
||||
import { defaultScatterConfig, ScatterFieldConfig, ScatterLineMode, XYChartOptions } from './models.gen'; |
||||
import { pointWithin, Quadtree, Rect } from '../barchart/quadtree'; |
||||
import { alpha } from '@grafana/data/src/themes/colorManipulator'; |
||||
import uPlot from 'uplot'; |
||||
import { DimensionValues, ScatterHoverCallback, ScatterSeries } from './types'; |
||||
import { isGraphable } from './dims'; |
||||
|
||||
export interface ScatterPanelInfo { |
||||
error?: string; |
||||
series: ScatterSeries[]; |
||||
builder?: UPlotConfigBuilder; |
||||
} |
||||
|
||||
/** |
||||
* This is called when options or structure rev changes |
||||
*/ |
||||
export function prepScatter( |
||||
options: XYChartOptions, |
||||
getData: () => DataFrame[], |
||||
theme: GrafanaTheme2, |
||||
ttip: ScatterHoverCallback |
||||
): ScatterPanelInfo { |
||||
let series: ScatterSeries[]; |
||||
let builder: UPlotConfigBuilder; |
||||
|
||||
try { |
||||
series = prepSeries(options, getData()); |
||||
builder = prepConfig(getData, series, theme, ttip); |
||||
} catch (e) { |
||||
console.log('prepScatter ERROR', e); |
||||
return { |
||||
error: e.message, |
||||
series: [], |
||||
}; |
||||
} |
||||
|
||||
return { |
||||
series, |
||||
builder, |
||||
}; |
||||
} |
||||
|
||||
interface Dims { |
||||
pointColorIndex?: number; |
||||
pointColorFixed?: string; |
||||
|
||||
pointSizeIndex?: number; |
||||
pointSizeConfig?: ScaleDimensionConfig; |
||||
} |
||||
|
||||
function getScatterSeries( |
||||
seriesIndex: number, |
||||
frames: DataFrame[], |
||||
frameIndex: number, |
||||
xIndex: number, |
||||
yIndex: number, |
||||
dims: Dims |
||||
): ScatterSeries { |
||||
const frame = frames[frameIndex]; |
||||
const y = frame.fields[yIndex]; |
||||
let state = y.state ?? {}; |
||||
state.seriesIndex = seriesIndex; |
||||
y.state = state; |
||||
|
||||
// Color configs
|
||||
//----------------
|
||||
let seriesColor = dims.pointColorFixed |
||||
? config.theme2.visualization.getColorByName(dims.pointColorFixed) |
||||
: getFieldSeriesColor(y, config.theme2).color; |
||||
let pointColor: DimensionValues<string> = () => seriesColor; |
||||
const fieldConfig: ScatterFieldConfig = { ...defaultScatterConfig, ...y.config.custom }; |
||||
let pointColorMode = fieldColorModeRegistry.get(FieldColorModeId.PaletteClassic); |
||||
if (dims.pointColorIndex) { |
||||
const f = frames[frameIndex].fields[dims.pointColorIndex]; |
||||
if (f) { |
||||
const disp = |
||||
f.display ?? |
||||
getDisplayProcessor({ |
||||
field: f, |
||||
theme: config.theme2, |
||||
}); |
||||
pointColorMode = getFieldColorModeForField(y); |
||||
if (pointColorMode.isByValue) { |
||||
const index = dims.pointColorIndex; |
||||
pointColor = (frame: DataFrame) => { |
||||
// Yes we can improve this later
|
||||
return frame.fields[index].values.toArray().map((v) => disp(v).color!); |
||||
}; |
||||
} else { |
||||
seriesColor = pointColorMode.getCalculator(f, config.theme2)(f.values.get(0), 1); |
||||
pointColor = () => seriesColor; |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Size configs
|
||||
//----------------
|
||||
let pointSizeHints = dims.pointSizeConfig; |
||||
let pointSizeFixed = dims.pointSizeConfig?.fixed ?? y.config.custom?.pointSizeConfig?.fixed ?? 5; |
||||
let pointSize: DimensionValues<number> = () => pointSizeFixed; |
||||
if (dims.pointSizeIndex) { |
||||
pointSize = (frame) => { |
||||
const s = getScaledDimensionForField( |
||||
frame.fields[dims.pointSizeIndex!], |
||||
dims.pointSizeConfig!, |
||||
ScaleDimensionMode.Quadratic |
||||
); |
||||
const vals = Array(frame.length); |
||||
for (let i = 0; i < frame.length; i++) { |
||||
vals[i] = s.get(i); |
||||
} |
||||
return vals; |
||||
}; |
||||
} else { |
||||
pointSizeHints = { |
||||
fixed: pointSizeFixed, |
||||
min: pointSizeFixed, |
||||
max: pointSizeFixed, |
||||
}; |
||||
} |
||||
|
||||
// Series config
|
||||
//----------------
|
||||
const name = getFieldDisplayName(y, frame, frames); |
||||
return { |
||||
name, |
||||
|
||||
frame: (frames) => frames[frameIndex], |
||||
|
||||
x: (frame) => frame.fields[xIndex], |
||||
y: (frame) => frame.fields[yIndex], |
||||
legend: (frame) => { |
||||
return [ |
||||
{ |
||||
label: name, |
||||
color: seriesColor, // single color for series?
|
||||
getItemKey: () => name, |
||||
yAxis: yIndex, // << but not used
|
||||
}, |
||||
]; |
||||
}, |
||||
|
||||
line: fieldConfig.line ?? ScatterLineMode.None, |
||||
lineWidth: fieldConfig.lineWidth ?? 2, |
||||
lineStyle: fieldConfig.lineStyle!, |
||||
lineColor: () => seriesColor, |
||||
|
||||
point: fieldConfig.point!, |
||||
pointSize, |
||||
pointColor, |
||||
pointSymbol: (frame: DataFrame, from?: number) => 'circle', // single field, multiple symbols.... kinda equals multiple series 🤔
|
||||
|
||||
label: VisibilityMode.Never, |
||||
labelValue: () => '', |
||||
|
||||
hints: { |
||||
pointSize: pointSizeHints!, |
||||
pointColor: { |
||||
mode: pointColorMode, |
||||
}, |
||||
}, |
||||
}; |
||||
} |
||||
|
||||
function prepSeries(options: XYChartOptions, frames: DataFrame[]): ScatterSeries[] { |
||||
let seriesIndex = 0; |
||||
if (!frames.length) { |
||||
throw 'missing data'; |
||||
} |
||||
|
||||
if (options.mode === 'explicit') { |
||||
if (options.series?.length) { |
||||
for (const series of options.series) { |
||||
if (!series?.x) { |
||||
throw 'Select X dimension'; |
||||
} |
||||
|
||||
if (!series?.y) { |
||||
throw 'Select Y dimension'; |
||||
} |
||||
|
||||
for (let frameIndex = 0; frameIndex < frames.length; frameIndex++) { |
||||
const frame = frames[frameIndex]; |
||||
const xIndex = findFieldIndex(frame, series.x); |
||||
|
||||
if (xIndex != null) { |
||||
// TODO: this should find multiple y fields
|
||||
const yIndex = findFieldIndex(frame, series.y); |
||||
|
||||
if (yIndex == null) { |
||||
throw 'Y must be in the same frame as X'; |
||||
} |
||||
|
||||
const dims: Dims = { |
||||
pointColorFixed: series.pointColor?.fixed, |
||||
pointColorIndex: findFieldIndex(frame, series.pointColor?.field), |
||||
pointSizeConfig: series.pointSize, |
||||
pointSizeIndex: findFieldIndex(frame, series.pointSize?.field), |
||||
}; |
||||
return [getScatterSeries(seriesIndex++, frames, frameIndex, xIndex, yIndex, dims)]; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
// Default behavior
|
||||
const dims = options.dims ?? {}; |
||||
const frameIndex = dims.frame ?? 0; |
||||
const frame = frames[frameIndex]; |
||||
const numericIndicies: number[] = []; |
||||
|
||||
let xIndex = findFieldIndex(frame, dims.x); |
||||
for (let i = 0; i < frame.fields.length; i++) { |
||||
if (isGraphable(frame.fields[i])) { |
||||
if (xIndex == null || i === xIndex) { |
||||
xIndex = i; |
||||
continue; |
||||
} |
||||
if (dims.exclude && dims.exclude.includes(getFieldDisplayName(frame.fields[i], frame, frames))) { |
||||
continue; // skip
|
||||
} |
||||
|
||||
numericIndicies.push(i); |
||||
} |
||||
} |
||||
|
||||
if (xIndex == null) { |
||||
throw 'Missing X dimension'; |
||||
} |
||||
|
||||
if (!numericIndicies.length) { |
||||
throw 'No Y values'; |
||||
} |
||||
return numericIndicies.map((yIndex) => getScatterSeries(seriesIndex++, frames, frameIndex, xIndex!, yIndex, {})); |
||||
} |
||||
|
||||
interface DrawBubblesOpts { |
||||
each: (u: uPlot, seriesIdx: number, dataIdx: number, lft: number, top: number, wid: number, hgt: number) => void; |
||||
disp: { |
||||
//unit: 3,
|
||||
size: { |
||||
values: (u: uPlot, seriesIdx: number) => number[]; |
||||
}; |
||||
color: { |
||||
values: (u: uPlot, seriesIdx: number) => string[]; |
||||
alpha: (u: uPlot, seriesIdx: number) => string[]; |
||||
}; |
||||
}; |
||||
} |
||||
|
||||
//const prepConfig: UPlotConfigPrepFnXY<XYChartOptions> = ({ frames, series, theme }) => {
|
||||
const prepConfig = ( |
||||
getData: () => DataFrame[], |
||||
scatterSeries: ScatterSeries[], |
||||
theme: GrafanaTheme2, |
||||
ttip: ScatterHoverCallback |
||||
) => { |
||||
let qt: Quadtree; |
||||
let hRect: Rect | null; |
||||
|
||||
function drawBubblesFactory(opts: DrawBubblesOpts) { |
||||
const drawBubbles: uPlot.Series.PathBuilder = (u, seriesIdx, idx0, idx1) => { |
||||
uPlot.orient( |
||||
u, |
||||
seriesIdx, |
||||
( |
||||
series, |
||||
dataX, |
||||
dataY, |
||||
scaleX, |
||||
scaleY, |
||||
valToPosX, |
||||
valToPosY, |
||||
xOff, |
||||
yOff, |
||||
xDim, |
||||
yDim, |
||||
moveTo, |
||||
lineTo, |
||||
rect, |
||||
arc |
||||
) => { |
||||
const scatterInfo = scatterSeries[seriesIdx - 1]; |
||||
let d = (u.data[seriesIdx] as unknown) as FacetSeries; |
||||
|
||||
let showLine = scatterInfo.line !== ScatterLineMode.None; |
||||
let showPoints = scatterInfo.point === VisibilityMode.Always; |
||||
if (!showPoints && scatterInfo.point === VisibilityMode.Auto) { |
||||
showPoints = d[0].length < 1000; |
||||
} |
||||
|
||||
// always show something
|
||||
if (!showPoints && !showLine) { |
||||
showLine = true; |
||||
} |
||||
|
||||
let strokeWidth = 1; |
||||
|
||||
u.ctx.save(); |
||||
|
||||
u.ctx.rect(u.bbox.left, u.bbox.top, u.bbox.width, u.bbox.height); |
||||
u.ctx.clip(); |
||||
|
||||
u.ctx.fillStyle = (series.fill as any)(); // assumes constant
|
||||
u.ctx.strokeStyle = (series.stroke as any)(); |
||||
u.ctx.lineWidth = strokeWidth; |
||||
|
||||
let deg360 = 2 * Math.PI; |
||||
|
||||
// leon forgot to add these to the uPlot's Scale interface, but they exist!
|
||||
//let xKey = scaleX.key as string;
|
||||
//let yKey = scaleY.key as string;
|
||||
let xKey = series.facets![0].scale; |
||||
let yKey = series.facets![1].scale; |
||||
|
||||
let pointHints = scatterInfo.hints.pointSize; |
||||
const colorByValue = scatterInfo.hints.pointColor.mode.isByValue; |
||||
|
||||
let maxSize = (pointHints.max ?? pointHints.fixed) * devicePixelRatio; |
||||
|
||||
// todo: this depends on direction & orientation
|
||||
// todo: calc once per redraw, not per path
|
||||
let filtLft = u.posToVal(-maxSize / 2, xKey); |
||||
let filtRgt = u.posToVal(u.bbox.width / devicePixelRatio + maxSize / 2, xKey); |
||||
let filtBtm = u.posToVal(u.bbox.height / devicePixelRatio + maxSize / 2, yKey); |
||||
let filtTop = u.posToVal(-maxSize / 2, yKey); |
||||
|
||||
let sizes = opts.disp.size.values(u, seriesIdx); |
||||
let pointColors = opts.disp.color.values(u, seriesIdx); |
||||
let pointAlpha = opts.disp.color.alpha(u, seriesIdx); |
||||
|
||||
let linePath: Path2D | null = showLine ? new Path2D() : null; |
||||
|
||||
for (let i = 0; i < d[0].length; i++) { |
||||
let xVal = d[0][i]; |
||||
let yVal = d[1][i]; |
||||
let size = sizes[i] * devicePixelRatio; |
||||
|
||||
if (xVal >= filtLft && xVal <= filtRgt && yVal >= filtBtm && yVal <= filtTop) { |
||||
let cx = valToPosX(xVal, scaleX, xDim, xOff); |
||||
let cy = valToPosY(yVal, scaleY, yDim, yOff); |
||||
|
||||
if (showLine) { |
||||
linePath!.lineTo(cx, cy); |
||||
} |
||||
|
||||
if (showPoints) { |
||||
u.ctx.moveTo(cx + size / 2, cy); |
||||
u.ctx.beginPath(); |
||||
u.ctx.arc(cx, cy, size / 2, 0, deg360); |
||||
|
||||
if (colorByValue) { |
||||
u.ctx.fillStyle = pointAlpha[i]; |
||||
u.ctx.strokeStyle = pointColors[i]; |
||||
} |
||||
|
||||
u.ctx.fill(); |
||||
u.ctx.stroke(); |
||||
opts.each( |
||||
u, |
||||
seriesIdx, |
||||
i, |
||||
cx - size / 2 - strokeWidth / 2, |
||||
cy - size / 2 - strokeWidth / 2, |
||||
size + strokeWidth, |
||||
size + strokeWidth |
||||
); |
||||
} |
||||
} |
||||
} |
||||
|
||||
if (showLine) { |
||||
let frame = scatterInfo.frame(getData()); |
||||
u.ctx.strokeStyle = scatterInfo.lineColor(frame); |
||||
u.ctx.lineWidth = scatterInfo.lineWidth * devicePixelRatio; |
||||
|
||||
const { lineStyle } = scatterInfo; |
||||
if (lineStyle && lineStyle.fill !== 'solid') { |
||||
if (lineStyle.fill === 'dot') { |
||||
u.ctx.lineCap = 'round'; |
||||
} |
||||
u.ctx.setLineDash(lineStyle.dash ?? [10, 10]); |
||||
} |
||||
|
||||
u.ctx.stroke(linePath!); |
||||
} |
||||
|
||||
u.ctx.restore(); |
||||
} |
||||
); |
||||
|
||||
return null; |
||||
}; |
||||
|
||||
return drawBubbles; |
||||
} |
||||
|
||||
let drawBubbles = drawBubblesFactory({ |
||||
disp: { |
||||
size: { |
||||
//unit: 3, // raw CSS pixels
|
||||
values: (u, seriesIdx) => { |
||||
return u.data[seriesIdx][2] as any; // already contains final pixel geometry
|
||||
//let [minValue, maxValue] = getSizeMinMax(u);
|
||||
//return u.data[seriesIdx][2].map(v => getSize(v, minValue, maxValue));
|
||||
}, |
||||
}, |
||||
color: { |
||||
// string values
|
||||
values: (u, seriesIdx) => { |
||||
return u.data[seriesIdx][3] as any; |
||||
}, |
||||
alpha: (u, seriesIdx) => { |
||||
return u.data[seriesIdx][4] as any; |
||||
}, |
||||
}, |
||||
}, |
||||
each: (u, seriesIdx, dataIdx, lft, top, wid, hgt) => { |
||||
// we get back raw canvas coords (included axes & padding). translate to the plotting area origin
|
||||
lft -= u.bbox.left; |
||||
top -= u.bbox.top; |
||||
qt.add({ x: lft, y: top, w: wid, h: hgt, sidx: seriesIdx, didx: dataIdx }); |
||||
}, |
||||
}); |
||||
|
||||
const builder = new UPlotConfigBuilder(); |
||||
|
||||
builder.setCursor({ |
||||
drag: { setScale: true }, |
||||
dataIdx: (u, seriesIdx) => { |
||||
if (seriesIdx === 1) { |
||||
hRect = null; |
||||
|
||||
let dist = Infinity; |
||||
let cx = u.cursor.left! * devicePixelRatio; |
||||
let cy = u.cursor.top! * devicePixelRatio; |
||||
|
||||
qt.get(cx, cy, 1, 1, (o) => { |
||||
if (pointWithin(cx, cy, o.x, o.y, o.x + o.w, o.y + o.h)) { |
||||
let ocx = o.x + o.w / 2; |
||||
let ocy = o.y + o.h / 2; |
||||
|
||||
let dx = ocx - cx; |
||||
let dy = ocy - cy; |
||||
|
||||
let d = Math.sqrt(dx ** 2 + dy ** 2); |
||||
|
||||
// test against radius for actual hover
|
||||
if (d <= o.w / 2) { |
||||
// only hover bbox with closest distance
|
||||
if (d <= dist) { |
||||
dist = d; |
||||
hRect = o; |
||||
} |
||||
} |
||||
} |
||||
}); |
||||
} |
||||
|
||||
return hRect && seriesIdx === hRect.sidx ? hRect.didx : null; |
||||
}, |
||||
points: { |
||||
size: (u, seriesIdx) => { |
||||
return hRect && seriesIdx === hRect.sidx ? hRect.w / devicePixelRatio : 0; |
||||
}, |
||||
fill: (u, seriesIdx) => 'rgba(255,255,255,0.4)', |
||||
}, |
||||
}); |
||||
|
||||
// clip hover points/bubbles to plotting area
|
||||
builder.addHook('init', (u, r) => { |
||||
u.over.style.overflow = 'hidden'; |
||||
}); |
||||
|
||||
let rect: DOMRect; |
||||
|
||||
// rect of .u-over (grid area)
|
||||
builder.addHook('syncRect', (u, r) => { |
||||
rect = r; |
||||
}); |
||||
|
||||
builder.addHook('setLegend', (u) => { |
||||
// console.log('TTIP???', u.cursor.idxs);
|
||||
if (u.cursor.idxs != null) { |
||||
for (let i = 0; i < u.cursor.idxs.length; i++) { |
||||
const sel = u.cursor.idxs[i]; |
||||
if (sel != null) { |
||||
ttip({ |
||||
scatterIndex: i - 1, |
||||
xIndex: sel, |
||||
pageX: rect.left + u.cursor.left!, |
||||
pageY: rect.top + u.cursor.top!, |
||||
}); |
||||
return; // only show the first one
|
||||
} |
||||
} |
||||
} |
||||
ttip(undefined); |
||||
}); |
||||
|
||||
builder.addHook('drawClear', (u) => { |
||||
qt = qt || new Quadtree(0, 0, u.bbox.width, u.bbox.height); |
||||
|
||||
qt.clear(); |
||||
|
||||
// force-clear the path cache to cause drawBars() to rebuild new quadtree
|
||||
u.series.forEach((s, i) => { |
||||
if (i > 0) { |
||||
// @ts-ignore
|
||||
s._paths = null; |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
builder.setMode(2); |
||||
|
||||
const frames = getData(); |
||||
let xField = scatterSeries[0].x(scatterSeries[0].frame(frames)); |
||||
|
||||
builder.addScale({ |
||||
scaleKey: 'x', |
||||
isTime: false, |
||||
orientation: ScaleOrientation.Horizontal, |
||||
direction: ScaleDirection.Right, |
||||
range: (u, min, max) => [min, max], |
||||
}); |
||||
|
||||
builder.addAxis({ |
||||
scaleKey: 'x', |
||||
placement: AxisPlacement.Bottom, |
||||
theme, |
||||
label: xField.config.custom.axisLabel, |
||||
}); |
||||
|
||||
scatterSeries.forEach((s) => { |
||||
let frame = s.frame(frames); |
||||
let field = s.y(frame); |
||||
|
||||
const lineColor = s.lineColor(frame); |
||||
const pointColor = asSingleValue(frame, s.pointColor) as string; |
||||
//const lineColor = s.lineColor(frame);
|
||||
//const lineWidth = s.lineWidth;
|
||||
|
||||
let scaleKey = field.config.unit ?? 'y'; |
||||
|
||||
builder.addScale({ |
||||
scaleKey, |
||||
orientation: ScaleOrientation.Vertical, |
||||
direction: ScaleDirection.Up, |
||||
range: (u, min, max) => [min, max], |
||||
}); |
||||
|
||||
builder.addAxis({ |
||||
scaleKey, |
||||
theme, |
||||
label: field.config.custom.axisLabel, |
||||
values: (u, splits) => splits.map((s) => field.display!(s).text), |
||||
}); |
||||
|
||||
builder.addSeries({ |
||||
facets: [ |
||||
{ |
||||
scale: 'x', |
||||
auto: true, |
||||
}, |
||||
{ |
||||
scale: scaleKey, |
||||
auto: true, |
||||
}, |
||||
], |
||||
pathBuilder: drawBubbles, // drawBubbles({disp: {size: {values: () => }}})
|
||||
theme, |
||||
scaleKey: '', // facets' scales used (above)
|
||||
lineColor: lineColor as string, |
||||
fillColor: alpha(pointColor, 0.5), |
||||
}); |
||||
}); |
||||
|
||||
/* |
||||
builder.setPrepData((frames) => { |
||||
let seriesData = lookup.fieldMaps.flatMap((f, i) => { |
||||
let { fields } = frames[i]; |
||||
|
||||
return f.y.map((yIndex, frameSeriesIndex) => { |
||||
let xValues = fields[f.x[frameSeriesIndex]].values.toArray(); |
||||
let yValues = fields[f.y[frameSeriesIndex]].values.toArray(); |
||||
let sizeValues = f.size; |
||||
|
||||
if (!Array.isArray(sizeValues)) { |
||||
sizeValues = Array(xValues.length).fill(sizeValues); |
||||
} |
||||
|
||||
return [xValues, yValues, sizeValues]; |
||||
}); |
||||
}); |
||||
|
||||
return [null, ...seriesData]; |
||||
}); |
||||
*/ |
||||
|
||||
return builder; |
||||
}; |
||||
|
||||
/** |
||||
* This is called everytime the data changes |
||||
* |
||||
* from? is this where we would support that? -- need the previous values |
||||
*/ |
||||
export function prepData(info: ScatterPanelInfo, data: DataFrame[], from?: number): FacetedData { |
||||
if (info.error) { |
||||
return [null]; |
||||
} |
||||
return [ |
||||
null, |
||||
...info.series.map((s, idx) => { |
||||
const frame = s.frame(data); |
||||
|
||||
let colorValues; |
||||
let colorAlphaValues; |
||||
const r = s.pointColor(frame); |
||||
if (Array.isArray(r)) { |
||||
colorValues = r; |
||||
colorAlphaValues = r.map((c) => alpha(c as string, 0.5)); |
||||
} else { |
||||
colorValues = Array(frame.length).fill(r); |
||||
colorAlphaValues = Array(frame.length).fill(alpha(r as string, 0.5)); |
||||
} |
||||
return [ |
||||
s.x(frame).values.toArray(), // X
|
||||
s.y(frame).values.toArray(), // Y
|
||||
asArray(frame, s.pointSize), |
||||
colorValues, |
||||
colorAlphaValues, |
||||
]; |
||||
}), |
||||
]; |
||||
} |
||||
|
||||
function asArray<T>(frame: DataFrame, lookup: DimensionValues<T>): T[] { |
||||
const r = lookup(frame); |
||||
if (Array.isArray(r)) { |
||||
return r; |
||||
} |
||||
return Array(frame.length).fill(r); |
||||
} |
||||
|
||||
function asSingleValue<T>(frame: DataFrame, lookup: DimensionValues<T>): T { |
||||
const r = lookup(frame); |
||||
if (Array.isArray(r)) { |
||||
return r[0]; |
||||
} |
||||
return r; |
||||
} |
@ -1,10 +1,60 @@ |
||||
import { OptionsWithTooltip, OptionsWithLegend } from '@grafana/schema'; |
||||
export interface XYDimensionConfig { |
||||
frame: number; |
||||
x?: string; // name | first
|
||||
exclude?: string[]; // all other numbers except
|
||||
import { DataFrame, Field, FieldColorMode } from '@grafana/data'; |
||||
import { LineStyle, VisibilityMode } from '@grafana/schema'; |
||||
import { VizLegendItem } from '@grafana/ui'; |
||||
import { ScaleDimensionConfig } from 'app/features/dimensions'; |
||||
import { ScatterLineMode } from './models.gen'; |
||||
|
||||
/** |
||||
* @internal |
||||
*/ |
||||
export type DimensionValues<T> = (frame: DataFrame, from?: number) => T | T[]; |
||||
|
||||
export interface ScatterHoverEvent { |
||||
scatterIndex: number; |
||||
xIndex: number; |
||||
pageX: number; |
||||
pageY: number; |
||||
} |
||||
|
||||
export type ScatterHoverCallback = (evt?: ScatterHoverEvent) => void; |
||||
|
||||
export interface LegendInfo { |
||||
color: CanvasRenderingContext2D['strokeStyle']; |
||||
text: string; |
||||
symbol: string; |
||||
openEditor?: (evt: any) => void; |
||||
} |
||||
|
||||
export interface Options extends OptionsWithLegend, OptionsWithTooltip { |
||||
dims: XYDimensionConfig; |
||||
// Using field where we will need formatting/scale/axis info
|
||||
// Use raw or DimensionValues when the values can be used directly
|
||||
export interface ScatterSeries { |
||||
name: string; |
||||
|
||||
/** Finds the relevant frame from the raw panel data */ |
||||
frame: (frames: DataFrame[]) => DataFrame; |
||||
|
||||
x: (frame: DataFrame) => Field; |
||||
y: (frame: DataFrame) => Field; |
||||
|
||||
legend: (frame: DataFrame) => VizLegendItem[]; // could be single if symbol is constant
|
||||
|
||||
line: ScatterLineMode; |
||||
lineWidth: number; |
||||
lineStyle: LineStyle; |
||||
lineColor: (frame: DataFrame) => CanvasRenderingContext2D['strokeStyle']; |
||||
|
||||
point: VisibilityMode; |
||||
pointSize: DimensionValues<number>; |
||||
pointColor: DimensionValues<CanvasRenderingContext2D['strokeStyle']>; |
||||
pointSymbol: DimensionValues<string>; // single field, multiple symbols.... kinda equals multiple series
|
||||
|
||||
label: VisibilityMode; |
||||
labelValue: DimensionValues<string>; |
||||
|
||||
hints: { |
||||
pointSize: ScaleDimensionConfig; |
||||
pointColor: { |
||||
mode: FieldColorMode; |
||||
}; |
||||
}; |
||||
} |
||||
|
Loading…
Reference in new issue