Repository: timqian/chart.xkcd Branch: master Commit: c6b3aaa84459 Files: 39 Total size: 135.7 KB Directory structure: gitextract_p4uq7k72/ ├── .eslintrc ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── chart-request.md │ │ └── feature_request.md │ ├── pull_request_template.md │ └── workflows/ │ └── nodejs.yml ├── .gitignore ├── LICENSE ├── contributing.md ├── docs/ │ ├── 01-intro.md │ ├── 02-getting-started.md │ ├── 03-install.md │ ├── 04-line.md │ ├── 05-XY.md │ ├── 06-bar.md │ ├── 07-stacked-bar.md │ ├── 08-pie.md │ └── 09-radar.md ├── examples/ │ ├── example.html │ ├── index.js │ └── readme.md ├── package.json ├── readme.md └── src/ ├── Bar.js ├── Line.js ├── Pie.js ├── Radar.js ├── StackedBar.js ├── XY.js ├── components/ │ └── Tooltip.js ├── config.js ├── index.js └── utils/ ├── addAxis.js ├── addFilter.js ├── addFont.js ├── addLabels.js ├── addLegend.js └── colors.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc ================================================ { "extends": ["airbnb-base"], "rules": { "no-console": "warn", "indent": ["warn", 2], "no-new": "off", "class-methods-use-this":"warn" }, "env": { "browser": true, "node": true, "mocha": true } } ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms custom: https://sponsor.cat/timqian.eth github: timqian # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: timqian # Replace with a single Patreon username open_collective: # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **Screenshot** If applicable, add screenshots to help explain your problem. **How to reproduce** code to reproduce the bug (Codepen link preferred) **Additional context** Add any other context about the problem here. Chart.xkcd is a chart library plots "sketchy", "cartoony" or "hand-drawn" styled charts. [![donate](https://flat.badgen.net/badge/support%20me/donate/?color=e85b46)](https://patreon.com/timqian) [![slack](https://flat.badgen.net/badge/chat%20with%20us/slack/green)](https://join.slack.com/t/t9tio/shared_invite/enQtNjgzMzkwMDM0NTE3LTE5ZTUzYjU4Y2I0YzRiZjNkYTkzMzE1ZmM0NDdmYzRlZmMxNGY1MzZlN2EwYjYyNWVlMWY0Nzk2MDBhNWZlY2I) [![stars](https://flat.badgen.net/github/stars/timqian/chart.xkcd?icon=github)](https://github.com/timqian/chart.xkcd) [![npm](https://flat.badgen.net/npm/v/chart.xkcd/?color=fb3e44)](https://www.npmjs.com/package/chart.xkcd) [![jsdelivr](https://data.jsdelivr.com/v1/package/npm/chart.xkcd/badge)](https://www.jsdelivr.com/package/npm/chart.xkcd) ## Sponsors [Madao](https://madao.me/) | [Become a sponsor](https://www.patreon.com/timqian) ================================================ FILE: docs/02-getting-started.md ================================================ --- title: Getting started --- It's easy to get started with chart.xkcd. All that's required is the script included in your page along with a single `` node to render the chart. In the following example we create a line chart.

See the Pen chart.xkcd example by timqian (@timqian) on CodePen.

