Skip to content

Commit

Permalink
feat: add PNG export (#451)
Browse files Browse the repository at this point in the history
- Add PNG export of chart (including IE11!)
- Story added in Interactions section 

Closes #82
  • Loading branch information
rshen91 authored Nov 22, 2019
1 parent 7738aa9 commit e844687
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 53 deletions.
2 changes: 1 addition & 1 deletion .playground/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
left: 0px;
}
.chart {
background: black;
background: white;
display: inline-block;
position: relative;
width: 900px;
Expand Down
99 changes: 52 additions & 47 deletions .playground/playgroud.tsx
Original file line number Diff line number Diff line change
@@ -1,62 +1,67 @@
import React from 'react';
import { Axis, Chart, getAxisId, getSpecId, Position, ScaleType, Settings, LineSeries } from '../src';
import { Fit } from '../src/chart_types/xy_chart/utils/specs';

const data = [
{ x: 0, y: null },
{ x: 1, y: 3 },
{ x: 2, y: 5 },
{ x: 3, y: null },
{ x: 4, y: 4 },
{ x: 5, y: null },
{ x: 6, y: 5 },
{ x: 7, y: 6 },
{ x: 8, y: null },
{ x: 9, y: null },
{ x: 10, y: null },
{ x: 11, y: 12 },
{ x: 12, y: null },
];

import {
Axis,
Chart,
getAxisId,
getSpecId,
Position,
ScaleType,
HistogramBarSeries,
Settings,
LIGHT_THEME,
niceTimeFormatter,
} from '../src';
import { KIBANA_METRICS } from '../src/utils/data_samples/test_dataset_kibana';
export class Playground extends React.Component {
chartRef: React.RefObject<Chart> = React.createRef();
onSnapshot = () => {
if (!this.chartRef.current) {
return;
}
const snapshot = this.chartRef.current.getPNGSnapshot({
backgroundColor: 'white',
pixelRatio: 1,
});
if (!snapshot) {
return;
}
const fileName = 'chart.png';
switch (snapshot.browser) {
case 'IE11':
return navigator.msSaveBlob(snapshot.blobOrDataUrl, fileName);
default:
const link = document.createElement('a');
link.download = fileName;
link.href = snapshot.blobOrDataUrl;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
render() {
const data = KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 100);

return (
<>
<button onClick={this.onSnapshot}>Snapshot</button>
<div className="chart">
<Chart className="story-chart">
<Settings
showLegend
theme={{
areaSeriesStyle: {
point: {
visible: true,
},
},
}}
/>
<Chart ref={this.chartRef}>
<Settings theme={LIGHT_THEME} showLegend={true} />
<Axis
id={getAxisId('bottom')}
id={getAxisId('time')}
position={Position.Bottom}
title={'Bottom axis'}
showOverlappingTicks={true}
tickFormat={niceTimeFormatter([data[0][0], data[data.length - 1][0]])}
/>
<Axis id={getAxisId('left')} title={'Left axis'} position={Position.Left} />
<LineSeries
id={getSpecId('test')}
<Axis id={getAxisId('count')} position={Position.Left} />

<HistogramBarSeries
id={getSpecId('series bars chart')}
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor={'x'}
yAccessors={['y']}
// curve={2}
// splitSeriesAccessors={['g']}
// stackAccessors={['x']}
fit={Fit.Linear}
xAccessor={0}
yAccessors={[1]}
data={data}
// fit={{
// type: Fit.Average,
// endValue: 0,
// }}
// data={data}
yScaleToDataExtent={true}
/>
</Chart>
</div>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions src/components/chart.snap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Chart } from '../components/chart';

describe('test getPNGSnapshot in Chart class', () => {
jest.mock('../components/chart');
it('should be called', () => {
const chart = new Chart({});
const spy = jest.spyOn(chart, 'getPNGSnapshot');
chart.getPNGSnapshot({ backgroundColor: 'white', pixelRatio: 1 });

expect(spy).toBeCalled();
});
});
58 changes: 56 additions & 2 deletions src/components/chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { isHorizontalAxis } 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';
import { Stage } from 'react-konva';
import Konva from 'konva';

interface ChartProps {
/** The type of rendered
Expand All @@ -38,9 +40,11 @@ export class Chart extends React.Component<ChartProps, ChartState> {
};
private chartSpecStore: ChartStore;
private chartContainerRef: React.RefObject<HTMLDivElement>;
private chartStageRef: React.RefObject<Stage>;
constructor(props: any) {
super(props);
this.chartContainerRef = createRef();
this.chartStageRef = createRef();
this.chartSpecStore = new ChartStore(props.id);
this.state = {
legendPosition: this.chartSpecStore.legendPosition.get(),
Expand Down Expand Up @@ -91,6 +95,56 @@ export class Chart extends React.Component<ChartProps, ChartState> {
}
}
}

getPNGSnapshot(
options = {
backgroundColor: 'transparent',
pixelRatio: 2,
},
): {
blobOrDataUrl: any;
browser: 'IE11' | 'other';
} | null {
if (!this.chartStageRef.current) {
return null;
}
const stage = this.chartStageRef.current.getStage().clone();
const width = stage.getWidth();
const height = stage.getHeight();
const backgroundLayer = new Konva.Layer();
const backgroundRect = new Konva.Rect({
fill: options.backgroundColor,
x: 0,
y: 0,
width,
height,
});

backgroundLayer.add(backgroundRect);
stage.add(backgroundLayer);
backgroundLayer.moveToBottom();
stage.draw();
const canvasStage = stage.toCanvas({
width,
height,
callback: () => {},
});
// @ts-ignore
if (canvasStage.msToBlob) {
// @ts-ignore
const blobOrDataUrl = canvasStage.msToBlob();
return {
blobOrDataUrl,
browser: 'IE11',
};
} else {
return {
blobOrDataUrl: stage.toDataURL({ pixelRatio: options.pixelRatio }),
browser: 'other',
};
}
}

getChartContainerRef = () => {
return this.chartContainerRef;
};
Expand Down Expand Up @@ -119,8 +173,8 @@ export class Chart extends React.Component<ChartProps, ChartState> {
<ChartResizer />
<Crosshair />
{// TODO reenable when SVG rendered is aligned with canvas one
renderer === 'svg' && <ChartContainer />}
{renderer === 'canvas' && <ChartContainer />}
renderer === 'svg' && <ChartContainer forwardRef={this.chartStageRef} />}
{renderer === 'canvas' && <ChartContainer forwardRef={this.chartStageRef} />}
<Tooltips getChartContainerRef={this.getChartContainerRef} />
<AnnotationTooltip getChartContainerRef={this.getChartContainerRef} />
<Highlighter />
Expand Down
4 changes: 3 additions & 1 deletion src/components/react_canvas/chart_container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import React from 'react';
import { inject, observer } from 'mobx-react';
import { ChartStore } from '../../chart_types/xy_chart/store/chart_state';
import { ReactiveChart } from './reactive_chart';
import { Stage } from 'react-konva';
interface ReactiveChartProps {
chartStore?: ChartStore; // FIX until we find a better way on ts mobx
forwardRef: React.RefObject<Stage>;
}

class ChartContainerComponent extends React.Component<ReactiveChartProps> {
Expand Down Expand Up @@ -36,7 +38,7 @@ class ChartContainerComponent extends React.Component<ReactiveChartProps> {
this.props.chartStore!.handleChartClick();
}}
>
<ReactiveChart />
<ReactiveChart forwardRef={this.props.forwardRef} />
</div>
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/components/react_canvas/reactive_chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { Clippings } from './utils/rendering_props_utils';

interface ReactiveChartProps {
chartStore?: ChartStore; // FIX until we find a better way on ts mobx
forwardRef: React.RefObject<Stage>;
}
interface ReactiveChartState {
brushing: boolean;
Expand Down Expand Up @@ -95,7 +96,6 @@ class Chart extends React.Component<ReactiveChartProps, ReactiveChartState> {
return [];
}
const highlightedLegendItem = this.getHighlightedLegendItem();

const element = (
<BarGeometries
key={'bar-geometries'}
Expand Down Expand Up @@ -412,6 +412,7 @@ class Chart extends React.Component<ReactiveChartProps, ReactiveChartState> {
height: '100%',
}}
{...brushProps}
ref={this.props.forwardRef}
>
<Layer hitGraphEnabled={false} listening={false}>
{this.renderGrids()}
Expand Down
66 changes: 65 additions & 1 deletion stories/interactions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
TooltipValueFormatter,
} from '../src/';

import { array, boolean, number, select } from '@storybook/addon-knobs';
import { array, boolean, number, select, button } from '@storybook/addon-knobs';
import { DateTime } from 'luxon';
import { switchTheme } from '../.storybook/theme_service';
import { BARCHART_2Y2G } from '../src/utils/data_samples/test_dataset';
Expand Down Expand Up @@ -635,4 +635,68 @@ storiesOf('Interactions', module)
{
info: 'Sends an event every time the cursor changes. This is provided to sync cursors between multiple charts.',
},
)
.add(
'PNG export action',
() => {
/**
* The handler section of this story demonstrates the PNG export functionality
*/
const data = KIBANA_METRICS.metrics.kibana_os_load[0].data.slice(0, 100);
const label = 'Export PNG';
const chartRef: React.RefObject<Chart> = React.createRef();
const handler = () => {
if (!chartRef.current) {
return;
}
const snapshot = chartRef.current.getPNGSnapshot({
// you can set the background and pixel ratio for the PNG export
backgroundColor: 'white',
pixelRatio: 2,
});
if (!snapshot) {
return;
}
// will save as chart.png
const fileName = 'chart.png';
switch (snapshot.browser) {
case 'IE11':
return navigator.msSaveBlob(snapshot.blobOrDataUrl, fileName);
default:
const link = document.createElement('a');
link.download = fileName;
link.href = snapshot.blobOrDataUrl;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
const groupId = 'PNG-1';
button(label, handler, groupId);
return (
<Chart className={'story-chart'} ref={chartRef}>
<Settings showLegend={true} />
<Axis
id={getAxisId('time')}
position={Position.Bottom}
tickFormat={niceTimeFormatter([data[0][0], data[data.length - 1][0]])}
/>
<Axis id={getAxisId('count')} position={Position.Left} />

<BarSeries
id={getSpecId('series bars chart')}
xScaleType={ScaleType.Linear}
yScaleType={ScaleType.Linear}
xAccessor={0}
yAccessors={[1]}
data={data}
yScaleToDataExtent={true}
/>
</Chart>
);
},
{
info:
'Generate a PNG of the chart by clicking on the Export PNG button in the knobs section. In this example, the button handler is setting the PNG background to white with a pixel ratio of 2. If the browser is detected to be IE11, msSaveBlob will be used instead of a PNG capture.',
},
);

0 comments on commit e844687

Please sign in to comment.