From d017bbd1ad19345af0121f8bb0cf805f977e55f6 Mon Sep 17 00:00:00 2001 From: Determinant Date: Sun, 24 Feb 2019 15:49:59 -0500 Subject: prettify the doughnut chart --- src/Analyze.tsx | 6 +- src/Chart.tsx | 254 +++++++++++++++++++++++++++++++++++++++++++++--------- src/Donut.tsx | 160 ---------------------------------- src/Doughnut.tsx | 160 ++++++++++++++++++++++++++++++++++ src/background.ts | 7 +- src/popup.tsx | 10 +-- src/tab.tsx | 38 ++------ 7 files changed, 386 insertions(+), 249 deletions(-) delete mode 100644 src/Donut.tsx create mode 100644 src/Doughnut.tsx (limited to 'src') 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 { - 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 ( - { + { 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 { } } -export const StyledPatternPieChart = withStyles(styles)(PatternPieChart); +interface DoughnutChartProps extends PatternPieChartProps { + height: number +} + +class _DoughnutChart extends React.Component { + 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) && +
+ +
) || +
+
+ +
+ + No matching events. + +
+
+
); + } +} + +export const DoughnutChart = withStyles(styles)(_DoughnutChart); type DoublePieChartProps = { classes: { @@ -112,12 +282,12 @@ type DoublePieChartProps = { function DoublePieChart(props: DoublePieChartProps) { return ( - - - + + + - - + + ); } diff --git a/src/Donut.tsx b/src/Donut.tsx deleted file mode 100644 index 00d472a..0000000 --- a/src/Donut.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import React from 'react'; -import { withStyles } from '@material-ui/core/styles'; -const styles = { - st0: {fill: '#F9CC8A'}, - st2: {fill: '#C0E5E0'}, - st3: {fill: '#FFFFFF'}, - st4: {fill: '#8B5E3C'}, - st5: {fill: '#FBD015'}, - st6: {fill: '#F69D98'}, -}; - -function Donut(props: { - style: {[key: string]: string | number }, - classes: { - st0: string, - st2: string, - st3: string, - st4: string, - st5: string, - st6: string, - }}) { - let {st0, st2, st3, st4, st5, st6 } = props.classes; - return ( - - - ); -} - -export default withStyles(styles)(Donut); diff --git a/src/Doughnut.tsx b/src/Doughnut.tsx new file mode 100644 index 0000000..09844b5 --- /dev/null +++ b/src/Doughnut.tsx @@ -0,0 +1,160 @@ +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; +const styles = { + st0: {fill: '#F9CC8A'}, + st2: {fill: '#C0E5E0'}, + st3: {fill: '#FFFFFF'}, + st4: {fill: '#8B5E3C'}, + st5: {fill: '#FBD015'}, + st6: {fill: '#F69D98'}, +}; + +function Doughnut(props: { + style: {[key: string]: string | number }, + classes: { + st0: string, + st2: string, + st3: string, + st4: string, + st5: string, + st6: string, + }}) { + let {st0, st2, st3, st4, st5, st6 } = props.classes; + return ( + + + ); +} + +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 { +class _Popup extends React.Component { msgClient: MsgClient; state = { patternGraphData: [] as GraphData[], @@ -108,7 +108,7 @@ class Popup extends React.Component { ${moment(d.end).format('ddd, MMM Do, YYYY')}`} {(d.data.some(dd => dd.value > 1e-3) && -
) || + ) || No matching events. } @@ -124,6 +124,6 @@ class Popup extends React.Component { } } -const StyledPopup = withStyles(styles)(Popup); +const Popup = withStyles(styles)(_Popup); -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render(, 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 { +class _Tab extends React.Component { msgClient: MsgClient; state = { patternGraphData: [] as GraphData[], @@ -106,40 +105,13 @@ class Tab extends React.Component { {`${moment(d.start).format('ddd, MMM Do, YYYY')} - ${moment(d.end).format('ddd, MMM Do, YYYY')}`} - {(d.data.some(dd => dd.value > 1e-3) && -
- -
) || -
-
- -
- - No matching events. - -
-
-
}
))) || (
@@ -152,6 +124,6 @@ class Tab extends React.Component { } } -const StyledTab = withStyles(styles)(Tab); +const Tab = withStyles(styles)(_Tab); -ReactDOM.render(, document.getElementById('root')); +ReactDOM.render(, document.getElementById('root')); -- cgit v1.2.3-70-g09d2