**JS part of the example** ```javascript const svg = document.querySelector('.line-chart') new chartXkcd.Line(svg, { title: '', xLabel: '', yLabel: '', data: {...}, options: {}, }); ``` ## Parameters description - `title`: optional title of the chart - `xLabel`: optional x label of the chart - `yLabel`: optional y label of the chart - `data`: the data you want to visulize - `options`: optional configurations to customize how the chart looks ================================================ FILE: docs/03-install.md ================================================ --- title: Installation --- You can install chart.xkcd via script tag in HTML or via npm ## Via Script Tag ```js ``` ## Via npm **Install** ```bash npm i chart.xkcd ``` **Usage** ```js import chartXkcd from 'chart.xkcd'; const myChart = new chartXkcd.Line(svg, {...}); ``` **Other ways** - React wrapper: [chart.xkcd-react](https://github.com/obiwankenoobi/chart.xkcd-react)
- Vue wrapper: - [chart.xkcd-vue](https://github.com/shiyiya/chart.xkcd-vue) - [chart.xkcd-vue-wrapper](https://github.com/wistcc/chart.xkcd-vue-wrapper) ================================================ FILE: docs/04-line.md ================================================ --- title: Line chart --- Line chart displays series of data points in the form of lines. It can be used to show trend data, or comparison of different data sets.

See the Pen chart.xkcd example by timqian (@timqian) on CodePen.

## JS part ```js const lineChart = new chartXkcd.Line(svg, { title: 'Monthly income of an indie developer', // optional xLabel: 'Month', // optional yLabel: '$ Dollars', // optional data: { labels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'], datasets: [{ label: 'Plan', data: [30, 70, 200, 300, 500, 800, 1500, 2900, 5000, 8000], }, { label: 'Reality', data: [0, 1, 30, 70, 80, 100, 50, 80, 40, 150], }], }, options: { // optional yTickCount: 3, legendPosition: chartXkcd.config.positionType.upLeft } }) ``` ## Customize chart by defining options - `yTickCount`: customize tick numbers you want to see on the y axis (default `3`) - `showLegend`: display legend near chart (default `true`) - `legendPosition`: specify where you want to place the legend. (default `chartXkcd.config.positionType.upLeft`) Possible values: - up left: `chartXkcd.config.positionType.upLeft` - up right: `chartXkcd.config.positionType.upRight` - bottom left: `chartXkcd.config.positionType.downLeft` - bottom right: `chartXkcd.config.positionType.downRight` - `dataColors`: array of colors for different datasets - `fontFamily`: customize font family used in the chart - `unxkcdify`: disable xkcd effect (default `false`) - `strokeColor`: stroke colors (default `black`) - `backgroundColor`: color for BG (default `white`) ================================================ FILE: docs/05-XY.md ================================================ --- title: XY chart --- XY chart is used to plot points by specifying their XY coordinates. You can also plot XY line chart by connecting the points.

See the Pen chart.xkcd XY by timqian (@timqian) on CodePen.

## JS part ```js const svg = document.querySelector('.xy-chart'); new chartXkcd.XY(svg, { title: 'Pokemon farms', //optional xLabel: 'Coordinate', //optional yLabel: 'Count', //optional data: { datasets: [{ label: 'Pikachu', data: [{ x: 3, y: 10 }, { x: 4, y: 122 }, { x: 10, y: 100 }, { x: 1, y: 2 }, { x: 2, y: 4 }], }, { label: 'Squirtle', data: [{ x: 3, y: 122 }, { x: 4, y: 212 }, { x: -3, y: 100 }, { x: 1, y: 1 }, { x: 1.5, y: 12 }], }], }, options: { //optional xTickCount: 5, yTickCount: 5, legendPosition: chartXkcd.config.positionType.upRight, showLine: false, timeFormat: undefined, dotSize: 1, }, }); ``` ## Customize XY chart by defining options - `xTickCount`: customize tick numbers you want to see on the x axis (default `3`) - `yTickCount`: customize tick numbers you want to see on the y axis (default `3`) - `showLegend`: display legend near chart (default `true`) - `legendPosition`: specify where you want to place the legend (default `chartXkcd.config.positionType.upLeft`) Possible values: - up left: `chartXkcd.config.positionType.upLeft` - up right: `chartXkcd.config.positionType.upLeft` - bottom left: `chartXkcd.config.positionType.downLeft` - bottom right: `chartXkcd.config.positionType.downRight` - `showLine`: connect the points with lines (default: `false`) - `timeFormat`: specify the time format if the x values are time (default `undefined`) chart.xkcd use [dayjs](https://github.com/iamkun/dayjs) to format time, you can find the all the available formats [here](https://github.com/iamkun/dayjs/blob/dev/docs/en/API-reference.md#list-of-all-available-formats) - `dotSize`: you can change size of the dots if you want (default `1`) - `dataColors`: array of colors for different datasets - `fontFamily`: customize font family used in the chart - `unxkcdify`: disable xkcd effect (default `false`) - `strokeColor`: stroke colors (default `black`) - `backgroundColor`: color for BG (default `white`) **Another example of XY chart: XY line chart with `timeFormat`**

See the Pen chart.xkcd XY line by timqian (@timqian) on CodePen.

================================================ FILE: docs/06-bar.md ================================================ --- title: Bar chart --- A bar chart provides a way of showing data values represented as vertical bars

See the Pen chart.xkcd bar by timqian (@timqian) on CodePen.

## JS part ```js const barChart = new chartXkcd.Bar(svg, { title: 'github stars VS patron number', // optional // xLabel: '', // optional // yLabel: '', // optional data: { labels: ['github stars', 'patrons'], datasets: [{ data: [100, 2], }], }, options: { // optional yTickCount: 2, }, }); ``` ## Customize chart by defining options - `yTickCount`: customize tick numbers you want to see on the y axis - `dataColors`: array of colors for different datasets - `fontFamily`: customize font family used in the chart - `unxkcdify`: disable xkcd effect (default `false`) - `strokeColor`: stroke colors (default `black`) - `backgroundColor`: color for BG (default `white`) ================================================ FILE: docs/07-stacked-bar.md ================================================ --- title: Stacked bar chart --- A stacked bar chart provides a way of showing data values represented as vertical bars

See the Pen chart.xkcd stacked bar by timqian (@timqian) on CodePen.

## JS part ```js new chartXkcd.StackedBar(svg, { title: 'Issues and PR Submissions', xLabel: 'Month', yLabel: 'Count', data: { labels: ['Jan', 'Feb', 'Mar', 'April', 'May'], datasets: [{ label: 'Issues', data: [12, 19, 11, 29, 17], }, { label: 'PRs', data: [3, 5, 2, 4, 1], }, { label: 'Merges', data: [2, 3, 0, 1, 1], }], }, }); ``` ## Customize chart by defining options - `yTickCount`: customize tick numbers you want to see on the y axis - `dataColors`: array of colors for different datasets - `fontFamily`: customize font family used in the chart - `unxkcdify`: disable xkcd effect (default `false`) - `strokeColor`: stroke colors (default `black`) - `backgroundColor`: color for BG (default `white`) - `showLegend`: display legend near chart (default `true`) - `legendPosition`: specify where you want to place the legend (default `chartXkcd.config.positionType.upLeft`) Possible values: - up left: `chartXkcd.config.positionType.upLeft` - up right: `chartXkcd.config.positionType.upLeft` - bottom left: `chartXkcd.config.positionType.downLeft` - bottom right: `chartXkcd.config.positionType.downRight` ================================================ FILE: docs/08-pie.md ================================================ --- title: Pie/Doughnut chart ---

See the Pen chart.xkcd pie by timqian (@timqian) on CodePen.

## JS part ```js const pieChart = new chartXkcd.Pie(svg, { title: 'What Tim is made of', // optional data: { labels: ['a', 'b', 'e', 'f', 'g'], datasets: [{ data: [500, 200, 80, 90, 100], }], }, options: { // optional innerRadius: 0.5, legendPosition: chartXkcd.config.positionType.upRight, }, }); ``` ## Customize chart by defining options - `innerRadius`: specify empty pie chart radius (default: `0.5`) - Want a pie chart? set `innerRadius` to `0` - `showLegend`: display legend near chart (default `true`) - `legendPosition`: specify where you want to place the legend. (default `chartXkcd.config.positionType.upLeft`) Possible values: - up left: `chartXkcd.config.positionType.upLeft` - up right: `chartXkcd.config.positionType.upLeft` - bottom left: `chartXkcd.config.positionType.downLeft` - bottom right: `chartXkcd.config.positionType.downRight` - `dataColors`: array of colors for different datasets - `fontFamily`: customize font family used in the chart - `unxkcdify`: disable xkcd effect (default `false`) - `strokeColor`: stroke colors (default `black`) - `backgroundColor`: color for BG (default `white`) ================================================ FILE: docs/09-radar.md ================================================ --- title: Radar chart ---

See the Pen chart.xkcd radar by timqian (@timqian) on CodePen.

## JS part ```js const radar = new chartXkcd.Radar(svg, { title: 'Letters in random words', // optional data: { labels: ['c', 'h', 'a', 'r', 't'], datasets: [{ label: 'ccharrrt', // optional data: [2, 1, 1, 3, 1], }, { label: 'chhaart', // optional data: [1, 2, 2, 1, 1], }], }, options: { // optional showLegend: true, dotSize: .8, showLabels: true, legendPosition: chartXkcd.config.positionType.upRight, }, }); ``` ## Customize chart by defining options - `showLabels`: display labels near every line (default `false`) - `ticksCount`: customize tick numbers you want to see on the main line (default `3`) - `dotSize`: you can change size of the dots if you want (default `1`) - `showLegend`: display legend near chart (default `false`) - `legendPosition`: specify where you want to place the legend. (default `chartXkcd.config.positionType.upLeft`) Possible values: - up left: `chartXkcd.config.positionType.upLeft` - up right: `chartXkcd.config.positionType.upRight` - bottom left: `chartXkcd.config.positionType.downLeft` - bottom right: `chartXkcd.config.positionType.downRight` - `dataColors`: array of colors for different datasets - `fontFamily`: customize font family used in the chart - `unxkcdify`: disable xkcd effect (default `false`) - `strokeColor`: stroke colors (default `black`) - `backgroundColor`: color for BG (default `white`) ================================================ FILE: examples/example.html ================================================

Source code of the examples


================================================ FILE: examples/index.js ================================================ // import chartXkcd from 'chart.xkcd'; // import chartXkcd from '../../dist/chart.xkcd'; import chartXkcd from '../src'; const svg = document.querySelector('.bar-chart'); new chartXkcd.Bar(svg, { title: 'Github stars VS patron number', xLabel: 'Month', yLabel: 'Count', data: { labels: ['github stars', 'patrons'], datasets: [{ data: [100, 2], }], }, // options: { // yTickCount: 2, // // unxkcdify: true, // // strokeColor: 'white', // // backgroundColor: 'black', // }, }); const svgStackedBar = document.querySelector('.stacked-bar-chart'); new chartXkcd.StackedBar(svgStackedBar, { title: 'Issues and PR Submissions', xLabel: 'Month', yLabel: 'Count', data: { labels: ['Jan', 'Feb', 'Mar', 'April', 'May'], datasets: [{ label: 'Issues', data: [12, 19, 11, 29, 17], }, { label: 'PRs', data: [3, 5, 2, 4, 1], }, { label: 'Merges', data: [2, 3, 0, 1, 1], }], }, // options: { // showLegend: true, // yTickCount: 2, // // unxkcdify: true, // // strokeColor: 'white', // // backgroundColor: 'black', // }, }); const svgPie = document.querySelector('.pie-chart'); new chartXkcd.Pie(svgPie, { title: 'What Tim is made of', data: { labels: ['a', 'b', 'e', 'f', 'g'], datasets: [{ data: [500, 200, 80, 90, 100], }], }, options: { innerRadius: 0.6, legendPosition: chartXkcd.config.positionType.upRight, // showLegend: true, // unxkcdify: true, // strokeColor: 'white', // backgroundColor: 'black', }, }); const svgLine = document.querySelector('.line-chart'); new chartXkcd.Line(svgLine, { title: 'Monthly income of an indie developer', xLabel: 'Month', yLabel: '$ Dollars', data: { labels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'], datasets: [{ label: 'Plan', data: [30, 70, 200, 300, 500, 800, 1500, 2900, 5000, 8000], }, { label: 'Reality', data: [0, 1, 30, 70, 80, 100, 50, 80, 40, 150], }], }, options: { // unxkcdify: true, // strokeColor: 'black', // backgroundColor: 'white', }, }); const svgXY = document.querySelector('.xyline-chart'); new chartXkcd.XY(svgXY, { title: 'stars', xLabel: 'wo', yLabel: 'Stars count', data: { datasets: [{ label: 'timqian', data: [{ x: 3, y: 10 }, { x: 4, y: 122 }, { x: 10, y: 180 }, { x: 1, y: 2 }, { x: 2, y: 4 }], }, { label: 'wewean', data: [{ x: 3, y: 122 }, { x: 4, y: 212 }, { x: -3, y: 100 }, { x: 1, y: 1 }, { x: 1.5, y: 12 }], }], }, options: { xTickCount: 5, yTickCount: 5, legendPosition: chartXkcd.config.positionType.downRight, showLine: false, // showLegend: true, // unxkcdify: true, // strokeColor: 'blue', // backgroundColor: 'black', }, }); const svgXY2 = document.querySelector('.xyline-chart2'); new chartXkcd.XY(svgXY2, { title: 'Github star history', xLabel: 'Month', yLabel: 'Stars abc', data: { datasets: [{ label: 'timqian/chart.xkcd', data: [{ x: '2015-03-01', y: 0 }, { x: '2015-04-01', y: 2 }, { x: '2015-05-01', y: 4 }, { x: '2015-06-01', y: 10 }, { x: '2015-07-01', y: 122 }], }, { label: 'timqian/star-history', data: [{ x: '2015-01-01', y: 0 }, { x: '2015-03-01', y: 1 }, { x: '2015-04-01', y: 12 }, { x: '2015-05-01', y: 122 }, { x: '2015-06-01', y: 212 }], }], }, options: { xTickCount: 3, yTickCount: 4, legendPosition: chartXkcd.config.positionType.upLeft, showLine: true, timeFormat: 'MM/DD/YYYY', dotSize: 0.5, // unxkcdify: true, // strokeColor: 'white', // backgroundColor: 'black', }, }); const svgRadar = document.querySelector('.radar-chart'); new chartXkcd.Radar(svgRadar, { title: 'Radar', data: { labels: ['c', 'h', 'a', 'r', 't'], datasets: [{ label: 'ccharrrt', data: [2, 1, 1, 3, 1], }, { label: 'chhaart', data: [1, 2, 2, 1, 1], }], }, options: { showLegend: true, dotSize: 0.8, showLabels: true, legendPosition: chartXkcd.config.positionType.upRight, // unxkcdify: true, // strokeColor: 'white', // backgroundColor: 'black', }, }); const svgLineCus = document.querySelector('.line-chart-cus'); new chartXkcd.Line(svgLineCus, { title: 'Customize Font & colors (定制外观)', xLabel: 'this is x label', yLabel: 'y label', data: { labels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'], datasets: [{ label: 'font', data: [30, 70, 200, 300, 500, 800, 100, 290, 500, 300], }, { label: 'color', data: [0, 1, 30, 70, 80, 100, 500, 80, 40, 250], }], }, options: { fontFamily: 'ZCOOL KuaiLe', dataColors: ['black', '#aaa'], legendPosition: chartXkcd.config.positionType.upRight, // strokeColor: 'white', // backgroundColor: 'black', }, }); const svgLineUnxkcdify = document.querySelector('.line-chart-unxkcdify'); new chartXkcd.Line(svgLineUnxkcdify, { title: 'Unxkcdify', xLabel: 'this is x label', yLabel: 'y label', data: { labels: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10'], datasets: [{ label: 'font', data: [30, 70, 200, 300, 500, 800, 100, 290, 500, 300], }, { label: 'color', data: [0, 1, 30, 70, 80, 100, 500, 80, 40, 250], }], }, options: { unxkcdify: true, // strokeColor: 'white', // backgroundColor: 'black', }, }); const svgDark = document.querySelector('.line-chart-dark'); new chartXkcd.XY(svgDark, { title: 'stars', xLabel: 'wo', yLabel: 'Stars count', data: { datasets: [{ label: 'timqian', data: [{ x: 3, y: 10 }, { x: 4, y: 122 }, { x: 10, y: 180 }, { x: 1, y: 2 }, { x: 2, y: 4 }], }, { label: 'wewean', data: [{ x: 3, y: 122 }, { x: 4, y: 212 }, { x: -3, y: 100 }, { x: 1, y: 1 }, { x: 1.5, y: 12 }], }], }, options: { xTickCount: 5, yTickCount: 5, legendPosition: chartXkcd.config.positionType.downRight, showLine: false, // unxkcdify: true, strokeColor: 'white', backgroundColor: 'black', }, }); ================================================ FILE: examples/readme.md ================================================ ## Preview https://timqian.com/chart.xkcd/example.html ## Try it yourself 1. clone this repo 1. `npm install` 2. `npm start` ================================================ FILE: package.json ================================================ { "name": "chart.xkcd", "version": "1.1.15", "description": "xkcd style chart lib", "jsdelivr": "dist/chart.xkcd.min.js", "unpkg": "dist/chart.xkcd.min.js", "main": "dist/index.js", "files": [ "dist/*" ], "scripts": { "buildUmd": "parcel build src/index.js --target browser --out-file chart.xkcd.min.js --no-source-maps --experimental-scope-hoisting --global chartXkcd", "build": "parcel build src/index.js --target node --bundle-node-modules --no-source-maps --experimental-scope-hoisting", "start": "parcel examples/example.html", "prepublishOnly": "rm -rf dist && npm run buildUmd && npm run build", "genDoc": "~/go/bin/static-docs --in docs --out docs-dist --title Chart.xkcd --subtitle 'xkcd styled chart lib'", "genExample": "parcel build examples/example.html --out-dir docs-dist --public-url /chart.xkcd", "deployDoc": "npm run genDoc && npm run genExample && gh-pages -d docs-dist", "lint": "./node_modules/.bin/eslint ./src" }, "repository": { "type": "git", "url": "https://github.com/timqian/chart.xkcd" }, "homepage": "https://timqian.com/chart.xkcd", "keywords": [ "chart", "graph", "xkcd", "hand-drawn" ], "author": "timqian", "license": "MIT", "devDependencies": { "eslint": "^6.2.1", "eslint-config-airbnb-base": "^14.0.0", "eslint-plugin-import": "^2.18.2", "gh-pages": "^2.1.1", "parcel-bundler": "^1.12.4" }, "dependencies": { "d3-axis": "^1.0.12", "d3-scale": "^3.2.0", "d3-selection": "^1.4.1", "d3-shape": "^1.3.7", "dayjs": "^1.8.17" } } ================================================ FILE: readme.md ================================================ [![](https://raw.githubusercontent.com/timqian/images/master/20190819131226.gif)](https://timqian.com/chart.xkcd/) > [Who is using chart.xkcd?](https://github.com/timqian/chart.xkcd/issues/14) ## About Chart.xkcd is a chart library that plots “sketchy”, “cartoony” or “hand-drawn” styled charts. Check out the [documentation](https://timqian.com/chart.xkcd/) for more instructions and links, or try out the [examples](https://timqian.com/chart.xkcd/example.html), or chat with us in [Slack](https://join.slack.com/t/t9tio/shared_invite/enQtNjgzMzkwMDM0NTE3LTE5ZTUzYjU4Y2I0YzRiZjNkYTkzMzE1ZmM0NDdmYzRlZmMxNGY1MzZlN2EwYjYyNWVlMWY0Nzk2MDBhNWZlY2I) ## Sponsors [琚致远](https://github.com/juzhiyuan) | [Bytebase](https://bytebase.com/) | [Madao](https://madao.me/) | [SecondState](https://bit.ly/3gfWwps) [Become a sponsor](https://github.com/sponsors/timqian) ## Quick start It’s easy to get started with chart.xkcd. All that’s required is the script included in your page along with a single `` node to render the chart. In the following example we create a line chart. > **[Preview and edit on codepen](https://codepen.io/timqian/pen/GRKqLaL)** ```html ``` ## Contributing - Code: read the [contributing.md](./contributing.md) file - Financial: - [Become a patron](https://www.patreon.com/timqian) - chart.xkcd is an MIT-licensed open source project with its ongoing development made possible entirely by the support of my patrons. If you like this tool, please consider supporting my work by becoming a patron. - [Fund issues on issuehunt](https://issuehunt.io/r/timqian/chart.xkcd?tab=idle) - Issues on chart.xkcd can be funded by anyone and the money will be distributed to contributors. ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=timqian/chart.xkcd&type=Date)](https://star-history.com/#timqian/chart.xkcd&Date) ================================================ FILE: src/Bar.js ================================================ import select from 'd3-selection/src/select'; import mouse from 'd3-selection/src/mouse'; import scaleBand from 'd3-scale/src/band'; import scaleLinear from 'd3-scale/src/linear'; import addAxis from './utils/addAxis'; import addLabels from './utils/addLabels'; import Tooltip from './components/Tooltip'; import addFont from './utils/addFont'; import addFilter from './utils/addFilter'; import colors from './utils/colors'; import config from './config'; const margin = { top: 50, right: 30, bottom: 50, left: 50, }; class Bar { constructor(svg, { title, xLabel, yLabel, data: { labels, datasets }, options, }) { this.options = { unxkcdify: false, yTickCount: 3, dataColors: colors, fontFamily: 'xkcd', strokeColor: 'black', backgroundColor: 'white', ...options, }; if (title) { this.title = title; margin.top = 60; } if (xLabel) { this.xLabel = xLabel; margin.bottom = 50; } if (yLabel) { this.yLabel = yLabel; margin.left = 70; } this.data = { labels, datasets, }; this.filter = 'url(#xkcdify)'; this.fontFamily = this.options.fontFamily || 'xkcd'; if (this.options.unxkcdify) { this.filter = null; this.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; } this.svgEl = select(svg) .style('stroke-width', '3') .style('font-family', this.fontFamily) .style('background', this.options.backgroundColor) .attr('width', svg.parentElement.clientWidth) .attr('height', Math.min((svg.parentElement.clientWidth * 2) / 3, window.innerHeight)); this.svgEl.selectAll('*').remove(); this.chart = this.svgEl.append('g') .attr('transform', `translate(${margin.left},${margin.top})`); this.width = this.svgEl.attr('width') - margin.left - margin.right; this.height = this.svgEl.attr('height') - margin.top - margin.bottom; addFont(this.svgEl); addFilter(this.svgEl); this.render(); } render() { if (this.title) addLabels.title(this.svgEl, this.title, this.options.strokeColor); if (this.xLabel) addLabels.xLabel(this.svgEl, this.xLabel, this.options.strokeColor); if (this.yLabel) addLabels.yLabel(this.svgEl, this.yLabel, this.options.strokeColor); const tooltip = new Tooltip({ parent: this.svgEl, title: 'tooltip', items: [{ color: 'red', text: 'weweyang: 12' }, { color: 'blue', text: 'timqian: 13' }], position: { x: 30, y: 30, type: config.positionType.upRight }, unxkcdify: this.options.unxkcdify, backgroundColor: this.options.backgroundColor, strokeColor: this.options.strokeColor, }); const xScale = scaleBand() .range([0, this.width]) .domain(this.data.labels) .padding(0.4); const allData = this.data.datasets .reduce((pre, cur) => pre.concat(cur.data), []); const yScale = scaleLinear() .domain([0, Math.max(...allData)]) .range([this.height, 0]); const graphPart = this.chart.append('g'); // axis addAxis.xAxis(graphPart, { xScale, tickCount: 3, moveDown: this.height, fontFamily: this.fontFamily, unxkcdify: this.options.unxkcdify, stroke: this.options.strokeColor, }); addAxis.yAxis(graphPart, { yScale, tickCount: this.options.yTickCount || 3, fontFamily: this.fontFamily, unxkcdify: this.options.unxkcdify, stroke: this.options.strokeColor, }); // Bars graphPart.selectAll('.xkcd-chart-bar') .data(this.data.datasets[0].data) .enter() .append('rect') .attr('class', 'xkcd-chart-bar') .attr('x', (d, i) => xScale(this.data.labels[i])) .attr('width', xScale.bandwidth()) .attr('y', (d) => yScale(d)) .attr('height', (d) => this.height - yScale(d)) .attr('fill', 'none') .attr('pointer-events', 'all') .attr('stroke', this.options.strokeColor) .attr('stroke-width', 3) .attr('rx', 2) // .attr('cursor','crosshair') .attr('filter', this.filter) .on('mouseover', (d, i, nodes) => { select(nodes[i]).attr('fill', this.options.dataColors[i]); // select(nodes[i]).attr('fill', 'url(#hatch00)'); tooltip.show(); }) .on('mouseout', (d, i, nodes) => { select(nodes[i]).attr('fill', 'none'); tooltip.hide(); }) .on('mousemove', (d, i, nodes) => { const tipX = mouse(nodes[i])[0] + margin.left + 10; const tipY = mouse(nodes[i])[1] + margin.top + 10; let tooltipPositionType = config.positionType.downRight; if (tipX > this.width / 2 && tipY < this.height / 2) { tooltipPositionType = config.positionType.downLeft; } else if (tipX > this.width / 2 && tipY > this.height / 2) { tooltipPositionType = config.positionType.upLeft; } else if (tipX < this.width / 2 && tipY > this.height / 2) { tooltipPositionType = config.positionType.upRight; } tooltip.update({ title: this.data.labels[i], items: [{ color: this.options.dataColors[i], text: `${this.data.datasets[0].label || ''}: ${d}`, }], position: { x: tipX, y: tipY, type: tooltipPositionType, }, }); }); } // TODO: update chart update() { } } export default Bar; ================================================ FILE: src/Line.js ================================================ import line from 'd3-shape/src/line'; import { monotoneX } from 'd3-shape/src/curve/monotone'; import select from 'd3-selection/src/select'; import mouse from 'd3-selection/src/mouse'; import { point as scalePoint } from 'd3-scale/src/band'; import scaleLinear from 'd3-scale/src/linear'; import addAxis from './utils/addAxis'; import addLabels from './utils/addLabels'; import Tooltip from './components/Tooltip'; import addLegend from './utils/addLegend'; import addFont from './utils/addFont'; import addFilter from './utils/addFilter'; import colors from './utils/colors'; import config from './config'; const margin = { top: 50, right: 30, bottom: 50, left: 50, }; class Line { constructor(svg, { title, xLabel, yLabel, data: { labels, datasets }, options, }) { this.options = { unxkcdify: false, yTickCount: 3, legendPosition: config.positionType.upLeft, dataColors: colors, fontFamily: 'xkcd', strokeColor: 'black', backgroundColor: 'white', showLegend: true, ...options, }; if (title) { this.title = title; margin.top = 60; } if (xLabel) { this.xLabel = xLabel; margin.bottom = 50; } if (yLabel) { this.yLabel = yLabel; margin.left = 70; } this.data = { labels, datasets, }; this.filter = 'url(#xkcdify)'; this.fontFamily = this.options.fontFamily || 'xkcd'; if (this.options.unxkcdify) { this.filter = null; this.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; } this.svgEl = select(svg) .style('stroke-width', '3') .style('font-family', this.fontFamily) .style('background', this.options.backgroundColor) .attr('width', svg.parentElement.clientWidth) .attr('height', Math.min((svg.parentElement.clientWidth * 2) / 3, window.innerHeight)); this.svgEl.selectAll('*').remove(); this.chart = this.svgEl.append('g') .attr('transform', `translate(${margin.left},${margin.top})`); this.width = this.svgEl.attr('width') - margin.left - margin.right; this.height = this.svgEl.attr('height') - margin.top - margin.bottom; addFont(this.svgEl); addFilter(this.svgEl); this.render(); } render() { if (this.title) addLabels.title(this.svgEl, this.title, this.options.strokeColor); if (this.xLabel) addLabels.xLabel(this.svgEl, this.xLabel, this.options.strokeColor); if (this.yLabel) addLabels.yLabel(this.svgEl, this.yLabel, this.options.strokeColor); const tooltip = new Tooltip({ parent: this.svgEl, title: '', items: [{ color: 'red', text: 'weweyang' }, { color: 'blue', text: 'timqian' }], position: { x: 60, y: 60, type: config.positionType.downRight }, unxkcdify: this.options.unxkcdify, backgroundColor: this.options.backgroundColor, strokeColor: this.options.strokeColor, }); const xScale = scalePoint() .domain(this.data.labels) .range([0, this.width]); const allData = this.data.datasets .reduce((pre, cur) => pre.concat(cur.data), []); const yScale = scaleLinear() .domain([Math.min(...allData), Math.max(...allData)]) .range([this.height, 0]); const graphPart = this.chart.append('g') .attr('pointer-events', 'all'); // axis addAxis.xAxis(graphPart, { xScale, tickCount: 3, moveDown: this.height, fontFamily: this.fontFamily, unxkcdify: this.options.unxkcdify, stroke: this.options.strokeColor, }); addAxis.yAxis(graphPart, { yScale, tickCount: this.options.yTickCount || 3, fontFamily: this.fontFamily, unxkcdify: this.options.unxkcdify, stroke: this.options.strokeColor, }); this.svgEl.selectAll('.domain') .attr('filter', this.filter); const theLine = line() .x((d, i) => xScale(this.data.labels[i])) .y((d) => yScale(d)) .curve(monotoneX); graphPart.selectAll('.xkcd-chart-line') .data(this.data.datasets) .enter() .append('path') .attr('class', 'xkcd-chart-line') .attr('d', (d) => theLine(d.data)) .attr('fill', 'none') .attr('stroke', (d, i) => this.options.dataColors[i]) .attr('filter', this.filter); // hover effect const verticalLine = graphPart.append('line') .attr('x1', 30) .attr('y1', 0) .attr('x2', 30) .attr('y2', this.height) .attr('stroke', '#aaa') .attr('stroke-width', 1.5) .attr('stroke-dasharray', '7,7') .style('visibility', 'hidden'); const circles = this.data.datasets.map((dataset, i) => graphPart .append('circle') .style('stroke', this.options.dataColors[i]) .style('fill', this.options.dataColors[i]) .attr('r', 3.5) .style('visibility', 'hidden')); graphPart.append('rect') .attr('width', this.width) .attr('height', this.height) .attr('fill', 'none') // .attr('stroke', 'black') .on('mouseover', () => { circles.forEach((circle) => circle.style('visibility', 'visible')); verticalLine.style('visibility', 'visible'); tooltip.show(); }) .on('mouseout', () => { circles.forEach((circle) => circle.style('visibility', 'hidden')); verticalLine.style('visibility', 'hidden'); tooltip.hide(); }) .on('mousemove', (d, i, nodes) => { const tipX = mouse(nodes[i])[0] + margin.left + 10; const tipY = mouse(nodes[i])[1] + margin.top + 10; const labelXs = this.data.labels.map((label) => xScale(label) + margin.left); const mouseLableDistances = labelXs.map( (labelX) => Math.abs(labelX - mouse(nodes[i])[0] - margin.left), ); const mostNearLabelIndex = mouseLableDistances.indexOf(Math.min(...mouseLableDistances)); verticalLine .attr('x1', xScale(this.data.labels[mostNearLabelIndex])) .attr('x2', xScale(this.data.labels[mostNearLabelIndex])); this.data.datasets.forEach((dataset, j) => { circles[j] .style('visibility', 'visible') .attr('cx', xScale(this.data.labels[mostNearLabelIndex])) .attr('cy', yScale(dataset.data[mostNearLabelIndex])); }); const tooltipItems = this.data.datasets.map((dataset, j) => ({ color: this.options.dataColors[j], text: `${this.data.datasets[j].label || ''}: ${this.data.datasets[j].data[mostNearLabelIndex]}`, })); let tooltipPositionType = config.positionType.downRight; if (tipX > this.width / 2 && tipY < this.height / 2) { tooltipPositionType = config.positionType.downLeft; } else if (tipX > this.width / 2 && tipY > this.height / 2) { tooltipPositionType = config.positionType.upLeft; } else if (tipX < this.width / 2 && tipY > this.height / 2) { tooltipPositionType = config.positionType.upRight; } tooltip.update({ title: this.data.labels[mostNearLabelIndex], items: tooltipItems, position: { x: tipX, y: tipY, type: tooltipPositionType, }, }); }); // Legend if (this.options.showLegend) { const legendItems = this.data.datasets .map((dataset, i) => ({ color: this.options.dataColors[i], text: dataset.label, })); addLegend(graphPart, { items: legendItems, position: this.options.legendPosition, unxkcdify: this.options.unxkcdify, parentWidth: this.width, parentHeight: this.height, backgroundColor: this.options.backgroundColor, strokeColor: this.options.strokeColor, }); } } // TODO: update chart update() { } } export default Line; ================================================ FILE: src/Pie.js ================================================ import select from 'd3-selection/src/select'; import mouse from 'd3-selection/src/mouse'; import pie from 'd3-shape/src/pie'; import arc from 'd3-shape/src/arc'; import Tooltip from './components/Tooltip'; import addLegend from './utils/addLegend'; import addLabels from './utils/addLabels'; import addFont from './utils/addFont'; import addFilter from './utils/addFilter'; import colors from './utils/colors'; import config from './config'; const margin = 50; class Pie { constructor(svg, { title, data: { labels, datasets }, options, }) { this.options = { unxkcdify: false, innerRadius: 0.5, legendPosition: config.positionType.upLeft, dataColors: colors, fontFamily: 'xkcd', strokeColor: 'black', backgroundColor: 'white', showLegend: true, ...options, }; this.title = title; this.data = { labels, datasets, }; this.filter = 'url(#xkcdify-pie)'; this.fontFamily = this.options.fontFamily || 'xkcd'; if (this.options.unxkcdify) { this.filter = null; this.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; } this.svgEl = select(svg) .style('stroke-width', '3') .style('font-family', this.fontFamily) .style('background', this.options.backgroundColor) .attr('width', svg.parentElement.clientWidth) .attr('height', Math.min((svg.parentElement.clientWidth * 2) / 3, window.innerHeight)); this.svgEl.selectAll('*').remove(); this.width = this.svgEl.attr('width'); this.height = this.svgEl.attr('height'); this.chart = this.svgEl.append('g') .attr('transform', `translate(${this.width / 2},${this.height / 2})`); addFont(this.svgEl); addFilter(this.svgEl); this.render(); } render() { if (this.title) { addLabels.title(this.svgEl, this.title, this.options.strokeColor); } const tooltip = new Tooltip({ parent: this.svgEl, title: 'tooltip', items: [{ color: 'red', text: 'weweyang: 12' }, { color: 'blue', text: 'timqian: 13' }], position: { x: 30, y: 30, type: config.positionType.upRight }, unxkcdify: this.options.unxkcdify, strokeColor: this.options.strokeColor, backgroundColor: this.options.backgroundColor, }); const radius = Math.min(this.width, this.height) / 2 - margin; const thePie = pie(); const dataReady = thePie(this.data.datasets[0].data); const theArc = arc() .innerRadius(radius * (this.options.innerRadius === undefined ? 0.5 : this.options.innerRadius)) .outerRadius(radius); this.chart.selectAll('.xkcd-chart-arc') .data(dataReady) .enter() .append('path') .attr('class', '.xkcd-chart-arc') .attr('d', theArc) .attr('fill', 'none') .attr('stroke', this.options.strokeColor) .attr('stroke-width', 2) .attr('fill', (d, i) => this.options.dataColors[i]) .attr('filter', this.filter) // .attr("fill-opacity", 0.6) .on('mouseover', (d, i, nodes) => { select(nodes[i]).attr('fill-opacity', 0.6); tooltip.show(); }) .on('mouseout', (d, i, nodes) => { select(nodes[i]).attr('fill-opacity', 1); tooltip.hide(); }) .on('mousemove', (d, i, nodes) => { const tipX = mouse(nodes[i])[0] + (this.width / 2) + 10; const tipY = mouse(nodes[i])[1] + (this.height / 2) + 10; tooltip.update({ title: this.data.labels[i], items: [{ color: this.options.dataColors[i], text: `${this.data.datasets[0].label || ''}: ${d.data}`, }], position: { x: tipX, y: tipY, type: config.positionType.downRight, }, }); }); // Legend const legendItems = this.data.datasets[0].data .map((data, i) => ({ color: this.options.dataColors[i], text: this.data.labels[i] })); // move legend down to prevent overlaping with title const legendG = this.svgEl.append('g') .attr('transform', 'translate(0, 30)'); if (this.options.showLegend) { addLegend(legendG, { items: legendItems, position: this.options.legendPosition, unxkcdify: this.options.unxkcdify, parentWidth: this.width, parentHeight: this.height, strokeColor: this.options.strokeColor, backgroundColor: this.options.backgroundColor, }); } } // TODO: update chart update() { } } export default Pie; ================================================ FILE: src/Radar.js ================================================ import select from 'd3-selection/src/select'; import line from 'd3-shape/src/line'; import curveLinearClosed from 'd3-shape/src/curve/linearClosed'; import scaleLinear from 'd3-scale/src/linear'; import addLegend from './utils/addLegend'; import addLabels from './utils/addLabels'; import Tooltip from './components/Tooltip'; import addFont from './utils/addFont'; import addFilter from './utils/addFilter'; import colors from './utils/colors'; import config from './config'; const margin = 50; const angleOffset = -Math.PI / 2; const areaOpacity = 0.2; class Radar { constructor(svg, { title, data: { labels, datasets }, options, }) { this.options = { showLabels: false, ticksCount: 3, showLegend: false, legendPosition: config.positionType.upLeft, dataColors: colors, fontFamily: 'xkcd', dotSize: 1, strokeColor: 'black', backgroundColor: 'white', ...options, }; this.title = title; this.data = { labels, datasets, }; // TODO: find the longest dataset or throw an error for inconsistent datasets this.directionsCount = datasets[0].data.length; this.filter = 'url(#xkcdify-pie)'; this.fontFamily = this.options.fontFamily || 'xkcd'; if (this.options.unxkcdify) { this.filter = null; this.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; } this.svgEl = select(svg) .style('stroke-width', '3') .style('font-family', this.fontFamily) .style('background', this.options.backgroundColor) .attr('width', svg.parentElement.clientWidth) .attr('height', Math.min((svg.parentElement.clientWidth * 2) / 3, window.innerHeight)); this.svgEl.selectAll('*').remove(); this.width = this.svgEl.attr('width'); this.height = this.svgEl.attr('height'); this.chart = this.svgEl.append('g') .attr('transform', `translate(${this.width / 2},${this.height / 2})`); addFont(this.svgEl); addFilter(this.svgEl); this.render(); } render() { if (this.title) { addLabels.title(this.svgEl, this.title, this.options.strokeColor); } const tooltip = new Tooltip({ parent: this.svgEl, title: '', items: [], position: { x: 0, y: 0, type: config.positionType.downRight }, unxkcdify: this.options.unxkcdify, strokeColor: this.options.strokeColor, backgroundColor: this.options.backgroundColor, }); const dotInitSize = 3.5 * (this.options.dotSize || 1); const dotHoverSize = 6 * (this.options.dotSize || 1); const radius = Math.min(this.width, this.height) / 2 - margin; const angleStep = (Math.PI * 2) / this.directionsCount; const allDataValues = this.data.datasets .reduce((acc, cur) => acc.concat(cur.data), []); const maxValue = Math.max(...allDataValues); const allMaxData = Array(this.directionsCount).fill(maxValue); const valueScale = scaleLinear() .domain([0, maxValue]) .range([0, radius]); const getX = (d, i) => valueScale(d) * Math.cos(angleStep * i + angleOffset); const getY = (d, i) => valueScale(d) * Math.sin(angleStep * i + angleOffset); const theLine = line() .x(getX) .y(getY) .curve(curveLinearClosed); // grid const ticks = valueScale.ticks(this.options.ticksCount || 3); const grid = this.chart.append('g') .attr('class', 'xkcd-chart-radar-grid') .attr('stroke-width', '1') .attr('filter', this.filter); grid.selectAll('.xkcd-chart-radar-level') .data(ticks) .enter() .append('path') .attr('class', 'xkcd-chart-radar-level') .attr('d', (d) => theLine(Array(this.directionsCount).fill(d))) .style('fill', 'none') .attr('stroke', this.options.strokeColor) .attr('stroke-dasharray', '7,7'); grid.selectAll('.xkcd-chart-radar-line') .data(allMaxData) .enter() .append('line') .attr('class', '.xkcd-chart-radar-line') .attr('stroke', this.options.strokeColor) .attr('x1', 0) .attr('y1', 0) .attr('x2', getX) .attr('y2', getY); grid.selectAll('.xkcd-chart-radar-tick') .data(ticks) .enter() .append('text') .attr('class', 'xkcd-chart-radar-tick') .attr('x', (d) => getX(d, 0)) .attr('y', (d) => getY(d, 0)) .style('font-size', '16') .style('fill', this.options.strokeColor) .attr('text-anchor', 'end') .attr('dx', '-.125em') .attr('dy', '.35em') .text((d) => d); if (this.options.showLabels) { grid.selectAll('.xkcd-chart-radar-label') .data(allMaxData.map((d) => d * 1.15)) .enter() .append('text') .attr('class', 'xkcd-chart-radar-label') .style('font-size', '16') .style('fill', this.options.strokeColor) .attr('x', (d, i) => (radius + 10) * Math.cos(angleStep * i + angleOffset)) .attr('y', (d, i) => (radius + 10) * Math.sin(angleStep * i + angleOffset)) .attr('dy', '.35em') .attr('text-anchor', (d, i, nodes) => { const node = select(nodes[i]); let anchor = 'start'; if (node.attr('x') < 0) { anchor = 'end'; } return anchor; }) .text((d, i) => this.data.labels[i]); } // layers const layers = this.chart.selectAll('.xkcd-chart-radar-group') .data(this.data.datasets) .enter() .append('g') .attr('class', 'xkcd-chart-radar-group') .attr('filter', this.filter) .attr('stroke', (d, i) => this.options.dataColors[i]) .attr('fill', (d, i) => this.options.dataColors[i]); layers.selectAll('circle') .data((dataset) => dataset.data) .enter() .append('circle') .attr('r', dotInitSize) .attr('cx', getX) .attr('cy', getY) .attr('pointer-events', 'all') .on('mouseover', (d, i, nodes) => { select(nodes[i]).attr('r', dotHoverSize); const tipX = getX(d, i) + this.width / 2; const tipY = getY(d, i) + this.height / 2; let tooltipPositionType = config.positionType.downRight; if (tipX > this.width / 2 && tipY < this.height / 2) { tooltipPositionType = config.positionType.downLeft; } else if (tipX > this.width / 2 && tipY > this.height / 2) { tooltipPositionType = config.positionType.upLeft; } else if (tipX < this.width / 2 && tipY > this.height / 2) { tooltipPositionType = config.positionType.upRight; } tooltip.update({ title: this.data.labels[i], items: this.data.datasets.map((dataset, datasetIndex) => ({ color: this.options.dataColors[datasetIndex], text: `${dataset.label || ''}: ${dataset.data[i]}`, })), position: { x: tipX, y: tipY, type: tooltipPositionType, }, }); tooltip.show(); }) .on('mouseout', (d, i, nodes) => { select(nodes[i]).attr('r', dotInitSize); tooltip.hide(); }); layers.selectAll('path') .data((dataset) => ([dataset.data])) .enter() .append('path') .attr('d', theLine) .attr('pointer-events', 'none') .style('fill-opacity', areaOpacity); // legend if (this.options.showLegend) { const legendItems = this.data.datasets .map((data, i) => ({ color: this.options.dataColors[i], text: data.label || '' })); // move legend down to prevent overlaping with title const legendG = this.svgEl.append('g') .attr('transform', 'translate(0, 30)'); addLegend(legendG, { items: legendItems, position: this.options.legendPosition, unxkcdify: this.options.unxkcdify, parentWidth: this.width, parentHeight: this.height, backgroundColor: this.options.backgroundColor, strokeColor: this.options.strokeColor, }); } } update() { } } export default Radar; ================================================ FILE: src/StackedBar.js ================================================ import select from 'd3-selection/src/select'; import mouse from 'd3-selection/src/mouse'; import scaleBand from 'd3-scale/src/band'; import scaleLinear from 'd3-scale/src/linear'; import addAxis from './utils/addAxis'; import addLabels from './utils/addLabels'; import Tooltip from './components/Tooltip'; import addLegend from './utils/addLegend'; import addFont from './utils/addFont'; import addFilter from './utils/addFilter'; import colors from './utils/colors'; import config from './config'; const margin = { top: 50, right: 30, bottom: 50, left: 50, }; class StackedBar { constructor(svg, { title, xLabel, yLabel, data: { labels, datasets }, options, }) { this.options = { unxkcdify: false, yTickCount: 3, dataColors: colors, fontFamily: 'xkcd', strokeColor: 'black', backgroundColor: 'white', legendPosition: config.positionType.upLeft, showLegend: true, ...options, }; if (title) { this.title = title; margin.top = 60; } if (xLabel) { this.xLabel = xLabel; margin.bottom = 50; } if (yLabel) { this.yLabel = yLabel; margin.left = 70; } this.data = { labels, datasets, }; this.filter = 'url(#xkcdify)'; this.fontFamily = this.options.fontFamily || 'xkcd'; if (this.options.unxkcdify) { this.filter = null; this.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; } this.svgEl = select(svg) .style('stroke-width', '3') .style('font-family', this.fontFamily) .style('background', this.options.backgroundColor) .attr('width', svg.parentElement.clientWidth) .attr('height', Math.min((svg.parentElement.clientWidth * 2) / 3, window.innerHeight)); this.svgEl.selectAll('*').remove(); this.chart = this.svgEl.append('g') .attr('transform', `translate(${margin.left},${margin.top})`); this.width = this.svgEl.attr('width') - margin.left - margin.right; this.height = this.svgEl.attr('height') - margin.top - margin.bottom; addFont(this.svgEl); addFilter(this.svgEl); this.render(); } render() { if (this.title) addLabels.title(this.svgEl, this.title, this.options.strokeColor); if (this.xLabel) addLabels.xLabel(this.svgEl, this.xLabel, this.options.strokeColor); if (this.yLabel) addLabels.yLabel(this.svgEl, this.yLabel, this.options.strokeColor); const tooltip = new Tooltip({ parent: this.svgEl, title: 'tooltip', items: [{ color: 'red', text: 'weweyang: 12' }, { color: 'blue', text: 'timqian: 13' }], position: { x: 30, y: 30, type: config.positionType.upRight }, unxkcdify: this.options.unxkcdify, backgroundColor: this.options.backgroundColor, strokeColor: this.options.strokeColor, }); const xScale = scaleBand() .range([0, this.width]) .domain(this.data.labels) .padding(0.4); const allCols = this.data.datasets .reduce((r, a) => a.data.map((b, i) => (r[i] || 0) + b), []); const yScale = scaleLinear() .domain([0, Math.max(...allCols)]) .range([this.height, 0]); const graphPart = this.chart.append('g'); // axis addAxis.xAxis(graphPart, { xScale, tickCount: 3, moveDown: this.height, fontFamily: this.fontFamily, unxkcdify: this.options.unxkcdify, stroke: this.options.strokeColor, }); addAxis.yAxis(graphPart, { yScale, tickCount: this.options.yTickCount || 3, fontFamily: this.fontFamily, unxkcdify: this.options.unxkcdify, stroke: this.options.strokeColor, }); // Merge all the lists into a single list, and store the // offests in a corresponding list. Use dataLength to // track how many total columns there should be. const mergedData = this.data.datasets .reduce((pre, cur) => pre.concat(cur.data), []); const dataLength = this.data.datasets[0].data.length; const offsets = this.data.datasets .reduce((r, x, i) => { if (i > 0) { r.push(x.data.map((y, j) => this.data.datasets[i - 1].data[j] + r[i - 1][j])); } else { r.push(new Array(x.data.length).fill(0)); } return r; }, []).flat(); // Bars graphPart.selectAll('.xkcd-chart-stacked-bar') .data(mergedData) .enter() .append('rect') .attr('class', 'xkcd-chart-stacked-bar') .attr('x', (d, i) => xScale(this.data.labels[i % dataLength])) .attr('width', xScale.bandwidth()) .attr('y', (d, i) => yScale(d + offsets[i])) .attr('height', (d) => this.height - yScale(d)) .attr('fill', (d, i) => this.options.dataColors[Math.floor(i / dataLength)]) .attr('pointer-events', 'all') .attr('stroke', this.options.strokeColor) .attr('stroke-width', 3) .attr('rx', 2) .attr('filter', this.filter) .on('mouseover', () => tooltip.show()) .on('mouseout', () => tooltip.hide()) .on('mousemove', (d, i, nodes) => { const tipX = mouse(nodes[i])[0] + margin.left + 10; const tipY = mouse(nodes[i])[1] + margin.top + 10; const tooltipItems = this.data.datasets.map((dataset, j) => ({ color: this.options.dataColors[j], text: `${this.data.datasets[j].label || ''}: ${this.data.datasets[j].data[i % dataLength]}`, })).reverse(); let tooltipPositionType = config.positionType.downRight; if (tipX > this.width / 2 && tipY < this.height / 2) { tooltipPositionType = config.positionType.downLeft; } else if (tipX > this.width / 2 && tipY > this.height / 2) { tooltipPositionType = config.positionType.upLeft; } else if (tipX < this.width / 2 && tipY > this.height / 2) { tooltipPositionType = config.positionType.upRight; } tooltip.update({ title: this.data.labels[i], items: tooltipItems, position: { x: tipX, y: tipY, type: tooltipPositionType, }, }); }); if (this.options.showLegend) { const legendItems = this.data.datasets.map((dataset, j) => ({ color: this.options.dataColors[j], text: `${this.data.datasets[j].label || ''}`, })).reverse(); addLegend(graphPart, { items: legendItems, position: this.options.legendPosition, unxkcdify: this.options.unxkcdify, parentWidth: this.width, parentHeight: this.height, strokeColor: this.options.strokeColor, backgroundColor: this.options.backgroundColor, }); } } // TODO: update chart update() { } } export default StackedBar; ================================================ FILE: src/XY.js ================================================ import line from 'd3-shape/src/line'; import { monotoneX } from 'd3-shape/src/curve/monotone'; import select from 'd3-selection/src/select'; import scaleLinear from 'd3-scale/src/linear'; import scaleTime from 'd3-scale/src/time'; import dayjs from 'dayjs'; import addAxis from './utils/addAxis'; import addLabels from './utils/addLabels'; import Tooltip from './components/Tooltip'; import addLegend from './utils/addLegend'; import addFont from './utils/addFont'; import addFilter from './utils/addFilter'; import colors from './utils/colors'; import config from './config'; const margin = { top: 50, right: 30, bottom: 50, left: 50, }; class XY { constructor(svg, { title, xLabel, yLabel, data: { datasets }, options, }) { this.options = { unxkcdify: false, dotSize: 1, showLine: false, timeFormat: '', xTickCount: 3, yTickCount: 3, legendPosition: config.positionType.upLeft, dataColors: colors, fontFamily: 'xkcd', strokeColor: 'black', backgroundColor: 'white', showLegend: true, ...options, }; // TODO: extract a function? if (title) { this.title = title; margin.top = 60; } if (xLabel) { this.xLabel = xLabel; margin.bottom = 50; } if (yLabel) { this.yLabel = yLabel; margin.left = 70; } this.data = { datasets, }; this.filter = 'url(#xkcdify)'; this.fontFamily = this.options.fontFamily || 'xkcd'; if (this.options.unxkcdify) { this.filter = null; this.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; } this.svgEl = select(svg) .style('stroke-width', 3) .style('font-family', this.fontFamily) .style('background', this.options.backgroundColor) .attr('width', svg.parentElement.clientWidth) .attr('height', Math.min((svg.parentElement.clientWidth * 2) / 3, window.innerHeight)); this.svgEl.selectAll('*').remove(); this.chart = this.svgEl.append('g') .attr('transform', `translate(${margin.left},${margin.top})`); this.width = this.svgEl.attr('width') - margin.left - margin.right; this.height = this.svgEl.attr('height') - margin.top - margin.bottom; addFont(this.svgEl); addFilter(this.svgEl); this.render(); } render() { if (this.title) addLabels.title(this.svgEl, this.title, this.options.strokeColor); if (this.xLabel) addLabels.xLabel(this.svgEl, this.xLabel, this.options.strokeColor); if (this.yLabel) addLabels.yLabel(this.svgEl, this.yLabel, this.options.strokeColor); const tooltip = new Tooltip({ parent: this.svgEl, title: '', items: [{ color: 'red', text: 'weweyang' }, { color: 'blue', text: 'timqian' }], position: { x: 60, y: 60, type: config.positionType.dowfnRight }, unxkcdify: this.options.unxkcdify, strokeColor: this.options.strokeColor, backgroundColor: this.options.backgroundColor, }); if (this.options.timeFormat) { this.data.datasets.forEach((dataset) => { dataset.data.forEach((d) => { // eslint-disable-next-line no-param-reassign d.x = dayjs(d.x); }); }); } const allData = this.data.datasets .reduce((pre, cur) => pre.concat(cur.data), []); const allDataX = allData.map((d) => d.x); const allDataY = allData.map((d) => d.y); let xScale = scaleLinear() .domain([Math.min(...allDataX), Math.max(...allDataX)]) .range([0, this.width]); if (this.options.timeFormat) { xScale = scaleTime() .domain([Math.min(...allDataX), Math.max(...allDataX)]) .range([0, this.width]); } const yScale = scaleLinear() .domain([Math.min(...allDataY), Math.max(...allDataY)]) .range([this.height, 0]); const graphPart = this.chart.append('g') .attr('pointer-events', 'all'); // axis addAxis.xAxis(graphPart, { xScale, tickCount: this.options.xTickCount === undefined ? 3 : this.options.xTickCount, moveDown: this.height, fontFamily: this.fontFamily, unxkcdify: this.options.unxkcdify, stroke: this.options.strokeColor, }); addAxis.yAxis(graphPart, { yScale, tickCount: this.options.yTickCount === undefined ? 3 : this.options.yTickCount, fontFamily: this.fontFamily, unxkcdify: this.options.unxkcdify, stroke: this.options.strokeColor, }); // lines if (this.options.showLine) { const theLine = line() .x((d) => xScale(d.x)) .y((d) => yScale(d.y)) .curve(monotoneX); graphPart.selectAll('.xkcd-chart-xyline') .data(this.data.datasets) .enter() .append('path') .attr('class', 'xkcd-chart-xyline') .attr('d', (d) => theLine(d.data)) .attr('fill', 'none') .attr('stroke', (d, i) => (this.options.dataColors[i])) .attr('filter', this.filter); } // dots const dotInitSize = 3.5 * (this.options.dotSize === undefined ? 1 : this.options.dotSize); const dotHoverSize = 6 * (this.options.dotSize === undefined ? 1 : this.options.dotSize); graphPart.selectAll('.xkcd-chart-xycircle-group') .data(this.data.datasets) .enter() .append('g') .attr('class', '.xkcd-chart-xycircle-group') .attr('filter', this.filter) .attr('xy-group-index', (d, i) => i) .selectAll('.xkcd-chart-xycircle-circle') .data((dataset) => dataset.data) .enter() .append('circle') .style('stroke', (d, i, nodes) => { // FIXME: here I want to pass xyGroupIndex down to the circles by reading parent attrs // It might have perfomance issue with a large dataset, not sure there are better ways const xyGroupIndex = Number(select(nodes[i].parentElement).attr('xy-group-index')); return this.options.dataColors[xyGroupIndex]; }) .style('fill', (d, i, nodes) => { const xyGroupIndex = Number(select(nodes[i].parentElement).attr('xy-group-index')); return this.options.dataColors[xyGroupIndex]; }) .attr('r', dotInitSize) .attr('cx', (d) => xScale(d.x)) .attr('cy', (d) => yScale(d.y)) .attr('pointer-events', 'all') .on('mouseover', (d, i, nodes) => { const xyGroupIndex = Number(select(nodes[i].parentElement).attr('xy-group-index')); select(nodes[i]) .attr('r', dotHoverSize); const tipX = xScale(d.x) + margin.left + 5; const tipY = yScale(d.y) + margin.top + 5; let tooltipPositionType = config.positionType.downRight; if (tipX > this.width / 2 && tipY < this.height / 2) { tooltipPositionType = config.positionType.downLeft; } else if (tipX > this.width / 2 && tipY > this.height / 2) { tooltipPositionType = config.positionType.upLeft; } else if (tipX < this.width / 2 && tipY > this.height / 2) { tooltipPositionType = config.positionType.upRight; } tooltip.update({ title: this.options.timeFormat ? dayjs(this.data.datasets[xyGroupIndex].data[i].x).format(this.options.timeFormat) : `${this.data.datasets[xyGroupIndex].data[i].x}`, items: [{ color: this.options.dataColors[xyGroupIndex], text: `${this.data.datasets[xyGroupIndex].label || ''}: ${d.y}`, }], position: { x: tipX, y: tipY, type: tooltipPositionType, }, }); tooltip.show(); }) .on('mouseout', (d, i, nodes) => { select(nodes[i]) .attr('r', dotInitSize); tooltip.hide(); }); // Legend if (this.options.showLegend) { const legendItems = this.data.datasets.map( (dataset, i) => ({ color: this.options.dataColors[i], text: dataset.label, }), ); addLegend(graphPart, { items: legendItems, position: this.options.legendPosition, unxkcdify: this.options.unxkcdify, parentWidth: this.width, parentHeight: this.height, strokeColor: this.options.strokeColor, backgroundColor: this.options.backgroundColor, }); } } // TODO: update chart update() { } } export default XY; ================================================ FILE: src/components/Tooltip.js ================================================ /* eslint-disable no-underscore-dangle */ import config from '../config'; class Tooltip { /** * * @param {String} parent * @param {String} title * @param {Array} items * @param {Object} position * @example * { * parent: {}, // a d3 selection component * title: 'tooltip title', * items:[{ * color: 'red', * text: 'tim: 13' * }], * position: { * type: 'upleft' * x: 100, * y: 230, * } * } */ constructor({ parent, title, items, position, unxkcdify, backgroundColor, strokeColor, }) { this.title = title; this.items = items; this.position = position; this.filter = !unxkcdify ? 'url(#xkcdify)' : null; this.backgroundColor = backgroundColor; this.strokeColor = strokeColor; this.svg = parent.append('svg') .attr('x', this._getUpLeftX()) .attr('y', this._getUpLeftY()) .style('visibility', 'hidden'); this.tipBackground = this.svg.append('rect') .style('fill', this.backgroundColor) .attr('fill-opacity', 0.9) .attr('stroke', strokeColor) // FIXME: find a good way to calculate boder color form this.strokeColor .attr('stroke-width', 2) .attr('rx', 5) .attr('ry', 5) .attr('filter', this.filter) .attr('width', this._getBackgroundWidth()) .attr('height', this._getBackgroundHeight()) .attr('x', 5) .attr('y', 5); this.tipTitle = this.svg.append('text') .style('font-size', 15) .style('font-weight', 'bold') .style('fill', this.strokeColor) .attr('x', 15) .attr('y', 25) .text(title); this.tipItems = items.map((item, i) => { const g = this._generateTipItem(item, i); return g; }); } show() { this.svg.style('visibility', 'visible'); } hide() { this.svg.style('visibility', 'hidden'); } // update tooltip position / content update({ title, items, position }) { if (title && title !== this.title) { this.title = title; this.tipTitle.text(title); } if (items && JSON.stringify(items) !== JSON.stringify(this.items)) { this.items = items; this.tipItems.forEach((g) => g.svg.remove()); this.tipItems = this.items.map((item, i) => { const g = this._generateTipItem(item, i); return g; }); const maxWidth = Math.max( ...this.tipItems.map((item) => item.width), this.tipTitle.node().getBBox().width, ); this.tipBackground .attr('width', maxWidth + 15) .attr('height', this._getBackgroundHeight()); } if (position) { this.position = position; this.svg.attr('x', this._getUpLeftX()); this.svg.attr('y', this._getUpLeftY()); } } _generateTipItem(item, i) { const svg = this.svg.append('svg'); svg.append('rect') .style('fill', item.color) .attr('width', 8) .attr('height', 8) .attr('rx', 2) .attr('ry', 2) .attr('filter', this.filter) .attr('x', 15) .attr('y', 37 + 20 * i); svg.append('text') .style('font-size', '15') .style('fill', this.strokeColor) .attr('x', 15 + 12) .attr('y', 37 + 20 * i + 8) .text(item.text); const bbox = svg.node().getBBox(); const width = bbox.width + 15; const height = bbox.height + 10; return { svg, width, height, }; } _getBackgroundWidth() { const maxItemLength = this.items.reduce( (pre, cur) => (pre > cur.text.length ? pre : cur.text.length), 0, ); const maxLength = Math.max(maxItemLength, this.title.length); return maxLength * 7.4 + 25; } _getBackgroundHeight() { const rows = this.items.length + 1; return rows * 20 + 10; } _getUpLeftX() { if (this.position.type === config.positionType.upRight || this.position.type === config.positionType.downRight) { return this.position.x; } return this.position.x - this._getBackgroundWidth() - 20; } _getUpLeftY() { if (this.position.type === config.positionType.downLeft || this.position.type === config.positionType.downRight) { return this.position.y; } return this.position.y - this._getBackgroundHeight() - 20; } } export default Tooltip; ================================================ FILE: src/config.js ================================================ const config = { positionType: { upLeft: 1, upRight: 2, downLeft: 3, downRight: 4, }, }; export default config; ================================================ FILE: src/index.js ================================================ import Bar from './Bar'; import StackedBar from './StackedBar'; import Pie from './Pie'; import Line from './Line'; import XY from './XY'; import Radar from './Radar'; import config from './config'; module.exports = { config, Bar, StackedBar, Pie, Line, XY, Radar, }; ================================================ FILE: src/utils/addAxis.js ================================================ import { axisBottom, axisLeft } from 'd3-axis/src/axis'; const yAxis = (parent, { yScale, tickCount, fontFamily, unxkcdify, stroke, }) => { parent .append('g') .call( axisLeft(yScale) .tickSize(1) .tickPadding(10) .ticks(tickCount, 's'), ); parent.selectAll('.domain') .attr('filter', !unxkcdify ? 'url(#xkcdify)' : null) .style('stroke', stroke); parent.selectAll('.tick > text') .style('font-family', fontFamily) .style('font-size', '16') .style('fill', stroke); }; const xAxis = (parent, { xScale, tickCount, moveDown, fontFamily, unxkcdify, stroke, }) => { parent .append('g') .attr('transform', `translate(0,${moveDown})`) .call( axisBottom(xScale) .tickSize(0) .tickPadding(6) .ticks(tickCount), ); parent.selectAll('.domain') .attr('filter', !unxkcdify ? 'url(#xkcdify)' : null) .style('stroke', stroke); parent.selectAll('.tick > text') .style('font-family', fontFamily) .style('font-size', '16') .style('fill', stroke); }; export default { xAxis, yAxis, }; ================================================ FILE: src/utils/addFilter.js ================================================ export default function addFilter(parent) { parent.append('filter') .attr('id', 'xkcdify') .attr('filterUnits', 'userSpaceOnUse') .attr('x', -5) .attr('y', -5) .attr('width', '100%') .attr('height', '100%') .call((f) => f.append('feTurbulence') .attr('type', 'fractalNoise') .attr('baseFrequency', '0.05') .attr('result', 'noise')) .call((f) => f.append('feDisplacementMap') .attr('scale', '5') .attr('xChannelSelector', 'R') .attr('yChannelSelector', 'G') .attr('in', 'SourceGraphic') .attr('in2', 'noise')); parent.append('filter') .attr('id', 'xkcdify-pie') .call((f) => f.append('feTurbulence') .attr('type', 'fractalNoise') .attr('baseFrequency', '0.05') .attr('result', 'noise')) .call((f) => f.append('feDisplacementMap') .attr('scale', '5') .attr('xChannelSelector', 'R') .attr('yChannelSelector', 'G') .attr('in', 'SourceGraphic') .attr('in2', 'noise')); // TODO bar chart hatch effect // parent.append('pattern') // .attr('id', 'hatch00') // .attr('patternUnits', 'userSpaceOnUse') // .attr('x', 0) // .attr('y', 0) // .attr('width', 10) // .attr('height', 10) // .call((f) => f.append('path') // .attr('d', 'M3,0 l7,7 l0,-2 l-5,-5 l-2,0 M0,7 l3,3 l2,0 l-5,-5 l0,2') // .attr('fill', colors[1]) // .attr('stroke', 'none')); } ================================================ FILE: src/utils/addFont.js ================================================ export default function addFont(parent) { parent.append('defs') .append('style') .attr('type', 'text/css') .text(`@font-face { font-family: "xkcd"; src: url(data:application/font-woff;charset=utf-8;base64,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) format('woff'); }`); } ================================================ FILE: src/utils/addLabels.js ================================================ const title = (parent, text, fill) => { parent .append('text') .style('font-size', '20') .style('font-weight', 'bold') .style('fill', fill) .attr('x', '50%') .attr('y', 30) .attr('text-anchor', 'middle') .text(text); }; const xLabel = (parent, text, fill) => { parent .append('text') .style('font-size', 17) .style('fill', fill) .attr('x', '50%') .attr('y', parent.attr('height') - 10) .attr('text-anchor', 'middle') .text(text); }; const yLabel = (parent, text, fill) => { parent .append('text') .attr('text-anchor', 'end') .attr('dy', '.75em') .attr('transform', 'rotate(-90)') .style('font-size', 17) .style('fill', fill) .text(text) .attr('y', 6) .call((f) => { const textLength = f.node().getComputedTextLength(); f.attr('x', 0 - (parent.attr('height') / 2) + (textLength / 2)); }); }; export default { title, xLabel, yLabel, }; ================================================ FILE: src/utils/addLegend.js ================================================ import config from '../config'; export default async function addLegend(parent, { items, position, unxkcdify, parentWidth, parentHeight, strokeColor, backgroundColor, }) { const filter = !unxkcdify ? 'url(#xkcdify)' : null; const legend = parent.append('svg'); const backgroundLayer = legend.append('svg'); const textLayer = legend.append('svg'); items.forEach((item, i) => { textLayer.append('rect') .style('fill', item.color) .attr('width', 8) .attr('height', 8) .attr('filter', filter) .attr('rx', 2) .attr('ry', 2) .attr('x', 15) .attr('y', 17 + 20 * i); textLayer.append('text') .style('font-size', '15') .style('fill', strokeColor) .attr('x', 15 + 12) .attr('y', 17 + 20 * i + 8) .text(item.text); }); // wait for textLayer to render, a bit wired await new Promise(resolve => setTimeout(resolve, 10)) const bbox = textLayer.node().getBBox(); const backgroundWidth = bbox.width + 15; const backgroundHeight = bbox.height + 10; let legendX = 0; let legendY = 0; if ( position === config.positionType.downLeft || position === config.positionType.downRight ) { legendY = parentHeight - backgroundHeight - 13; } if ( position === config.positionType.upRight || position === config.positionType.downRight ) { legendX = parentWidth - backgroundWidth - 13; } // add background backgroundLayer.append('rect') .style('fill', backgroundColor) .attr('filter', filter) .attr('fill-opacity', 0.85) .attr('stroke', strokeColor) .attr('stroke-width', 2) .attr('width', backgroundWidth) .attr('height', backgroundHeight) .attr('rx', 5) .attr('ry', 5) .attr('x', 8) .attr('y', 5); // get legend legend .attr('x', legendX) .attr('y', legendY); } ================================================ FILE: src/utils/colors.js ================================================ // TODO: use: https://github.com/mrmrs/colors ? // use: https://gui.apex.sh/ export default ['#dd4528', '#28a3dd', '#f3db52', '#ed84b5', '#4ab74e', '#9179c0', '#8e6d5a', '#f19839', '#949494'];