diff options
author | Determinant <[email protected]> | 2019-02-24 15:49:59 -0500 |
---|---|---|
committer | Determinant <[email protected]> | 2019-02-24 15:49:59 -0500 |
commit | d017bbd1ad19345af0121f8bb0cf805f977e55f6 (patch) | |
tree | 899cda0a10b366bfe6d708c22ddaa197efe12052 /src | |
parent | 0d665c8de086d79d8c49e01a262a520c65505fbf (diff) |
prettify the doughnut chart
Diffstat (limited to 'src')
-rw-r--r-- | src/Analyze.tsx | 6 | ||||
-rw-r--r-- | src/Chart.tsx | 254 | ||||
-rw-r--r-- | src/Doughnut.tsx (renamed from src/Donut.tsx) | 4 | ||||
-rw-r--r-- | src/background.ts | 7 | ||||
-rw-r--r-- | src/popup.tsx | 10 | ||||
-rw-r--r-- | src/tab.tsx | 38 |
6 files changed, 228 insertions, 91 deletions
diff --git a/src/Analyze.tsx b/src/Analyze.tsx index e7563d5..aa77cb3 100644 --- a/src/Analyze.tsx +++ b/src/Analyze.tsx @@ -21,11 +21,9 @@ import * as gapi from './gapi'; import { MsgType, MsgClient } from './msg'; import { Pattern, PatternEntry, PatternEntryFlat } from './pattern'; import { AnalyzePieChart } from './Chart'; -import { getGraphData } from './graph'; +import { getGraphData, PatternGraphData } from './graph'; -const defaultChartData = [ - {name: 'Work', value: 10, color: cyan[300]}, - {name: 'Wasted', value: 10, color: deepOrange[300]}]; +const defaultChartData = [] as PatternGraphData[]; const styles = (theme: Theme) => ({ buttonSpacer: { diff --git a/src/Chart.tsx b/src/Chart.tsx index 170b5fa..0c8f958 100644 --- a/src/Chart.tsx +++ b/src/Chart.tsx @@ -2,11 +2,24 @@ import React from 'react'; import { Theme, withStyles } from '@material-ui/core/styles'; import Grid from '@material-ui/core/Grid'; import cyan from '@material-ui/core/colors/cyan'; -import { Doughnut } from 'react-chartjs-2'; +import Typography from '@material-ui/core/Typography'; +import { Doughnut as DChart, Chart } from 'react-chartjs-2'; import 'chartjs-plugin-labels'; import Color from 'color'; -import { defaultChartColor } from './theme'; +import { defaultChartColor, theme } from './theme'; import { PatternGraphData } from './graph'; +import Doughnut from './Doughnut'; + +declare module 'react-chartjs-2' { + export var Chart: { + controllers: { + doughnut: any, + pie: any + }, + elements: { Arc: any }, + pluginService: any + }; +} const styles = (theme: Theme) => ({ pieChart: { @@ -14,67 +27,176 @@ const styles = (theme: Theme) => ({ } }); -type PatternPieChartProps = { +interface PatternPieChartProps { classes: { patternTableWrapper: string, pieChart: string - }, - height?: number | string, - data: PatternGraphData[], - borderWidth: number, - labelFontSize : number, - paddingTop: number, - paddingBottom: number, - paddingLeft: number, - paddingRight: number, + }; + data: PatternGraphData[]; + borderWidth: number; + labelFontSize : number; + paddingTop: number; + paddingBottom: number; + paddingLeft: number; + paddingRight: number; }; +Chart.elements.Arc.prototype.draw = function() { + let ctx = this._chart.ctx; + const vm = this._view; + const sA = vm.startAngle; + const eA = vm.endAngle; + const pixelMargin = (vm.borderAlign === 'inner') ? 0.33 : 0; + let angleMargin; + + ctx.save(); + + const delta = 3; + const deltaOuter = Math.asin(delta / vm.outerRadius); + const deltaInner = Math.asin(delta / vm.innerRadius); + + let sA1 = sA; + let sA2 = sA; + let eA1 = eA; + let eA2 = eA; + + if ((eA - sA) > 2 * deltaInner + 0.05) { + sA1 += deltaOuter; + eA1 -= deltaOuter; + sA2 += deltaInner; + eA2 -= deltaInner; + } + + ctx.beginPath(); + ctx.arc(vm.x, vm.y, Math.max(vm.outerRadius - pixelMargin, 0), sA1, eA1); + ctx.arc(vm.x, vm.y, vm.innerRadius, eA2, sA2, true); + ctx.closePath(); + + ctx.fillStyle = vm.backgroundColor; + ctx.fill(); + + if (vm.borderWidth) { + if (vm.borderAlign === 'inner') { + // Draw an inner border by cliping the arc and drawing a double-width border + // Enlarge the clipping arc by 0.33 pixels to eliminate glitches between borders + ctx.beginPath(); + angleMargin = pixelMargin / vm.outerRadius; + ctx.arc(vm.x, vm.y, vm.outerRadius, sA - angleMargin, eA + angleMargin); + if (vm.innerRadius > pixelMargin) { + angleMargin = pixelMargin / vm.innerRadius; + ctx.arc(vm.x, vm.y, vm.innerRadius - pixelMargin, eA + angleMargin, sA - angleMargin, true); + } else { + ctx.arc(vm.x, vm.y, pixelMargin, eA + Math.PI / 2, sA - Math.PI / 2); + } + ctx.closePath(); + ctx.clip(); + + ctx.beginPath(); + ctx.arc(vm.x, vm.y, vm.outerRadius, sA, eA); + ctx.arc(vm.x, vm.y, vm.innerRadius, eA, sA, true); + ctx.closePath(); + + ctx.lineWidth = vm.borderWidth * 2; + ctx.lineJoin = 'round'; + } else { + ctx.lineWidth = vm.borderWidth; + ctx.lineJoin = 'bevel'; + } + + ctx.strokeStyle = vm.borderColor; + ctx.stroke(); + } + + ctx.restore(); +}; + +// Code adapted from https://stackoverflow.com/a/43026361/544806 +Chart.pluginService.register({ + beforeDraw: function (chart: any) { + if (chart.config.options.elements.center) { + //Get ctx from string + let ctx = chart.chart.ctx; + //Get options from the center object in options + const centerConfig = chart.config.options.elements.center; + const fontStyle = centerConfig.fontStyle || 'Noto Sans'; + const txt = centerConfig.text; + const color = centerConfig.color || '#000'; + const sidePadding = centerConfig.sidePadding || 20; + const sidePaddingCalculated = (sidePadding/100) * (chart.innerRadius * 2) + //Start with a base font of 30px + ctx.font = "12px " + fontStyle; + + //Get the width of the string and also the width of the element minus 10 to give it 5px side padding + const stringWidth = ctx.measureText(txt).width; + const elementWidth = (chart.innerRadius * 2) - sidePaddingCalculated; + + // Find out how much the font can grow in width. + const widthRatio = elementWidth / stringWidth; + const newFontSize = Math.floor(30 * widthRatio); + const elementHeight = (chart.innerRadius * 2); + + // Pick a new font size so it will not be larger than the height of label. + const fontSizeToUse = Math.min(newFontSize, elementHeight, centerConfig.maxFontSize); + + // Set font settings to draw it correctly. + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + const centerX = ((chart.chartArea.left + chart.chartArea.right) / 2); + const centerY = ((chart.chartArea.top + chart.chartArea.bottom) / 2); + ctx.font = fontSizeToUse+"px " + fontStyle; + ctx.fillStyle = color; + + // Draw text in center + ctx.fillText(txt, centerX, centerY); + } + } +}); + export class PatternPieChart extends React.Component<PatternPieChartProps> { - public static defaultProps = { - borderWidth: 1, - labelFontSize: 12, - paddingTop: 0, - paddingBottom: 0, - paddingLeft: 0, - paddingRight: 0, - }; render() { - let { height, data, labelFontSize } = this.props; - const theme = { - labels: { - text: { - fontSize: labelFontSize - } - } - }; + let { data, labelFontSize } = this.props; const colors = data.map(p => p.color ? p.color: defaultChartColor); + const totalValue = data.map(p => p.value).reduce((ans, v) => ans + v); return ( - <Doughnut data={(canvas: any) => { + <DChart data={() => { return { datasets: [{ data: data.map(p => p.value), backgroundColor: colors, borderWidth: data.map(() => this.props.borderWidth), + borderColor: colors.map(c => Color(c).darken(0.1).string()), hoverBorderWidth: data.map(() => this.props.borderWidth), - hoverBorderColor: colors.map(c => Color(c).darken(0.2).string()) + hoverBorderColor: colors.map(c => Color(c).darken(0.3).string()) }], labels: data.map(p => p.name) }; }} options={{ + elements: { + center: { + text: `${totalValue.toFixed(2)} hr`, + color: theme.palette.text.secondary, + maxFontSize: 20, + sidePadding: 50 + } + }, tooltips: { callbacks: { - label: (item: any, data: any) => ( + label: (item: { index: number, datasetIndex: number }, + data: { labels: string[], datasets: { data: number[] }[] }) => { + const v = data.datasets[item.datasetIndex].data[item.index]; + return ( `${data.labels[item.index]}: ` + - `${data.datasets[item.datasetIndex].data[item.index].toFixed(2)} hr` - ) + `${v.toFixed(2)} hr (${(v / totalValue * 100).toFixed(2)} %)` + ); + } } }, plugins: { labels: { - render: (args: any) => `${args.value.toFixed(2)} hr`, - fontColor: (data: any) => { + render: (args: { value: number }) => `${args.value.toFixed(2)} hr`, + fontColor: (data: { index: number, dataset: { backgroundColor: string[] } }) => { var rgb = Color(data.dataset.backgroundColor[data.index]).rgb().object(); - var threshold = 140; + var threshold = 150; var luminance = 0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b; return luminance > threshold ? 'black' : 'white'; }, @@ -99,7 +221,55 @@ export class PatternPieChart extends React.Component<PatternPieChartProps> { } } -export const StyledPatternPieChart = withStyles(styles)(PatternPieChart); +interface DoughnutChartProps extends PatternPieChartProps { + height: number +} + +class _DoughnutChart extends React.Component<DoughnutChartProps> { + public static defaultProps = { + height: 300, + borderWidth: 1, + labelFontSize: 12, + paddingTop: 0, + paddingBottom: 0, + paddingLeft: 0, + paddingRight: 0, + }; + render() { + const h = this.props.height; + const ih = h * (1 - 0.05 - 0.20); + return ((this.props.data.some(dd => dd.value > 1e-3) && + <div style={{height: h}}> + <PatternPieChart {...this.props} /> + </div>) || + <div style={{ + marginTop: 0.05 * h, + marginBottom: 0.20 * h, + textAlign: 'center' + }}> + <div style={{ + position: 'relative', + height: ih, + display: 'inline-block' + }}> + <Doughnut style={{ + height: '100%' + }} /> + <div style={{ + position: 'absolute', + bottom: -ih * 0.15, + left: ih * 0.5 - 73, + }}> + <Typography variant="subtitle1" align="center" color="textSecondary"> + No matching events. + </Typography> + </div> + </div> + </div>); + } +} + +export const DoughnutChart = withStyles(styles)(_DoughnutChart); type DoublePieChartProps = { classes: { @@ -112,12 +282,12 @@ type DoublePieChartProps = { function DoublePieChart(props: DoublePieChartProps) { return ( - <Grid container spacing={0}> - <Grid item md={12} lg={12} style={{height: 300}}> - <StyledPatternPieChart data={props.patternGraphData} /> + <Grid container spacing={16}> + <Grid item md={12} sm={6} xs={12}> + <DoughnutChart height={300} data={props.patternGraphData} /> </Grid> - <Grid item md={12} lg={12} style={{height: 300}}> - <StyledPatternPieChart data={props.calendarGraphData} /> + <Grid item md={12} sm={6} xs={12}> + <DoughnutChart height={300} data={props.calendarGraphData} /> </Grid> </Grid>); } diff --git a/src/Donut.tsx b/src/Doughnut.tsx index 00d472a..09844b5 100644 --- a/src/Donut.tsx +++ b/src/Doughnut.tsx @@ -9,7 +9,7 @@ const styles = { st6: {fill: '#F69D98'}, }; -function Donut(props: { +function Doughnut(props: { style: {[key: string]: string | number }, classes: { st0: string, @@ -157,4 +157,4 @@ function Donut(props: { </svg>); } -export default withStyles(styles)(Donut); +export default withStyles(styles)(Doughnut); diff --git a/src/background.ts b/src/background.ts index 84f1f43..a9fed12 100644 --- a/src/background.ts +++ b/src/background.ts @@ -370,9 +370,6 @@ chrome.tabs.onCreated.addListener(function(tab) { } }); -chrome.runtime.onInstalled.addListener(async () => { - try { - await auth.logout(); - calData = {}; - } catch (_) {} +chrome.runtime.onInstalled.addListener(() => { + chrome.tabs.create({ url: "index.html" }); }); diff --git a/src/popup.tsx b/src/popup.tsx index d0f714b..81b42ae 100644 --- a/src/popup.tsx +++ b/src/popup.tsx @@ -11,7 +11,7 @@ import CircularProgress from '@material-ui/core/CircularProgress'; import Logo from './Logo'; import { theme } from './theme'; -import { StyledPatternPieChart } from './Chart'; +import { DoughnutChart } from './Chart'; import { MsgType, MsgClient } from './msg'; import { GraphData } from './graph'; import moment from 'moment'; @@ -47,7 +47,7 @@ type PopupProps = { } }; -class Popup extends React.Component<PopupProps> { +class _Popup extends React.Component<PopupProps> { msgClient: MsgClient; state = { patternGraphData: [] as GraphData[], @@ -108,7 +108,7 @@ class Popup extends React.Component<PopupProps> { ${moment(d.end).format('ddd, MMM Do, YYYY')}`} </Typography> {(d.data.some(dd => dd.value > 1e-3) && - <div style={{height: 300}}><StyledPatternPieChart data={d.data} /></div>) || + <DoughnutChart height={300} data={d.data} />) || <Typography variant="subtitle1" align="center" color="textSecondary"> No matching events. </Typography>} @@ -124,6 +124,6 @@ class Popup extends React.Component<PopupProps> { } } -const StyledPopup = withStyles(styles)(Popup); +const Popup = withStyles(styles)(_Popup); -ReactDOM.render(<StyledPopup />, document.getElementById('root')); +ReactDOM.render(<Popup />, document.getElementById('root')); diff --git a/src/tab.tsx b/src/tab.tsx index 86e7af3..6cbd36b 100644 --- a/src/tab.tsx +++ b/src/tab.tsx @@ -10,9 +10,8 @@ import Grid from '@material-ui/core/Grid'; import CircularProgress from '@material-ui/core/CircularProgress'; import Logo from './Logo'; -import Donut from './Donut'; import { theme } from './theme'; -import { StyledPatternPieChart } from './Chart'; +import { DoughnutChart } from './Chart'; import { MsgType, MsgClient } from './msg'; import { GraphData } from './graph'; import moment from 'moment'; @@ -45,7 +44,7 @@ type TabProps = { }; -class Tab extends React.Component<TabProps> { +class _Tab extends React.Component<TabProps> { msgClient: MsgClient; state = { patternGraphData: [] as GraphData[], @@ -106,40 +105,13 @@ class Tab extends React.Component<TabProps> { {`${moment(d.start).format('ddd, MMM Do, YYYY')} - ${moment(d.end).format('ddd, MMM Do, YYYY')}`} </Typography> - {(d.data.some(dd => dd.value > 1e-3) && - <div style={{height: 400}}> - <StyledPatternPieChart + <DoughnutChart data={d.data} height={400} borderWidth={2} paddingTop={20} paddingBottom={50} labelFontSize={14} /> - </div>) || - <div style={{ - marginTop: 20, - marginBottom: 60, - textAlign: 'center' - }}> - <div style={{ - position: 'relative', - height: 270, - display: 'inline-block' - }}> - <Donut style={{ - height: '100%' - }} /> - <div style={{ - position: 'absolute', - bottom: -40, - left: 60, - }}> - <Typography variant="subtitle1" align="center" color="textSecondary"> - No matching events. - </Typography> - </div> - </div> - </div>} </Grid> ))) || ( <div className={classes.loading}><CircularProgress color="primary" /></div> @@ -152,6 +124,6 @@ class Tab extends React.Component<TabProps> { } } -const StyledTab = withStyles(styles)(Tab); +const Tab = withStyles(styles)(_Tab); -ReactDOM.render(<StyledTab />, document.getElementById('root')); +ReactDOM.render(<Tab />, document.getElementById('root')); |